# Référence des fonctions Lua

Cette page recense les fonctions publiques disponibles dans les trajets Jitsuri actuels. Les noms sont sensibles à la casse.

## Contrôle du script

### `printMessage(message, couleur)`

Affiche une ligne dans la console du trajet.

```lua
printMessage("Départ du trajet.", "green")
```

### `delay(millisecondes)`

Attend avant de poursuivre.

```lua
delay(750)
```

### `stopScript()`

Arrête proprement le trajet en cours.

## État du personnage

| Fonction | Résultat |
|---|---|
| `getCurrentPos()` | Coordonnées actuelles. |
| `getMapId()` | MapID actuel. |
| `getCharacterLevel()` | Niveau du personnage. |
| `getJobLevel(jobId)` | Niveau du métier demandé. |
| `getPods()` | Pods utilisés. |
| `getPodsMax()` | Pods maximum. |
| `getLifePoints()` | Points de vie actuels. |
| `getClass()` | Classe du personnage. |
| `getTeam()` | Informations sur l’équipe. |
| `getStranger()` | Personnages étrangers détectés sur la carte. |
| `isFighting()` | `true` pendant un combat. |

```lua
if getPods() >= getPodsMax() * 0.9 then
    printMessage("Inventaire bientôt plein.", "yellow")
end
```

## Navigation et interactions

| Fonction | Usage |
|---|---|
| `goToCellId(cellId)` | Déplace le personnage vers une cellule. |
| `changeMapByCellId(cellId)` | Utilise une cellule de changement de carte. |
| `goToMapId(mapId)` | Calcule et suit un chemin vers un MapID. |
| `goUseInteractive(cellId)` | Rejoint et utilise l’élément interactif de la cellule. |
| `teamGoToCellId(cellId)` | Déplace les membres pilotés vers une cellule. |
| `teamGoUseInteractive(cellId)` | Utilise l’interactif de la cellule avec l’équipe. |
| `goToAndEnterHouse(...)` | Rejoint puis ouvre une maison configurée. |
| `goAndOpenChest(...)` | Rejoint puis ouvre un coffre configuré. |
| `exitDungeon()` | Tente de quitter le donjon courant. |

Les fonctions de navigation retournent une information de réussite. Testez-la avant d’enchaîner une action sensible.

```lua
if not goToCellId(355) then
    printMessage("Cellule inaccessible.", "red")
    return
end
```

## Recherche de zones intelligentes depuis le Lua

Ces fonctions permettent de découvrir les cartes sans les écrire dans le trajet.
Elles nécessitent une version de Jitsuri qui expose ces nouvelles fonctions.

| Fonction | Description |
|---|---|
| `getSmartGatheringPlan(gid)` | Recherche les lieux de récolte du **GID de l'objet produit**, dans l'index local `maps.bin` v3. |
| `getSmartCombatPlan(monsterIds)` | Recherche les zones d'une liste de GID de monstres séparés par des virgules, via le service utilisé par le mode intelligent. |

Le résultat contient `success`, `code`, `message`, `mapIds`, `elementIds`,
`maps` (MapID séparés par des virgules) et `mapScores` (scores du catalogue).
La recherche n'entraîne aucun déplacement ni combat.

Exemple fictif : remplacez le GID `0` par celui de votre propre matière
première. Il ne correspond pas à un parcours prêt à lancer.

```lua
local plan = getSmartGatheringPlan(0) -- GID de ressource à renseigner
if not plan.success then
    printMessage(plan.code .. " : " .. plan.message, "red")
    stopScript()
    return
end

ELEMENTS_TO_GATHER = plan.elementIds
setGatheringGoal(0, 25, "total")
local destination = selectSmartGatheringTargetWithHistory(
    plan.maps, "", 20, plan.mapScores, ""
)
```

`selectSmartGatheringTargetWithHistory(maps, cartesRécentes, exploration,
scoresCatalogue, scoresPersonnels)` et
`selectSmartCombatTargetWithHistory(maps, cartesRécentes, exploration,
scoresCatalogue)` choisissent une destination avec le moteur intelligent
existant. `exploration` va de 0 à 100 ; les cartes récentes sont une chaîne
de MapID séparés par des virgules. Une destination `0` signifie qu'aucune
carte n'a pu être retenue. Le script doit alors attendre ou s'arrêter,
pas lancer un combat sans filtre.

Les plans ne recopient pas automatiquement les paramètres, les stockages,
les classements personnels ni l'équipe choisis dans l'interface intelligente.
Les niveaux de métiers, les accès et la navigation restent à vérifier.
Un échec de l'API ou un index absent renvoie un résultat d'échec ; aucune
zone de remplacement arbitraire n'est sélectionnée.

Consultez [Zones intelligentes et achats](approvisionnement-intelligent.md)
pour combiner ces recherches avec une politique de récolte ou d'achat
adaptée à votre trajet.

## PNJ et dialogues

| Fonction | Description |
|---|---|
| `talkNpc(actorId)` | Ouvre le dialogue avec un acteur présent sur la carte. |
| `talkNpcId(npcId)` | Cherche le PNJ par son identifiant puis ouvre le dialogue. |
| `replyNpc(index)` | Sélectionne une réponse visible par son index, à partir de 0. |
| `replyNpcAndChangeMap(index)` | Répond et attend le changement de carte. |
| `leaveDialog()` | Ferme le dialogue courant. |

```lua
if talkNpcId(925) then
    replyNpc(0)
end
```

!!!info
Un `npcId` identifie le modèle du PNJ, tandis qu’un `actorId` identifie sa présence sur la carte courante. Préférez `talkNpcId` dans un trajet réutilisable.
!!!

## Inventaire et équipement

