Piloter l'IA de combat avancée en Lua

L'API avancée permet de décider du placement et des actions de chaque tour depuis un trajet Lua. Elle est adaptée aux combats qui demandent une stratégie particulière : choisir une zone, avancer puis revenir, lire les états, respecter un placement ou déléguer certains tours à l'IA configurée dans Jitsuri.

Elle complète l'API simple basée sur fight(). Pour un même script, choisissez de préférence un seul modèle :

  • fight() et les fonctions teamMember... pour une boucle de combat simple ;
  • onFightStart, onPlacement, onTurn et onFightEnd pour le moteur événementiel avancé.

Cycle d'un combat

Callback Moment d'appel Usage principal
onFightStart(context) À l'entrée en combat Initialiser les compteurs propres au combat.
onPlacement(context) Pendant la phase de placement Choisir les cellules du personnage et des alliés pilotés.
onTurn(context) Au début du tour de chaque personnage piloté Lire le combat, se déplacer, lancer les sorts et terminer le tour.
onFightEnd(context) À la fin du combat Nettoyer ou enregistrer l'état utile au trajet.

Le paramètre context contient :

Champ Description
event Nom de l'événement courant.
processId Processus Jitsuri du personnage concerné.
characterName Nom du personnage concerné.
characterId Identifiant du personnage.
breedId Identifiant de sa classe.
mapId MapID du combat.
sessionId Identifiant de la session de combat.
round Tour courant ; 0 pour les événements hors tour.
challengerCells Cellules de placement du camp attaquant.
defenderCells Cellules de placement du camp défenseur.

Les listes de placement sont surtout utiles dans onPlacement. Un callback de placement qui renvoie explicitement false indique que le placement n'a pas abouti.

Squelette minimal

Les identifiants de sorts et les cellules de cet exemple sont fictifs.

local SORT_PRINCIPAL = 123456

function onFightStart(context)
    global:AddInGlobalMemory("combat-" .. context.sessionId, true)
end

function onPlacement(context)
    local cellules = context.challengerCells
    for _, entite in ipairs(fightAction:getAllEntities()) do
        if entite.Id == context.characterId and entite.Team then
            cellules = context.defenderCells
            break
        end
    end

    local cellule = cellules[1]
    if cellule == nil then
        return false
    end

    return fightAction:chooseCell(cellule)
end

function onTurn(context)
    if not fightCharacter:isItMyTurn() then
        return
    end

    local maCellule = fightCharacter:getCellId()
    local entites = fightAction:getAllEntities()
    local monCamp = nil

    for _, entite in ipairs(entites) do
        if entite.Id == context.characterId then
            monCamp = entite.TeamId
            break
        end
    end

    for _, entite in ipairs(entites) do
        if monCamp ~= nil and entite.TeamId ~= monCamp
            and entite.IsAlive and entite.IsPositionKnown then
            if fightAction:canCastSpellOnCell(
                maCellule,
                SORT_PRINCIPAL,
                entite.CellId
            ) == 0 then
                fightAction:castSpellOnCell(SORT_PRINCIPAL, entite.CellId)
                break
            end
        end
    end

    fightAction:passTurn()
end

function onFightEnd(context)
    global:printSuccess("Combat terminé : " .. tostring(context.sessionId))
end

Le combattant courant : fightCharacter

Méthode Retour Description
fightCharacter:getBreed() nombre Identifiant de classe du personnage courant.
fightCharacter:getCellId() nombre Cellule actuellement connue du personnage.
fightCharacter:isItMyTurn() booléen true si le personnage courant peut jouer.
fightCharacter:getKnownSpellIds() liste Identifiants de tous les sorts connus par le personnage.
fightCharacter:getConfiguredSpellIds() liste Identifiants des sorts enregistrés dans sa stratégie Jitsuri.
fightCharacter:getActiveStateIds() liste États actifs actuellement connus sur le personnage.
fightCharacter:hasState(stateId) booléen Indique si le personnage possède l'état demandé.
fightCharacter:getMP() nombre PM actuellement disponibles.

Lire les combattants

fightAction:getAllEntities() renvoie une liste Lua. Chaque entrée peut contenir les champs suivants :

