Battle scripts: how hero mechanics work in the game's code

A reference for the compiled battle Lua in igame.pkg. It covers how the code is laid out, how to read it, the engine rules it reveals, and how each stargazer hero filter maps to a game rule. Game build 14368 (season 108), researched 2026-09-27.

Contents

  1. About this page
  2. What the game ships
  3. Reading the scripts
  4. Anatomy of a hero script
  5. Engine rules learned from the code
  6. Skill slots and unlock levels
  7. Stargazer filters as game rules
  8. Checking a hero
  9. What only the game can settle
  10. Why code, not skill text
  11. Size and cost
  12. Open questions and next steps

1  About this page

Some hero mechanics are not in the config tables. Which damage counts as DoT, which units count as summons, and which buffs a hero's payoff counts are all decided in the battle scripts. Earlier tools guessed these from skill text and were often wrong. The scripts ship with the game as compiled Lua 5.4, and reading them answers these questions from the game's own logic.

This page is the durable record: layout, reading method, rules and procedure. The hero-by-hero result of applying it to stargazer's filters is a separate output: python3 tools/stargazer_filter_audit.py writes the audit page out/tools/stargazer-filter-audit.html. It holds the audit's findings with evidence, verdict, slot and level, and confidence, and reads stargazer's current tags live. The reading tool is tools/battle_scripts.py. Their output under out/tools/ is not committed.

Terms 'dot' (quoted) is the damage kind the engine passes with damage; IsDot is the SkillBuff column; DoT is the gameplay idea. Summon means a unit whose config.is_summoned is true. Slots: ultimate (ult), skill2 (atk2), skill3 (atk3), ex (atk4), awakening (awake), mastery, and special modes (imba: Honor Duel and Dura's Trials).

2  What the game ships

igame.pkg holds 11,139 entries, all Lua. The battle code is the 3,632 chunks under Src/Battle/ (17.4 MB):

Path under Src/Battle/FilesWhat it is
Logic/Heroes/610One script per unit (heroes, summons, monsters, bosses), named by Unit.Script, usually the codename. Passives, hooks, event handlers. 18,654 functions.
Logic/SkillScripts/1,255Skill behaviours shared by name: season gear (gem_*), seasonal and boss-mode skills, AddSummon. Referenced from skill rows' SkillScript.
Logic/SkillDatas/519Per-unit skill data scripts.
Logic/Map/1,102Battle maps and stage logic.
BattleStandardPass/141Replay inputs (seed, formation, operations) with no recorded outcomes; they can't verify anything without running the engine.
other5BattleLuaRequireLRU, DefendCycleSpawn, the unit-label trigger, PuppetBloodConfig, BattleTerrain.

Src/EffectScore/ (418 files) only lists each unit's VFX prefabs. The engine the scripts call (BattleEngine, AddBuff, TakeDamage, …) is native code: an IL2CPP build whose metadata strings are obfuscated. What a script asks the engine to do is readable. What the engine does on its own is inferred from how scripts use it, and every such inference is marked below. The AFKJourneyData dump never had these files.

3  Reading the scripts

The chunks are stripped bytecode. Local variable names are gone, but every global, field and method name is a string constant, and each function keeps its source line span. afkj_extract.formats.luadis reads them (load, walk, dis) and lifts them. lift(chunk) tracks each register's content as an expression, so a function prints as statements with readable operands instead of register moves. Control flow stays as if … goto N; this is not a decompiler, and doesn't need to be.

$ python3 tools/battle_scripts.py show vulture OtherChangeHp
-- main/18@667  r1.OtherChangeHp  lines 667-690 params=8
    8  a0(a1, a2, a3, a4, a5, a6, a7)
    9  if a3 >= 0 goto 86
   11  if a7 ~= 'dot' goto 86
   ...
   60  r1.atk3buff_CurseAccumulate(a1.vluture_atk3_buffs[r11], -a3, a1, …)

4  Anatomy of a hero script

Each hero script (Logic/Heroes/<Unit.Script>.luae) builds one module table, ed.battlehero.<codename>:

5  Engine rules learned from the code

5.1 Damage and damage kinds

TakeDamage(amount, type, caster, skill, flag, kind) and LostHp(amount, source, …, kind) carry a kind as their last argument, and HP-change handlers receive it (OtherChangeHp(base, self, target, hpChange, source, …, kind)). The type is 'AP', 'AD', 'Holy' or a buff's DotDmgType. Kinds seen:

Shadewing's curse (vulture.OtherChangeHp) accumulates only when kind == 'dot'. So damage every second is DoT only if it carries that kind. Tasi's ultimate re-hits every second, and Voracia's roast re-casts every 0.5 s (TakeEffectAt from a custom update). Both are ordinary skill hits.

The handler tests the damage, not who dealt it. An enemy that carries her curse (10602, which her skill3 puts on every enemy unit that can be targeted) and loses HP with kind 'dot' adds that loss to the curse, whatever the source: another hero, a summon, or Shadewing herself. One source counts at 20%: unit 5109 (camppettwinboth, not a hero and disabled in the unit table). So every hero DoT that lands on an enemy feeds the curse (checked for all 27 attachments, 2026-10-11).

5.2 Buffs, stats and FixOneAttrib

5.3 Ticking buffs (IsDot)

A SkillBuff with IsDot ticks every DotInterval (first tick after DotInitialInterval). The tick amount is the DotAttrib stat of the caster or target (DotAttribOwner) × DotDmgAttribCoef:

So IsDot alone says nothing about damage: most hero IsDot buffs are heals, shields or timers. Most DoT buffs have coefficient 0 in the table, and the applying hook sets it from an SArg (SetAttribute('DotDmgAttribCoef', skill.info[r1.ultdot_coef])). A script can also turn a buff into one at run time (SetAttribute('IsDot', true), Bonnie).

Inferred engine rule. No script defines the tick, but facelessmagewater.ult_BuffUpdate reimplements it exactly as above. Two things support it:

The replica reads a column the table no longer has (DotHidePop), so it may be older than the engine.

5.4 Healing

HEAL is healing received and CURE healing done. Alna's WinterladyTakeHeal reimplements the heal:

A heal-over-time is an IsDot buff with a negative coefficient (§5.3).

OnHealDivision returns the healing that passes: the engine replaces the running amount with the hook's result (brulespirit.CalcHpHealPreview, a Lua copy of the calculation, moves it back into the amount at pc 125). Kordan's wall returns heal × 0.6, a 40% cut, though his text says 60%. The same buff's OnDamagedDivision returns damage × the same value (ult_wallcoef), so if that hook's result replaces the damage as the healing one does, outside damage is cut by 40% too. Not tested in game.

5.5 Control flags

A buff's ControlEffects names flags, and the game's own tables (SkillBuffUI, SkillKeywords) define them:

stun and frozen contain silence, disarm, unmanual and immobilize (CoverEffectNames). supersilence and uncastmanual also exist. A hero can also block enemy casts directly: Lily-May's CanOtherUnitCastSkill returns false for an enemy's ultimate.

5.6 Energy and casting the ultimate

5.7 Death, revival and kills

5.8 Summons and created units

Two cases this explains:

5.9 Teams, targets and "all enemies"

5.10 Areas, regions and "tiles"

6  Skill slots and unlock levels

7  Stargazer filters as game rules

Each of stargazer's hero filters maps to a rule in the code. Where a hero's code consumes the mechanic, that code is the definition, and it is exact. Elsewhere, the definition is what the tagged heroes' code does. A hero qualifies at the slot and level where its code meets the rule. Stargazer treats every hero as fully unlocked and leveled, so the level in a tag ({ex: 4}) records where the effect starts; it is not a condition for the filter (user, 2026-10-02).

FilterRule in codeSource
DoTDeals damage or HP loss of kind 'dot': scripted, or through an IsDot buff with a positive coefficient (§5.1, §5.3).Shadewing's curse
DoT (Shadewing Synergy)DoT that feeds her curse. Her handler tests the damage's kind and that the enemy carries the curse, never the source (§5.1), so every DoT attachment carries the modifier, her own included. User, 2026-10-11.Shadewing's curse
SummonCreates a unit that has is_summoned when it enters the battle (§5.8).Aurora, Florabelle
Temp BuffGives another ally a buff with timer ≤ 100 that raises a listed stat (§5.2). Shields, immunity, damage multipliers, permanent and self buffs don't count. The recipient must be able to be an ally hero: check the skill's targeting hooks, not only its target camp (Florabelle's skill2 only reaches her Bulbsprites). An area's own buff applied by the engine gets a short duration even when its table Time is 9999 (Tilaya's ex 3 zone stacks them in game; user, 2026-10-03). A buff reaching only fixed partners still counts, with a note on stargazer's skill page (Niru's awakening 2: Shemira and Daimon). Tags describe a fully built hero: an upgrade that makes the buff last all battle removes the tag, with a note on the skill page (Velara's Enhance Force; user, 2026-10-03).Dionel, Orion, Perseus
Temp Buff (Opening)The same, at battle start: from RunBattleStartLogic, PlayPrepareSkillEffect, InitRelationship; a prepare skill, or the hero's first cast (InitCD 0, top priority); a region that is on from the start; or a one-shot first Update. A buff riding on the battle-start cast counts even through another skill (Smokey & Meerky's aroma, Ravion's and Daimon's skill2), and so does a first cast that reaches an ally only when one stands near its target (Fay). A buff that waits on something else first doesn't (Pandora's 4 s seal, Kordan's ult reaching an enemy). User, 2026-10-03. Nor does one that comes from a battle-start cast but lands only after a unit takes its time to reach the field: Damian's ex halo is switched on by his second cast, Emergency Support, right after the first builds his chariot, but the chariot arrives too late for it to count (user, 2026-10-23).Temp Buff + timing
Temp Buff (Dionel Synergy), (Orion Synergy), (Perseus Synergy)A temp buff that hero can receive and count. The three hooks run the same test (§5.2), so every Temp Buff attachment carries all three, with two exceptions. Niru's awakening 2 carries none: it reaches only Shemira and Daimon. Perseus's own skill3 lacks perseus: his hook skips a buff he cast. A buff that reaches them only in some lineups still carries them (Rolan's awakening 1 goes to the team's largest faction when that is Lightbearer or Celestial, so to Orion and Perseus or to Dionel). Of the four season charms tagged Temp Buff, three carry them (gem_ep8heal, gem_ep8shield, gem_ep8frontbuff: 8 s buffs cast by the charm's holder) and gem_ep8buffboost doesn't (§5.2). User, 2026-10-11.Dionel, Orion, Perseus
Temp Buff (Global), (AOE), (Single Target)Who a temp buff reaches. Every Temp Buff attachment carries exactly one of global, aoe and single. Global: the skill takes the whole team: a loop over the hero's side with no distance test, a skill row whose area is the whole team with no target filter (§5.9), or a field over the whole battlefield (Kazim's wind field). An eligibility test after that leaves it global when it isn't about position (Rolan's awakening 1 goes through every hero and buffs those of the largest faction; Mikola's skill2 needs her side to hold the arena). AOE: everyone inside a shape of more than one tile, at the cast or while it lasts: a skill row's area, a region, a lookup of the units around a point. A whole-team loop whose test is where a unit stands is AOE too (Taichi & Agumon's awakening 1: every hero deployed behind him). Single target: units the skill picks, the buff staying with them: a target priority, a sort that keeps the first few, a stored partner. A range limit on the pick doesn't make it AOE. A buff tied to one tile is single target, since one tile holds one unit (Hugin's sigil, Gunnar's Doomfield). So is a buff that waits on each unit separately, even when the code goes through the whole team to find them (Pang's ex 3 at 850 energy, Hewynn's awakening 1 on a low-HP heal), as with Eryndor's ex 3 for debuffs. Of the charms, gem_ep8frontbuff is AOE, and the three that buff whoever their holder just healed, shielded or buffed (gem_ep8heal, gem_ep8shield, gem_ep8buffboost) carry no targeting modifier: they show under Temp Buff alone. Each attachment's row and its evidence are in research/tempbuff_targets.py. User, 2026-10-11.The tagged heroes, the user's rulings
Haste (Global), (AOE), (Single Target)Haste (HAST) a skill gives another hero on the hero's team, however long it lasts: a buff with HAST above zero in its table row or through SetAttribute, on a unit that isn't the hero or one of its own units. ATK SPD (ATKHAST) is a stat of its own and doesn't count. Ruled (user, 2026-10-23): Haste that lasts the rest of the battle counts (Dunlingr's ex 4, Elijah & Lailah's mastery, Ravion's ex 1); Haste for summons only doesn't, since the recipient must be able to be an ally hero, as for Temp Buff (Aurora's ultimate, Florabelle's ex 3); Haste behind a chance counts (Laios's dish, one random extra buff per ingredient). Haste behind a team state counts too, as a state does for Debuff (Rolan's skill2 at 60% Morale). Every haste attachment carries exactly one of global, aoe and single, by Temp Buff's targeting rule. The charm gem_ep8buffboost (Haste for the ally its holder gives a temporary stat buff) carries none, like its Temp Buff tag. Candidates come from tools/haste_targets.py sweep (§8.1); each attachment's row and evidence are on the audit page. In stargazer since 2026-10-23: 16 heroes, 18 attachments, and the charm ep8buffboost.The tagged heroes, the user's rulings
Haste (Opening)The same Haste at battle start, by Temp Buff (Opening)'s timing rule: Dunlingr's ex 4 (his prepare skill blesses the ally he protects), Kazim's skill3 (a field on from the start) and Zandrok's skill2 (the charge is his first cast). Haste that waits on something else isn't: Smokey & Meerky's aroma gives it only from aroma level 3, after two casts of the ultimate, and Damian's halo waits for his chariot to reach the field (Temp Buff (Opening); user, 2026-10-23).Haste + timing
Debuff (Global), (AOE), (Single Target)A debuff is anything a skill puts on an enemy that works against it in numbers: a stat lowered (any stat, max HP included), damage taken up (TakeDmgMulti), damage dealt down (MakeDmgDivision, a negative MakeDmgMulti) or HP loss taken up (TakeHpLossMulti). A damage rule attached to the enemy's buff (OnDamagedMulti, MakeDamageDivision) counts when it raises the damage the enemy takes from anyone or lowers the damage it deals, and not when it only raises the hero's own damage against it (Nerion's ultimate, Vala's awakening 1). Healing works the same way (user, 2026-10-14): a heal block (unheal, onlycureself) is control with no number, so it isn't a debuff and stays Heal Denial's; a Vitality or Healing cut (HEAL, CURE below zero) is a debuff that Eryndor counts; a healing rule on the enemy's buff (OnHealDivision, a smaller TakeHeal) that cuts the healing it receives from anyone is a debuff without eryndor, but not when the same buff also protects the holder (Kordan's circle cuts healing and damage from outside alike, for his allies too). Control, plain damage or HP loss, and markers without a number are not debuffs; a fear that slows (MSFix) is. Whether Eryndor counts it plays no part. User, 2026-10-06 and 2026-10-14. Every debuff attachment carries exactly one reach modifier, by Temp Buff's rule applied to enemies. Global: every enemy hero: an enemy-camp loop or a map-wide skill (§5.9); a per-enemy chance or state afterwards still counts (Eryndor's dice, Mehira's awakening), and so do shots aimed at every enemy in turn (Hodgkin's awakening 1, Shemira's ex 3). AOE: everyone in an area: a skill's area, a region, the units around a point, or a test of standing inside or outside an area (Mikola's arena); also a debuff that needs a second hit when, against a full team, only a neighbour's splash gives one (Hodgkin's awakening 2). Single target: the enemies a skill picks, or each enemy on its own trigger, even when the code goes through the whole team to find them (Eryndor's ex 3, Berial's isolated enemies, Valka's panic at 4 stacks), and a debuff that comes with the hero's hits (Baelran's and Natsu's max HP cuts). One slot can carry two reaches as two attachments (Pandora's ex 1, Evie's skill2, Daimon's ex 1, Velara's ultimate, Mehira's awakening 1, Viperian's ultimate, Shakir's awakening 1; user, 2026-10-14). Each attachment's row and evidence are on the audit page; candidates come from tools/debuff_targets.py sweep (§8). In stargazer since 2026-10-14: 79 heroes, 133 attachments, and the charms ep8dot (Single Target) and ep8defrush (AOE).The tagged heroes, the user's rulings
Debuff (Eryndor Synergy)A debuff Eryndor's ultimate counts as a stack, whatever its reach (Global, AOE or Single Target; user, 2026-10-13): its buff pushes one of his 31 stats below zero (§5.2). His list decides only this modifier: damage taken up, damage dealt down, max HP cuts, HP loss and damage rules are debuffs outside it, so they keep their reach modifier without this one (Kulu's ex, Mehira's awakening). Eryndor counts himself: the rule tests the caster's camp, not who it is (user, 2026-10-06). Each caster gives an enemy one stack, so his ex 3 never adds one: its enemy has already paid him ATK for a lost game (user, 2026-10-14).Eryndor's ultimate
Heal DenialHealing blocked or cut on enemies: a buff with unheal or onlycureself, or a hook that shrinks healing received (OnHealDivision, a smaller TakeHeal). Vitality (HEAL) and Healing (CURE) debuffs are stat reductions and don't count (user, 2026-10-02); they are Debuff's. A healing cut through a rule on the enemy's buff is both, unless the buff also protects the holder (Debuff row; user, 2026-10-14). Skill text calls the Vitality stat both "Vitality" and "healing received" (§5.4).Tagged heroes
Cheat DeathStops a killing blow or revives, for self or allies: a TryDieWhenDefeated override, an OnFinalDamaged lethal check that saves the unit, a SetHp floor, or Revive (§5.7). A save triggered at an HP threshold counts when its test is on the HP the hit would leave, because every killing hit then trips it (Mehira's ex; user, 2026-10-13).Tagged heroes
Cheat Death (Self), Cheat Death (Ally)Who is saved (user, 2026-10-03). Self: the hero's own death hook or SetHp floor, or a lethal-hit buff that sits on the hero. Ally: the buff is put on another hero on its side, or an ally is revived. Who pays doesn't matter: Thoran's ex costs his pact ally HP and saves Thoran, so it is Self. One effect that saves both carries both modifiers (Aliceth, Alna, Ravion); two skills are two attachments (Talene: skill3 Self, ex 4 Ally). A skill aimed at an ally is Ally alone, even when its targeting priority can land on the caster (Niru's ult: weakest unit on his side, no notself; in game he targets himself when no other ally is left).Cheat Death + the unit saved
InvincibilityMakes another unit invulnerable, outside a death save (user, 2026-10-02): an invulnerable buff on an ally from a skill that doesn't target enemies. Self-only invincibility counts for neither filter.Tagged heroes
ExecuteKills an enemy outright: Defeated (a normal defeat that death prevention can still stop) or Exterminated (removed: no revival, no immunity), or LostHp of its whole HP (§5.7). Killing the hero's own units doesn't count.Tagged heroes
Energy BatteryPositive energy to another unit on the hero's side, not the hero or its own units; a fixed partner counts (user, 2026-10-03; §5.6).Tagged heroes
Energy DenialDrains or blocks enemy energy: negative 'mp', lowering SetMp, noAddMp, AllMpRatio < 0 (§5.5, §5.6).Tagged heroes
Ult (Opening)Can cast the ultimate at battle start: starting energy reaches what the cast needs (the effective CostMP, or the script's own threshold in Ult_CanCastWithCaster) and no other cast condition blocks it, or a script casts it free at battle start (§5.6, inferred rule).Tagged heroes
Ult Denialsilence / unmanual-type flags on enemies outside a stun bundle, or a hook blocking enemy ultimates (§5.5).Tagged heroes

Derived, not hand-kept: DoT, Summon, Heal Denial, Cheat Death, Invincibility, Execute, Energy Denial, Ult Denial and Energy Battery. tools/filter_derive.py applies these rules to every hero (tests: tests/test_filter_derive.py). Where an effect sits:

Special Target and Tile Effect are chosen by hand for guides and skill simulation, not by game rules, so they are out of scope. §5.9 and §5.10 record the code facts behind them. Honor Duel / Dura's Trials-only effects are not tagged.

8  Checking a hero

For a new release, or to re-check an existing hero:

  1. Refresh the data. With the game closed, run the routine (./afkj-extract --locales-from cdn, ./afkj-parse build) so out/raw/tables and out/api match the install. The hero's codename is out/internal/keys.json → heroes[slug].codename, the same as Unit.Script.
  2. Map the skills. Run python3 tools/battle_scripts.py show <codename> Init_hero for the skill bindings, then show <codename> for the settings (buff ids, SArg names).
  3. Run the audit. make audit runs tools/filter_derive.py (the nine derived filters for every hero, compared with stargazer; exits 1 on a difference or a fact whose slot it can't place) and then rebuilds the audit page. Each derived tag lists the code fact behind it. A decision the code can't make goes in its REVIEWED table with the reason (today Yamato & Gabumon's cheer-partner hook and Nerion's awakening energy).
  4. Work the "Needs review" panel at the top of the audit page. It lists:
    • heroes new on stargazer since the last full review: the derived filters cover them, the hand-reviewed filters (Debuff (Global), Debuff (Eryndor Synergy), Haste with its four modifiers, Temp Buff with its targeting modifiers, Temp Buff (Opening), Ult (Opening), and Cheat Death's Self and Ally modifiers) need the checks below;
    • heroes in the game data that aren't on stargazer yet (unreleased: handled when they release). A released hero's level can be unreleased too: the game hides an Awakening Lv2 with IsShow false until a later update (Harak, Ulmus, Sinbad and Mikola on build 14368; raw-data §9.2), and the feed and stargazer's skill data leave it out. A pin on a level stargazer's skill data doesn't have is never tagged (stargazer_tags.released): the audit shows its rows as Waits for release, and the code check lists it as waiting instead of a difference. Both lift on their own when the update brings the level in (user, 2026-10-14);
    • a warning when the game data build changed since the review;
    • rows made from the code check's differences, not reviewed yet. They also appear in each derived filter's table. Keep each one as a reviewed row (with its evidence) or fix the rule in filter_derive.py.
    After a full review, run python3 tools/stargazer_filter_audit.py --mark-reviewed: it records the date, data build and stargazer's heroes in tools/filter_audit_baseline.json.
  5. Run the per-filter checks. battle_scripts.py dot --evidence and summons write every hero's DoT and summon evidence. For the other filters, show <codename> <word> with the rule's calls or fields:
    • Temp Buff: AddBuff, SetAttribute, SetTimer.
    • Debuff (Global): ForEachAliveUnit, ForEachAliveHeroUnit, and TakeDmgMulti, MakeDmgDivision, MakeDmgMulti, MaxHpChange, TakeHpLossMulti and damage rules on a buff, which are debuffs whatever Eryndor's list says.
    • Debuff (Global), (Single Target) and (AOE): python3 tools/debuff_targets.py sweep <slug> lists every buff the hero's code (and its created units') puts on another unit that turns a number against it, with its slot and level, the team loops and area lookups in the function, and the skill rows' targeting fields. Read the function and whatever picks the units, then sort it by the Temp Buff targeting rule (§7) applied to enemies. Compare the skill text too: the sweep reads one function at a time. Then nets and the audit page's sweep coverage line (§8.1).
    • Debuff (Eryndor Synergy): for each debuff attachment, whatever its reach, the buff's table row and its SetAttribute calls, against the stat list in §5.2.
    • Temp Buff and DoT synergy modifiers: who can receive the buff (a fixed partner, as in Niru's awakening 2, takes them off) and who casts it (§7). python3 research/synergy_check.py tags compares stargazer's hero files and charm tags with those rules and exits 1 on a difference. Its sweep lists every timed buff with a listed stat on another unit, for a hand review of what is untagged, and lists compares the hooks' stat lists.
    • Temp Buff targeting (Global, AOE, Single Target): python3 research/tempbuff_targets.py rows <slug> prints each attachment's skill text and its skill rows' targeting fields. Then read the function that applies the buff and whatever picks its recipients, and for an area the skill's IsSpecialIgnoreTarget, CollectAoeTarget and SelectTarget hooks (§5.9). Sort it by §7's rule and add its row to the script. check exits 1 while an attachment has no row or stargazer's modifier differs, and page writes every row with its evidence to out/tools/temp-buff-targeting-review.html.
    • Haste (Global, AOE, Single Target, Opening): python3 tools/haste_targets.py sweep <slug> lists every buff the hero's code (and its created units') puts on another unit that raises Haste, with its slot and level, the team loops and area lookups in the function, and the skill rows' targeting fields. Read the function and whatever picks the recipients, then sort it by Temp Buff's targeting and timing rules (§7) and add its rows to the audit page. Then nets and the page's Haste sweep coverage line (§8.1).
    • Heal Denial: unheal, onlycureself, OnHealDivision, TakeHeal.
    • Cheat Death: TryDieWhenDefeated, OnFinalDamaged, SetHp, Revive. For Self or Ally, read which unit the hook or its buff is put on (a1:AddBuff(buff, a1) is the hero). Most final-damage hooks only share, cut or count damage, so read for the lethal check. A save may have no fatal-blow wording in its text: Mehira's reads "a hit that would reduce her HP below 20%", so for an HP threshold read what the test compares. The table flag immoratal (cannot die) is used by modes only; a hero buff carrying it would be a save the code check doesn't read.
    • Execute: Defeated, Exterminated.
    • Energy: mp, SetMp, AllMpRatio, noAddMp.
    • Starting energy and Ult (Opening): StartMP, CostMP.
    • Ult Denial: silence, unmanual, CanOtherUnitCastSkill.
  6. Resolve the values. Look each buff up in out/raw/tables/SkillBuff.json (stats, Time, ControlEffects, IsDot fields). Read each coefficient's sign from the SArg on the hero's skill rows (out/raw/tables/Skill/).
  7. Follow created units. Take the unit id from AddSummonUnit or SummonUnitList, its Unit.Script, and repeat steps 2–4 on that script.
  8. Find slot and level. Use the bindings (§6), CheckLevel / CheckAwakenLevel, or the first non-zero SArg level. A level check isn't enough on its own: Dionel's EX checks level 2, but its HEAL value is 0 until level 3.
  9. Check the whole condition. A matching call is not a verdict. Before recording, confirm:
    • who receives the effect: targeting hooks, and which unit the buff lands on (Pandora's EX energy fills her box, not an ally);
    • the cast guards and alternate states that gate it;
    • that the coefficient is non-zero at the claimed level;
    • the scripts of any units the skill creates.
  10. Record and apply. Fact-check every result in the code before recording it, including rows the code check made and any review notes: a call that looks like a match is a claim, not a fact. Add rows to tools/stargazer_filter_audit.py with the evidence, regenerate the audit page, then edit stargazer's character file. The page's By hero view gives each tag's value to set, in stargazer's format; a row in a filter with a modifier edits mods on the slot's attachment. A Check row states engine behavior the code doesn't show and ends with Test: …. A Decide row states a question about the filter's definition and ends with Decide: …. The generator refuses rows missing these. Rows stargazer already carries, compared slot by slot, show as done. Stargazer is public: never name the private repos or the extraction there.

8.1 Adding a filter or changing its rule

A new tag, a new modifier or a changed rule means reading every hero again under it. The Debuff reach review (2026-10-13) missed five effects, each for one of the reasons below (2026-10-14), so follow these steps:

  1. Write the rule from the code and the owner's rulings alone, in §7 and in the section's definition. Keep it apart from other filters' modifiers: what makes a debuff doesn't depend on Eryndor's stat list, which only decides eryndor.
  2. Build the candidate list by script, never by hand. Run the sweep (tools/debuff_targets.py sweep for Debuff, tools/haste_targets.py sweep for Haste). Every hero and slot it lists must end with a row, a stargazer tag or a reviewed reason. The audit page's Needs review panel lists any that don't (sweep_gaps, reasons in SWEEP_REVIEWED and HASTE_SWEEP_REVIEWED). Valen was missed because a hand-made reviewer list skipped him. When the reading is split between agents, generate the groups from every stargazer hero and check that each output covers every hero in its group.
  3. Re-read every slot, tagged ones included. A slot can carry several effects that reach differently, and each is its own attachment: Pandora's ex 1, Evie's skill2, Daimon's ex 1, Velara's ultimate, Mehira's awakening 1, Viperian's ultimate. Velara and Mehira were missed because their slots were already tagged Global. A modifier row (eryndor) on such a slot names its reach in the label, Hero {ex: 1} (aoe); the page generator stops on an unnamed one.
  4. Check the sweep's blind spots by hand.
    • The slot is decided by the gate, not by the buff's name. The sweep flags an AWAKENING GATE up the call chain; Viperian's awakening 2 viper carries a buff named for his ultimate.
    • A buff written into skill data at run time (x.BuffID = …, then WrapTable) is applied by the engine on hit, with no AddBuff. The sweep lists it as skill data (BuffID set) since 2026-10-14; Berial's ultimate fear was missed before.
    • A value written as current - v is a cut (Senea's and Voracia's max HP cuts).
    • A register can hold several buffs in one function. Since 2026-10-23 the sweep gives each setting to the buff the register holds at that point, and lists buffs that are never stored one by one. Before, the last buff took everything: Mehira's skill2 Haste was hidden behind a special-mode buff, and on the Debuff side three sites whose debuffs are tagged on another slot (Eironn's ultimate, Kafra's skill2 and ultimate).
    • One skill adding another skill's buff (Gwyneth's ex splash gives her skill3 burn through skill_atk3:AddBuff), and a skill's area rewritten at run time (Shakir's Wolf Form arc, Velara's circles). The sweep prints both as note: lines under the hero; read each.
    • Created units, and damage rules on a buff.
  5. Run the checks that don't depend on the sweep. tools/debuff_targets.py nets searches the skill text and the buff table for debuffs no row covers. It exits 1 on a hit not yet read (reasons in TEXT_REVIEWED, BUFF_REVIEWED). tools/haste_targets.py nets searches the skill text for Haste raised for someone else on a slot no Haste row covers (reasons in its TEXT_REVIEWED). Community lists are leads to compare, not sources.
  6. Hold unreleased levels. A level stargazer's skill data doesn't show waits for release (stargazer_tags.released, §8 step 4).
  7. Put the open questions to the owner plainly. A Decide row says what the skill does, the question after Decide:, what each answer changes, and a recommendation. Record each ruling with its date in the row and in §7.

9  What only the game can settle

These rules are inferred, not read from the engine. Each has one short in-game test:

10  Why code, not skill text

The retired viewer (afkj-data-viewer, scripts/build-data.ts) tagged mechanics with keyword and regex searches over skill text, plus IsDot copied from buffs and SummonUnitList. Text is a poor guide:

Effects gated behind EX 4 or awakening (Aliceth, Contess, Rolan) are easy to miss in tests. The code shows them with their level.

11  Size and cost

PartSizeReading
All battle code3,632 chunks, 29,779 functionsall lift; about 35 s
Hero scripts610 (heroes, summons, monsters, bosses)18,654 functions
The 129 heroes in the feed7,118 functions, 27,399 call sitesabout 8 s per report

Most filters reduce to a few call shapes, so a mechanical reader covers them. Per-hero care is still needed for:

12  Open questions and next steps