| Fonction | Description |
|---|---|
| `getInventoryItemCount(gid)` | Quantité totale du GID dans l’inventaire. |
| `getTeamInventoryItemCount(gid)` | Quantité du GID dans les inventaires de toute l’équipe pilotée, meneur inclus. Renvoie `nil` si le comptage n’est pas disponible. |
| `getInventoryItemByGid(gid)` | Retourne notamment `uid`, `gid` et `quantity`. |
| `getEquippedItemAtPosition(position)` | Lit l’objet porté à une position. |
| `useInventoryItem(gid, quantity)` | Utilise une quantité d’objets. |
| `teamUseInventoryItem(gid, quantity)` | Utilise l’objet pour l’équipe pilotée. |
| `equipItem(uid, position)` | Équipe l’UID à la position demandée. |
| `unequipItem(uid)` | Déséquipe l’objet. |
| `deleteItem(gid)` | Supprime définitivement la pile du GID. |
| `DropItem(gid, quantity)` | Jette une quantité au sol. |

```lua
local item = getInventoryItemByGid(12345)

if item ~= nil then
    equipItem(item.uid, 0)
end
```

!!!warning
`deleteItem` et `DropItem` sont destructifs. Le nom `DropItem` commence par une majuscule.
!!!

### Compter une ressource dans toute l’équipe

`getTeamInventoryItemCount(gid)` fonctionne depuis le trajet du meneur : les
suiveurs n’ont pas besoin d’exécuter leur propre Lua. Elle additionne toutes les
piles du GID dans les inventaires des membres pilotés par ce trajet, meneur
inclus et sans compter deux fois un personnage. En solo, elle lit uniquement
l’inventaire du personnage qui exécute le script.

```lua
-- GID fictif : remplacez-le par celui de votre ressource.
local GID_RESSOURCE = 12345
local OBJECTIF_EQUIPE = 10

function objectifEquipeAtteint()
    local total = getTeamInventoryItemCount(GID_RESSOURCE)
    if total == nil then
        printMessage("Comptage équipe indisponible : attendre puis revérifier.", "yellow")
        return nil
    end

    printMessage("Stock équipe : " .. total .. "/" .. OBJECTIF_EQUIPE, "green")
    return total >= OBJECTIF_EQUIPE
end

-- Dans votre logique de trajet, hors combat :
local atteint = objectifEquipeAtteint()
if atteint == nil then
    -- Ne pas décider de farmer davantage avec un total incomplet.
    -- Réessayer au prochain point sûr du trajet.
elseif atteint then
    -- Passer à votre prochaine étape.
else
    -- Continuer la recherche de cette ressource.
end
```

!!!warning Comptage indisponible ≠ zéro
La fonction renvoie `nil` si l’instance d’un membre n’est plus disponible, si son inventaire n’est
pas encore synchronisé, s’il combat encore ou charge une carte. Elle attend
également une seconde de stabilisation après une fin de combat ou une mise à
jour d’inventaire : pendant ce délai, elle renvoie `nil` sans bloquer le Lua.
Réessayez ensuite, même si le meneur a déjà terminé son combat. Un total fiable
de zéro renvoie bien le nombre `0`. Ne remplacez pas `nil` par `0`.
!!!

Le GID doit être un entier positif. Ce compteur lit le **stock actuel**, y
compris les objets déjà possédés avant le trajet, et non le cumul des drops.
Il ne consulte pas les banques, coffres ou inventaires de joueurs externes à
l’équipe pilotée. Il ne compte pas les ressources contenues dans des sacs non
ouverts et ne transfère aucun objet entre les personnages.

## Stockage

| Fonction | Description |
|---|---|
| `putAllItems()` | Dépose tout ce qui peut l’être. |
| `putExistingItems()` | Dépose les objets déjà présents dans le stockage. |
| `getAllItems()` | Retire tous les objets disponibles. |
| `getExistingItems()` | Retire les objets déjà présents dans l’inventaire. |
| `getMaxQuantitesByGid(gid)` | Retire du stockage la quantité maximale possible du GID et renvoie la réussite. |
| `openGuildChest(number)` | Ouvre un coffre de guilde numéroté. |
| `takeGuildChestItems(number, maxPods, delayMs)` | Retire les objets d’un coffre jusqu’au seuil de pods. |
| `openBank()` | Ouvre la banque sur la carte actuelle sans déposer automatiquement l'inventaire. |
| `isStorageOpen()` | Confirme que le contenu du stockage courant est chargé. |
| `getStorageItemCount(gid)` | Compte le GID dans le stockage courant. |
| `getStorageItems()` | Liste les piles du stockage courant. |
| `withdrawStorageItem(gid, quantity)` | Retire une quantité précise. |
| `withdrawCraftIngredients(itemGid, quantity)` | Retire les ingrédients manquants d'une recette. |
| `depositStorageItem(gid, quantity)` | Dépose une quantité précise, ou tout le GID avec `0`. |
| `closeStorage()` | Ferme le stockage par requête de dialogue, sans action sur l'interface via Frida, et attend la confirmation serveur. |

Pour retirer uniquement les ingrédients d'une recette, suivre une quantité
récoltée ou composer un parcours banque → récolte → atelier, consultez
[Récolter puis fabriquer](craft-et-recolte.md).

Un retrait qui rejoint une pile déjà présente dans l'inventaire est reconnu,
même si son UID diffère de celui de la pile bancaire. Les piles retirées
entièrement disparaissent également du stock local.