Champ Description
Id, ActorId Identifiant de l'acteur dans ce combat.
CellId Cellule observée. Ne l'utilisez que si IsPositionKnown vaut true.
CreatureGenericId Identifiant générique du monstre ; 0 ou une valeur non pertinente pour certains alliés.
Team Côté serveur : false pour le camp attaquant, true pour le camp défenseur. Ce champ n'indique pas directement si l'entité est alliée.
TeamId Identifiant du camp. Comparez-le à celui du personnage courant pour distinguer alliés et ennemis.
IsAlive Indique si le combattant est encore vivant.
IsSummon Indique une invocation.
IsCompanion Indique un compagnon.
ControllerId Identifiant du contrôleur lorsqu'il est connu.
CompanionModelId Modèle du compagnon lorsqu'il est connu.
IsPositionKnown Indique si la position est exploitable.
IsInvisible Indique si le combattant est invisible.
Level Niveau connu ; 0 si indisponible.
ActionPoints, MovementPoints PA et PM observés ; peuvent valoir nil.
LifePercent Pourcentage de vie observé.
StateIds Liste des états reçus.
StatesKnown true lorsque la liste des états est considérée complète.
IsInvulnerable true pour l'état général d'invulnérabilité, false s'il est absent d'une liste complète, sinon nil.

Les immunités de mêlée ou de distance sont des états distincts. IsInvulnerable ne les regroupe pas.

Méthode Retour Description
fightAction:getAllEntities() liste Instantané des combattants connus.
fightAction:getEntityStateIds(actorId) liste, booléen États de l'acteur, puis indicateur de complétude.
fightAction:hasEntityState(actorId, stateId) booléen ou nil true si présent, false si absent d'une liste complète, nil si l'information est inconnue.
fightAction:getCurrentTurn() nombre Numéro du tour courant.
fightAction:getCurrentWave() nombre Dernier numéro de vague reçu, ou 0.
fightAction:getActiveChallengeIds() liste Challenges actifs déjà synchronisés.
fightAction:areChallengesKnown() booléen Indique si la liste complète des challenges a été reçue.

Pour les limites liées aux états et aux vagues, consultez Lire les états en combat.

Placement

Méthode Retour Description
fightAction:chooseCell(cellId) booléen Place le personnage courant sur une cellule disponible.
fightAction:chooseAllyCell(actorId, cellId) booléen Place un allié piloté identifié par son ActorID.

Utilisez ces méthodes uniquement pendant onPlacement. Les cellules disponibles se trouvent dans context.challengerCells et context.defenderCells. Le camp du personnage dépend de la façon dont le combat a été lancé.

function onPlacement(context)
    local entites = fightAction:getAllEntities()
    local moi = nil
    for _, entite in ipairs(entites) do
        if entite.Id == context.characterId then
            moi = entite
            break
        end
    end

    local disponibles = moi ~= nil and moi.Team
        and context.defenderCells
        or context.challengerCells
    if #disponibles == 0 then
        return false
    end

    local place = fightAction:chooseCell(disponibles[1])

    -- Exemple générique : répartir les autres alliés sur les places restantes.
    local index = 2
    for _, entite in ipairs(entites) do
        if moi ~= nil and entite.TeamId == moi.TeamId
            and entite.Id ~= context.characterId and disponibles[index] then
            fightAction:chooseAllyCell(entite.Id, disponibles[index])
            index = index + 1
        end
    end

    return place
end

Exemple complet en équipe

Dans un trajet d'équipe, onTurn(context) est appelé pour chaque personnage connecté et piloté par Jitsuri lorsque son tour commence. Le contexte et les objets fightCharacter et fightAction concernent alors ce personnage précis, pas systématiquement le meneur.

Les compagnons apparaissent dans getAllEntities() avec IsCompanion = true. Ils peuvent être pris en compte pendant le placement, mais ils ne constituent pas une session Jitsuri supplémentaire : n'attendez pas un callback de personnage connecté distinct pour eux.

Les identifiants de sorts de l'exemple suivant sont fictifs. Le meneur et les autres personnages utilisent chacun un sort différent. Si leur action particulière est impossible, ils délèguent le reste du tour à leur stratégie configurée dans l'interface.

local SORT_MENEUR = 123456
local SORT_MEMBRE = 123457

local function combattantCourant()
    local personnageId = character:id()
    for _, entite in ipairs(fightAction:getAllEntities()) do
        if entite.Id == personnageId then
            return entite
        end
    end
    return nil
end

local function cibleEnnemieLaPlusProche()
    local moi = combattantCourant()
    if moi == nil then
        return nil
    end

    local origine = fightCharacter:getCellId()
    local meilleure = nil
    local meilleureDistance = 999

    for _, entite in ipairs(fightAction:getAllEntities()) do
        if entite.TeamId ~= moi.TeamId
            and entite.IsAlive and entite.IsPositionKnown then
            local distance = fightAction:getDistance(origine, entite.CellId)
            if distance < meilleureDistance then
                meilleure = entite
                meilleureDistance = distance
            end
        end
    end

    return meilleure
end

local function essayerSort(sortId)
    local cible = cibleEnnemieLaPlusProche()
    if cible == nil then
        return false
    end

    local origine = fightCharacter:getCellId()
    if fightAction:canCastSpellOnCell(origine, sortId, cible.CellId) ~= 0 then
        return false
    end

    return fightAction:castSpellOnCell(sortId, cible.CellId)
end

function onPlacement(context)
    local moi = combattantCourant()
    local cellules = moi ~= nil and moi.Team
        and context.defenderCells
        or context.challengerCells
    local indexEquipe = character:getInTeamIndex()
    local cellulePersonnelle = cellules[indexEquipe] or cellules[1]

    if cellulePersonnelle == nil then
        return false
    end

    local place = fightAction:chooseCell(cellulePersonnelle)

    -- Le meneur réserve les premières cellules aux personnages connectés,
    -- puis place les compagnons sur les cellules encore disponibles.
    if global:isBoss() then
        local reservees = {}
        local membres = global:getTeamMembersIds()
        for index = 1, #membres do
            if cellules[index] ~= nil then
                reservees[cellules[index]] = true
            end
        end

        local indexCellule = #cellules
        for _, entite in ipairs(fightAction:getAllEntities()) do
            if moi ~= nil and entite.TeamId == moi.TeamId
                and entite.IsAlive and entite.IsCompanion then
                while indexCellule > 0 and reservees[cellules[indexCellule]] do
                    indexCellule = indexCellule - 1
                end

                local destination = cellules[indexCellule]
                if destination ~= nil then
                    if fightAction:chooseAllyCell(entite.Id, destination) then
                        reservees[destination] = true
                    end
                    indexCellule = indexCellule - 1
                end
            end
        end
    end

    return place
end

function onTurn(context)
    if not fightCharacter:isItMyTurn() then
        return
    end

    local sortId = global:isBoss() and SORT_MENEUR or SORT_MEMBRE

    if essayerSort(sortId) then
        fightAction:passTurn()
        return
    end

    -- Chaque membre délègue uniquement son propre tour.
    fightBasic:playTurn(2)
end

Dans cet exemple, global:isBoss() signifie « le personnage courant est le meneur du trajet ». Le nom de la méthode est historique : elle ne recherche pas un monstre de type boss. character:getInTeamIndex() commence à 1 et suit l'ordre de l'équipe pilotée.

Géométrie et chemins

Méthode Retour Description
fightAction:getDistance(fromCell, toCell) nombre Distance entre deux cellules.
fightAction:isHandToHand(fromCell, toCell) booléen true lorsque la distance est inférieure ou égale à une case.
fightAction:isWalkable(cellId) booléen Indique si le terrain de la cellule est marchable.
fightAction:getAdjacentCells(cellId) liste Cellules adjacentes sans diagonales.
fightAction:getShortestPath(fromCell, toCell, obstacles) liste Plus court chemin en ajoutant les cellules de obstacles aux combattants déjà présents. L'origine n'est pas incluse.
fightAction:cellsAligned(firstCell, secondCell) booléen Indique si deux cellules sont alignées.
fightAction:getCells_cross(centerCell, minimumRange, maximumRange) liste Cellules en croix dans l'intervalle demandé.
fightAction:getCells_square(centerCell, minimumRange, maximumRange) liste Cellules selon la distance de carte dans l'intervalle demandé.
fightAction:getTerrainLineOfSight(fromCell, toCell) nombre 1 si le terrain laisse la ligne de vue, 0 s'il la bloque, -1 si le calcul est impossible. Les combattants sont ignorés.

Les portées de getCells_cross et getCells_square sont limitées à 39. Une cellule marchable n'est pas forcément libre : vérifiez aussi les entités et les chemins.

Déplacements

Méthode Retour Description
fightAction:getReachableCells() liste Cellules atteignables selon l'état de combat courant.
fightAction:getRealReachableCells() liste Même instantané atteignable dans la version actuelle.
fightAction:moveTowardCell(cellId) booléen Tente de progresser vers la cellule avec les PM disponibles.
fightAction:moveOneCell(cellId) booléen Accepte uniquement un chemin contenant exactement un pas.
fightAction:getSafeMovementCells() liste Cellules retenues par la politique prudente de tacle.
fightAction:moveToCellSafely(cellId) booléen Revalide cette politique puis tente le déplacement.
fightAction:getTacticalCells(requiredAp, reserveMp) liste Positions tactiques candidates en conservant les PA et PM demandés.

