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 fonctionsteamMember...pour une boucle de combat simple ;onFightStart,onPlacement,onTurnetonFightEndpour le moteur événementiel avancé.
Cycle d'un combat
Le paramètre context contient :
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.
Quand onTurn se termine alors que le personnage peut encore jouer, Jitsuri passe automatiquement son tour. Une erreur Lua provoque également un passage de tour de sécurité. Effectuez donc toutes les actions du tour avant de quitter le callback.
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
Lire les combattants
fightAction:getAllEntities() renvoie une liste Lua. Chaque entrée peut contenir les champs suivants :
Les immunités de mêlée ou de distance sont des états distincts. IsInvulnerable ne les regroupe pas.
Pour les limites liées aux états et aux vagues, consultez Lire les états en combat.
Placement
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.
Le placement peut changer entre deux événements et une cellule peut être refusée. Vérifiez les booléens retournés, ne supposez pas que tous les alliés sont pilotables et prévoyez toujours une cellule de remplacement.
Géométrie et chemins
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
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
La convention de canCastSpellOnCell est numérique : 0 signifie autorisé. Elle est différente du booléen renvoyé par canCastSpellInSimulation.
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é
Un plan de sort contient :
status:cast-now,cast-after-move,progressouimpossible;spellId,targetActorId,targetCellId;moveCell,castCell,futureCastCell;movementCost,remainingDistance,actionPointCostetscore;reason, qui explique la décision ;castsThisTurn,castsOnTarget,maxCastsPerTurn,maxCastsPerTarget,cooldownRemainingetstateCriterion.
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.
Options disponibles :
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.
Un plan est temporaire, lié au combat, au tour et à l'état qui ont servi à le calculer. Exécutez-le immédiatement. Ne mémorisez pas son planId pour un tour ultérieur et ne tentez pas de reconstruire manuellement une table de plan.
Déléguer ou terminer le 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
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,IsPositionKnownet 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
nilcommefalsepour 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
reasondes plans refusés.