Les transferts distinguent `SERVER_REJECTED` (refus reçu) de
`TRANSFER_UNCONFIRMED` (confirmation manquante). Dans le second cas, vérifiez
l'inventaire avant de réessayer : le transfert peut avoir eu lieu.
`closeStorage()` retourne `CLOSE_UNCONFIRMED` si la fermeture n'est pas
confirmée, et réussit sans nouvelle action si le stockage est déjà fermé.
Utilisez `closeCraftWorkshop()` pour les ateliers.

## Hôtel de vente et artisanat

| Fonction | Description |
|---|---|
| `openSellerHdv(cellId)` | Ouvre le mode vendeur de l’HDV courant. |
| `closeSellerHdv()` | Ferme l’interface vendeur. |
| `sellAllItems()` | Met en vente les objets compatibles selon la configuration. |
| `buyMissingResource(gid, targetQuantity, hdvMapId, maximumKamas)` | Rejoint l'HDV choisi et achète le manque pour atteindre un objectif d'inventaire, au coût minimal des lots disponibles, sans surplus. |
| `craftItem(itemGid, bankMapId, workshopMapId, specialCraft, returnBank)` | Prépare et exécute une fabrication. |
| `getCraftRecipe(itemGid)` | Lit la recette locale et ses ingrédients. |
| `getCraftableQuantity(itemGid)` | Calcule la quantité fabricable depuis l'inventaire. |
| `getMissingCraftIngredients(itemGid, quantity, source)` | Liste les ingrédients manquants. |
| `openCraftWorkshop(itemGid)` | Ouvre l'atelier compatible présent sur la carte. |
| `craftFromInventory(itemGid, quantity)` | Fabrique depuis l'inventaire déjà préparé. |
| `closeCraftWorkshop()` | Ferme l'atelier courant. |

Les fonctions de craft séparées (`getCraftRecipe`, `openCraftWorkshop`,
`craftFromInventory`, etc.) sont décrites dans le guide
[Récolter puis fabriquer](craft-et-recolte.md). `craftItem(...)` reste
disponible pour les anciens trajets.

Pour une gestion régulière de plusieurs stocks et catégories d’HDV, utilisez plutôt le [bot HDV](../Utilisation/hdv.md), qui ajoute les protections de prix et le routage des objets.

La quantité passée à `buyMissingResource` est un **stock cible**, pas une
quantité supplémentaire. Son budget s'applique à cet appel, hors frais de
trajet. Le script choisit quand acheter et gère lui-même un budget partagé
entre plusieurs ressources. Voir [les paramètres, résultats et exemples](approvisionnement-intelligent.md).

## Objectifs de récolte

| Fonction | Description |
|---|---|
| `setGatheringGoal(gid, quantity, mode)` | Crée un objectif `total` ou `additional`. |
| `getGatheringGoal(gid)` | Lit l'état complet d'un objectif. |
| `getGatheringGoals()` | Liste tous les objectifs actifs. |
| `isGatheringGoalReached(gid)` | Indique si le GID a atteint son objectif. |
| `areAllGatheringGoalsReached()` | Indique si tous les objectifs sont terminés. |
| `clearGatheringGoal(gid)` | Retire un objectif. |
| `clearAllGatheringGoals()` | Retire tous les objectifs. |
| `createGatheringGoalsForCraft(itemGid, quantity)` | Crée les objectifs récoltables depuis une recette. |
| `requestRouteRefresh()` | Demande au trajet de recalculer sa phase. |

Les callbacks facultatifs `onInventoryItemChanged`, `onItemGathered`,
`onGatheringGoalProgress` et `onGatheringGoalReached` permettent de suivre ces
changements en direct.

## Suspendre le suivi de l’équipe

| Fonction | Description |
|---|---|
| `setTeamFollowingEnabled(false)` | Suspend les actions de suivi du trajet : les déplacements et actions collectives concernés sont exécutés uniquement par le meneur. |
| `setTeamFollowingEnabled(true)` | Réactive un suivi suspendu et attend le regroupement avant de renvoyer `true`. Si le suivi est déjà actif, ne force pas une nouvelle vérification. En cas d’échec de la reprise, renvoie `false` et conserve la pause. Pour préparer un échange, utilisez `teamRegroup()`. |
| `isTeamFollowingEnabled()` | Indique si le suivi du trajet est activé. |

Appelez ces commandes depuis une action `custom` du meneur, hors combat.
En solo, elles ne déclenchent aucun déplacement de groupe. Les membres restent
dans l’équipe : leurs inventaires restent consultables et aucun paramètre de
déplacement enregistré n’est modifié. La pause est remise à zéro à l’arrêt et
au démarrage d’un nouveau trajet.

```lua
-- Exemple générique : encadrer une phase de récolte de votre propre trajet.
local carteAttente = nil

function commencerRecolteSolo()
    if getTeamSize() <= 1 or carteAttente ~= nil then return true end
    if not setTeamFollowingEnabled(false) then return false end
    carteAttente = getMapId()
    return true
end

function terminerRecolteSolo()
    if carteAttente == nil then return true end
    -- Revenir chercher les équipiers évite de les envoyer vers une zone de récolte.
    if getMapId() ~= carteAttente and not goToMapId(carteAttente) then return false end
    if not setTeamFollowingEnabled(true) then return false end
    carteAttente = nil
    return true
end
```

Appeler la première fonction avant de produire les actions de récolte, puis la
seconde avant les échanges ou les combats. Si une fonction renvoie `false`,
arrêtez le trajet ou gérez l’échec explicitement ; ne poursuivez pas le combat.
Pendant la pause, le trajet ne lance pas de combat automatique. Le détour
bancaire individuel décrit ci-dessous reste actif si `TEAM_BANK` est configuré :
la mule retourne ensuite sur sa carte d’attente, sans rejoindre la zone de récolte.

