Skip to content

Instantly share code, notes, and snippets.

@Suor
Created April 17, 2026 16:42
Show Gist options
  • Select an option

  • Save Suor/44c4e6d2670aa64773c4829450a387e7 to your computer and use it in GitHub Desktop.

Select an option

Save Suor/44c4e6d2670aa64773c4829450a387e7 to your computer and use it in GitHub Desktop.
Bro Skipping turns
// Targeted debug logging for a single autopilot bro.
// Set TARGET to a substring of the bro's name. Enabled via include from the mod entry.
local TARGET = "Эрхард";
local mod = ::Hooks.getMod("mod_autopilot_new");
local function isTarget(_actor) {
return _actor != null && typeof _actor == "instance" && !_actor.isNull()
&& _actor.getName().find(TARGET) != null;
}
local function p(_msg) {::logInfo("dbg: " + _msg)}
local function dumpOpps(_agent) {
local opps = _agent.getKnownOpponents();
p(" known=" + opps.len() + " hasVisible=" + _agent.hasVisibleOpponent()
+ " hasKnown=" + _agent.hasKnownOpponent());
foreach (i, o in opps) {
local a = o.Actor;
local isNull = a == null || (typeof a == "instance" && a.isNull());
if (isNull) {p(" opp[" + i + "] NULL"); continue}
p(" opp[" + i + "] " + a.getName()
+ " f=" + a.getFaction()
+ " alive=" + a.isAlive()
+ " placed=" + a.isPlacedOnMap()
+ " attackable=" + a.isAttackable()
+ " tileVis=" + o.Tile.IsVisibleForEntity
+ " hiding=" + a.getTile().IsHidingEntity);
}
}
local function dumpState(_agent, _actor, _label) {
if (!isTarget(_actor)) return;
local strat = _agent.getStrategy();
p("== " + _label + " " + _actor.getName()
+ " agent=" + _agent.ClassName
+ " faction=" + _actor.getFaction()
+ " stratFaction=" + strat.m.Faction
+ " auto=" + ("_autopilot" in _actor.m)
+ " morale=" + _actor.getMoraleState()
+ " ap=" + _actor.getActionPoints()
+ " rooted=" + _actor.getCurrentProperties().IsRooted
+ " placed=" + _actor.isPlacedOnMap()
+ " ctrl=" + _actor.m.IsControlledByPlayer
+ " zoc=" + _actor.getTile().getZoneOfControlCountOtherThan(_actor.getAlliedFactions()));
local alliedFactions = _actor.getAlliedFactions();
local allied = "";
foreach (f in alliedFactions) allied += f + ",";
p(" alliedFactions=[" + allied + "]");
dumpOpps(_agent);
// Raw tactical entities — count per faction and how many are attackable from this bro
local instances = ::Tactical.Entities.getAllInstances();
foreach (f, list in instances) {
if (list.len() == 0) continue;
local alive = 0, attackable = 0, visTile = 0;
foreach (e in list) {
if (e.isAlive()) alive++;
if (e.isAttackable()) attackable++;
if (e.getTile().IsVisibleForEntity) visTile++;
}
p(" tact f=" + f + " total=" + list.len()
+ " alive=" + alive + " attackable=" + attackable + " tileVis=" + visTile);
}
}
mod.hook("scripts/ai/tactical/agent", function (q) {
q.onTurnStarted = @(__original) function () {
__original();
dumpState(this, this.m.Actor, "onTurnStarted");
}
q.onTurnResumed = @(__original) function () {
__original();
dumpState(this, this.m.Actor, "onTurnResumed");
}
q.pickBehavior = @(__original) function () {
local actor = this.m.Actor;
if (isTarget(actor)) {
p("-- pickBehavior " + actor.getName() + " ap=" + actor.getActionPoints());
dumpOpps(this);
}
return __original();
}
})
// Instrumented copy of ai_attack_bow.onEvaluate with per-step logging for our target
mod.hook("scripts/ai/tactical/behaviors/ai_attack_bow", function (q) {
q.onEvaluate = @(__original) function (_entity) {
if (!isTarget(_entity)) return __original(_entity);
p("bow: enter ap=" + _entity.getActionPoints() + " morale=" + _entity.getMoraleState());
this.m.TargetTile = null;
this.m.SelectedSkill = null;
local score = this.getProperties().BehaviorMult[this.m.ID];
p("bow: behaviorMult=" + score);
if (_entity.getActionPoints() < this.Const.Movement.AutoEndTurnBelowAP) {
p("bow: BAIL low AP");
return this.Const.AI.Behavior.Score.Zero;
}
if (_entity.getMoraleState() == this.Const.MoraleState.Fleeing) {
p("bow: BAIL fleeing");
return this.Const.AI.Behavior.Score.Zero;
}
if (!this.getAgent().hasVisibleOpponent()) {
p("bow: BAIL !hasVisibleOpponent (known="
+ this.getAgent().getKnownOpponents().len() + ")");
return this.Const.AI.Behavior.Score.Zero;
}
local skills = [];
foreach (skillID in this.m.PossibleSkills) {
local s = _entity.getSkills().getSkillByID(skillID);
if (s != null) {
p("bow: skill " + skillID + " usable=" + s.isUsable()
+ " affordable=" + s.isAffordable());
if (s.isUsable() && s.isAffordable()) skills.push(s);
}
}
if (skills.len() == 0) {
p("bow: BAIL no usable skills");
return this.Const.AI.Behavior.Score.Zero;
}
local targets = this.getAgent().getKnownOpponents();
p("bow: targets=" + targets.len());
score *= this.selectBestTargetAndSkill(_entity, targets, skills);
p("bow: afterSelect score=" + score
+ " TargetTile=" + (this.m.TargetTile == null ? "null" : "set")
+ " SelectedSkill=" + (this.m.SelectedSkill == null ? "null" : this.m.SelectedSkill.getID()));
if (this.m.TargetTile == null || this.m.SelectedSkill == null) {
p("bow: BAIL selectBestTargetAndSkill gave nothing");
return this.Const.AI.Behavior.Score.Zero;
}
local fm = this.getFatigueScoreMult(this.m.SelectedSkill);
score *= fm;
p("bow: fatigueMult=" + fm + " score=" + score);
if (this.getAgent().getIntentions().IsChangingWeapons) {
score *= this.Const.AI.Behavior.AttackAfterSwitchWeaponMult;
p("bow: changingWeapons applied score=" + score);
}
local final = this.Const.AI.Behavior.Score.Attack * score;
p("bow: RESULT " + final);
return final;
}
})
// ai_engage_ranged is a generator — log key per-step checks via helpers wrapping
mod.hook("scripts/ai/tactical/behaviors/ai_engage_ranged", function (q) {
q.onEvaluate = @(__original) function (_entity) {
if (!isTarget(_entity)) return __original(_entity);
p("engR: enter ap=" + _entity.getActionPoints()
+ " rooted=" + _entity.getCurrentProperties().IsRooted
+ " morale=" + _entity.getMoraleState()
+ " zoc=" + _entity.getTile().getZoneOfControlCountOtherThan(_entity.getAlliedFactions())
+ " hasKnown=" + this.getAgent().hasKnownOpponent());
local gen = __original(_entity);
local ret;
while (true) {
ret = resume gen;
if (ret != null) {
p("engR: RESULT " + ret
+ " target=" + (this.m.TargetTile == null ? "null" : "set")
+ " waiting=" + this.m.IsWaiting
+ " forNextTurn=" + this.m.IsTargetForNextTurn
+ " valid=" + this.m.ValidTargets.len());
return ret;
}
yield null;
}
}
})
// ai_attack_default — our custom version
mod.hook("scripts/ai/tactical/behaviors/ai_attack_default", function (q) {
q.onEvaluate = @(__original) function (_entity) {
if (!isTarget(_entity) || this.ClassName != "ai_attack_default") return __original(_entity);
p("aDef: enter ap=" + _entity.getActionPoints()
+ " morale=" + _entity.getMoraleState()
+ " hasVisible=" + this.getAgent().hasVisibleOpponent());
local ret = __original(_entity);
p("aDef: RESULT " + ret
+ " target=" + (this.m.TargetTile == null ? "null" : "set")
+ " skill=" + (this.m.Skill == null ? "null" : this.m.Skill.getID()));
return ret;
}
})
mod.hook("scripts/ai/tactical/strategy", function (q) {
q.compileKnownOpponents = @(__original) function () {
local actor = null;
// Strategy is per-agent; find the owning agent via Tactical for logging only when target
// We cannot easily access actor here, so snapshot before/after len for our target agent.
local before = this.m.KnownOpponents.len();
__original();
local after = this.m.KnownOpponents.len();
// Find target bro and check if this strategy belongs to him
foreach (e in ::Tactical.Entities.getAllInstances()[::Const.Faction.Player]) {
if (isTarget(e) && e.getAIAgent() != null && e.getAIAgent().getStrategy() == this) {
p("compileKnownOpponents " + e.getName()
+ " before=" + before + " after=" + after + " stratFaction=" + this.m.Faction);
break;
}
}
}
q.onOpponentSighted = @(__original) function (_entity) {
__original(_entity);
foreach (e in ::Tactical.Entities.getAllInstances()[::Const.Faction.Player]) {
if (isTarget(e) && e.getAIAgent() != null && e.getAIAgent().getStrategy() == this) {
local target = _entity;
if (target instanceof ::WeakTableRef) target = target.get();
p("onOpponentSighted " + e.getName() + " <- "
+ (target == null ? "null" : target.getName())
+ " (known=" + this.m.KnownOpponents.len() + ")");
break;
}
}
}
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment