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.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.
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).
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/ | Files | What it is |
|---|---|---|
Logic/Heroes/ | 610 | One script per unit (heroes, summons, monsters, bosses),
named by Unit.Script, usually the codename. Passives, hooks, event handlers. 18,654 functions. |
Logic/SkillScripts/ | 1,255 | Skill behaviours shared by name: season gear
(gem_*), seasonal and boss-mode skills, AddSummon. Referenced from skill rows'
SkillScript. |
Logic/SkillDatas/ | 519 | Per-unit skill data scripts. |
Logic/Map/ | 1,102 | Battle maps and stage logic. |
BattleStandardPass/ | 141 | Replay inputs (seed, formation, operations) with no recorded outcomes; they can't verify anything without running the engine. |
| other | 5 | BattleLuaRequireLRU, 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.
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, …)
a0…, and call results are named after their register (r6 = a2:AddBuff(…)).
A value that differs by path shows its alternatives, sorted: (3 || a1.info.SArg1) is 3 on one path
and info.SArg1 on another (often compiled from info.SArg1 or 3); || here is
not Lua's or. The module table is r1.r9 = r6, r6 = 0. Before 2026-10-03 these were
hidden, so a value replaced on one path looked unchanged: Lyca's target list (both branches return every target)
was misread as trimmed, and lethal-hit saves that zero the damage before return, and heal hooks
whose result replaces the heal, were easy to miss. Copies that only load a call's function and arguments, or a
loop's bounds, stay hidden. When a result depends on a list built in a loop, a count, or which value a register
holds at a return, still confirm it in the bytecode (luadis.dis).battle_scripts.py calls AddSummonUnit lists all 227 call sites with their arguments.
show <codename> <word> prints every function whose name contains the word or whose
constants include it.<list>. To read one, such as the stat lists below, walk the bytecode
back from the SETFIELD to its NEWTABLE and collect the constants.FORLOOP and TFORLOOP jump back to
pc + 1 − Bx: the VM has already moved past the instruction (lvm.c). Until 2026-09-29 the
reader used pc − Bx, which sent every for … in loop back to its set-up and skipped the body.
That hid 406 calls in 54 heroes, mostly the "for each enemy hero, add a buff" shape, and changed eight audit
verdicts. tests/test_formats.py compiles loops with lupa to keep this fixed.r2), so every function settles. Each lifted function carries complete. If the safety
budget ever ran out, the function would still be listed in full, with plain operands, and render
would mark it -- incomplete. Today all 29,779 are complete.Each hero script (Logic/Heroes/<Unit.Script>.luae) builds one module table,
ed.battlehero.<codename>:
r1.ult_sleepBuff = 5602) and SArg names
(r1.ult_sleeptime = 'SArg0'). Skill numbers are read at run time as
skill.info[r1.ult_sleeptime], the skill row's SArg0 at the hero's level, so a number in
the code traces to a table value. The parse model already reads the integer settings (HeroScripts,
Raw data §10.5).<Slot>_<Event> (Ult_TakeEffectOn,
Atk3_AddBuff, Atk2_OnAttackFrame, Atk3_CustomUpdate). Init_hero
installs each on a named skill: ed.override(a0.skills[name .. '_ult'].OnAttackFrame, r1.ult_OnAttackFrame).
ed.override(original, new) passes the original as the hook's first parameter, which the hook usually
calls first. The same pattern installs hooks on buffs (buff.IntervalUpdate, OnRemoved,
OnFinalDamaged) and on units (unit.TryDieWhenDefeated, unit.Die).BuffID is applied by the engine when the skill hits.
The hero's <Slot>_AddBuff hook wraps that and usually sets the buff's runtime numbers
(SetAttribute).OtherChangeHp (any unit's HP changed; 29 heroes),
OtherUnitDie (36), BattleEngineAddUnit (a unit entered the battle; 12),
OtherAddBuff, OtherUnitRevive, SuccessMakeDamage,
FixOneAttrib, RunBattleStartLogic, Reset, Update.target:AddBuff(id, caster, time),
buff:SetAttribute(stat, v), buff:SetTimer(t), target:TakeDamage(…),
unit:TakeHeal(v, 'hp'|'mp', src), ed.BattleEngine:AddSummonUnit(…),
ed.BattleEngine:ForEachAliveUnit(camp). Files using them: AddBuff 1,632,
SetAttribute 1,129, ForEachAliveHeroUnit 672, TakeDamage 257,
AddSummonUnit 154.Unit.Script. For example,
Pandora's box is unit 5023 with script pandorabox, and Phraesto's Illusion runs scorpion,
the same script as Phraesto. Anything those units do is in their script, not the hero's.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:
'dot': 33 call sites.'share_damage', 'drain' and 'skill': a handful each.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).
FixOneAttribAddBuff(id, caster, duration) returns a buff whose
info is its SkillBuff row, with per-instance overrides via
info:WrapTable. buff.timer is the time left, and buff.attrib lists the stats
it sets. SetAttribute(stat, v) sets a stat at run time: stat is additive,
stat_x multiplicative.FixOneAttrib. Whenever a buff sets one of a unit's stats, the engine calls the
unit's FixOneAttrib(self, stat, old, new, mode, buff) (mode is 'adds' or
'coefs'; roles inferred from use). Hooks on it are how payoffs count buffs:
godspear), Orion (prince) and Perseus's ex
(perseus) run the same check. They count a buff from another unit on the same team, with
buff.timer ≤ 100 when applied, a positive new value, and a stat from the list below. Each ally
caster counts once. The hook is on the hero's own unit, so the buff has to land on that hero. What
differs is how long a caster stays counted: Dionel and Orion keep it for the whole battle, while Perseus
counts the casters whose buff is still on him (his Update drops a caster when that buff is
removed). In normal modes nothing else adds to the three counts (Orion's AddFriend is called
from special-mode code only).gem_ep8buffboost (in
SkillScripts) hooks FixOneAttrib on every ally of its holder, with the same 31
stats and a positive value. Its caster test is the holder, and its timer test is
0 < timer < 9000, wider than the heroes' 100. The Haste it then adds (4022200) is
added with a 99999 timer and ended with the source buff, so Dionel, Orion and Perseus never count it.hermit.OtherAddBuff,
BadLuckTarget_FixOneAttrib, CanAddBadLuckStack) counts a buff cast by his team that
pushes a listed stat below zero on an enemy that is a common unit (IsCommonUnit) and not a
summon. It can do that in the table (info[stat] or info[stat..'_x'] < 0 when
added) or later through FixOneAttrib. Each caster gives an enemy one stack
(hermit_badLuckCountBySourceTarget[caster:target] against the ult's SArg9 = 1), up to SArg0 = 3
per enemy, so a full count needs three different casters on that enemy. The stack is a 9999 s buff, so the
debuff's own length doesn't matter. The only test on the caster is its camp: nothing excludes Eryndor, so
his own stat cuts (the awakening's entry fee, the ATK a lost game pays) read as one caster too, and a summon
is a caster of its own. Not tested in game.MakeDmgMulti, TakeDmgDivision, TakeDmgMulti,
ExRange and shields.MSFix < 0 does.FixOneAttrib. A stat_x change arrives under the base stat
name with mode 'coefs': Eryndor's hook accepts 'adds' or 'coefs' for base
names, and Lamentis's mirror replays info[stat..'_x'] as (stat, 0, value, 'coefs'). So
an ARM_x buff counts as ARM. MaxHpChange and MaxHpChangeValue arrive under their own
names (bwmitem.FixOneAttrib tests them) and are not in the list (Rolan's ultimate doesn't count).AddSource (owner or caster on a SkillBuff
row) says whose stat an _x or _a value scales from. It doesn't change the buff's caster
(hermit.Atk4_CollectPositiveBuffAttribs, perseus, phantom).atk4_iconIgnoredAttribs;
ult_badLuckAttribs is the list).Time is 9999: Tilaya's ex 3 zone (10504, Time 9999, its hook
passes the engine's duration through) stacks Dionel, Orion and Perseus in game (user, 2026-10-03). Region hooks
that want a set length pass one (Kordan's ultd_AddBuff 0.5 s, Galahad's Ultb_AddBuff
1.25 s). A script's own AddBuff(id, caster, 9999) does last 9999.CheckAwakenLevel(1) and 9999 or SArg3 (shown as (9999 || …)): 10 s, or all battle once
awakened.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:
'dot', of type DotDmgType; HP loss of kind
'dot' if the type is LostHp. Scaled by the target's DOTRATIO.DotAttrib: only a timer. Tasi's sleep buff is one: each tick re-hits with the
ultimate.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:
LostHp IsDot buff.buff:IntervalUpdate(amount): a positive amount is a
'dot' hit, a negative one heals. Nara's skill2 does this once per tile it pulls an enemy, so it counts as DoT
(a player's test agrees); wheel_common_lowhprecover heals this way. These are the only two
callers.The replica reads a column the table no longer has (DotHidePop), so it may be older than the
engine.
HEAL is healing received and CURE healing done. Alna's WinterladyTakeHeal
reimplements the heal:
unheal gets nothing;onlycureself heals only itself;sk_GetSupportCoef).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.
A buff's ControlEffects names flags, and the game's own tables (SkillBuffUI,
SkillKeywords) define them:
silence: normal attacks only.unmanual: ultimate forbidden.unheal: recovery forbidden.noAddMp: energy gain forbidden.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.
unit:TakeHeal(amount, 'mp', src) (negative drains) and
unit:SetMp(v). AllMpRatio is energy recovery speed.Unit.StartMP, unless a script replaces it:
Init_hero sets StartMP in the skill info, often after CheckLevel or
CheckAwakenLevel. A script can also add energy at battle start (TakeHeal(x, 'mp') in
RunBattleStartLogic, SetMp in Reset).thorn.Init_hero sets StartMP = SArg6), Valen 500 at
awakening 2, Hodgkin +400 at battle start at awakening 2, Lyca 200 + 300 at awakening 2, Odie 600 at ex 3, and
Cyran 400, then 600 from awakening 1. Hewynn's loli.Init_hero sets StartMP to
loli_atk4 SArg2 = 300 at ex 2+: from 200 it becomes 300, it does not gain 300. Nara's
Reset calls SetMp with her own StartMP, 750. No mastery skill sets or adds
starting energy (all 127 read, 2026-10-03). Stargazer holds these in each hero file's energy: the
StartMP first, then what the script adds. Its Init Energy filter is worked out from the two, so it
is not a tag and the audit has no row for it._ult row's CostMP, which a script can
override (Reset sets CostMP).Unit.BaseMP) is 1000, so StartMP 1000 starts full.CostMP. Aster's 600-cost ultimate has
no script check, and its text says it consumes 600. Most cheap ultimates add their own threshold in
Ult_CanCastWithCaster (1000 for Galahad, Marcille, Pang, Senea, Velara and Zorya; Shakir 700 with
his ex).Priority
(InitCD 0, cost met). Hidden auto-cast sub-skills (IsShow false, ActiveType active) are
cast by the engine too: every hero's basic attack is one, and Smokey & Meerky's aroma
(meerkat_ultb) starts that way, since no script casts it at battle start. When its hit lands is
animation timing, which the tables don't hold (about 3 s in game for Smokey's).bombgirl_atk2b can never be
cast (atk2b_CanCastWithCaster returns false); Faramor's and Pippa's are cast at 0 s like any first
cast.Ult_CanCastWithCaster and
Ult_CanCastWithUnit before calling an ultimate instant. Faramor's returns false below 1000 energy,
so his first cast's CostMP 250 doesn't help. Valka's needs a target with 4 panic stacks, or the target her EX
just blocked.unit.TryDieWhenDefeated that returns false;OnFinalDamaged hook that compares HP with the incoming damage and answers with
SetHp(1), an invulnerable buff or a morph;unit.SetHp that tests the new HP and passes a higher value on. Mehira's ex
passes 20% of max HP on whenever the new HP is lower and a voidling is left. Inferred: every HP
change reaches SetHp (her hook counts her healing and catches hits in the same place), a
killing hit arrives as a value at or below zero, and the engine decides death from the HP
SetHp leaves. Thoran's script expects the same: its SetHp turns a negative HP
into 1 behind ult_cantdie, a flag no script sets, so that code never runs;deadUnit:Revive(...).Die overrides only clean up, so they are not evidence.target:Defeated(killer), target:Exterminated(killer)
or LostHp(target.hp), usually behind an HP threshold. Calls on a hero's own objects (summons, rocks,
seeds) are cleanup.unit.config.is_summoned is true; 261 scripts test it.
Unit.UnitType is not what the code checks.ed.BattleEngine:AddSummonUnit(unitId, level, summoner, pos, attrs, …) (227 call sites) and a skill
row's SummonUnitList create units with the flag set. Its two boolean arguments (positions 7 and 8)
don't decide it: every combination scripts pass includes a confirmed summoner.Unit.SubordinateID. It is placed with the hero, fetched by
GetSubordinateUnits in InitRelationship, and has the flag cleared there.AddSummonUnit returns.BattleEngineAddUnit(self, unit) and check the camp and
the flag at that moment.isUnitCarrier = true
in AddSummonUnit's last argument (read back as config.isUnitCarrier or
unit:IsUnitCarrier()): helpers, partners and props. Examples are Taichi's and Yamato's partners,
Sylphira's music note, Contess's energy bar, Kulu's Boomboom and Damian's plane (special modes only). The engine also marks some units
config.is_tool; no script sets it. The game's own summon counting
(fever_ep7skill_summonnumadd.BattleEngineAddUnit, bossrace_summon_atk.IsSummon) skips
both, and names its exceptions: Zanie's low turret (5020), Laios's unit (5045), and Berial's summons (hero 31).
Aurora's and Florabelle's handlers don't test the carrier flag. So for stargazer's Summon filter a carrier is not a
summon, though in principle it could still pass their checks.Unit.Script is empty), no or one placeholder skill row, no monster type,
and sit in faction HeroTagID 7 ("Other"); the hero's code drives them. Players report that summon
buffs (Aurora, Florabelle) don't apply to them. Every real summon has its own script, except Daimon's Stitchy,
which is monster-typed (UnitType 3) and has its own skill rows.battle_scripts.unit_kind, used by battle_scripts.py
summons). A created unit is a summon if all of these hold:
UnitType 1 or 2);Two cases this explains:
AddSummonUnit.ForEachAliveUnit(camp), ForEachAliveHeroUnit(camp) and
AliveHeroUnitIterator(camp) iterate a team. -camp is the enemy team.-camp with no distance filter, or a skill row with AOE around [20] and no
target-filter hook (IsSpecialIgnoreTarget, CollectAoeTarget), reaches every enemy.[20] and an empty AOEShape also collects
the whole team. imprison, set up the same way, has to table.remove the extras to keep N,
while Lyca's skill3 collector only reorders the list. Radius 10 from self is how the game writes the whole field:
Alna's ultimate text says "all units except Alna" (除阿尔娜外的所有单位), and her blizzard and Igor's and
Bonnie's ultimates use it.TargetCamp / AffectedCamp: 1 is allies, −1 is enemies.
TargetPriority self only anchors a skill on the hero: its area can still reach allies
(Laios's dish), and a script can retarget it (Antandra's ex sets a1.target to her guarded ally).TargetPriority (front_start, back_start, farthest, …) and
FirstPriority (notself) pick targets. The common values are target,
self, current_target, h_mostunits and enemy nearest.battleStart_hexgonPosition.unit:GetAroundUnit(radius, …) returns the units
around a unit (Shakir's ex aura, the charm gem_ep8frontbuff). skill:CollectAoeTarget(pos, …)
runs the skill row's own area from a position, which is how a lasting zone finds who is inside it
(ult_CheckRegion in Fay's and Mikola's ultimates). A CollectAoeTarget hook can move the
origin (Zandrok's charge line).Init_hero wraps
AOEShapeArgs on his skill3 row (the table says radius 1, the text 2 tiles). A
SelectTarget hook can return a unit the script stored (Isabella's companion, Faramor's blessed
hero), and IsSpecialIgnoreTarget drops units from an area (Kazim's wind field skips Kazim).battleStart_hexgonPosition: the unit that started
on a given tile (Gunnar's Doomfield, Faramor's blessing) or behind the hero (Taichi & Agumon's awakening).
Hugin's sigil is the other kind: it looks up whoever stands on the tile now (BehindUnit reads
cubePosition).CreateProjectile with *_tile_Hit handlers,
and skill rows' TileArt.SetRegionState(true), SetRegionPos).
Inferred: a region is on from battle start unless a script switches it off: 16 heroes switch theirs off
in Reset/Init_hero, and Kazim's wind field, never switched on, works from the start
("covering the battlefield" when a battle starts). Its buff's duration is the engine's (§5.2).AddBlock, Wreck, _AddUnitBlock and
StartFreezeArea.Atk3_…) suggest a slot, but Init_hero says
for sure. For example, Arden's Atk2_AddBuff is also bound to her ex skill. To find the slots behind
an effect, read the ed.override(a0.skills[name .. '_X'].Event, r1.fn) bindings and follow calls, and
hooks installed on buffs, from those functions. Hero-wide handlers (Update,
BattleEngineAddUnit, …) belong to whichever skill's data they read.atk2b or ultc belong to their parent skill.
FatherSkillSlot links others: Ludovic's pharmacist_atk5 is part of his ultimate.SkillLevel with their own SArgs, and a
script checks skill:CheckLevel(n). An effect unlocks at the first level whose value is non-zero
or that passes the check. For example, Aliceth's ex DoT coefficient is 0 until ex 4, and Phraesto's contract
energy is 0 at skill level 1.CheckAwakenLevel(n), with values on the <code>_awake_n
rows. A script may read them through another skill's info (skill_ult.info[r1.awaken_…]).*_imba_1 rows and imba code apply only in Honor Duel
and Dura's Trials.{slot: level} (for example {"ex": 4}), with one entry per level where the
effect changes, and optionally the modifiers that describe it ("mods"). A filter is a tag, or a tag
narrowed by a modifier. Temp Buff (Opening) is the tag temp-buff on an attachment that carries
"mods": ["opening"], so an opening buff is one attachment in both filters:
{"skill3": 1, "mods": ["opening"]}. Debuff (Global), (AOE) and (Single Target) are
debuff with global, aoe or single, one per attachment,
Debuff (Eryndor Synergy) is debuff with eryndor (a hero's slug as a modifier marks a
synergy with that hero), and Ult (Opening) is ult with opening. A global debuff his
ultimate counts is one attachment in both: {"ex": 1, "mods": ["eryndor", "global"]}. Cheat Death
takes self and
ally (who is saved), and one effect that saves both carries both:
{"ex": 1, "mods": ["ally", "self"]}. Two separate effects on one skill level are two
attachments. Until stargazer's tag refactor (2026-10-03) these three were tags of their own
(temp-buff-opening, debuff-global, opening-ult).
tools/stargazer_tags.py reads the format for both filter tools. Since 2026-10-11
temp-buff attachments carry dionel, orion and perseus, and
dot attachments carry shadewing (§7). A synergy never folds into its tag's name, so
DoT still reads "DoT" with "Shadewing Synergy" as a choice under it. Each temp-buff attachment
also carries one targeting modifier, global, aoe or single (§7), and so
does each haste attachment, with opening beside it when the Haste lands at battle
start: {"skill2": 1, "mods": ["aoe", "opening"]}.research/synergy_check.py tags covers their synergy modifiers, and
research/tempbuff_targets.py check their targeting modifier).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).
| Filter | Rule in code | Source |
|---|---|---|
| DoT | Deals 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 |
| Summon | Creates a unit that has is_summoned when it enters the battle (§5.8). | Aurora, Florabelle |
| Temp Buff | Gives 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 Denial | Healing 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 Death | Stops 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 |
| Invincibility | Makes 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 |
| Execute | Kills 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 Battery | Positive 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 Denial | Drains 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 Denial | silence / 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:
Atk4_, Ult_; derived skills
such as atk5c through the row's FatherSkillSlot); the skill it is installed as a hook
of; a level check on a skill, which moves the effect to that skill; the skill whose data the value is read from
(Nerion's drown damage is the EX's numbers, applied by the ultimate); a skill named mid-name
(OpenAtk4Circle); then its callers. Awakening and special-mode code keep those slots
(*awake* buffs and functions, imba code and branches, Duel leader skills
DuelSkill_*). A skill named through a module variable (r1.atk4Name = r1.name .. '_atk4',
Shakir, Pang) is read as that skill.CheckLevel on the way to the effect, and the first level at
which its value is non-zero in the skill rows (Saida's trap is 0 until EX 4). A flag or a count switched on
behind a level check gates the code that tests it (Rowan's energy potion: mpbottlenum = 1 after
CheckLevel(3)). A check covers only the code it guards: code reached by a jump from before the check
isn't gated by it (Nara's normal path runs inside the span of a special-mode check).imba) code is never tagged, in
every filter (user, 2026-10-03).TwineBuff_Update).'dot' hit, an IsDot buff with a positive coefficient (table or set at
run time; a skill row's own buff with its AddBuff hook, on a ground area where the area is switched
on: Gunnar's scorched area at awakening 1), a buff made IsDot at run time, or a tick run by hand. Awakening
buffs need a positive coefficient too.unheal/onlycureself buff on a unit that isn't the
hero's own (the same test as the other filters, and not a protection buff that is also invulnerable or untargetable), a skill row's own such buff (a
ground area at the place it is switched on; preparation-display rows don't hit), or an
OnHealDivision/TakeHeal hook that passes on less healing. Where several skills apply
the same buff, its name decides (oldgod_ult_… is the ultimate's).SetHp hook counts when it tests the new HP and hands the engine another value, on the
skill whose data it reads (Mehira's ex), unless the test sits behind a flag the script never sets (Thoran's).
A lethal check that lets the hit through doesn't count, nor does a low-HP trigger whose test a killing hit can
pass (user, 2026-10-03). Scarlita's awakening 2 is one: it fires once the ally is already at or below 20% HP,
or on a single hit of at least 80% of its max HP (the code subtracts the hit from max HP, not from current
HP), so an ally at 50% that takes a 55% hit dies.invulnerable buff outside death hooks, on a unit that isn't the
hero's own: not caster, not the hero inside its own hooks, not a unit it created or keeps in a field
filled with one, not a hook's unit installed on such a unit. The skill must not target enemies. A transition that
happens to be invulnerable isn't protection (Reinier's skill2 swap; user, 2026-10-03).Defeated/Exterminated call, or a LostHp of the
whole HP, on a unit that isn't the hero's own (own also covers lists of created units, and the holder of a buff
the hero put on its own unit). Clean-up at battle end, and functions named for destroying the hero's own props
(destroy/destory: rocks, stones, seeds, ice), don't count.TakeHeal 'mp', a lowering
SetMp), a regen cut (AllMpRatio, GainMpRatio, VictimMpRatio or MpP below zero; UnitAttr: MpP is
energy gained when hit; regen cuts count, user, 2026-10-03) or noAddMp, on a unit that isn't the hero's own. A buff stored on the hero
is the hero's (Shakir's wolf-form cost), so is a buff a unit puts on itself (Zorya). A table buff named for a
skill counts on that skill (Viperian's ult mark).silence/supersilence/unmanual/uncastmanual
on others, not with stun, freeze (frozen, forcefreeze), sleep, fear, petrify, entangle
(twine), imprisonment, disableAI or a polymorph; or a CanOtherUnitCastSkill
hook that returns false or drains the caster. A flag gates code only when it is switched on behind a level check
(Orion's silence at ex 3); a flag set at level 1 is plain state.TakeHeal 'mp' or a raising SetMp on a
unit that isn't the hero's own. Also own: the hit unit of a projectile homing on the hero (Isabella's ult
tiles, Bryon's absorb), the target of a self-aimed shot (TargetPriority self, no area,
TileCollideType target, not aimed at enemies and not retargeted by the script: Arden), a summon's
config.summoner (Damian's car), the
holder of a buff the hero puts on itself (Lenya) or gets through its own AddBuff hook
(Galahad). TargetPriority self alone isn't enough: a script can retarget the skill (Antandra's ex
sets a1.target to her guarded ally) or its area can reach allies (Laios's dish). A fixed partner counts (Laios → Marcille, Taichi & Agumon → Gabumon). The level is the first at which
every table value the amount reads is non-zero (Reinier's ally energy at skill3 4).Init_hero are handled as above: the saved unit's state isn't denial, and
hooks installed in Init_hero count.if caster.skills[.. '_atk4'] then … places the code on
that skill at level 1 (Odie's kill rides on his ultimate's poison but needs the ex); a level check outranks
it.AddSummonUnit(enemy.info.ID, …)). Not: carriers, units with no script of their own unless
monster-typed (UnitType 3, Daimon's ghost),
hero/NPC units, and companions whose flag is cleared at battle preparation (Elijah & Lailah's twin, the first
Illusion). A copy kept hidden until it casts still counts: Galahad's ultimate makes one of herself, invisible
with its AI off, and it takes summon buffs in game (user, 2026-10-04). Until then the check left such copies
out as helpers, which the code never said. Unit ids are followed through parameters to the callers' values.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.
For a new release, or to re-check an existing hero:
./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.python3 tools/battle_scripts.py show <codename> Init_hero
for the skill bindings, then show <codename> for the settings (buff ids, SArg names).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).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);filter_derive.py.python3 tools/stargazer_filter_audit.py --mark-reviewed: it records the
date, data build and stargazer's heroes in tools/filter_audit_baseline.json.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:
AddBuff, SetAttribute, SetTimer.ForEachAliveUnit, ForEachAliveHeroUnit, and
TakeDmgMulti, MakeDmgDivision, MakeDmgMulti,
MaxHpChange, TakeHpLossMulti and damage rules on a buff, which are debuffs
whatever Eryndor's list says.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 attachment, whatever its reach, the buff's table
row and its SetAttribute calls, against the stat list in §5.2.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.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.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).unheal, onlycureself, OnHealDivision, TakeHeal.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.Defeated, Exterminated.mp, SetMp, AllMpRatio, noAddMp.StartMP, CostMP.silence, unmanual, CanOtherUnitCastSkill.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/).AddSummonUnit or
SummonUnitList, its Unit.Script, and repeat steps 2–4 on that script.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.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.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:
eryndor.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.eryndor) on such a slot names its reach in the label,
Hero {ex: 1} (aoe); the page generator stops on an unnamed one.AWAKENING GATE up the call chain; Viperian's awakening 2 viper carries a buff named for his
ultimate.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.current - v is a cut (Senea's and Voracia's max HP cuts).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.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.stargazer_tags.released, §8 step 4).Decide:, what each answer changes, and a recommendation. Record each ruling with its
date in the row and in §7.These rules are inferred, not read from the engine. Each has one short in-game test:
AddSummonUnit: do they trigger summon payoffs? Test: Aurora with Sylphira (music note) or Contess
(energy bar); does her mastery ATK rise?MaxHpChange reaches
FixOneAttrib under its own name (§5.2). An area's buff gets a short duration even with table Time 9999
(Tilaya ex 3, seen in game; §5.2). Galahad's own shadow, the hidden copy her ultimate makes, takes summon buffs
in game, as does the ex's shadow of an ally (user, 2026-10-04). The Summon rule in §7 counts hidden copies
since then.SetHp leaves (§5.7). Test: Mehira with a
voidling left takes one hit larger than her current HP; does she survive at 20% and heal?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:
IsDot marks heals and timers;UnitType doesn't decide summons.Effects gated behind EX 4 or awakening (Aliceth, Contess, Rolan) are easy to miss in tests. The code shows them with their level.
| Part | Size | Reading |
|---|---|---|
| All battle code | 3,632 chunks, 29,779 functions | all lift; about 35 s |
| Hero scripts | 610 (heroes, summons, monsters, bosses) | 18,654 functions |
| The 129 heroes in the feed | 7,118 functions, 27,399 call sites | about 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:
tools/filter_derive.py covers nine filters. Debuff with
its reach and Eryndor modifiers and Haste with its four modifiers (their sweeps and coverage lines help, §8.1),
Temp Buff with its targeting modifiers, Temp Buff (Opening) and Ult (Opening) are still reviewed by hand, so a new hero needs the §8 checks for them. Moving them into the
derivation would make every release a routine check. Cheat Death itself is derived, but its Self and Ally
modifiers are read by hand: the derivation finds the hook, not the unit it saves.debuff attachment carries one of global, aoe and single, plus
eryndor when Eryndor's ultimate counts it. Global and Eryndor Synergy came first (2026-10-06); AOE
and Single Target were reviewed on 2026-10-13 and 2026-10-14 and are in stargazer since 2026-10-14. The rule and
the user's rulings are in §7, every attachment's row and evidence on the audit page. How it went: a code sweep
listed the candidates (tools/debuff_targets.py sweep), readers wrote a row for each, and the user
ruled on the definition questions (max HP cuts, HP loss taken up, damage rules on a buff, reaches between two
kinds; Eryndor's list decides only eryndor). The evidence was rewritten in plain English with code
terms marked. A comparison with two community lists and seven checks against the code then found missed
effects (Valen, Velara, Mehira, Viperian, Berial, Daimon), and a full reading of all 129 heroes, in groups made
by script with coverage checked, found two more (Gwyneth, Shakir). Their causes and the fixes now in the tools
and the checklist are in §8.1. Still open: Mikola's awakening 2 (AOE, eryndor) waits for its
release, and Lamentis's ultimate stays Single Target, since its cut reaches every enemy only with five or more
apostles. Ruled last, the same day: heal blocks are not debuffs, Vitality and Healing cuts are (with
eryndor), and a healing rule on an enemy's buff counts unless it also protects the holder (§7). No
hero has such a rule today, so it changed no tag.tools/haste_targets.py sweep (the
Debuff sweep's reader with the test turned around) listed the candidates, every site was read, and the user
ruled on lasting Haste, summons and Laios's random pick (§7). The audit page has a section per modifier. Haste
itself isn't derived yet: its rule is mechanical enough for tools/filter_derive.py, but each new
site needs its reach read anyway, and the sweep's coverage line flags it at every audit.research/tempbuff_targets.py check is the check, and its
rows are hand-reviewed. A skill row's area and target priority could sort many of them mechanically; the
script-chosen ones (stored partners, sorts, triggers) need reading.research/synergy_check.py tags is the check until they are. Open: whether the charm
gem_ep8buffboost keeps its Temp Buff tag on stargazer. By the heroes' test its Haste isn't a temp
buff (99999 timer); the charm reads temp buffs the way the three heroes do (§5.2).IsShow false), and the tools hold any tag on it until stargazer
shows the level (§8).buffs.json files under a hero by name
are not named by that hero's script settings. The call index can show what applies them.