!!!warning Portée de la pause
Il s’agit d’une pause du suivi du trajet, pas d’un verrouillage des personnages.
Elle n’annule pas un mouvement déjà engagé et ne désactive pas les actions
manuelles, les agressions ni les autres automatismes indépendants, comme
l’élevage. Réactivez le suivi hors combat, sur une carte accessible au groupe.
!!!

## Banque individuelle des équipiers

Pour permettre à un équipier aux pods pleins de se vider sans arrêter le Lua du
meneur, déclarez `TEAM_BANK` au début du script, avant le premier appel à `move()`.
Le seuil utilisé est `MAX_PODS`.

```lua
-- Exemple fictif : remplacer les identifiants par ceux de votre propre trajet.
MAX_PODS = 85
TEAM_BANK = {
    mapId = 123456789,        -- carte intérieure de la banque, avec son PNJ accessible
    depositGids = { 100001, 100002 }, -- seuls ces objets peuvent être déposés
    keepGids = { 100003 },   -- objets à conserver, prioritaires sur la liste de dépôt
    -- recipeGid = 100004,   -- facultatif : donner les ingrédients au meneur AVANT la banque
    exchangeTimeoutMs = 15000, -- délai par étape d'échange (1 000 à 60 000 ms)
}
```

Entre deux actions, hors combat, le meneur attend pendant que **seul l’équipier
plein** rejoint la banque, dépose les objets autorisés, ferme le dialogue et
revient. Le Lua reste actif et reprend après confirmation du retour et de la
baisse des pods sous le seuil. Plusieurs équipiers pleins sont traités l’un
après l’autre. Si le suivi est suspendu, chacun revient sur sa carte d’attente.

Les objets équipés, favoris, de quête et ceux de `keepGids` sont conservés.
Avec `recipeGid`, l’objet fabriqué est également conservé. Avant de rejoindre la
banque, l’équipier donne au meneur **toutes les piles transférables des ingrédients
de cette recette**, pas seulement la quantité d’un lot. Les autres membres ne
participent pas à cet échange. Si nécessaire, seul cet équipier rejoint d’abord
la carte du meneur ; le suivi suspendu n’est pas réactivé et il revient ensuite
sur sa carte d’attente. Sans ingrédients à donner, l’échange est ignoré.

La réception doit être confirmée avant le départ en banque. Si le meneur manque
de pods ou si l’échange échoue, **aucun dépôt bancaire n’est lancé** : le trajet
attend sans réessayer en boucle. Vérifiez l’échange et libérez de la place chez
le meneur, puis relancez le Lua. Les ingrédients utiles sont exclus des dépôts,
même s’ils figurent aussi dans `depositGids`. Aucun dépôt global n’est effectué.
Chaque équipier utilise **sa propre banque** : les dépôts ne deviennent pas
accessibles dans l’inventaire ou la banque du meneur.

Si la configuration, le dépôt, la fermeture ou le retour échoue, un message
`[BANQUE EQUIPE]` indique le motif. Le trajet reste en attente, sans répéter les
requêtes. Pour reprendre, libérez les pods de l’équipier et ramenez-le sur la
carte d’attente, banque fermée ; ou corrigez la configuration puis relancez le Lua.
Un arrêt manuel annule le détour.

Cette option concerne les équipiers d’un trajet Lua, pas le mode atelier dédié.
En solo, et pour le meneur lui-même, la fonction `bank()` conserve son rôle.
Sans `TEAM_BANK`, les anciens trajets conservent leur gestion bancaire existante.

## Échanges entre personnages

Les échanges Lua utilisent les confirmations du serveur : une fonction ne
renvoie `true` que lorsque l'étape correspondante a réellement été confirmée.
Les deux personnages doivent être connectés au même serveur, présents sur la
même carte et visibles l'un pour l'autre.

| Fonction | Description |
|---|---|
| `getCharacterId()` | Identifiant du personnage qui exécute le trajet. |
| `getTeam()` | Membres de l'équipe du trajet : `name`, `id` (identifiant du personnage), `level` et `breedId`. Dans la liaison NLua actuelle, le résultat est un tableau C# indexé de `0` à `Length - 1`. |
| `exchangeExpect(characterId, timeoutMs)` | Autorise temporairement une invitation provenant exactement de ce personnage. |
| `exchangeWaitIncoming(characterId, timeoutMs)` | Autorise puis attend son invitation. |
| `exchangeRequest(characterId, timeoutMs)` | Demande un échange et attend son ouverture. |
| `exchangeAccept(characterId, timeoutMs)` | Accepte l'invitation reçue du personnage attendu. |
| `exchangeSetKamas(quantity, timeoutMs)` | Définit les kamas proposés par le personnage local. |
| `exchangeMoveItem(objectUid, quantity)` | Ajoute ou retire une quantité d'une pile grâce à son UID. Une quantité négative la retire de l'échange. |
| `exchangeReady(timeoutMs)` | Valide l'offre locale et attend sa confirmation. |
| `exchangeWaitResult(timeoutMs)` | Attend la fermeture réussie de l'échange. |
| `exchangeCancel()` | Annule l'échange courant. |
| `exchangeGetState()` | Retourne les participants, les kamas, les états de validation et le résultat. |
| `exchangeTransferKamas(recipientId, quantity, reserve, timeoutMs)` | Depuis le donneur, réalise tout l'échange avec un autre bot Jitsuri connecté. |
| `exchangeRequestKamas(soldeCible, reserveDonneur, timeoutMs)` | Depuis le receveur, choisit un autre compte Jitsuri disponible sur la carte et demande uniquement le montant manquant. |
| `getTeamSize()` | Nombre de personnages pilotés par le trajet, meneur inclus ; `1` en solo. |
| `teamRegroup()` | Depuis le meneur, réactive le suivi et force le regroupement sur sa carte, même si le suivi était déjà actif. Attend ensuite des personnages immobiles et des inventaires stabilisés. Renvoie `success`, `reason` et `details`. En solo, ne fait rien. |
| `teamCollectItems(gids, timeoutMs)` | Depuis le meneur, récupère toutes les piles non équipées des GID sélectionnés auprès des suiveurs. Renvoie une table de résultat. |
| `teamCollectAllItems(timeoutMs)` | Depuis le meneur, récupère les objets des inventaires des mules sans fournir de GID. Exclut les équipements portés, favoris et objets de quête. Ne transfère pas les kamas. Renvoie `success`, `reason` et `completedMembers`. |
| `teamTransferItemsAndKamas(donorId, recipientId, items, kamas, timeoutMs)` | Depuis le meneur, pilote un échange unique entre deux membres précis : quantités par GID et kamas. `items` est une liste de `{ gid = ..., quantity = ... }`. Renvoie une table de résultat. |
| `teamCollectAllItemsAndKamas(timeoutMs, reserveKamas)` | Depuis le meneur, récupère les objets non protégés et les kamas de chaque mule dans un même échange par mule. `reserveKamas` est le montant conservé par chacune ; `0` transfère tout son solde. Renvoie une table de résultat. |

