Created
April 17, 2026 16:42
-
-
Save Suor/44c4e6d2670aa64773c4829450a387e7 to your computer and use it in GitHub Desktop.
Bro Skipping turns
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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