Un résultat positif confirme la requête traitée, mais l'état peut changer immédiatement après. Après un déplacement, relisez fightCharacter:getCellId(), les PM et les positions avant l'action suivante.

La page Positions tactiques détaille le tacle, les abris et les simulations.

Sorts et simulations

Méthode Retour Description
fightAction:getSpellApCost(spellId) nombre Coût en PA du niveau connu ; 0 si les données sont absentes.
fightAction:getSpellZoneFromCell(spellId, fromCell, targetCell) liste Zone d'effet depuis une origine hypothétique.
fightAction:getSpellSelfRangePenalty(spellId) nombre Estimation prudente du malus de portée appliqué par le sort sur soi.
fightAction:getSpellCellsWithRangeOffset(spellId, fromCell, offset) liste Cellules lançables avec une variation de portée hypothétique comprise entre −100 et +100.
fightAction:canCastSpellInSimulation(fromCell, spellId, targetCell, occupiedCells) booléen Vérifie disponibilité, PA et géométrie avec une occupation hypothétique. N'envoie aucune action.
fightAction:canCastSpellOnCell(fromCell, spellId, cellId) nombre Renvoie 0 si le lancer est permis, 1 sinon.
fightAction:canCastSpellOnCellAfterMove(fromCell, spellId, cellId) nombre Même vérification depuis une origine hypothétique dans la version actuelle. Ne déplace pas le personnage.
fightAction:castSpellOnCell(spellId, cellId) booléen Vérifie les restrictions connues puis demande le lancer.

La simulation ne calcule pas les dégâts, les poussées, les morts, les effets différés ni les futures modifications d'état. Revalidez chaque action sur l'état observé après la précédente.

Préparer un lancer ciblé

Méthode Retour Description
fightAction:getBestCellToCastSpell(spellId, targetCellId) nombre Cellule depuis laquelle lancer maintenant ou après un déplacement ; -1 si aucun lancer direct n'est trouvé.
fightAction:getSpellCastPlan(spellId, targetCellId) table Plan détaillé de lancer, de rapprochement ou motif d'impossibilité.

Un plan de sort contient :

  • status : cast-now, cast-after-move, progress ou impossible ;
  • spellId, targetActorId, targetCellId ;
  • moveCell, castCell, futureCastCell ;
  • movementCost, remainingDistance, actionPointCost et score ;
  • reason, qui explique la décision ;
  • castsThisTurn, castsOnTarget, maxCastsPerTurn, maxCastsPerTarget, cooldownRemaining et stateCriterion.

Le statut progress indique qu'aucun lancer n'est possible ce tour, mais qu'une cellule permet de progresser vers une position future. Le plan ne se déplace pas et ne lance rien tout seul.

local plan = fightAction:getSpellCastPlan(123456, 250)

if plan.status == "cast-now" then
    fightAction:castSpellOnCell(plan.spellId, plan.castCell)
elseif plan.status == "cast-after-move" then
    if fightAction:moveToCellSafely(plan.moveCell) then
        local origine = fightCharacter:getCellId()
        if fightAction:canCastSpellOnCell(origine, plan.spellId, plan.castCell) == 0 then
            fightAction:castSpellOnCell(plan.spellId, plan.castCell)
        end
    end
elseif plan.status == "progress" then
    fightAction:moveToCellSafely(plan.moveCell)
else
    global:printWarning("Aucun plan : " .. tostring(plan.reason))
end

Plans tactiques natifs

Les plans tactiques recherchent une séquence de déplacements et de sorts à partir de l'état courant.

Méthode Description
fightAction:planBestAreaCast(spellId, options) Cherche le meilleur lancer du sort de zone, éventuellement après une avance et avec retour.
fightAction:planAttackAndReturn(spellIds, options) Cherche une séquence parmi plusieurs sorts, avec retour obligatoire vers une cellule sûre.
fightAction:executeCombatPlan(plan) Revalide puis exécute un plan créé par l'une des deux méthodes.

Options disponibles :