### Regrouper l'équipe avant un échange

Un inventaire d'équipe peut être compté à distance : un total disponible ne
garantit pas que les mules soient sur la carte du meneur. Avant un transfert,
notamment après une visite en banque, appelez `teamRegroup()` depuis le `custom`
du meneur, hors combat et après fermeture du stockage.

```lua
local regroupement = teamRegroup()
if not regroupement.success then
    printMessage("Regroupement impossible : " .. regroupement.reason ..
        " | " .. regroupement.details, "red")
    stopScript()
    return
end
local resultat = teamCollectItems({ 12345 }, 15000) -- GID fictif
if not resultat.success then
    printMessage(resultat.reason .. " | " .. resultat.details, "red")
    stopScript()
    return
end
```

Le regroupement utilise la navigation habituelle de chaque mule, avec ses
zaaps connus lorsque le calcul de trajet les retient. Si cette navigation
échoue, une tentative à pied est conservée depuis sa position actuelle,
uniquement si elle est disponible (hors combat, déplacement et chargement).
Une annulation du trajet ne déclenche pas ce repli. Les logs indiquent la
navigation puis, si nécessaire, le repli à pied.

La destination reste le **MapID exact du meneur**, y compris l'intérieur
d'une banque ou d'un bâtiment accessible. L'extérieur aux mêmes coordonnées
ne compte pas comme une arrivée : les mules empruntent la transition d'entrée
connue du navigateur. Une porte inaccessible ou une condition d'accès non
remplie ne sont pas contournées ; le regroupement doit être confirmé avant
de poursuivre les échanges.

La récupération des suiveurs conserve sa fenêtre de
120 secondes sur une carte du meneur inchangée, puis attend jusqu'à 15 secondes
leur immobilité et la stabilisation des inventaires. Elle ne lance aucun échange.
Un échec de regroupement conserve le suivi en pause ; aucun transfert ne doit
être lancé avant un résultat positif. Si les mules étaient volontairement
garées ailleurs, ramenez d'abord le meneur sur leur carte d'attente.

Si un personnage est sur la carte mais que sa position reste non synchronisée,
le regroupement demande au serveur un rafraîchissement de sa carte, sans le
déplacer. Ce contrôle concerne aussi le meneur et s'applique aux attentes de
groupe des trajets Lua et du mode intelligent. Les demandes sont limitées à
trois par personnage et épisode d'attente, espacées de dix secondes après une
attente initiale de deux secondes. Aucune position n'est supposée valide pour
forcer la reprise.

Pour diagnostiquer un blocage persistant, conservez les lignes
`[TEAM POSITION STATE]`, `[TEAM POSITION REFRESH]` et `[TEAM POSITION READY]`.
Elles précisent la carte, la présence du personnage dans les données reçues,
la cellule connue, l'état du complément de carte et la source de position.

En cas de `team_not_ready`, le champ `details` des fonctions de transfert
indique les personnages bloquants et leur état observé : carte différente,
déplacement, combat, chargement, déconnexion, serveur inconnu/différent ou
inventaire non synchronisé/en stabilisation. Un contrôle réussi n'empêche pas
un changement d'état ultérieur : vérifiez aussi le résultat du transfert.

### Trouver l'identifiant d'un membre de l'équipe

Un **identifiant de personnage** sert à cibler le receveur d'un échange. Ce
n'est ni son nom de compte, ni son ProcessID, ni le GID ou l'UID d'un objet.
Lancez ce petit trajet **sur le meneur avec l'équipe configurée** pour afficher
les noms et identifiants à reporter dans les exemples suivants :

```lua
function move()
    return {{ map = tostring(getMapId()), custom = function()
        printMessage("Meneur : ID=" .. tostring(getCharacterId()), "yellow")
        local equipe = getTeam()
        -- getTeam() renvoie actuellement un tableau C#, pas une table Lua.
        for i = 0, equipe.Length - 1 do
            local membre = equipe[i]
            printMessage(membre["name"] .. " : ID=" .. tostring(membre["id"]), "yellow")
        end
        stopScript()
    end }}
end
```

Pour retrouver un membre par son **nom de personnage exact**, utilisez la même
lecture dans une action `custom` :

```lua
local function identifiantMembre(nom)
    local equipe = getTeam()
    for i = 0, equipe.Length - 1 do
        local membre = equipe[i]
        if membre["name"] == nom then return membre["id"] end
    end
    return nil -- absent : ne pas choisir un autre destinataire par défaut
end
```

