afkj-parse reads them by. Covers both raw layouts: the AFKJourneyData dump and this repo's extract output.This page is the field-level reference for the raw game data that afkj-parse reads. It is ported
from afkj-data-viewer's docs/raw-data-reference.md and the "Verified facts" list in that repo's
CLAUDE.md, with the corrections the Season 8 run (2026-09-25, build 14368) found. The viewer's UI,
filter chips, effect tags, text scanning and search are out of scope.
The code that applies these rules is in src/afkj_parse/: the source adapters, which turn either raw
layout into plain rows, in source/, and the model, which joins the rows into heroes, artifacts,
phantimals, charms and buffs, in model/ (constants with their rationale in
model/constants.py). Hand-kept hero lists are in config.json.
GsRatio, base stats and per-level scaling) are not
part of the lean model. Their sections stay, each marked with a note, so that adding one back is a table read and a
module. The viewer's heuristic layer (buff-tag, marker and visibility classification, text scans, chips) is not
documented here; it went with afkj-data-viewer, which was retired and deleted.
The same game data reaches afkj-parse in one of two layouts. The extract output under
out/raw/ is the primary source; the AFKJourneyData dump is the fallback, chosen for a whole run with
--source ../AFKJourneyData. Both describe the same tables; for the same game build they differ by format,
not content.
AFKJourneyData/AFKJourneyData/
├── README.md
├── Tables/ one JSON file per table (1,670 files in the 1.7.4 dump)
│ ├── Skill/ per-caster skill partitions: Skill_0.json … (858 in 1.7.4)
│ ├── SkillIndex.json master skill id → partition
│ ├── Skill*.json SkillBuff, SkillBuffUI, SkillKeywords, SkillCommonRes, SkillEffectConfig, SkillDamagePopup, SkillTutorial
│ ├── Unit.json unit registry: heroes, monsters, summons, NPCs
│ ├── UnitFaction.json UnitRarity.json UnitTag.json Job.json enum lookups
│ ├── Exclusive.json ItemHeroExclusive.json ExclusiveHeroLevel.json EX weapon
│ ├── MasteryLevel.json TavernPool.json Season.json Fairy*.json CampPet*.json Gem*.json
│ ├── Array.json the global $arrayRef pool
│ └── … many more (Avatar*, Item*, Charge*, Camp*, Plot*, …)
├── Config/ thin index and navigation JSON
└── Localization/ 17 languages, one strings.json each
├── cn/ en/ fr/ ge/ id/ it/ jp/ kr/
└── pl/ po/ ru/ sp/ th/ tr/ tw/ vn/ zh/
The dump is versioned by its git history (for example [1.7.4] Build 2837 | Data 14242) and lags a
new game build by days: on the Season 8 cutover day it was a build behind.
out/raw/out/raw/
├── tables/
│ ├── <Table>.json { fields[], arrayFields[], rows: { "<key>": row } }
│ ├── Skill/Skill_<n>.json per-caster skill partitions
│ ├── SkillIndex.json rows { ID, Partition } (Partition is "_<n>")
│ ├── HeroScripts.json rows { Script, Name, Value }: hero battle script settings (section 10.5)
│ └── _stage.json { formatVersion, extractor, gameVersion, fileTable, complete }
├── locales/
│ ├── <code>.json flat { key: text }, same shape as the dump's strings.json
│ └── _stage.json { formatVersion, extractor, gameVersion, fileTable, from: install | cdn, cdnBuild?, locales }
└── cdn/<build>/ download cache for --locales-from cdn
The install only holds the languages selected in game; the CDN option fetches all 17, verified against the CDN's
xxHash64 and the install's file table so that text and tables are one build. Each stage stamps its own directory;
out/manifest.json describes only the last invocation and is not a version record. Details are in
architecture §9 and integrations §1.
AFKJourneyData/ | out/raw/ | |
|---|---|---|
| Table file | Tables/<T>.json: { _meta, default, row_keys[], rows[] }; rows are sparse (fields equal to default omitted); mapping tables (SkillIndex, Array) use data: {} | tables/<T>.json: { fields[], arrayFields[], rows: { "<key>": row } }; rows complete, defaults applied, verified cell by cell against the game's own Lua accessors |
| Arrays | { "$arrayRef": N } into Tables/Array.json, or inline 1-based maps { "1": v, "2": v } (section 6) | resolved inline as JSON arrays; null holes preserved |
| Grouped tables | parallel row_keys (the SkillID for Skill partitions, empty for other grouped tables) | keys are JSON-array strings ("[1,1]") with keyFields and rowKeyFormat: "json-array" in the file |
| Skill partitions | Tables/Skill/Skill_<n>.json, mapped by data.Skill[masterId] = "_<n>" | tables/Skill/Skill_<n>.json, mapped by SkillIndex rows { ID, Partition } |
| Text fields | the localization key where one exists, else the literal | the Chinese source text; the adapter turns it into keys (section 5.3) |
| Localization | Localization/<code>/strings.json, 17 codes | locales/<code>.json, same shape |
| Hero scripts | none | tables/HeroScripts.json, read from the battle scripts' bytecode (section 10.5) |
| Version | git history of the dump | tables/_stage.json and locales/_stage.json |
Against the 1.7.4 dump, the extract matched Unit in 907 of 923 rows and Skill in 9,896 of
10,319; every difference was a game change between the builds. A cross-source comparison is meaningful only for the
same build.
Both trees are large. Never load these whole into a conversation or an editor: grep for a key or a
name, stream them in a script, or read the model dump instead. Run ls -lah before reading a file you
have not seen, and read at most one or two Skill_<n>.json samples (about 95 KB each) to see the
schema. Both source trees are read-only: never write into AFKJourneyData/.
| File | AFKJourneyData/ (1.7.4) | out/raw/ (14368) | Notes |
|---|---|---|---|
| Locale dicts | Localization/<code>/strings.json, 25 to 52 MB | locales/<code>.json, 25 to 54 MB | grep one key; only en and cn stay resident in a run |
SkillBuff | 10 MB | 7.8 MB | grep by Name or ID |
Array | 10 MB | absent (arrays inline) | the $arrayRef pool; load once in a script |
ExclusiveHeroLevel | 4.4 MB | 3.3 MB | only four fields are read (section 12.2) |
UnitEpLevel | 2.5 MB | not in the measured extract | not read (section 13.2) |
Unit | 1.5 MB | 1.2 MB | borderline; grep for ad-hoc lookups |
The dump also carries large tables nothing here reads (SeasonBookResetReward 10 MB,
UnitQuality 9.5 MB, Plot 7.9 MB, Gem 7.0 MB, and others). Sizes were measured
on 2026-09-26; the extract sizes come from a build 14368 extract.
Almost every Tables/*.json file shares this wrapper:
{
"_meta": {
"table": "...",
"header_table": "...",
"format": "flat_rows" | "mapping_table",
"schema_size": <int>,
"row_count": <int>,
"array_fields": [...], // which fields use $arrayRef
"unresolved_array_refs": <int> // count of $arrayRef pointers in this file
},
"default": { /* every field with a typed zero value */ },
"row_keys": [ /* primary keys, parallel to rows */ ],
"rows": [ /* records, rows[i] ↔ row_keys[i] */ ]
}
mapping_table files (SkillIndex.json, Avatar_Index.json,
Array.json) replace default, row_keys and rows with a
data: {...} object. Three properties matter:
default is the schema. Every field appears with a typed zero value. Rows are
sparse: a field equal to its default is omitted, so always read row.get(field, default[field]).row_keys[i] belongs to rows[i]. Index by position,
then check the key against the row's primary-key field (for example SkillID).$arrayRef is global. A field listed in _meta.array_fields is
{} (empty), {"$arrayRef": N} or an inline 1-based map. The ref namespace spans the
whole export, not one file.The dump adapter materializes {**default, **row} for every table and checks row_keys[i]
against the row's primary key where one exists.
Extract rows are complete objects keyed by primary key, with arrays resolved inline. Grouped tables (whose inner
keys repeat across groups) use JSON-encoded composite keys such as "[1,1]"; the key fields are also
present in each row, so the adapter reads rows.values() as plain rows. null elements in an
array are holes the game's data has; the model treats a None element as absent. See
architecture §7 for how the tables are recovered.
A localization dict (Localization/<code>/strings.json in the dump,
locales/<code>.json in the extract) is a flat { "<key>": "<text>" } object,
one per language. A key is the xxHash64 (seed 0) of the Chinese source text, written as a decimal string of 1 to 20
digits (for example "18148772017108329122"). In the dump,
Skill_92.json's ult DisplayName: "18148772017108329122" resolves in en to
"Killer Flush", and the same key resolves in all 17 languages.
The rule for reading one:
"沙利叶", "普通攻击(不用翻译)").en holds 301,423 keys, three of them 14 digits long
(hash values below 1014), so a 15-to-20-digit pattern such as the viewer's
/^\d{15,20}$/ calls them literals.null there
(and is reported), never to its digits.Icon are never keys.<TAG>: an attribute icon or colour (<ATK>, <HP>,
<MR>, <ARM>, …).{FieldName}: the value of that field on the matching skill row; {FieldName%} is the
same as a percentage (times 100, with %). The vocabulary measured on 2026-09-22 is
SArgN, SArgN%, PlusRatio% and KnockBack.[name]label[/]: an inline glossary reference keyed into SkillKeywords
(section 11.4). Strip the brackets before matching text.(不用翻译) ("do not translate") are dev-only; the player never sees them.The game's own tables, and therefore the extract output, hold the Chinese source text where the dump holds keys.
The rule that reproduces the dump: a top-level string field whose text_key (xxHash64 of the
string) is in the cn locale becomes that key; anything else stays literal. It applies to ASCII
strings as the dump does (X, Transfer), and never inside arrays, where the only hits are
animation names. On build 14368 it reproduced 4,065 of 4,065 keyed fields and kept 1,107 literals (such as
不用翻译 dev names).
The dump also hashes the literal "1" in a few columns (UnitAttr.AlarmType,
BuffAffectType, SkillDesIcon, Unit.WalkSpeed,
ReselectTargetArgs). That key resolves to nothing and nothing here reads those columns, so the extract
adapter leaves them literal.
The extract adapter keeps only the cn key set, not the cn dict, and needs it: without a
cn locale in out/raw/locales/ the run stops with --locales-from cdn as the
fix.
In the dump, Tables/Array.json is a mapping_table with two top-level keys:
{
"_meta": { "table": "Array", "format": "mapping_table", "entry_count": 2 },
"data": {
"1": { "1": <v>, "2": <v>, ..., "310327": <v> }, // flat values (heterogeneous)
"2": { "1": 1, "2": 7, "3": 13, ..., "115083": <values + 1> } // 1-based start of each array, then a sentinel
}
}
data["2"][N] is the 1-based start of array N, and the last entry is a
sentinel one past the last value. Array N is
values[start(N) .. start(N+1) − 1]:
start = data["2"][N] # 1-based start of array N
end = data["2"][N + 1] # start of the next array (the last entry is the sentinel)
array = [data["1"][i] for i in range(start, end)]
This is the layout of the game's own Config/Array.luae (architecture
§7.4). data["2"] begins 1, 7, 13 and ends one past the last value.
data["2"] as cumulative ends until 2026-09-25. That reading shifts every array one
element into its predecessor: common_taunt's ControlEffects came out as
[uncontrollable, invulnerable, disableAI, taunted] instead of [taunted]. Against the
extract's inline values, the starts reading matches 9,896 skill rows and the ends reading 872. Two viewer rules had
adapted to the shift (the artifact anchor as the last SkillID, the artifact stat code as the last
string of AttrParamN); both are the first element (section 14).
An array field may instead hold an inline map {"1": v, "2": v, ...} with no $arrayRef;
its values are read in numeric key order. A field in _meta.array_fields that is neither is a source
error. In the extract output every array is already inline, with null holes kept in place.
Skill rows are split into one partition per caster (master skill id). SkillIndex maps a master id to
its partition:
// AFKJourneyData: Tables/SkillIndex.json (Config/SkillIndex.json is equivalent)
{ "_meta": {...}, "data": { "Skill": {
"1": "_0", // master skill id 1 → Skill_0.json
"92": "_87", // master skill id 92 → Skill_87.json
"98": "_92", // master skill id 98 → Skill_92.json (Ravion, "mask")
...
}}}
// out/raw: tables/SkillIndex.json
{ "fields": ["ID", "Partition"], "arrayFields": [], "rows": { "1": { "ID": 1, "Partition": "_0" }, ... } }
Unit.ID is the master skill id: feed the unit id into this map.SkillIndex,
never hard-code a file number. Shared caster files (artifacts 4009, charms 4022, rivalry 4031) follow the same
rule.Unit (UnitType = 1, Enable | TempEnable | AsrEnable, or in config.json heroes.forceParse)
│ filter: master id present in SkillIndex
▼
{ Unit.ID, Unit.Name (codename), Unit.HeroTagID, Unit.UnitRarity, Unit.UnitJob, ... }
│
├─ Unit.DisplayName → key → display name [authoritative]
├─ Unit.DisplayTitle → key → epithet (e.g. "Twilight's Burden")
├─ TavernPool.UpHeroName → key → gacha-banner label; fallback only
│
▼ SkillIndex[ Unit.ID ]
Skill_<n>.json about 28 rows, one per skill and upgrade level
│ row.CasterID == Unit.ID
│ row.SkillName == "<codename>_<slot>[...]" e.g. "mask_atk", "mask_ult", "mask_atk2_task"
│
▼ per row:
├─ DisplayName / Description / SimpleDescription (keys → text)
├─ SArg0..SArgN + PlusAttr / PlusRatio (interpolated into the description)
├─ Buff (name) / BuffID → SkillBuff
├─ SummonUnitList (array of unit ids) → Unit (not implemented, section 9.6)
└─ VFX arrays (Action(s), CastDisplays, …) → not read
Unit.DisplayName is the canonical name: a key for every current roster hero. Upstream has shipped raw
Chinese literals there ("沙利叶") for heroes that were new or CN-only at the time, and keyed them in a
later build, so the name follows a fallback chain:
Unit.DisplayName, if it is a key;TavernPool.UpHeroName;cn dict (literals of at most 24 characters; on a
tie the shortest unpunctuated EN text wins);TavernPool.UpHeroName is a gacha-banner label, not the unit's name, so it is never the first choice.
In cn, zh, tw and jp it bundles the title into the name ("浪心剑客-维伦" where DisplayName
is "维伦"), and for a collab unit it names only the human partner ("Taichi Yagami" for
"Taichi & Agumon"). Hero slugs come from the EN resolution of this chain, so preferring the banner
label would slug a hero wrong.
In the viewer's 1.7.x measurement, 130 of the 907 Unit rows are UnitType == 1; 126 of
those have a SkillIndex entry and at least one Enable flag, and 122 are in the roster, the union of
TavernPool.TavernUpHeroID and Exclusive.HeroID. The four outside it (mousebro
Hammie, mousesis Chippy, soldiermelee, soldierranged) are enabled but neither
in the gacha pool nor with an EX weapon: they are acquired through story or event paths.
Hand-kept exceptions live in config.json under heroes:
forceParse: disabled heroes (all three Enable flags false) to include anyway. Kept tiny on
purpose: relaxing the gate to any unit with skill data would also pull in dev placeholders
(HeroNameXXX) and event-mode reskins (wheellorsan) that collide on codename with live
heroes. Empty.blocklist: heroes to keep out of the feed. An entry marked "permanent": true is a
hero that is not in the public game and never will be: Hogan (38, general, CN-only) and the story
soldiers Guywin (39) and Joey (40), the same Unit IDs as extract's EXCLUDED_HEROES (a test keeps the
two lists in step). Other entries are temporary, for a hero the game enables before its text is ready; M12
suggests removing those once the English text resolves. Karma (doublehead) was such a case on the
viewer's list until build 14368 shipped text for his Ultimate level 1.awakeningLv2Blocklist: an override for a release where IsShow lags. Normally empty,
because IsShow already hides unreleased Awakening Lv2 rows (section
9.2).Unit rows with UnitType == 1, a SkillIndex entry and all three Enable flags
false are heroes that exist in the data but are not live. The set grows as new heroes are datamined, so re-list it
after every data update rather than trusting a list. Only the rows that explain config entries or common traps are
recorded here:
| ID | Codename | Script | DisplayName | Notes |
|---|---|---|---|---|
| 38 | general | general | 霍根 (Hogan) | real CN-side hero, disabled on global; a permanent blocklist entry |
| 101 | lorsan | wheellorsan | Lorsan | event-mode reskin of live Lorsan (ID 74); the wheel* script prefix marks event reskins; its codename collides with the live hero |
| 900 | HeroNameXXX | (empty) | 小明 (不用翻译) | dev placeholder ("Xiao Ming" is the Chinese "John Doe"); recognizable by the dev-marked DisplayName and empty Script |
deathknight appears in older notes but has no Unit row, only a stale codename in unrelated
tables. The unallocated ids 51, 54, 55, 63, 93, 107 and 112 have neither a Unit row nor a
SkillIndex entry (1.7.x).
A standalone listing against the dump (sparse rows, so read each field through default):
import json, os
base = "../AFKJourneyData"
L = lambda p: json.load(open(os.path.join(base, p)))
u = L("Tables/Unit.json"); d = u["default"]
rows = [{k: r.get(k, d.get(k)) for k in d} for r in u["rows"]]
gallery = {r["ID"] for r in L("Tables/GalleryHero.json")["rows"]} # released hero ids
en = L("Localization/en/strings.json") # 25 MB; fine in a script, never in a conversation
resolve = lambda v: en.get(str(v)) if v is not None else None
for r in rows:
if r.get("UnitType") == 1 and r.get("Enable") is False:
rid = r.get("ID")
print(rid, r.get("Name"), "rarity", r.get("UnitRarity"), "job", r.get("UnitJob"),
"inGallery", rid in gallery,
"EN-name", resolve(r.get("DisplayName")) or r.get("DisplayName"))
Against out/raw/ the rows are already complete: iterate
json.load(open("out/raw/tables/Unit.json"))["rows"].values(), and remember that text fields hold the
Chinese source text, not keys (section 5.3). To triage a hit:
Name (the codename) is the stable identity. The Script
prefix classifies it: a plain codename is a real hero, a wheel* prefix an event reskin, an
empty Script with a 不用翻译 DisplayName a dev placeholder.inGallery false confirms it is not released; a live hero with a temporary
Enable: false would still be in GalleryHero.DisplayName that resolves to clean English is further along than a raw Chinese
one ending in 不用翻译. DisplayTitle sometimes resolves when DisplayName
does not.SkillIndex (the file number is
not the unit id) and read the rows' Description. Localized, mechanically detailed text is a genuine
upcoming hero; the placeholder "mysterious power … can't gather detailed information" is stubbed content.
SkillName is an engine codename (prince_ult), not a localized title.Exclusive row with HeroID == <ID> (and a
resolved SignatureName) signals release-readiness.Questions like "how does Florabelle's Protective Blessing know to target Cecia's summons?" or "how does Dionel
detect a non-permanent buff from an ally?" have no answer in the tables. The behaviour is in script handlers named by
Unit.Script (multiplants for Florabelle, godspear for Dionel,
thornknight for Cecia's summon), which are Lua bytecode under Src/Battle/Logic/Heroes/ in
igame.pkg, not tables. Their logic is not read by the pipeline; Battle scripts reads it for research. The data holds the hooks those scripts read at runtime,
and each script's integer settings, among them the buff ids it applies (section 10.5):
| Hook | Source field | Used for |
|---|---|---|
| is this unit a summon? | none: the scripts test unit.config.is_summoned, set at run time (Battle scripts §5.8); Unit.UnitType ∉ {1, 2} was the old viewer's approximation | summon-aware effects (Florabelle, Cecia, Sonja, …); section 9.6 |
| is this buff permanent? | SkillBuff.Time >= 9999 | "non-permanent buff" conditions (Dionel) |
| is this a stat boost? | any of ATK, HP, HAST, … non-zero | stat-boost conditions (Dionel) |
| self or ally? | SkillBuff.AddSource ("owner" = self-applied) | telling self-applied from ally-applied buffs |
| which control effects? | SkillBuff.ControlEffects[] | the Buff → SkillBuffUI chain |
| can it be dispelled? | SkillBuff.CanDispel | counter-play |
| which skill made this buff? | name prefix <codename>_<slot>_… | implicit link by shared codename (section 10.2) |
| which buffs does the hero apply? | HeroScripts settings (atk2_buff = 1106) | attribution by the hero's own script (section 10.5) |
Skill_<n>.json rows have a 121-field schema. Each row has a Slot (internal mechanic
id) and a DisplaySlot (the slot the player sees). They disagree systematically; rows are grouped by
(DisplaySlot || Slot, DisplayName ref).
Slot | DisplaySlot | Player label | Meaning |
|---|---|---|---|
| 0 | 0 | Basic Attack | *_atk |
| 1 | 1 | Ultimate | *_ult, one row per upgrade level (keys 11 to 15) |
| 2 | 2 | Skill 2 | *_atk2 (keys 21 to 24) |
| 3 | 3 | Skill 3 | keys 31 to 34 |
| 6 | 4 | Mastery | *_mastery, keys 2001 to 2003; joins MasteryLevel |
| 4 | 5 | EX Skill | *_atk4, keys 41 to 44, UnlockExclusive 5 / 10 / 15 (the EX-weapon-locked skill) |
| 1 / 2 / 3 | 6 | Awakening | *_awake_1, rewrites the matching skill |
| 100 | 0 | Skill 2 sub-trigger | *_atk2b; internal |
| 101 | 0 | EX Skill sub-trigger | *_atk4b; internal |
| 4001 | 0 | Special modes | *_imba_1: Honor Duel and Dura's Trials variants. The display name is dev-marked but the Description resolves to real player text shown in those modes |
| 1 / 2 / 3 | 1 / 2 / 3 (keys 3001 to 3003) | (hidden) | per-Skill-Power increments, dev-only |
| 5 | 5 | EX Skill (rare) | a few heroes use this directly |
With the grouping key DisplaySlot || Slot, sub-trigger rows land at 100, 101 or 102 and are internal;
4001 is the special-modes group; the rest is the kit. The *_imba_1 rows carry mode-specific tuning data
(SummonUnitList references, ARM debuffs) that does not describe the main kit.
IsShow不用翻译. A slot whose
levels are all dev is internal.IsShow is the game's own visibility flag. A row with IsShow false is
an internal or hidden sub-skill the game never lists. Hiding is per level, because an Awakening slot mixes a shown
Lv1 with a hidden Lv2; a level that is dev or hidden never reaches the feed, and a slot whose levels are all dev or
hidden is internal.IsShow false sub-skill, Enchanted Mist
(camppetsandboss_atk2b, Slot 100), with no level-1 Description. It looks like missing
text but is internal; every phantimal shows five skills (section 15).IsShow false rows in slots 1 to 6 are the four
unreleased Awakening Lv2 rows of Harak, Ulmus, Sinbad and Mikola. All 40 released Lv2 rows are
IsShow true.| Field | Meaning |
|---|---|
CasterID | int; constant within one partition and equal to its master skill id |
SkillID | int; the row key |
SkillName | <codename>_<slot>[...]; the prefix is the hero codename (mask, chris, fw, …) |
DisplayName / Description / SimpleDescription | keys (or literals on dev rows) |
IsShow | the game's visibility flag (section 9.2) |
SkillType | "basic", "manual", "passive", … |
IsPositiveManual, ActiveType | activation flags |
Slot, DisplaySlot, FatherSkillSlot | placement |
SkillLevel | upgrade tier |
UnlockLevel / UnlockExclusive / UnlockMastery / UnlockAwakenLevel / UnlockSeasonLevel / UnlockByImba | unlock conditions; UnlockExclusive gates each EX upgrade tier (Florabelle's atk4 tiers at 5 / 10 / 15) |
EnhanceAttr | which attribute the upgrade boosts; observed BAP (Skill Power) and UAP (Ult Power) |
CD, InitCD, GlobalCD, CostMP, GainMP, VictimMP | combat economy. CD and InitCD are seconds between casts and before the first; 9999 and above (9999, 99999, 999999, 9999990) mean no timer (passives; energy-gated ultimates, whose CD of 0–3 s is an internal lock). GlobalCD is the lock after any cast (usually 1.25 s). CostMP is the energy a cast costs (1000 on most ultimates; a few cost 200–600, a few Skill 2s 300). GainMP is the energy the skill gives its caster and varies by level. VictimMP scales the energy the target gains (1 normal, 0 none). The feed carries all six in skillNumbers.json (Model & feed §3.10). The hero's Unit row adds StartMP (starting energy, 0–1000), BaseMP (the maximum, 1000) and VictimMPCoef (a multiplier on the energy it gains when hit), which the feed also carries, per hero; StartMP is the skill panel's "Initial Energy" tag on the ultimate |
TargetType / TargetShape / TargetCamp / AffectedCamp / AOEOrigin / AOEShape | targeting and area (section 9.4). The first TargetShapeArgs value is the range in tiles the skill panel shows ("Global" from 15 up); the feed carries it as range in skillNumbers.json (Model & feed §3.10) |
PlusAttrUnit / PlusAttr / PlusRatio | scaling; placeholders such as <ATK>{PlusRatio%} interpolate it |
DamageType | AD, AP, Holy, holy (section 17) |
DamageTrigger, DamageTriggerParam | damage formula |
SArg0 … SArg20 | numeric script arguments, interpolated by token name ({SArg0}, {SArg2%}). Rows carry 0 to 20 and descriptions use up to 16, so scan fields by /^SArg(\d+)$/ rather than a fixed range |
Buff / BuffID | join keys into SkillBuff |
SummonUnitList (array) | unit ids (section 9.6) |
MoveForward, KnockUp, KnockBack, FallFrozenTime, KnockNotFall | physics and control |
Action(s), DisplayAction(s), CastDisplays, HitDisplay, Trajectory, SkinId, ResourceList | arrays for VFX and animation; not read |
ChainJumps / ChainGap / ChainJumpDistance / MaxAffectTimes / ChainEffect | schema slots that are never populated |
AffectedCamp: 1 = ally side (explicit), 0 = no camp affinity stated (not
enemy: auras, mastery passives), -1 = the enemy side. Of the skill rows that deal damage
(PlusRatio > 0 with a DamageType), 433 of 486 are -1. The battle scripts
agree (Frieren's skill3 debuff goes on enemies with -1; Battle
scripts §5.9). Earlier notes read -1 as "not applicable". Lorsan's slot 3 (ally protection) and
Aurora's slot 6 (ally summon buff) both have 0.TargetCamp: the same -1 / 0 / 1 vocabulary with a different distribution; sometimes
more reliable than AffectedCamp (Lorsan's slot 3: AffectedCamp 0,
TargetCamp 1), but its meaning is less consistent across heroes.TargetPriority: a string; observed "target", "self",
"h_mostunits", "nearest", "current_target", null.
"self" (about 139 slots) is a strong self-targeting signal.UnitBriefBattleConfig)UnitBriefBattleConfig is the game's own skill-type taxonomy: 495 unit rows, each with up to five
SkillNType array fields (N = 1 to 5). Only the string elements of a resolved array are
codes; leading integers ([4, 5, 7, 9, 'A_ad']) are positional markers and are ignored. There are only
seven codes; an unknown code is dropped and reported.
| Code | Meaning |
|---|---|
A_ad | physical attack |
A_ap | magical attack |
A_ctrl | attack with control |
C_buff | applies buffs |
C_debuff | applies debuffs |
D_heal | heal |
D_shield | shield |
SkillNType is keyed by internal Slot N, not by DisplaySlot:
| Field | Internal Slot | DisplaySlot |
|---|---|---|
Skill1Type | 1 | 1, Ultimate |
Skill2Type | 2 | 2 |
Skill3Type | 3 | 3 |
Skill4Type | 4 | 5, EX Skill |
Skill5Type | rare | merged into 5 |
Example: Florabelle (multiplants) Skill4Type = ['D_heal', 'D_shield'] is her EX Skill
(Protective Blessing, heal and shield on summons), matching the game. Because the viewer misread the array pool
(section 6), its skill-type codes on dump input were wrong until 2026-09-25.
SummonUnitList)scripts/build-data.ts implemented it, has been deleted, so this section is the record.
SummonUnitList is an array of unit ids, heterogeneous: it can include slot-marker strings such as
"atk2b", which are ignored. It also holds heroes' unit ids as tuning data and NPC-typed obstacles, so
keep only entries that resolve to a Unit row with UnitType ∉ {1, 2}:
1 are heroes (tuning data);2 are generic NPCs and obstacles with shared scripts such as blocknpc (Kulu's
bombgirlrock1);null, 3 (monster-typed entries such as Hodgkin's
dieseatank, Daimon's daimonghost, Sonja's ybttank_xw), 4
(summon puppets) and 5.This is a table-side approximation. In the game's code a summon is a unit whose config.is_summoned is
true, set by AddSummonUnit and cleared for starting companions (Battle
scripts §5.8).
Known gaps of this rule: Daimon's daimonghost is on a slot-0 dev row, which the dev filter drops
(section 20); Sonja summons only in her imba_1 special-modes row. Several
summoners (Zanie, Damian, Berial, Bryon, Galahad, Phraesto) create their units from Unit.Script with no
SummonUnitList entry at all, so no table rule finds them.
SkillBuff: the buff registry3,266 buff rows in the viewer's measurement, ids 1 to 600,001, in a
117-field schema. Names are codename-prefixed (mask_atk2_task, chris_atk2_stun).
| Field | Meaning |
|---|---|
Name / ID | identity; the only name is this codename, with no localized title field |
Time | duration in seconds; Time >= 9999 is permanent |
IsDot, DotInterval | damage over time and its tick interval |
ControlEffects (array) | control-state names the buff carries ("stun", "silence", …) |
stat fields (HP, ATK, ARM, MR, CRIT, HAST, LFS, …) | flat modifier |
<stat>_a / <stat>_x | additive (ATK_a: ATK adds N) and multiplicative (ATK_x: ATK × (1 + N); -0.3 reads as -30 %) variants |
AddSource | "owner" = self-applied |
CanDispel, MaxStackTimes, ShieldType, HidePopup, ClearOnCasterDeath | dispellable, stack cap, shield kind, popup suppression, removal on the caster's death |
BuffType | 0 common and system buffs (common_stun, commonDash, faction defence; about 80 %), 1 positive-leaning hero buffs (about 14 %), 2 a mixed bucket (about 5 %); not a clean self / ally / enemy classifier |
visual (Effect, Puppet, EffectBone, Prefab*, …) | rendering; not read |
ChainEffect or BuffOnTrigger; the
only chain is Buff → ControlEffects[] → SkillBuffUI. Of 3,266 buffs, 932 have non-empty
ControlEffects. Six have a Script (small helpers like script_mp) and seven a
PortraitBuff (a UI icon swap).ARM / ARM_a / ARM_x on an attached or
codename-linked buff (Lyca's rabbitbow_ult, ARM_x: -0.3).Duel, Tournament, season, s1
(PvP and seasons); Gem, GemBuff (charms); camppet, camptalent
(faction passives); FieldNPC1, EffectCellNpc, story (PvE);
fever, wheel, CustomTrial, sindbad, fourhands
(events); per-hero codenames.For buffs.json, stats keeps the 21 base stat codes with their _a and
_x variants (TakeDmg* and MakeDmg* excluded), and only records a hero links, files by name or
names in its script are kept (section 10.6).
A skill row often has Buff: null, BuffID: 0, SkillScript: null yet clearly does something
(Florabelle's Protective Blessing grants permanent shields to summons). The mechanism is a passive buff whose name
starts with the hero's codename, applied by the script handler at runtime. No skill row references these
buffs; they are linked by the naming convention, and the hero's battle script, which applies them, names
most of them by id (section 10.5). In the viewer's count, about 960 such buffs across 113
heroes, against 46 directly referenced.
Because the name link is a convention and not a fact, it can misassign. The feed's buffs.json keeps
the sources apart: attached (a row links the record), inferredPassives (records whose name
starts with <codename>_, case-insensitive) and scriptBuffs (the hero's script names
the record).
Florabelle (multiplants):
| Buff | Time | Distinctive fields | Likely role |
|---|---|---|---|
multiplants_atk4_shield | 9999 | ShieldType: 'common' | permanent shield on summons (Protective Blessing) |
multiplants_globalBuff | 9999 | HidePopup: true, ControlEffects[…] | hero-wide passive applying control immunities |
multiplants_enterBuff | 9999 | HAST non-zero, HidePopup: true | on-entry haste |
multiplants_atk4_mega | 8 | AddSource: 'owner' | temporary self-buff during atk4 |
For Dionel (godspear), the stack gained on receiving a non-permanent ally stat boost is
godspear_atk3_passivebuff: Time: 9999, MaxStackTimes: 3,
AddSource: 'owner', ATK_a: 'ATK'. Whether to grant a stack is decided by the
godspear script.
Passive names follow <codename>_<slotToken>_<rest>. The rest of the name after the
codename is split on _, case-insensitive, and the first token in a priority-ordered list that starts a
segment wins; specific tokens win over general ones, so <hero>_awaken_atk2buff maps to Skill 2, the
skill it modifies, not to Awakening.
| Token (priority order) | DisplaySlot | Label |
|---|---|---|
atk4 / atk5 | 5 | EX Skill (wins over awake and ult) |
atk3 | 3 | Skill 3 |
atk2 | 2 | Skill 2 |
ult | 1 | Ultimate |
awake / awaken | 6 | Awakening |
mastery | 4 | Mastery |
imba | 4001 | Special modes |
atk (fallback) | 0 | Basic Attack |
A codename-prefixed name with no token is hero-wide (<hero>_globalBuff,
<hero>_enterBuff, concept names like oldsheep_ThornBuffShield or
meerkat_aroma). Coverage in the viewer's count of 960 passives:
slot 1 (Ultimate) 210 slot 4 (Mastery) 13
slot 2 (Skill 2) 167 slot 0 (Basic Attack) 6
slot 3 (Skill 3) 162 slot 4001 (Special modes) 50
slot 5 (EX Skill) 214 slot 6 (Awakening) 43
no token (hero-wide) 95 (about 10 %)
SkillBuffUI: control-effect popups17 rows: stun, frozen, silence, disarm, fear,
taunted, unmanual, unheal, noAddMp, twine,
immobilize, invulnerable, uncontrollable, blind,
suppress, sleep, unshield. Each row has BuffEffectName (the name
ControlEffects uses), PopupText (a key: "Stunned", …), PopupIcon,
IsBuff (positive or negative) and PopupType.
SkillBuffUI row are internal mechanics with no player-visible
popup; the viewer counted about 34 (stable, phaseMove, disableAI,
root, supersilence, notPush, ignoreboundary,
launch, unHASTaddable, untargetableToAOE, invulnerableToE,
…). Derive the set by subtraction rather than keeping a list.immobilize and twine are two codes for one mechanic: both lock movement and skill use.
The game shows different popups ("Immobilized", "Entangled") and uses twine (CN 缠绕) for vine,
grasp and plant effects (Arden, Cecia, Galahad). Only immobilize has a SkillKeywords
entry. EN text sometimes flattens 缠绕 to "immobilizes" (Galahad).HeroScripts: buff ids in hero battle scriptsA hero's battle script (Src/Battle/Logic/Heroes/<script>.luae in igame.pkg, where
<script> is the unit's Unit.Script) sets its tuning as module-level integers and applies
buffs by those ids. The extract's tables stage records every such name = <integer>
setting in out/raw/tables/HeroScripts.json (game.hero_script_table), one row
{Script, Name, Value} per setting, in the grouped form (keyFields: [Script, Name],
rowKeyFormat: "json-array"). The settings are read from the bytecode
(formats/luadis.py integer_assignments: a store of a constant, or of a register just loaded
with LOADI/LOADK), and the script is never run; a later store wins, as at run time. The
table is written by the same stage and under the same stamp as the Config/ tables, so both are one build.
Build 14368 has 610 scripts and 2,898 rows. The AFKJourneyData dump has no such table.
{ "Script": "rs", "Name": "atk2_buff", "Value": 1106 } // rs_atk2_shield
{ "Script": "rs", "Name": "ult_controlbuff", "Value": 1112 }
{ "Script": "rs", "Name": "ult_buffid", "Value": 3 } // common_taunt
A setting names a buff when its name contains buff (case-insensitive), the name is not a quantity
(time, duration, a trailing dur, stack, count,
num, value, coef, ratio, rate,
interval, max, min, chance, percent;
BUFF_QUANTITY), and its value is a SkillBuff id. atk2_debuffDuration = 5 is five
seconds, not record 5, and ult_darkBuff_maxStacks = 9 is a cap. Small ids are real shared buffs:
1 is common_stun, 3 is common_taunt.
In build 14368, hero scripts name 885 buff ids; 837 of the 1,102 records filed by name (section 10.2) are among
them, and 14 records no naming rule reaches (shared ones such as common_taunt) join
buffs.json this way. Lumont (rs)'s script names 1106 rs_atk2_shield (the other
record with that name, 1101, belongs to an enemy), 1107, 1108, 1111, 1112 and 3 (common_taunt); the pair
1109/1110 (rs_atk2_debuff), which no script names, stays only inferred.
afkj-parse links buffsBuffID, or by its
Buff name when the row has no id and exactly one record has that name. Records are keyed by id
because 30 names are shared by records that differ (rs_atk2_debuff: ids 1109 and 1110, with
different ARM_x and MR_x). In build 14368 all 338 rows with a BuffID
point at the record their Buff names._, case-insensitive, so a codename that prefixes another
(ls, lsword) claims only its own.dragonBow_atk2stunBuff, named by
dragonbow_atk2b, lands under Skill 2). The viewer dropped these records: 12 across 11 heroes in build
14368.HeroScripts (the dump), attribution
is by name only.The output shape is Model & feed §3.9; the model's records are §2.10.
Unit907 rows in the viewer's 1.7.x measurement. UnitType: 1 hero (130), 2 NPC (7),
3 monster (363), 4 summon-only puppet (117), 5 other (15), null
summon (275). For heroes:
| Field | Meaning |
|---|---|
ID | the master skill id |
Name | codename ("mask") |
DisplayName | a key, occasionally a raw Chinese literal (section 8.2) |
DisplayTitle, Description | keys: epithet, flavour text |
Enable / TempEnable / AsrEnable | live flags |
UnitType | 1 for heroes |
UnitRace | not the faction; use HeroTagID |
UnitJob, UnitRarity, HeroTagID | codes into Job, UnitRarity, UnitTag (faction) |
Gender | "male" / "female" |
SeasonType | non-null for season heroes |
DamageType, IsMelee | "AD" / "AP"; bool |
HP / ATK / ARM / MR / BaseMP / StartMP / BaseCRIT / GrowthValue / AttrIncreaseRatio | base stats (section 13.2) |
Script | the script handler (section 8.5) |
IconSquare / UltIconSquare / HeadEntranceIcon | image paths |
DisplaySkillID | always 0 for heroes; never read it, use Unit.ID |
UnitDetail maps UnitID → PagePuppet, which confirms codenames; it is not needed.
| Table | Key field | Hero field | Values |
|---|---|---|---|
Job | Name | UnitJob | 1 Tank, 2 Warrior, 3 Mage, 4 Support, 5 Marksman, 6 Rogue |
UnitRarity | Name | UnitRarity | 1 S, 2 A, 3 Rare, 4 Common, 5 (blank; special or NPC tier). The localized names are uneven |
UnitTag | Tag | HeroTagID | 1 Lightbearer, 2 Mauler, 3 Wilder, 4 Graveborn, 5 Hypogean, 6 Celestial, 7 Other, 8 Dimensional |
UnitTag has both a Tag and a Name field per row and they do not agree:
key by Tag, which is what HeroTagID matches. DisplayName on enum
rows is sometimes a key and sometimes raw Chinese.
TavernPoolOne row per gacha banner: TavernUpHeroID (= Unit.ID) and UpHeroName (a key,
the banner label). 68 distinct hero ids in the viewer's measurement. Used for roster membership and as the second
step of the name chain.
SkillKeywords: the glossary57 entries (weakest, strongest, frontest, backest,
dotDmg, meleeDmg, nukedmg, lonely, fatal,
invincible, …), each with DisplayContent (a key or a Chinese literal; the friendly name),
DisplayTips (a key; the tooltip body) and an optional DisplayPic. Descriptions reference them
as [name]label[/] ([invincible]invincible[/], [weakest]the weakest[/]).
DisplayPic exists for the positional keywords (frontest, backest,
sameline, symmetric, frontonehex, backonehex,
centralaxis, standardline), but the dump carries only the paths, not the images.
UnitAttr: the stat dictionary52 stat codes (ATK, ARM, MR, HP, MP,
HEAL, CURE, ATKHAST, LFS, CRIT, RES,
HAST, BLOCK, PT, HalfHp, SUP, CritP,
CritD, MpD, MpP, DebuffP/D, PRFC, PVP*,
DmgP/D, MakeDmgMulti/Division, TakeDmgMulti/Division, DEF,
BAP, UAP, MakeHpLoss*, TakeHpLoss*,
ATKHASTIgnUnctrl, HASTIgnUnctrl). Each row has Name (the code),
Tips (full name; a key or a Chinese literal), SimpleTips, Description (a key),
Icon / BuffIcon / DebuffIcon paths and an IsPvPAttr flag.
afkj-parse does not read it: the Refine 4 labels come from a fixed EN table (section
12.4).
Exclusive and ItemHeroExclusiveExclusive and ItemHeroExclusive) is not implemented by afkj-parse;
documented for future use. afkj-data-viewer, whose scripts/build-data.ts implemented it, has been
deleted, so this section is the record.
Exclusive: 124 rows, one per EX-bearing hero.
HeroID = Unit.ID.SignatureName: a key, the EX weapon's name ("Leaf-Fall" for Ravion).MaxLevel, HeroDetailDisplayIcon, SignaturePic, SignatureRes,
SkillIcon: visuals.SkillIDs and SkillUnlockLevel (arrays) are shared cluster references:
every hero's row points at the same arrays. They are not a per-hero unlock map. The per-level ladder is in
ExclusiveHeroLevel; the per-tier unlock is on the skill rows' UnlockExclusive.An Exclusive row for a hero also signals release-readiness (section
8.4). ItemHeroExclusive: 99 thin (HeroID → ItemID) rows. Older heroes use a different
linkage and may be missing, so not all 124 EX-bearing heroes are covered.
ExclusiveHeroLevelPer hero and EX level: 3,780 rows (30 levels × 126 heroes) in the viewer's measurement, 51 fields:
{ HeroID, Level,
GsRatio, GsConst, PVPGsRatio,
HP_PR, ATK_PR, ARM_PR, MR_PR,
HEAL, CURE, ATKHAST, LFS, CRIT, RES, HAST, BLOCK, PT, HalfHp, SUP,
CritP, CritD, MpD, MpP,
PVPHEAL, PVPCURE, PVPATKHAST, PVPLFS, PVPCRIT, PVPRES, PVPHAST,
PVPBLOCK, PVPPT, PVPHalfHp, PVPSUP, PVPCritP, PVPCritD, // Refine 4 reads these
PVPDmgP, PVPDmgD, PVPMpD, PVPMpP, PVPDebuffP, PVPDebuffD,
Attributes (array), // ARM / MR per-faction modifiers
SkillIDs (array), // changes at levels 5 / 10 / 15: the EX skill-unlock ladder
ExtraSkill, ExtraSkillCaster, // Refine 2: the rivalry skill
SignatureRes,
UpgradeCosts (array), // material cost to reach this level
CommonMaterialCost (array),
TotalCosts }
For Valen (HeroID 1) the SkillIDs ref changes at Lv0 (ref 406), Lv5 (73627), Lv10 (73628) and Lv15
(73581); each transition is a new EX skill becoming active. The source is ambiguous on the _PR suffix: the
viewer's schema note calls it "per rank", while in FairyLevel a trailing PR marks a
percentage. Only the refinement inputs are read: ExtraSkill and ExtraSkillCaster for
Refine 2, the PVP* codes at levels 28 and 29 for Refine 4.
Beyond the level-15 ladder, the EX panel shows two refinement tiers, appended after the kit ladder of the EX slot. Refine 1 (EX +26) and Refine 3 (EX +28) are skipped because the game skips them: those rows only raise values of stats already introduced.
Refine 2, rivalry skill (EX +27). ExtraSkill with ExtraSkillCaster
(4031, the rivalry pseudo-caster; it must be a single value across heroes) keys into that caster's partition
(Skill_618.json in build 14368, found through SkillIndex), rows 101 to 106: Tank, Support,
Marksman, Mage, Rogue, Warrior, one row per class with one level each. The row's DisplayName reads
"Rivalry Skill: Rogue" and so on; its description interpolates like any skill row.
Refine 4, new attributes (EX +29), is derived: per hero, compare the level-28 and level-29 rows of
ExclusiveHeroLevel and collect the PVP* codes that flip from 0 to above 0. Each hero gets one
to three codes. The candidates are the UnitAttr rows flagged IsPvPAttr that
ExclusiveHeroLevel has a column for, in column order: 19 on build 14368 (PVPPT, PVPHalfHp, PVPHAST,
PVPATKHAST, PVPCRIT, PVPCritP, PVPCritD, PVPRES,
PVPBLOCK, PVPCURE, PVPLFS, PVPHEAL, PVPSUP,
PVPDmgP/D, PVPMpD/P, PVPDebuffP/D). The level has no source row and no
description key; its text is the localized template 15370264355377780556
("<color=${1}>Refine ${2}: In Rivalry Mode, increases ${3}.</color>") with ${2}
the tier ("4") and ${3} a comma-joined list of <value> <label>;
colour tags are stripped. The template is localized, and so are the stat names: each is its
UnitAttr.Tips (PVPPT "DEF Penetration" / 防御穿透, PVPHalfHp "Execution" / 终结).
The viewer used a fixed English table for them in every locale; its English equals the game's for all 19 stats.
MasteryLevelGsRatio from MasteryLevel is not implemented by afkj-parse; documented for future
use. afkj-data-viewer, whose scripts/build-data.ts implemented it, has been deleted, so this section is
the record.
369 rows = 123 heroes × 3 mastery tiers. Per row: (UnitID, MasteryLevel),
MasterySkillID (matches skill row keys 2001 to 2003), Cost (array), GsRatio
(gear-score gain, typically 0.06 / 0.08 / 0.10) and RecordScore. It mostly repeats the mastery rows in the
skill partition; GsRatio is its own contribution.
UnitLevel, UnitEpLevelUnitLevel, UnitEpLevel) are not implemented by
afkj-parse; documented for future use. afkj-data-viewer, whose scripts/build-data.ts read them, has
been deleted, so this section is the record; the per-level formula below was never implemented there either.
Both are generic per-level tables, not per hero. UnitLevel (420 KB): 800 rows (Level 1 to 800) with
Rank, Season, AddBase (level-base multiplier), RankAddBase,
TotalAddBase, GsRatio, AddPRFC, AddCRIT,
AbyssAddBase and the UpToCost / UpToTotalCost arrays. UnitEpLevel
(2.5 MB): 3,000 rows of ascension (EP) scaling, adding PVE fields (AddPVEDmgP/D,
AddPVEMpP/D, AddPVEDebuffP/D, PVEGSCoef).
The viewer's notes gave a hero's stats at level N as Unit.HP / ATK / … ×
UnitLevel[N].AddBase, plus the ascension contribution from UnitEpLevel. It was never
implemented, so the formula is unverified.
UnitRank has the same shape as UnitLevel and plateaus at rank 24.
Artifacts, the equippable battle Spells (Awakening, Starshard, …), are internally "Fairy".
Registry: Fairy (127 rows): FairyID, DisplayName (a key,
"Awakening Spell"), SeasonType, FairyType, Icon, MinLevel /
MaxLevel, SkillID (array). Two sets are read:
| Set | Filter | Count | Levels | FairyIDs (Season 8) |
|---|---|---|---|---|
| pre-season | SeasonType == 1 && FairyType == 1 | 6 | 3 | 1, 2, 3, 5, 7, 9 |
| season | SeasonType == the current season | 12 | 7 | 8001 to 8012 |
Other FairyTypes are out of scope: 2 duel charms (Golden Blooms, …,
DuelEnable), 3 hero-specific picks ("Reinier's Grasp"), 4 single-level
quick-pick variants.
The current season is derived, never hard-coded. In Season, take the online row
(IsOnline == true) with the highest SeasonType > 100; SeasonType 1, also
online, is the always-available base game. Numbering is offset: Season labels SeasonType 108
"Season 7" (Foundation Season and Seasons 1 to 6 precede it), asset paths say ep8, and stargazer numbers
it 8. Compare only the raw SeasonType, never a display number. Extract uses the same rule.
Skills are in the leaderavatar caster file. Caster 4009 → SkillIndex →
Skill_<n>.json (Skill_602.json in build 14368, about 920 rows keyed by
SkillID). Rows interpolate like hero skill rows. A skill's own DisplayName ("Radiant Life")
differs from the artifact name for the pre-season set; for the season set they are identical.
Anchor and family. The artifact's anchor is the first element of the resolved
Fairy.SkillID (Awakening [101, 102, 103], Candleflame [8901 … 8907]): its
level-1 skill. The family is every row sharing that row's Slot and SkillName, sorted
by SkillLevel; one artifact is one (Slot, SkillName) family. Slot alone does not identify an
artifact: the next season's skill rows land in the leaderavatar file one patch before their Fairy rows
exist and reuse the live season's Slot numbers. In 1.7.4, Season 8's leaderavatar_candlelight (8901 to
8907) shares Slot 87 with Season 7's leaderavatar_ep7healwave (7701 to 7707), and likewise across all
twelve season slots (87 to 98). Every SkillName lives in exactly one Slot, so the pair is unambiguous.
Stat bonuses. An equipped artifact also grants stats that scale per level, from
FairyLevel (766 rows, per (FairyID, Level)). The displayed bonuses pair
AttrParam1..4 with AttrValue1..4; the stat code is the first element of
AttrParamN, and a following PR marks a percentage (["ATK", "PR"]). They mirror
the named *_PR and flat stat fields on the row. Only the max-level set is read (Fairy.MaxLevel:
10 pre-season, 30 seasonal). Codes seen: ATK, ARM, MR, HP,
HAST, ATKHAST, DEF, PT, BLOCK, HEAL,
LFS.
The viewer took the last element for both the anchor and the stat code; those rules compensated for its array misread (section 6) and were changed to the first element on 2026-09-25.
Icons. Fairy.Icon is an asset path; the dump does not ship the asset. This repo's
extract exports the art (images §6).
Phantimals, the seasonal summon companions, are internally "CampPet".
Roster: CampPet, five rows, one per camp slot, listing the current season's five by
CampPetUnitID (Season 8: 5116 to 5120, Gervan, Seralyth, Crystal Beetle, Plague Creeper and Wedge of
Matter, whose CampPetSubUnitID 5078 is its companion). Exactly that roster is read. Each unit id resolves
to a Unit row (UnitType == 3); its skill file's camppet codename prefix
(camppetcolossuslight_ult, …) is checked as a guard.
HeroTagID, never CampPet.Camp
A phantimal's faction is its unit's Unit.HeroTagID: 1 Lightbearer, 2 Mauler, 3 Wilder, 4 Graveborn,
5 Hypogean, 6 Celestial. CampPet.Camp is a slot, not a faction: camp 1 human, 2 elf, 3 orc, 4 undead,
5 goddevil (Hypogean and Celestial combined). In Seasons 7 and 8 camp 2 held the Wilder and camp 3 the Mauler, and
three of five hand-entered factions taken from the camp were wrong (Seralyth Wilder, Crystal Beetle Mauler, Wedge of
Matter Celestial).
The full roster across seasons is 15 phantimals in three faction-complete sets of five (unit ids 5101 to 5105,
5106 to 5110, 5111 to 5115), all findable by the camppet skill prefix. Their Unit rows carry
no usable SeasonType, so there is no per-unit season number to group by; an archive would iterate
camppet-prefixed UnitType == 3 units instead of the CampPet roster.
Skills are hero-shaped. Each phantimal's partition (master id → SkillIndex) holds its
abilities plus dev and internal rows. Keep the shown, non-dev rows, group them by DisplaySlot, sort levels
by SkillLevel. Levels 2 and up are upgrade deltas ("Increases base damage to …"). The skill's own
DisplayName ("Radiant Slash") is the heading; phantimals do not use the hero slot labels.
Five shown skills each. Ultimate, Skill 2, Phantimal Focus, a Spirit Mark and a fifth ability.
IsShow false rows are internal sub-skills (Slot or DisplaySlot 100 and up): Season 8's Crystal Beetle
carries a sixth, Enchanted Mist (camppetsandboss_atk2b, Slot 100, no Description); Gervan's
ultb (Slot 101) is the same kind and is also dev-marked.
The basic attack is hidden too. It is the row at Slot 0 (camppetsandboss_atk). Its
first TargetShapeArgs value is how far the phantimal attacks (Season 8: 1 for Seralyth and Crystal Beetle,
20 for the other three), and Unit.DamageType says whether those attacks are physical or magic.
CampPetPhase (6 phases × 5 camps) and CampPetLevel hold per-phase and per-level scaling
(phase unlocks gated on IsSeasonPhase(N)); not read. Unit.IconSquare's asset is not in the
dump; this repo's extract exports phantimal art (images §6.5).
Charms, the hero-equippable Magic Charms, are internally "Gem".
Link table: GemSuit (1,269 rows). Each row is (HeroID, SuitUnlockQuality 4..12,
CasterID 4022, SkillID), the hero-to-charm link. SkillID = base × 100 + SkillLevel, so the nine
qualities collapse pairwise onto four skill levels (4 to 5 unlock level 1, 6 to 7 level 2, 8 to 9 level 3, 10 to 12
level 4). Some heroes have SkillID 0 (no charm): released heroes without one (Guywin 39, Joey 40) and dev,
disabled or unreleased records; those links are skipped. The charm set is whatever GemSuit references with
SkillID ≠ 0 (19 bases, 120 to 138, in Season 8); nothing derives from a season literal, so a season
rollover needs no code change.
Skills are in one shared caster file. Caster 4022 → SkillIndex →
Skill_<n>.json (Skill_610.json in build 14368, 522 gemskill_* rows keyed
by SkillID). The build fails if an emitted base does not resolve exactly SkillLevels 1 to 4, or if two
heroes sharing a base disagree on the quality-to-SkillID mapping.
Interpolate each row. Adjacent tiers can share one Description key and differ only in
SArg values (SkillIDs 11501 and 11502 share a key with SArg1 25 and 30). Never dedupe tiers by
description key.
Tier names: GemQuality (12 rows; Name is a key for qualities 3 and up).
The four qualities that first unlock a level name the tiers: 4 "Elite", 6 "Epic", 8 "Legendary", 10 "Mythic"; the "+"
qualities between reuse the previous level's skill.
No player-facing names. Every charm row's DisplayName is dev-marked
((不用翻译)…); only descriptions localize. A charm is identified by its slug, the
SkillName minus the gemskill_ prefix (gemskill_ep7mpregen →
ep7mpregen).
Codes seen in EnhanceAttr, PlusAttr and DamageType. There is no in-game table
for these meanings; they are inferred from usage and the community's vocabulary.
| Code | Meaning | Where seen |
|---|---|---|
BAP | Skill Power | EnhanceAttr (253 rows) |
UAP | Ult Power | EnhanceAttr (126 rows) |
ATK | Attack | PlusAttr, <ATK> |
HP | Health | PlusAttr, <HP> |
ARM | Armor | PlusAttr, <ARM> |
MR | Magic Resist | PlusAttr, <MR> |
CRIT | Crit Chance | PlusAttr |
CritP | Crit Power | PlusAttr |
HAST / ATKHAST | Haste / Attack Haste | PlusAttr |
LFS | Lifesteal (the game's text says "Life Drain") | PlusAttr |
HEAL / CURE | Healing / Cure | PlusAttr |
DODG | Dodge | rare |
RES | Tenacity | rare |
DmgD / DmgP | Damage Reduction / Bonus | PlusAttr |
BLOCK | Block | PlusAttr |
GainMpRatio | MP gain | PlusAttr |
TeamATK / TeamHP | team-wide stats | PlusAttr |
PT | Penetration | PlusAttr |
SUP | Support | PlusAttr |
AD | Physical | hero and skill DamageType |
AP | Magical | hero and skill DamageType |
Holy (capital) | true damage | skill DamageType |
holy (lowercase) | an HP-drain mechanic, not true damage | skill DamageType; only Thoran's "Soul Plunder" |
TR | True | implied by the schema; not seen in data |
DamageType is not a reliable sole signal: the viewer found 142 skills whose description says "true
damage" with DamageType: null.
Not read; schemas noted so they need not be re-investigated.
SkillCommonRes (5.1 KB): shared particle and effect resource names.SkillEffectConfig (1.7 KB): per-effect direction-mode flags.SkillDamagePopup (19 KB): damage-number popup formatting.SkillTutorial: tutorial pointers.UnitForm: a four-row lookup of transform forms (heroes 62 and 72).UnitRank: generic per-rank scaling, plateauing at rank 24; same shape as UnitLevel.UnitSeasonSkill (10 KB): season-specific skill overrides.UnitLabelType (19 KB): codes for the Unit.UnitLabelType array field (mostly empty for
heroes).GalleryHero: the released hero ids; a cross-check for "is this hero live" (section 8.4).Each of these has been paid for once; do not re-investigate.
Skill_<n>.json file number is not the master skill id; SkillIndex maps them
(master 98 → file 92, Ravion / mask).CasterID is constant within one partition and equals its master skill id.Unit.ID is the master skill id. Unit.DisplaySkillID is always 0 for heroes:
never use it.SkillName prefix is the hero codename (mask, chris,
fw, …).$arrayRef namespace is global. Array.json's data["2"] holds 1-based
starts with a final sentinel, not cumulative ends (section 6).(不用翻译) are dev-only.IsShow false marks internal or hidden skill rows; each phantimal shows five skills; on build 14368
the only non-dev hidden hero kit rows are the four unreleased Awakening Lv2 rows (Harak, Ulmus, Sinbad,
Mikola).ChainJumps, ChainGap, ChainJumpDistance,
MaxAffectTimes, ChainEffect) are never populated.Buff → ControlEffects[] → SkillBuffUI.SummonUnitList holds heroes' unit ids as tuning data and NPC-typed obstacles; summons are the
entries whose Unit.UnitType ∉ {1, 2}. UnitType 3 entries are kept: they include real
summons (Hodgkin's tank, Daimon's ghost, Sonja's Honor Duel tank).Unit.DisplayName is the canonical hero name; TavernPool.UpHeroName is a banner label
and only a fallback (section 8.2).Time in seconds; Time >= 9999 is permanent, on buffs and on CD
fields alike.UnitType, Time, AddSource, codename prefixes).Buff: null, BuffID: 0, SkillScript: null may still have effects, usually through
codename-prefixed passive buffs the script manages.Unit.Script) sets the buff ids it applies as module-level integers
(atk2_buff = 1106); HeroScripts records them (section 10.5).
A setting whose name is a quantity (atk2_debuffDuration) is not a buff id.AddSource: 'owner' on a buff means self-applied._a (additive) and _x (multiplicative) suffixes.ARM / ARM_a / ARM_x on
buffs; skill text rarely names it.immobilize and twine are two control codes for one mechanic.[name]label[/] in descriptions is an inline glossary reference into SkillKeywords.SkillKeywords.DisplayPic icons exist only as paths; the images are not in the dump.Exclusive.SkillIDs and Exclusive.SkillUnlockLevel are shared cluster references, not a
per-hero map; the per-level ladder is in ExclusiveHeroLevel.UnitTag is keyed by Tag: 1 Lightbearer, 2 Mauler, 3 Wilder, 4 Graveborn, 5 Hypogean,
6 Celestial, 7 Other, 8 Dimensional.Unit.HeroTagID, never CampPet.Camp.Fairy.SkillID; the artifact stat code is the first
element of AttrParamN.AffectedCamp 0 means no camp affinity stated, not enemy; TargetPriority "self" is a
strong self-targeting signal (section 9.4).Work that was considered and deliberately left, with what would reopen it.
ControlEffects can list what it applies or what it
makes its target immune to (uncontrollable, invulnerable, …). A list of what a buff
grants immunity to is not derived. The blocker: AffectedCamp is not applicable on 56 % of slots, so
deciding who receives the buff needs several signals together (AffectedCamp, AOEOrigin,
AddSource, duration). Revisit when a consumer wants "Immune to: stun, silence".daimonghost is in SummonUnitList on a slot-0
dev-flagged row, which the dev filter drops. Letting slot-0 SummonUnitList entries through would find
it, but relaxing the dev-row filter risks pulling in dev placeholders for other heroes and needs a careful audit.
Summons themselves are not implemented (section 9.6).UnitLevel scaling. Stats at a given level (section
13.2) are not computed. Revisit when a consumer needs per-level stats; start from section 13.2.