Option Description
safeCells Jusqu'à 32 cellules de retour acceptées. Sans cellule fournie, un plan avec retour utilise la position courante.
maxAdvance Avance maximale autorisée, entre 0 et 20.
mustReturn Pour planBestAreaCast, impose un retour après le lancer. Valeur par défaut : false.
avoidAllies Évite les zones touchant des alliés. Valeur par défaut : true.

planAttackAndReturn accepte jusqu'à 16 identifiants de sorts et impose toujours le retour.

Le plan renvoyé contient notamment planId, status, round, startCell, moveCell, returnCell, les coûts de déplacement et de PA, score, reason, safeCells, ainsi qu'une liste actions. Chaque action expose spellId, castCell, actionPointCost, enemiesHit, alliesHit et score.

local SORT_ZONE = 123456
local plan = fightAction:planBestAreaCast(SORT_ZONE, {
    safeCells = { 500, 501 },
    maxAdvance = 3,
    mustReturn = true,
    avoidAllies = true
})

if plan.status ~= "impossible" and plan.actionCount > 0 then
    local resultat = fightAction:executeCombatPlan(plan)
    global:printMessage(
        "Actions réussies : " .. tostring(resultat.successfulActions) ..
        ", retour : " .. tostring(resultat.returned)
    )
end
local plan = fightAction:planAttackAndReturn(
    { 123456, 123457, 123458 },
    {
        safeCells = { fightCharacter:getCellId() },
        maxAdvance = 4,
        avoidAllies = true
    }
)

if plan.status ~= "impossible" then
    local resultat = fightAction:executeCombatPlan(plan)
    if not resultat.success then
        global:printWarning("Plan interrompu : " .. tostring(resultat.reason))
    end
end

Un résultat d'exécution contient success, planId, attemptedActions, successfulActions, returned, finalCell, reason et les actions réellement exécutées.

Déléguer ou terminer le tour

Méthode Retour Description
fightBasic:playTurn(mode) booléen Délègue le reste du tour à l'IA configurée dans Jitsuri. Le paramètre est conservé pour la compatibilité ; la stratégie configurée reste celle utilisée.
fightAction:passTurn() booléen Demande explicitement la fin du tour.

Exemple de stratégie hybride :

function onTurn(context)
    local tourSpecial = context.round == 1

    if tourSpecial then
        -- Actions particulières du premier tour.
        fightAction:passTurn()
        return
    end

    -- Les autres tours utilisent les sorts configurés dans l'interface.
    fightBasic:playTurn(2)
end

Après fightBasic:playTurn, ne lancez pas d'autres actions Lua pendant ce tour. La délégation empêche le passage automatique supplémentaire.

Aides disponibles dans les callbacks

Méthode Description
global:delay(milliseconds) Attend le délai demandé. Évitez les attentes longues pendant un tour.
global:random(minimum, maximum) Entier aléatoire dans l'intervalle.
global:printMessage(message) Journal d'information.
global:printSuccess(message) Journal de réussite.
global:printWarning(message) Journal d'avertissement.
global:printError(message) Journal d'erreur.
global:printColor(color, message) Journal d'information ; la couleur n'est pas garantie par toutes les interfaces.
global:AddInGlobalMemory(key, value) Mémorise une valeur partagée pendant l'exécution du script.
global:GetInGlobalMemory(key) Relit cette valeur, ou nil.
global:getTeamMembersIds() Identifiants des personnages pilotés par le trajet.
global:isBoss() Indique si le personnage courant est le meneur du trajet. Le nom est historique.
global:getCurrentScriptDirectory() Dossier du script courant.
global:stopScript() Arrête le trajet.
character:id() Identifiant du personnage concerné par le callback.
character:getInTeamIndex() Position du personnage dans l'équipe pilotée, à partir de 1.

La mémoire avancée est vidée lorsque le moteur de combat du script est désactivé. Pour un suivi persistant entre deux lancements, utilisez un stockage prévu à cet effet plutôt que cette mémoire temporaire.

Règles de fiabilité

  • Filtrez toujours IsAlive, IsPositionKnown et le camp avant de choisir une cible.
  • Relisez les entités après un déplacement, un sort, une invocation, une mort ou l'arrivée d'une vague.
  • Ne considérez pas nil comme false pour une information encore inconnue.
  • Vérifiez le résultat des actions, mais ne répétez pas aveuglément une demande dont la confirmation est incertaine.
  • Gardez une limite claire dans toutes les boucles de recherche.
  • Prévoyez une action de repli : délégation à l'IA configurée ou fin de tour explicite.
  • Testez d'abord la stratégie sur un combat sans enjeu et conservez les journaux reason des plans refusés.