Hors trajet d'équipe, cette liste peut être vide : `getCharacterId()` reste le
moyen de lire l'identifiant du personnage local. Ne parcourez pas ce tableau
C# avec `ipairs` comme s'il s'agissait d'une table Lua.

### Exemple : X donne 20 objets d'un GID et 10 000 kamas à Y

Lancez **un seul trajet sur le meneur** de l'équipe. X et Y peuvent être deux
mules, ou l'un d'eux peut être le meneur. Les deux personnages doivent appartenir
à l'équipe pilotée et être présents sur la même carte. Aucun script séparé
n'est nécessaire sur le donneur ou le receveur.

Les noms X et Y et le GID ci-dessous sont fictifs : remplacez-les par ceux de
vos personnages et de l'objet souhaité.

```lua
local DONNEUR = "X"
local RECEVEUR = "Y"
local OBJETS = { { gid = 12345, quantity = 20 } } -- GID fictif
local KAMAS = 10000
local tentativeEffectuee = false

local function identifiantMembre(nom)
    local equipe = getTeam()
    for i = 0, equipe.Length - 1 do
        if equipe[i]["name"] == nom then return equipe[i]["id"] end
    end
    return nil
end

function move()
    return {{ map = tostring(getMapId()), custom = function()
        if tentativeEffectuee then stopScript(); return end
        tentativeEffectuee = true
        local donneurId = identifiantMembre(DONNEUR)
        local receveurId = identifiantMembre(RECEVEUR)
        if donneurId == nil or receveurId == nil or donneurId == receveurId then
            printMessage("Donneur ou receveur absent/invalide dans l'équipe.", "red")
            stopScript(); return
        end
        local resultat = teamTransferItemsAndKamas(
            donneurId, receveurId, OBJETS, KAMAS, 15000)
        printMessage("Échange " .. DONNEUR .. " -> " .. RECEVEUR .. " : " .. resultat.reason,
            resultat.success and "green" or "red")
        stopScript()
    end }}
end
```

Les **20 objets et les 10 000 kamas sont proposés dans le même échange**, puis
validés ensemble. La quantité demandée peut être répartie sur plusieurs piles :
Jitsuri retrouve les UID et ne prend que les quantités indiquées. Pour plusieurs
GID, ajoutez des entrées à `OBJETS` :

```lua
local OBJETS = {
    { gid = 12345, quantity = 20 },
    { gid = 12346, quantity = 5 },
}
```

Chaque GID ne doit apparaître qu'une fois ; les GID et quantités doivent être
des entiers strictement positifs. Avec `KAMAS = 0`, seuls les objets sont
transférés. Avec `OBJETS = {}` et un montant positif, seuls les kamas le sont.
Les équipements portés, favoris et objets de quête ne sont pas sélectionnés.
Si le donneur n'a pas les quantités transférables ou les kamas nécessaires,
ou si le receveur manque de pods, aucun échange n'est lancé.

### Exemple : X, Y et Z donnent leurs objets et kamas à B, le meneur

Configurez l'équipe **B (meneur), X, Y, Z** et lancez ce trajet uniquement sur B.
Il vérifie la composition de l'équipe, puis effectue **un échange par mule** :
objets et kamas de X vers B, puis de Y vers B, puis de Z vers B. L'ordre exact
des mules suit l'ordre de l'équipe, sans échanges simultanés.

```lua
local MENEUR = "B"
local DONNEURS = { "X", "Y", "Z" }
local RESERVE_KAMAS_PAR_MULE = 0 -- 0 = tout ; 50000 = garder 50 000 par mule
local tentativeEffectuee = false

function move()
    return {{ map = tostring(getMapId()), custom = function()
        if tentativeEffectuee then stopScript(); return end
        tentativeEffectuee = true
        local ids = {}
        local equipe = getTeam()
        for i = 0, equipe.Length - 1 do
            local membre = equipe[i]
            ids[membre["name"]] = membre["id"]
            printMessage(membre["name"] .. " : ID=" .. tostring(membre["id"]), "yellow")
        end
        local valide = getTeamSize() == 4 and ids[MENEUR] == getCharacterId()
        for _, nom in ipairs(DONNEURS) do
            if ids[nom] == nil then valide = false end
        end
        if not valide then
            printMessage("Lancer sur B avec exactement B, X, Y et Z dans l'équipe.", "red")
            stopScript(); return
        end
        local resultat = teamCollectAllItemsAndKamas(15000, RESERVE_KAMAS_PAR_MULE)
        printMessage("Objets et kamas vers B : " .. resultat.reason ..
            " | Mules terminées : " .. resultat.completedMembers,
            resultat.success and "green" or "red")
        stopScript()
    end }}
end
```

« Tous les objets » conserve les protections : **équipements portés, favoris
et objets de quête restent sur les mules**. Les autres objets, y compris les
équipements non portés et non favoris, sont concernés. Si une mule possède
moins que la réserve, elle conserve tout son solde ; ses objets peuvent quand
même être transférés. Une mule n'ayant que des kamas peut aussi être traitée ;
une mule n'ayant rien à donner est ignorée.

Le meneur doit pouvoir recevoir **l'ensemble des objets** avant le premier
échange. La fonction ne lance aucun dépôt bancaire pour libérer de la place.
En solo, la collecte ne fait rien. Elle ne cible jamais un personnage extérieur
à l'équipe du trajet.

!!!warning Confirmations et transferts partiels
Les offres d'objets et de kamas sont vérifiées des deux côtés avant validation.
Le numéro de validation (`step`) est calculé automatiquement à partir des
notifications de modification de l'offre, séparément pour chaque personnage.
Il ne correspond pas au nombre de piles : un ajout groupé compte comme une
seule notification. Aucun `step` n'est à configurer dans vos Lua. Si l'offre
change entre les validations, le transfert est interrompu plutôt que de
valider un contenu modifié.
Le succès exige ensuite la confirmation du serveur, des quantités reçues et
des variations de solde. `success`, `reason` et `completedMembers` permettent
de contrôler le résultat. Les délais sont bornés de 1 à 60 secondes par étape.

Un échange réunit les objets et les kamas d'un donneur ; **la collecte de
plusieurs mules n'est pas une opération atomique**. Si Y échoue après X, les
biens déjà reçus de X restent chez B. Une confirmation manquante peut également
masquer un échange effectué : vérifiez inventaires et soldes avant de relancer.
La collecte entière n'est jamais relancée automatiquement. Pour la validation
du donneur uniquement, une seule relance du `Ready` est autorisée si aucune
confirmation n'a été reçue et si l'offre est strictement inchangée. Elle ne
redépose pas les objets et n'ouvre pas un nouvel échange.

Le donneur invite le receveur. Après confirmation des objets et kamas des
deux côtés, l'offre doit rester inchangée pendant 2,5 secondes avant validation.
Ce délai est une précaution issue de captures manuelles réussies. Le receveur
valide seulement lorsque la validation du donneur est confirmée sur les deux
comptes. En cas d'échec, `details` précise les personnages, leurs `step` et les
confirmations observées.

Les motifs peuvent notamment être `member_not_in_team`, `invalid_offer`,
`insufficient_items`, `insufficient_kamas`, `insufficient_pods`,
`unknown_item_weight`, `team_not_ready`, `kamas_changed`,
`offer_not_confirmed`, `exchange_refused` ou `inventory_not_confirmed`.
Lancez ces appels depuis une action `custom` du meneur, hors combat, sans banque,
atelier ou autre échange ouvert. Les suiveurs n'exécutent pas leur propre Lua.
!!!

### Regrouper les ingrédients avant un craft

Appelez cette fonction depuis une action `custom` du trajet du meneur, hors
combat et avant d’ouvrir une banque ou un atelier. Les personnages doivent être
sur la même carte, visibles et immobiles ; la fonction attend brièvement les
retardataires mais ne calcule pas de trajet pour les rejoindre. Les suiveurs
n’ont pas besoin d’exécuter leur propre Lua.

```lua
-- GID fictifs, à remplacer par les ingrédients de votre recette.
local INGREDIENTS = { 12345, 12346 }

function preparerLesIngredients()
    if getTeamSize() > 1 then
        local resultat = teamCollectItems(INGREDIENTS, 15000)
        if not resultat.success then
            printMessage("Collecte interrompue : " .. resultat.reason ..
                " / équipiers terminés : " .. resultat.completedMembers, "red")
            return false -- ne pas continuer vers le craft avec un bilan incomplet
        end
    end
    -- Maintenant seulement : lire getInventoryItemCount pour chaque ingrédient,
    -- vérifier votre banque si nécessaire, puis calculer ce qu’il reste à obtenir.
    return true
end
```

La collecte prend **toutes les quantités disponibles des GID choisis**, pas
seulement le manque de la recette. Les kamas, équipements portés et autres GID
ne sont pas transférés. Chaque mule termine son échange avant la suivante.
La fonction vérifie les offres des deux côtés, le succès serveur et les
quantités reçues ; elle envoie ensuite une fermeture de dialogue unique sur
chaque personnage pour nettoyer les fenêtres résiduelles. En solo, même un
appel direct renvoie immédiatement `{ success = true, reason = "solo",
completedMembers = 0 }`, sans échange ni attente.

Le résultat contient `success`, `reason` et `completedMembers` (nombre de mules
dont le transfert et le nettoyage ont terminé). `reason` vaut notamment `ok`,
`nothing_to_transfer`, `team_not_ready`, `insufficient_pods`, `unknown_item_weight`,
`offer_not_confirmed`, `exchange_refused`, `inventory_not_confirmed` ou
`cleanup_failed`. Le délai, en millisecondes, est borné de 1 à 60 secondes **par
étape**, pas pour l’équipe entière. L’arrêt du trajet interrompt les attentes.

!!!warning Transferts partiels
Un échec n’annule pas les échanges précédemment réussis. Même l’échange courant
peut avoir transféré les objets si seule la confirmation d’inventaire ou la
fermeture a échoué. Relisez les inventaires avant de prendre une décision ;
ne relancez pas la collecte en boucle. En cas de pods insuffisants, prévoyez un
dépôt ou arrêtez le trajet : la fonction ne dépose et ne détruit rien automatiquement.
!!!

### Vider les inventaires des mules vers le meneur

`teamCollectAllItems(15000)` détecte automatiquement les piles présentes dans
les inventaires des suiveurs. Lancez le trajet sur le meneur avec son équipe
configurée. Tous les personnages doivent être connectés, sur le même serveur,
sur la même carte, visibles et hors combat. La fonction ne les déplace pas.

```lua
-- Transfert unique : lancer sur le meneur avec l'équipe configurée.
local tentativeEffectuee = false

function move()
    return {{ map = tostring(getMapId()), custom = function()
        if tentativeEffectuee then stopScript(); return end
        tentativeEffectuee = true
        local resultat = teamCollectAllItems(15000)
        printMessage("Transfert : " .. resultat.reason ..
            " | Mules terminées : " .. resultat.completedMembers,
            resultat.success and "green" or "red")
        stopScript()
    end }}
end
```

Les kamas, équipements portés, favoris et objets de quête restent sur les mules.
Les autres objets, y compris les équipements non portés et non favoris, sont
concernés. Le meneur doit disposer de suffisamment de pods pour **l'ensemble**
des objets sélectionnés : la vérification précède le premier échange. Si le
poids d'un objet est inconnu, aucun transfert n'est lancé.

Les mêmes confirmations, délais et règles de transfert partiel que
`teamCollectItems` s'appliquent. En solo, l'appel ne fait rien. Aucun dépôt,
destruction, découpage selon les pods ou nouvel essai automatique n'est effectué.
Un objet non échangeable peut faire échouer le transfert : vérifiez le résultat
avant de relancer. Marquez les objets à conserver comme favoris.

Sur une version ne proposant pas encore `teamCollectAllItems`, utilisez
`teamCollectItems({ gid1, gid2 }, 15000)` avec une liste de GID connus.
`getTeamInventoryItemCount(gid)` compte un type d'objet ; il ne permet pas
d'énumérer les GID ni les UID de tous les inventaires. Une liste vide passée à
`teamCollectItems` reste invalide et ne signifie jamais « tout transférer ».

### Transfert automatique entre deux bots Jitsuri

Le donneur peut piloter les deux côtés sans second script. La réserve empêche
son solde de descendre sous le montant choisi.

```lua
local RECEVEUR_ID = 4205379938
local MONTANT = 12345
local RESERVE_DONNEUR = 50000

if not exchangeTransferKamas(RECEVEUR_ID, MONTANT, RESERVE_DONNEUR, 15000) then
    printMessage("Échange de kamas annulé ou refusé.", "red")
end
```

Le receveur peut aussi demander à atteindre un solde. Jitsuri recherche alors
le compte distinct possédant le plus de kamas sur la même carte et le même
serveur, sans entamer sa réserve.

```lua
exchangeRequestKamas(100000, 50000, 15000)
```

### Échange piloté étape par étape

Sur le receveur :

```lua
local DONNEUR_ID = 57757270370

if exchangeWaitIncoming(DONNEUR_ID, 15000)
    and exchangeAccept(DONNEUR_ID, 15000)
    and exchangeReady(15000) then
    exchangeWaitResult(15000)
end
```

Sur le donneur :

```lua
local RECEVEUR_ID = 4205379938

if exchangeRequest(RECEVEUR_ID, 15000)
    and exchangeSetKamas(12345, 10000)
    and exchangeReady(15000) then
    exchangeWaitResult(15000)
end
```

!!!warning
N'utilisez jamais un `actorId` trouvé sur une ancienne carte. L'autorisation
d'une invitation est liée à l'identifiant exact et expire automatiquement.
Un échec ou un délai dépassé doit conduire le script à annuler puis réessayer
plus tard, et non à envoyer les requêtes en boucle.
!!!

## Caractéristiques

Les valeurs sont passées dans l’ordre : agilité, force, vitalité, chance, intelligence et sagesse.

```lua
upgradeCharacterStatsBatch(0, 0, 100, 0, 0, 0)
upgradeCharacterStatsBatchByName("Pandawa", 0, 0, 100, 0, 0, 0)
```

Le mode Starter n’appelle pas ces fonctions automatiquement : la répartition des caractéristiques reste votre choix.

## Combat Lua

Si le script déclare `fight()`, Jitsuri l’appelle pendant les tours de combat. Sans cette fonction, les sorts configurés dans l’interface restent la méthode habituelle.

### Lire le combat

| Fonction | Description |
|---|---|
| `getCharacterTurn()` | Personnage actuellement jouable, ou `nil`. |
| `getAttackers()` | Combattants du côté allié. |
| `getDefenders()` | Combattants du côté ennemi. |
| `getTeamMemberReachableCells(name)` | Cellules atteignables par le membre. |

Les combattants exposent notamment `actorId`, `gid`, `cellId`, `isAlive`, `isSummon`, `teamId`, `lifePct` et leurs caractéristiques disponibles.

### Déplacer et lancer un sort

| Fonction | Description |
|---|---|
| `teamMemberMoveToCellId(name, cellId)` | Déplace le membre courant. |
| `teamMemberCastSpellOnCellId(name, spellId, cellId)` | Lance un sort sur une cellule. |
| `getTeamMemberSpellPossibleCells(name, spellId)` | Cellules de lancement possibles. |
| `getTeamMemberSpellPossibleCellsOnCellId(name, spellId, cellId)` | Cellules permettant de viser la cellule demandée. |
| `getTeamMemberSpellZoneOnCellId(name, spellId, cellId)` | Zone couverte par le sort sur cette cellule. |
| `teamMemberFinishTurn(name)` | Termine le tour du membre. |

```lua
local ATTACK_SPELL = 1234

function fight()
    local character = getCharacterTurn()

    if character == nil then
        return
    end

    local enemies = getDefenders()

    for _, enemy in ipairs(enemies) do
        if enemy.isAlive and teamMemberCastSpellOnCellId(
            character.name,
            ATTACK_SPELL,
            enemy.cellId
        ) then
            break
        end
    end

    teamMemberFinishTurn(character.name)
end
```

!!!warning
Les anciens exemples basés sur `onTurn`, `onFightStart`, `fightAction` ou `jitsuriController` ne correspondent pas à l’API de cette branche. Utilisez `fight()` et les fonctions listées ci-dessus.
!!!

## Bonnes pratiques

- Vérifiez toujours les valeurs `nil` et les résultats des actions.
- Évitez les boucles sans limite et ajoutez un délai lorsque vous attendez un état serveur.
- Utilisez les MapID pour les intérieurs et les étages.
- Conservez les mots de passe et informations privées hors des scripts partagés.
- Testez un trajet avec un personnage et un inventaire adaptés avant de le déployer à une équipe.
