Ported by hand from PR #209 by @qingjingshenghuo (its one-line data/config.json was not taken): - server/match/gamedata.js bossPoolShareOf (used by GameData.bossPoolShare and by finalAssault.bossPoolHp for a plain game-data object): solo × `solo` (1); co-op with `perPlayer` true × `coop` (1) × the players alive at the boss round's start (bots and AI 托管 seats count, eliminated / departed seats do not; at most aliveFull 4; a count left out = a full team). The fixed pool stays restorable through config: bossHpScale perPlayer false + solo 0.25 (and its optional aliveScaling). - tools/build-data.mjs: the global bossHpScale carries the rule (perPlayer true, solo 1); the modes' entries keep only bloodPointKey / unaffectedByEnemyScale. data/config.json regenerated offline — JSON compare: only bossHpScale (global and the 9 mode entries) changed. - The problem #209 documented: a 胄 drone death is 2 % of the pool and 限伤 cancels a hit of ceil ≥ 300000, so the hidden 胄 终极 pool at 3 / 4 players (21.6M / 28.8M) would cancel every drone (432000 / 576000). The drone link is a share of the pool, no hit: Battle.loseHp takes noHitLimit and the link passes the limit [ASSUMED: the official client checks every damage modifier and PRTS calls it 真实伤害, but which max HP it reads is undocumented]; a part's 传递 and every hit stay under the limit. Golden (regenerated; why: the pool size): - fields: boss-boss_1..7-pair/-solo and hidden-boss_8..10-pair/-solo (all 20 boss / hidden scenarios) — a pair field's pool × 2 (two players), a solo template's × 4 (solo × 1, was × 0.25); boss-boss_1-pair, boss-boss_1-solo, boss-boss_3-pair, boss-boss_3-solo, boss-boss_5-solo, boss-boss_7-pair and hidden-boss_9-solo now end at the 200 s cap instead of clearing; the two 联防 scenarios are unchanged. - matches: solo-FUNNY-2, solo-NORMAL-1, solo-HARD-1, solo-HARD-2 now lose the Final Assault (solo leaders have 4× the HP: team LP 0 with 93088–176395 of the pool left); solo-FUNNY-1, solo-NORMAL-2, solo-HARD-9-boosted (its Hidden Core too), coop2-NORMAL-3, coop4-FUNNY-6, coop2-ABYSS-10-boosted, coop2-NORMAL-70-diy still win with more leader damage (pool × players alive; solo-FUNNY-1 / solo-HARD-9-boosted end with 6 / 1 team LP less). Unchanged: coop2-NORMAL-8-serverrun and coop2-NORMAL-14-standins (one player left at R14: × 1 as before) and the matches that end before R14; roster / bonds / standins / diy unchanged. Tests: test/match/boss-hp-scaling.test.js (from PR #209, adapted — every leader and difficulty × 1–4 players and solo; R14 / R15 leave out two departed players; AI 托管 and bots count, an eliminated player does not; config restores the fixed pool, the opposite of the PR's test; the plain-object fallback agrees; only the leader's HP scales); test/content/enemies_bosses.test.js (hidden 胄 终极 at 3 / 4 players: the drone share lands, a hit of the same size is cancelled — fails without the change). Updated on purpose: finalAssault, balance, bosshp, connection, docs-consistency (the §20.9 record points to §25.13.4; the data carries perPlayer true), sim/playtest6_limits (the drone share lands past the line), match/playtest6-bosspool (the 5 % knob halved to keep the 2-player pool's size: that test is about the pool's arithmetic). Docs: PLAYING §7, DATA, META, BALANCE, SIM, design engine.md / network.md, the §20.9 / §20.10 records, research 00 / 01 / 08, the code comments, DESIGN §25.13.4 and the §25.10 table.
56 KiB
08 · Official waves, enemy counts, routes and preview (卫戍协议:盟约 下半)
Written 2026-09-28 in response to the user's test report. The user asked for enemy kinds, counts, routes and numbers to follow the official game (items 1, 2, 3, 4 and 9).
This file supersedes 05-enemies-levels.md §3.2 steps 1–7 (the generation algorithm and the k copies) and 00-INDEX §8 #12. It also supersedes every [ASSUMED] pathing statement in 05 §1 and §2.
Tags
- [DATA]: official game data or official client code.
- Game data: Kengxxiao zh_CN
activity_table,levels/*,enemy_database. - Client code means the user's local Windows client. The method names come from
global-metadata.dat(IL2CPP v29, not encrypted). The method bodies come from disassemblingGameAssembly.dll. The IL2CPP method RVAs are listed in Appendix B.
- Game data: Kengxxiao zh_CN
- [WIKI]: PRTS or BWIKI text.
- [VIDEO]: in-game screenshots. These are the PRTS
战场一览map images, taken at round-1 prep. - [ASSUMED]: not proven.
0. TL;DR
- Our enemy counts are 2–4× too high, and our faction mixing is wrong.
- Official rule [DATA, client
RandomEnemyGenerater]: each round uses exactly one special-enemy entry. - Every placeholder spawn action (not every enemy) is replaced by
clamp(roundHalfEven(n·BE_template / BE_new), 1, 5)enemies in total. - The placeholders of the other movement class (fly placeholders in a ground round, walk placeholders in a FLY round) are not spawned at all.
- Official multi 险境 R3 has 6–10 enemies (average 7.7). Ours averages 28 (max 68). R13 is ≈ 37 officially vs ours ≈ 81 (max 115).
- Official rule [DATA, client
- Factions per match. 3 random types. Each is given exactly 3 of the 15 round slots; SPECIAL gets the other 6. The slot order is shuffled, so there is one type per round [DATA].
- Lanes. Official pathing is a 4-directional SPFA flow field (unit cost, crate tiles 1000), followed by line-of-sight smoothing [DATA]. We use 8-directional A* with √2 diagonals.
- That is why upper-gate enemies drop into the lower lane on 战场#02 (act1 m02) and 战场#04 (act1 m04).
- Our data also turns on four act1 m02 crates that are hidden in the 下半 level and absent in the 下半 screenshot. Those crates block the top road.
- Preview. Each spawn action is previewed at its route's start gate + 6 rows [DATA]:
- Upper gate (12,10) → zone rows 17–18.
- Lower gate (9,10) → zone rows 14–15.
- Up to 50 models are shown. Elites and bosses are always shown.
- Custom balance to remove:
- All of
data/tuning.jsonenemyHpMul/enemyAtkMul/bossHpMul. - The
k-copies logic and the kept fly placeholders. - The extra ×0.7 on solo bounties.
- The custom 联防 slot grouping.
- The details are in §6 and §7.
- All of
1. Sources and method
- Game data [DATA]:
autoChessData.constData:min/maxReplacedEnemyCount1/5,templateEnemy*,maxLevelCnt15,specialEnemyNum3,enemyTypeIdentifierToFillRandom1, and the BE factors 1/5/3/3.act2autochess.specialEnemyInfoDict(67 entries) andspecialEnemyRandomTypeDict(count 3 / weight 3 per random type; SPECIAL −1).modeDataDict[*].inactiveEnemyKey, the round templateslevel_act1autochess_01…h06andh07_*/h08_*/escaped_*, the stage fileslevel_act{1,2}autochess_m0X, andenemy_database.
- Client code [DATA]. Class and field names come from
global-metadata.dat. The x64 bodies were disassembled with capstone and annotated with IL2CPP method names.RandomEnemyGenerater(sic):_GenerateSpecialEnemyTypesLocal,_GenerateRandomEnemyData,_DoReplaceLevelDataClient,_DoReplaceActionDataClient,_CalculateActionBattleEffectiveness,_CalculateEnemyCountByBattleEffectiveness,_GetEnemyAttrPower,_InitData,ShouldActionUpToServer,_GenerateRoundEnemyData.Torappu.Battle.SPFA:_GenerateNextMapImpl,_PostprocessAndMakeNextMapSmoothly,_RaycastSegmentLine,_RaycastBresenhamLine.- Also
Torappu.Battle.Tile.get_moveCost, theGridPositionstatic constructor,AutoChessEnemyPreviewManager, andAutoChessLevelEnemyManager.
- Wiki [WIKI]: PRTS
卫戍协议:盟约 下半(§开始模拟, the enemy multiplier table),卫戍协议/帮助,卫戍协议:盟约(上半 update log),…下半/战场一览and…下半/PRTS盟约记录, and BWIKI盟约. - Screenshots [VIDEO]: PRTS
卫戍协议盟约 战场01…08 (ver2) 地图.png. These are round-1 prep screenshots, and they show the round-1 enemies in the preview pen (§2.6). - No community per-round enemy tables exist. PRTS and BWIKI list only the faction pools (
战术特训敌人). - Scratch scripts (not project files) are in
…/scratchpad/research2/waves/:comp.py/official_counts.py: the count algorithm.simofficial.py: the distribution.akpath.py/akpath2.py: the official pathing.pathtest.mjs/ourcounts.mjs: our current behaviour.akdis.py: the disassembler.
2. Official per-round enemy composition
2.1 Match setup: the type schedule [DATA, _GenerateSpecialEnemyTypesLocal]
types = [t for t in autoChessData.enemyTypeDatas if t.involveRandom] // FLY TIMES ELEMENT DOT INVISIBLE REFLECTION
shuffle(types, battleRng); types = first specialEnemyNum (3) of them // uniform, distinct
slots = []
for t in types: repeat specialEnemyRandomTypeDict[t].count (3) times: slots.push(t.typeIdentifier)
while slots.length < maxLevelCnt (15): slots.push(enemyTypeIdentifierToFillRandom = 1 = SPECIAL)
shuffle(slots, battleRng) // slots[r-1] = type of round r
- Every match therefore has 9 rounds of its 3 chosen types (3 each) and 6 rounds of SPECIAL. The positions are random across R1–R15.
- R14 (leader) and R15 (hidden core) consume slots 14 and 15.
- The solo 标准 leader round R9 consumes slot 9.
- The briefing shows "三种特训敌人的类型" [WIKI 帮助: "您将事先得知本次战斗即将迎战的领袖单位,三种特训敌人的类型"]. SPECIAL is the filler and is not one of the three.
- In co-op the server sends the per-round types (
AutoChessBattleRoundEnemyInfo{enemyType, enemyKey, actionIndex, count, round}). The local generator is used when none are sent. The whole team shares one composition per round, because the round data has no player index [DATA].
2.2 Per-round pick [DATA, _GenerateRandomEnemyData]
for round r in 1..15:
typeId = slots[r-1]; if typeId == 0: skip
half = r <= maxLevelCnt/2 (=7) // R1–R7 first half, R8–R15 second half
cands = [e in specialEnemyInfoDict if typeOf(e.type).typeIdentifier == typeId
and e.isInFirstHalf == half
and e.specialEnemyKey ∉ mode.inactiveEnemyKey]
e = weightedPick(cands, e.randomWeight) // weights 8/10/15 as in data
special[r] = e.specialEnemyKey
normal[r] = e.attachedNormalEnemyKeys[randInt(len)] // always 1 element in 下半 data
elite[r] = e.attachedEliteEnemyKeys[randInt(len)] // always 1 element
inactiveEnemyKeyfilters only the special key. Attached normal and elite keys are not filtered. For example, 疯狂的逐腐兽 can still appear as the elite of 假想敌:蚀裂 in 险境. This matches the 上半 note [WIKI]: "以上调整仅针对战术特训敌人,不影响悬赏决策出场".- In every entry the special, normal and elite share the same fly flag [DATA check]: FLY entries are all flyers, every other entry is all walkers.
2.3 Template action → actual spawns [DATA, _DoReplaceLevelDataClient / _DoReplaceActionDataClient]
The placeholder map is built from constData in _InitData:
| template key | class |
|---|---|
enemy_1422_lrsldr |
NormalWalk |
enemy_1005_yokai |
NormalFly |
enemy_1427_lrnazg |
EliteWalk |
enemy_1042_frostd |
EliteFly |
enemy_1425_lrcmra |
SpecialWalk |
enemy_1040_bombd |
SpecialFly |
Token placeholders (1000_gopro_2, 1041_lazerd) are not in the map. They are only used by the 联防 templates (§5).
For every SPAWN action of waves[0].fragments[0].actions (processed last to first):
if action.key not in placeholderMap: // literal (e.g. R1 源石虫 ×2, boss escorts): keep as is
keep
else:
cls, isFlySlot = placeholderMap[action.key]
newKey = {Normal: normal[r], Elite: elite[r], Special: special[r]}[cls]
if isFly(newKey) != isFlySlot: // walk slot but FLY round, or fly slot but ground round
action.isValid = false // NOT spawned, NOT sent to server, NOT previewed
else:
originBE = action.count * P(action.key) / f(action.key) // float32
newBE = P(newKey) / f(newKey)
n' = clamp(RoundHalfToEven(originBE / newBE), minReplacedEnemyCount 1, maxReplacedEnemyCount 5)
action.interval = action.count * action.interval / n' // same time window
action.key = newKey; action.count = n'
P(k) = maxHp*1 + atk*5 + def*3 + magicResistance*3 // enemy_database, level = randomEnemyAttributeDict[k].level (all 0)
f(k) = randomEnemyAttributeDict[k].enemyBattleEffectivenessFactor
Notes on the count rule:
- The cap of 5 applies per action. One template action (for example R3 "N ×10") becomes at most 5 enemies. It never becomes 10·k.
- Rounding is C#
Math.Round(double), which is banker's rounding. The only exact .5 case in the data is 2 × 灵幛 → 隐形弩手组长: ratio 4.5 gives 4. - P uses
enemy_databasevalues. Thelevel_autochess_enemy_dataoverrides (灼藤 and 元核孽生者 ATK 400) are not used for the count. They still apply to the spawned stats. - Spawn timing [DATA,
AutoChessLevelEnemyManager._CalculateActionPredelay]: the i-th unit of an action spawns atpreDelay + i·step.step = max(window/n', 0.05·window), wherewindow = originalCount·originalInterval.MIN_ACTION_INTERVAL_RATIO= 0.05.- For n' ≤ 20 this equals the rescaled interval.
ShouldActionUpToServer[DATA] excludes:- invalid actions,
- boss enemies (
AutoChessBattleUtil.IsBossEnemy), - non-SPAWN actions,
- actions with a
randomSpawnGroupKey.
Kinds per round: at most 3 faction kinds (normal, elite, special), plus the literal 源石虫 in R1. They are all flyers (FLY round) or all walkers (every other type).
2.4 Leader and hidden rounds [DATA]
- The
h07_*/h08_*templates contain only E and EF placeholders (escorts), for example h07_01:lrnazg1+1 walk andfrostd1+2 fly. They are replaced with slot 14 / 15's elite by the same rule.- Ground round: E becomes the elite and EF is dropped.
- FLY round: the reverse.
- Literal escorts (重装防御组长 ×14 and the like) and the boss itself are kept.
- The leader's HP is not scaled [WIKI: "领袖单位于服务器的生命值加成不受上述加成影响"].
2.5 Enemy stat scaling
- The per-mode, per-round
atk × 1.1^kA,hp × 1.2^kHtable with base 0.7 / 0.8 / 1 is the only source [WIKI, PRTS 下半 §开始模拟, flagged "敌方属性加成情况由用户提供"].data/config.json → modes[*].enemyScalereproduces it. I spot-checked 险境 multi R1/R2/R4/R8/R9; docs/BALANCE.md §2 reports every mode and round as matching.- The same table is in 01-core-data.json
_criticAddendum.enemyStatMultipliers.
- Base values [WIKI, mode descriptions]:
- Solo: "所有敌人的生命值、攻击力降低至70%". 绝境 80 %; 终极 not reduced.
- Co-op: 80 % in 标准 and 险境; 绝境 and 终极 not reduced.
- DEF: the 上半 11/18 note said "敌人生命值、攻击力和防御力降至70%/80%". The 下半 text names only HP and ATK. We apply HP/ATK only, which matches 下半. Adding DEF would be optional [ASSUMED].
- 终极: every enemy moves at ×1.15 from R3 [WIKI].
- The scaling applies to wave, faction and bounty enemies. It does not apply to the leader's server HP pool [WIKI].
- The BE count rule (§2.3) uses unscaled DB stats [DATA]. Scaling happens at spawn.
2.6 Evidence from official screenshots [VIDEO]
The PRTS map images are R1 prep screenshots, 1 280 × 720, with the preview pen at the top right (rows 14–15). They match §2.3 exactly and contradict our k-copies rule.
| image | visible R1 enemies | official prediction (entry → R1) | ours would give |
|---|---|---|---|
| 战场02 ver2 (act1 m02) | 源石虫 ×2, 猎狗pro ×4, no drones | SPECIAL 高级军用猎狗 → 源石虫 ×2 + 猎狗pro ×4 (N 2 × 6620/3060 = 4.33 → 4) | 源石虫 ×2 + 猎狗pro ×4 + 妖怪 ×2 |
| 战场07 ver2 (act2 m03) | 源石虫 ×2, 猎狗pro ×4 | same | same as above |
| 战场04 (act1 m04) | 源石虫 ×2, 灼热源石虫 ×5 | ELEMENT 炽焰源石虫 → 源石虫 ×2 + 灼热源石虫 ×5 (2 × 6620/1450 = 9.1 → cap 5) | 源石虫 ×2 + 灼热源石虫 ×10 + 妖怪 ×2 |
| 战场02 (上半) / 06 ver2 | 源石虫 ×2 + 3 humanoid walkers (上半: white Sarkaz swordsmen) | e.g. SPECIAL 萨卡兹大剑组长 → 萨卡兹刀兵 ×3 (2 × 6620/4660 = 2.84 → 3); 粉碎攻坚手 / 山海众头目 → 宿主士兵 ×3 also fit | +妖怪 ×2 |
| 战场01 | 源石虫 ×2 + drone(s), no ground N | a FLY round (walk N dropped, NF replaced) | ground N ×2 + flyers |
| 战场03 / 05 ver2 / 08 | 源石虫 ×2 + 2 walkers, no drones | e.g. TIMES/DOT/INVISIBLE picks with N ×2 | +妖怪 ×2 |
Every screenshot shows only one movement class besides the 源石虫. This confirms that the placeholders of the other class are dropped.
2.7 Resulting counts
Distribution: official vs ours, 同盟 险境 (mode_multi_normal), enemies per board per round.
- Official: 4 000 simulated matches of §2.1–2.3.
- Ours:
server/match/waves.jsover 200 seeds.
| R | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| official avg | 4.7 | 7.7 | 7.7 | 10.3 | 20.0 | 23.2 | 26.2 | 18.2 | 20.3 | 22.2 | 23.6 | 35.0 | 37.5 |
| official min–max | 3–7 | 4–10 | 6–10 | 7–20 | 13–30 | 14–30 | 17–35 | 8–29 | 8–33 | 8–35 | 10–35 | 14–53 | 12–57 |
| ours avg | 7.1 | 17.6 | 28.3 | 24.5 | 42.8 | 50.5 | 58.5 | 39.8 | 42.1 | 47.1 | 51.4 | 80.1 | 81.0 |
| ours max | 14 | 39 | 68 | 48 | 93 | 113 | 134 | 54 | 60 | 65 | 69 | 107 | 115 |
- 绝境 and 终极 differ only by the unbanned keys: R12 ≈ 36.8, R13 ≈ 38.7 on average.
- A FLY round happens in ≈ 10 % of rounds (3 of 15 slots in the half of the matches that draw FLY).
Totals per possible round pick [DATA, computed with §2.3 on the real templates] follow. Per-action breakdowns for all 67 entries are regenerable with comp.py, and §7 step 1 describes the test fixtures.
First half (R1–R7). Total enemies spawned per board when this entry is the round pick. ⛔ = special key banned in 标准/险境.
| type | special (key) | w | attached N / E | R1 | R2 | R3 | R4 | R5 | R6 | R7 |
|---|---|---|---|---|---|---|---|---|---|---|
| SPECIAL | 萨卡兹大剑组长 (1010_demon_2) |
10 | 萨卡兹刀兵 / 萨卡兹大剑手 | 5 | 8 | 7 | 9 | 19 | 23 | 27 |
| SPECIAL | 粉碎攻坚手 (1045_hammer) |
10 | 宿主士兵 / 步兵 | 5 | 10 | 10 | 12 | 24 | 27 | 28 |
| SPECIAL | 拳师囚犯 (1118_lidbox_2) |
10 | 普通囚犯 / 老练囚犯 | 4 | 6 | 7 | 8 | 17 | 21 | 25 |
| SPECIAL | 高级军用猎狗 (1325_cbgpro_2) |
10 | 猎狗pro / 军用猎狗 | 6 | 10 | 8 | 14 | 27 | 30 | 33 |
| SPECIAL | 重犯 (1121_lifbos) |
10 | 普通囚犯 / 老练囚犯 | 4 | 6 | 7 | 7 | 16 | 19 | 22 |
| SPECIAL | 弧光锋卫 (1328_cbjedi) |
10 | Dor-1号失败品 / 莱茵生命防卫科高级成员 | 5 | 8 | 7 | 9 | 21 | 24 | 26 |
| FLY | 法术大师A1 (1041_lazerd) |
10 | “萨科塔之翼” / 妖怪MKII | 3 | 7 | 9 | 14 | 18 | 22 | 23 |
| FLY | 护障 (1355_mrfly) |
10 | 妖怪 / 妖怪MKII | 4 | 9 | 9 | 14 | 19 | 23 | 23 |
| FLY | 御4 (1017_defdrn) |
10 | 妖怪 / 妖怪MKII | 4 | 9 | 9 | 16 | 21 | 26 | 25 |
| FLY | “萨科塔之眼” (10084_hlegle) |
10 | “萨科塔之翼” / 妖怪MKII | 3 | 7 | 9 | 11 | 15 | 19 | 21 |
| FLY | 远眺 (1407_hummbd) |
10 | 妖怪 / 妖怪MKII | 4 | 9 | 9 | 15 | 20 | 24 | 23 |
| FLY | 护障·P ⛔ (1355_mrfly_2) |
8 | 妖怪 / 御4 | 4 | 9 | 9 | 14 | 19 | 22 | 23 |
| TIMES | 明鉴 (1195_sfyin_2) |
10 | 磨砻 / 俗心 | 4 | 6 | 6 | 7 | 16 | 20 | 24 |
| TIMES | 深池逐火精锐战士 (1288_duskls_2) |
10 | 深池逐火战士 / 深池逐火战士 | 4 | 4 | 7 | 9 | 16 | 19 | 23 |
| TIMES | 俗心 (1197_sfshu) |
10 | 磨砻 / 俗心 | 4 | 6 | 6 | 7 | 16 | 19 | 23 |
| TIMES | 雅气 ⛔ (1197_sfshu_2) |
8 | 磨砻 / 雅气 | 4 | 6 | 6 | 6 | 14 | 17 | 21 |
| ELEMENT | 控潮术师 (1161_tidmag) |
10 | 潜水员 / 码头水手 | 5 | 8 | 6 | 8 | 18 | 21 | 25 |
| ELEMENT | 炽焰源石虫 (1305_mhslim_2) |
10 | 灼热源石虫 / 炽焰源石虫 | 7 | 10 | 10 | 20 | 30 | 30 | 35 |
| DOT | 深溟巢涌者 (1234_dsubrl) |
10 | 单核掠食者 / 异光体掠食者 | 4 | 4 | 7 | 7 | 13 | 14 | 17 |
| DOT | 萨卡兹枯朽战车 (1272_nhtank) |
10 | 萨卡兹枯朽战士 / 萨卡兹枯朽战士组长 | 4 | 6 | 8 | 8 | 17 | 19 | 21 |
| DOT | 逐腐兽 (1270_nhstlk) |
10 | 萨卡兹枯朽战士 / 萨卡兹枯朽战士组长 | 4 | 6 | 8 | 9 | 18 | 21 | 24 |
| INVISIBLE | 山海众头目 (1299_ymkilr) |
10 | 宿主士兵 / 隐形弩手 | 5 | 10 | 8 | 10 | 24 | 27 | 28 |
| INVISIBLE | 节日爵士乐手 (10034_cnvsax) |
10 | 宿主士兵 / 隐形弩手 | 5 | 10 | 8 | 11 | 25 | 28 | 30 |
| INVISIBLE | 扶桥老手 (10042_prtrop_2) |
10 | 寻险水手 / 架桥船工 | 4 | 6 | 7 | 8 | 17 | 21 | 25 |
| REFLECTION | 深池伙友卫队 (1174_duholy) |
10 | 深池侦察兵 / 深池重甲卫士 | 5 | 8 | 6 | 8 | 19 | 22 | 26 |
| REFLECTION | 深池暗影术师 (1168_dumage) |
10 | 深池侦察犬 / 深池方阵步兵 | 5 | 8 | 8 | 13 | 24 | 27 | 32 |
| REFLECTION | 深池重甲卫士 (1170_dushld) |
10 | 深池侦察兵 / 深池方阵步兵 | 5 | 8 | 8 | 11 | 22 | 26 | 29 |
Second half (R8–R13). Same columns.
| type | special (key) | w | attached N / E | R8 | R9 | R10 | R11 | R12 | R13 |
|---|---|---|---|---|---|---|---|---|---|
| SPECIAL | 重装五十夫长 (1006_shield_3) |
10 | 宿主士兵 / 重装侦察兵 | 24 | 30 | 30 | 28 | 39 | 41 |
| SPECIAL | 弧光镜卫 (1329_cbshld) |
10 | Dor-1号失败品 / 莱茵生命防卫科高级成员 | 26 | 29 | 31 | 33 | 51 | 50 |
| SPECIAL | 粉碎攻坚组长 (1045_hammer_2) |
10 | 宿主重装士兵 / 宿主重装士兵 | 9 | 10 | 13 | 15 | 20 | 26 |
| SPECIAL | 访问团持盾者 (1387_winshd) |
10 | 访问团新兵 / 访问团老兵 | 15 | 16 | 18 | 19 | 32 | 38 |
| SPECIAL | 传奇重犯 (1121_lifbos_2) |
10 | 老练囚犯 / 强壮囚犯 | 14 | 16 | 18 | 18 | 30 | 35 |
| SPECIAL | 萨卡兹悖谬暴虐兵长 (1320_wdrrl_2) |
10 | 萨卡兹枯朽辟路前锋 / 萨卡兹大剑手 | 18 | 22 | 23 | 24 | 33 | 37 |
| SPECIAL | 集团军中坚盾卫 (10124_uashld_2) |
10 | 宿主士兵 / 宿主重装士兵 | 23 | 30 | 30 | 29 | 38 | 43 |
| SPECIAL | 测试用动力装甲 ⛔ (1254_lypa_2) |
10 | Dor-1号失败品 / 莱茵生命防卫科高级成员 | 29 | 31 | 34 | 36 | 57 | 55 |
| SPECIAL | 渎罪奇美拉 ⛔ (1425_lrcmra_2) |
10 | 萨卡兹枯朽辟路前锋 / 萨卡兹大剑手 | 19 | 22 | 24 | 26 | 37 | 41 |
| SPECIAL | 弧光镜卫长 ⛔ (1329_cbshld_2) |
10 | Dor-α号失败品 / Dor-β号复制体 | 23 | 27 | 30 | 28 | 42 | 48 |
| SPECIAL | 萨卡兹穿刺手 ⛔ (1072_dlancer) |
10 | 萨卡兹刀兵 / 萨卡兹大剑组长 | 26 | 29 | 32 | 36 | 50 | 49 |
| FLY | 威龙 (1005_yokai_3) |
10 | 妖怪 / 妖怪MKII | 19 | 19 | 20 | 25 | 39 | 31 |
| FLY | 帝国炮火先兆者 ⛔ (1112_emppnt) |
10 | 妖怪 / 妖怪MKII | 19 | 19 | 20 | 25 | 39 | 31 |
| FLY | 法术大师A2 (1041_lazerd_2) |
10 | 妖怪MKII / 寒霜 | 12 | 12 | 12 | 15 | 24 | 22 |
| FLY | 假想敌:黑云 (9009_acfort) |
10 | 妖怪MKII / 威龙 | 8 | 8 | 8 | 10 | 14 | 12 |
| TIMES | 深池逐火护卫 (1292_duskld) |
10 | 深池逐火战士 / 深池逐火精锐战士 | 19 | 18 | 23 | 28 | 43 | 45 |
| TIMES | 沉沙 ⛔ (1207_sfji) |
10 | 磨砻 / 俗心 | 17 | 21 | 21 | 21 | 32 | 33 |
| TIMES | 身观 (1199_sfjin) |
10 | 磨砻 / 俗心 | 17 | 21 | 21 | 22 | 32 | 35 |
| TIMES | 假想敌:再生 (9010_acpupp) |
10 | 俗心 / 雅气 | 11 | 10 | 14 | 17 | 25 | 31 |
| ELEMENT | 掠海漂移体 (2025_syufo) |
10 | 骨海漂流体 / 骨海漂流体 | 24 | 26 | 29 | 30 | 45 | 48 |
| ELEMENT | 领潮员 (1161_tidmag_2) |
10 | 潜水员 / 码头水手长 | 20 | 24 | 26 | 28 | 34 | 35 |
| ELEMENT | 元核孽生者 (1439_dslntf) |
10 | 底海滑动者 / 富营养的滑动者 | 26 | 31 | 31 | 31 | 45 | 41 |
| ELEMENT | 萨卡兹王庭军精锐术师 ⛔ (1275_dwlock_2) |
10 | 萨卡兹王庭军战士 / 萨卡兹王庭军精锐战士 | 16 | 18 | 20 | 18 | 32 | 35 |
| ELEMENT | 假想敌:淤困 (9007_acelem) |
10 | 萨卡兹王庭军战士 / 萨卡兹王庭军精锐战士 | 15 | 18 | 19 | 17 | 30 | 32 |
| ELEMENT | 灼藤 ⛔ (10067_ftsjc) |
8 | 卷心籽 / 灼藤 | 20 | 24 | 25 | 25 | 31 | 29 |
| DOT | 富营养的巢涌者 ⛔ (1234_dsubrl_2) |
10 | 异光体掠食者 / 异光体掠食者 | 15 | 18 | 18 | 17 | 30 | 32 |
| DOT | 集团军重型火炮 ⛔ (10122_uacann_2) |
10 | 萨卡兹枯朽战士组长 / 萨卡兹枯朽战士组长 | 20 | 20 | 23 | 27 | 42 | 42 |
| DOT | 尖端萨卡兹枯朽战车 ⛔ (1272_nhtank_2) |
10 | ���卡兹枯朽战士组长 / 疯狂的逐腐兽 | 13 | 15 | 17 | 20 | 25 | 27 |
| DOT | 假想敌:蚀裂 (9006_actoxi) |
15 | 萨卡兹枯朽战士组长 / 疯狂的逐腐兽 | 15 | 15 | 19 | 23 | 31 | 34 |
| DOT | 疯狂的逐腐兽 ⛔ (1270_nhstlk_2) |
8 | 萨卡兹枯朽战士 / 萨卡兹枯朽战士组长 | 24 | 25 | 28 | 29 | 47 | 49 |
| INVISIBLE | 访问团强攻冠军 (1389_winbab_2) |
10 | 访问团新兵 / 隐形弩手组长 | 18 | 18 | 21 | 22 | 38 | 44 |
| INVISIBLE | 山海众秘使 (1299_ymkilr_2) |
10 | 萨卡兹枯朽辟路前锋 / 隐形弩手组长 | 20 | 23 | 24 | 25 | 38 | 41 |
| INVISIBLE | 重弩突袭者 (1404_msnip) |
10 | 潜伏者 / 隐形弩手组长 | 29 | 33 | 35 | 35 | 53 | 57 |
| INVISIBLE | 假想敌:骨刺 ⛔ (9008_acbunn) |
10 | 业余竞演者 / 隐形弩手组长 | 20 | 23 | 25 | 24 | 38 | 39 |
| INVISIBLE | 家族灭迹人 ⛔ (1283_sgkill) |
8 | 业余竞演者 / 隐形弩手组长 | 20 | 23 | 25 | 24 | 38 | 39 |
| REFLECTION | 深池伙友卫队精英 ⛔ (1174_duholy_2) |
10 | 深池侦察队长 / 深池重甲卫士队长 | 18 | 20 | 22 | 24 | 34 | 39 |
| REFLECTION | 深池重甲卫士队长 (1170_dushld_2) |
10 | 深池侦察队长 / 深池方阵指挥官 | 22 | 23 | 26 | 29 | 46 | 49 |
| REFLECTION | 守墓石像 (1172_dugago) |
10 | 深池侦察队长 / 深池方阵指挥官 | 20 | 23 | 24 | 24 | 38 | 39 |
| REFLECTION | 假想敌:镜膜 (9011_acrefr) |
10 | 深池侦察队长 / 深池重甲卫士队长 | 18 | 20 | 22 | 24 | 34 | 39 |
| REFLECTION | 深池伙友影刃精英 ⛔ (1175_dushdo_2) |
8 | 深池侦察犬 / 深池伙友卫队精英 | 20 | 24 | 26 | 28 | 34 | 35 |
Two concrete example matches (同盟 险境). Per round: slot type, the special entry, the total, and the kinds and counts per gate (what the preview shows).
Example A. Types INVISIBLE / FLY / REFLECTION; slots SPEC INVI SPEC FLY REFL REFL SPEC FLY INVI SPEC FLY SPEC REFL SPEC INVI.
| R | type | entry | total | lower gate (9,10) | upper gate (12,10) |
|---|---|---|---|---|---|
| 1 | SPECIAL | 萨卡兹大剑组长 | 5 | 萨卡兹刀兵 ×3, 源石虫 ×2 | — |
| 2 | INVISIBLE | 节日爵士乐手 | 10 | 宿主士兵 ×10 (2 actions × 5) | — |
| 3 | SPECIAL | 重犯 | 7 | 普通囚犯 ×5, 老练囚犯 ×2 | — |
| 4 | FLY | 法术大师A1 | 14 | 妖怪MKII ×5, 法术大师A1 ×4, “萨科塔之翼” ×3 | “萨科塔之翼” ×2 |
| 5 | REFLECTION | 深池伙友卫队 | 19 | 深池侦察兵 ×8, 深池重甲卫士 ×3, 深池伙友卫队 ×1 | 深池侦察兵 ×7 |
| 8 | FLY | 法术大师A2 | 12 | 妖怪MKII ×2, 寒霜 ×2, 法术大师A2 ×1 | 寒霜 ×3, 妖怪MKII ×2, 法术大师A2 ×2 |
| 12 | SPECIAL | 集团军中坚盾卫 | 38 | 宿主士兵 ×21, 宿主重装士兵 ×6, 集团军中坚盾卫 ×3 | 宿主重装士兵 ×4, 宿主士兵 ×3, 集团军中坚盾卫 ×1 |
| 13 | REFLECTION | 深池重甲卫士队长 | 49 | 深池方阵指挥官 ×15, 深池重甲卫士队长 ×14, 深池侦察队长 ×12 | 深池方阵指挥官 ×4, 深池重甲卫士队长 ×3, 深池侦察队长 ×1 |
Example B. Types DOT / TIMES / ELEMENT.
| R | type | entry | total | lower gate | upper gate |
|---|---|---|---|---|---|
| 1 | SPECIAL | 高级军用猎狗 | 6 | 猎狗pro ×4, 源石虫 ×2 | — |
| 3 | TIMES | 俗心 | 6 | 磨砻 ×5, 俗心 ×1 | — |
| 4 | SPECIAL | 拳师囚犯 | 8 | 普通囚犯 ×2, 老练囚犯 ×2, 拳师囚犯 ×2 | 普通囚犯 ×2 |
| 7 | DOT | 逐腐兽 | 24 | 萨卡兹枯朽战士 ×8, 萨卡兹枯朽战士组长 ×5, 逐腐兽 ×5 | 萨卡兹枯朽战士 ×6 |
| 8 | ELEMENT | 假想敌:淤困 | 15 | 萨卡兹王庭军战士 ×5, 萨卡兹王庭军精锐战士 ×2, 假想敌:淤困 ×1 | 萨卡兹王庭军精锐战士 ×4, 萨卡兹王庭军战士 ×2, 假想敌:淤困 ×1 |
| 11 | DOT | 假想敌:蚀裂 | 23 | 萨卡兹枯朽战士组长 ×8, 假想敌:蚀裂 ×6, 疯狂的逐腐兽 ×4 | 萨卡兹枯朽战士组长 ×4, 假想敌:蚀裂 ×1 |
| 12 | SPECIAL | 弧光镜卫 | 51 | Dor-1号失败品 ×18, 莱茵生命防卫科高级成员 ×15, 弧光镜卫 ×8 | 莱茵生命防卫科高级成员 ×5, Dor-1号失败品 ×3, 弧光镜卫 ×2 |
3. Routes and lanes
3.1 Official ground pathfinding [DATA, client Torappu.Battle.SPFA + Tile + GridPosition]
- Flow field per destination. The engine computes one flow field per destination (a route's end, or a MOVE checkpoint) over the whole map grid.
PathRequestis{targetPos, motionMode, allowDiagonalMove}.- It uses a FIFO queue (SPFA), seeded with the destination at
dist0.
- Neighbours. The neighbour order is
GridPosition.GRID_FOUR_WAYS= UP (row+1), RIGHT (col+1), DOWN (row−1), LEFT (col−1). There are no diagonal edges. - Edge cost. The cost is
Tile.moveCostof the neighbour: 1, or 1000 if the tile is "obstacle-like" (a tile holding an active 阻隔工事). FLY always costs 1. - Passability. A neighbour is used only if it is passable for the motion. For WALK that is
passableMaskALL (floor, road, start, end, telin, telout, mire, smog, deepsea, infection).fence_bound, wall and forbidden tiles are not walkable. - Relaxation. Relax only on strict improvement:
if unvisited or dist[cur]+cost < dist[nb]: dist[nb] = dist[cur]+cost; parent[nb] = cur; enqueue if not queued. With equal costs, a tile keeps the first parent that reached it. That tie-break comes from the BFS order plus the UP/RIGHT/DOWN/LEFT neighbour order. - Smoothing (
_PostprocessAndMakeNextMapSmoothly):- Visit tiles row 0→H−1, col 0→W−1, updating in place.
- For each tile n:
b = parent[n]; while parent[b] exists and LOS(n, parent[b]): b = parent[b]; parent[n] = b. - Every autochess route has
allowDiagonalMove: true, so LOS is the Bresenham variant (_RaycastBresenhamLine). Every tile on the line must be valid and not obstacle-like (Node.CheckObstacleLikeOrInvalid). A diagonal step also needs both orthogonal neighbours clear. - With
allowDiagonalMove: false, LOS is true only for the same row or column (_RaycastSegmentLine).
- Movement. An enemy moves from its current tile straight toward
parent[tile]. The field is rebuilt when tiles change, for example when a crate dies (ClearCache/GenerateNextMap). - Crates (
trap_1105_accrate阻隔工事: "改变敌人的行进路线 / 如果阻隔工事阻挡了敌人,敌人会破坏此工事" [DATA]).- They are passable at cost 1000, so enemies route around them when any detour is < 1000 tiles longer.
- If a crate is the only way, the enemy walks into it, is blocked, and destroys it.
- Moving up and back down is not special-cased. Lane choice is purely this BFS.
3.2 Expected lanes per stage (official algorithm, crates as in each level file's non-hidden predefines)
| stage | upper gate (12,10) → goal (9,2) | lower gate (9,10) → goal |
|---|---|---|
| act1 m01 #01 | row 12 → (12,4) → col 4 → (9,4) → E. Waypoints (12,10)→(12,4)→(9,4)→(9,2) | (9,9) → col 9 up to row 12 → row 12 → col 4 → E. Merges into the upper road. |
| act1 m02 #02 (no crates in 下半) | row 12 all the way → (12,3) → col 3 → (9,3) → E | (9,9) → (11,9) → row 11 → (11,6) → col 6 → row 9 → E |
| act1 m03 #03 | row 12 → col 6 → row 10 → (10,3) → (9,3) → E. If 射击台 blocks (10,3)/(10,4) [ASSUMED], it goes (10,5) → (9,5) → row 9. | row 9 straight |
| act1 m04 #04 | (12,9) → (11,9) → row 11 → (11,4) → (10,4) → (9,4) → E. Smoothed waypoints: (12,10) → (12,9) → (11,9) → (11,4) → (9,4) → E, so it stays on row 11 to col 4. | (9,9) → (10,9) → row 10 → (10,4) → (9,4) → E |
| act2 m01 #05 | one snake for both gates: col 9 down → row 9 → col 7 up → row 12 → col 5 down → row 9 → E | same snake (enters at (9,9)) |
| act2 m02 #06 | row 12 → col 7 (mire) → row 9 → E. Crates on col 9 at (10,9)/(11,9). | row 9 straight |
| act2 m03 #07 | row 12 → col 4 → E | (9,9) → (10,9) → row 10 → (10,6) → (9,6) → row 9 → E |
| act2 m04 #08 | row 12 → (12,4) → col 4 → E | row 9 → (9,6) → col 6 up (deep water) → row 11 → (11,4) → col 4 → E |
The raw BFS chains and smoothed waypoints are in scratch lanes_official.txt.
Remake deviation (user playtest #2, 2026-09-29; narrowed after the community report D5, 2026-10-03). The user
reported the 战场#01 lower-gate enemies walking up the col-9 floor lane, where no operator can block them.
server/sim/grid.js keeps every official route LENGTH and the official route itself unless the 2026-09-29
road-over-floor preference route (equal-length ties to the chain with the fewest non-blockable floor / gate tiles,
smoothing that never cuts across floor its grid route does not walk, the corner tiles of a diagonal step included)
crosses strictly fewer non-blockable tiles; in that comparison a segment that only touches a floor tile's corner does
not cross it. Used alone (0.1.0) the preference also bent official diagonals into L shapes — the D5 report after
0.1.0: on 战场#04 the lower-gate enemies walked (9,10) → (9,8) → (10,8) instead of the official diagonal (9,10) →
(10,7), which only brushes the corner of the floor (10,9). Lanes that still differ from the table above (21 of the 154
stage × gate × field routes, test/sim/pathing-official.test.js):
- act1 m01 #01, lower gate: (9,10) → (9,8) → col 8 road up → (12,8) → row 12 → col 4 → E (official: (9,9) → (12,8) through the floor (10,9)); the 联防 partner routes pass the same exit.
- Boss fields: the exits from (2,10) on 战场#01 / #02 avoid part of the arena's central floor; 战场#02's 联防 partner lower gate avoids (12,17); the inactive 战场#07 (上半) 联防 partner upper gate takes row 9 instead of row 12's floor.
- The inactive 战场#05 (上半): its 深水区 refuses deployment (player report #3 after 0.1.0) and the sim does not model the 特制水上平台 canoes over it (they only enter the prep's deploy map), so the water counts as non-blockable and 7 of its routes take the floor lanes beside it (one non-blockable tile fewer than the official routes). Floor the terrain forces stays (gates, (12,9) beside the upper gate, the col-9 floor where walls or a crate block the road: #02 lower (10,9)/(11,9), #04 upper (11,9), #05 upper (11,9)/(10,9), #07 lower (10,9)). Full audit: test/sim/pathing-blockable.test.js.
- Official again since D5 (0.1.0 took L routes there): 战场#04's lower gate (normal, 联防) and boss exits, the boss exit (5,10) of 战场#05(下半), the boss exit (2,10) of 战场#07(下半) and the inactive 战场#06(上半). The diagonal first step out of the (2,10) exit of 战场#07(下半) (and #06(上半)) passes its side tiles (2,9) / (2,11) at exactly √0.5 ≈ 0.70711, just beyond the ground block radius 0.70709997 (PRTS 游戏数据基础 §阻挡半径), so an operator standing there no longer catches those enemies; every other blocking tile of the 154 routes is unchanged.
3.3 What we do and the root cause of "upper-gate enemies walk the lower lane"
- Our pathfinder.
server/sim/grid.jsis 8-directional A* with diagonal cost √2 and a no-corner-cut rule. It is limited to the field rect (rows 9–12, cols 0–10). Neighbour order is W, E, S, N, then diagonals. - Crates and platforms. Crates and platforms/mounds are impassable (with an
ignoreObstaclesfallback) instead of cost 1000.
| stage | ours, upper gate | official | effect |
|---|---|---|---|
| act1 m02 | 12,10 → 12,9 → 12,8 → 12,7 → 12,6 → 11,6 → 10,6 → 9,6 → row 9 (lower road) | row 12 → col 3 (top road) | Wrong lane. Two causes: (a) data/stages.json marks crates trap_1105_accrate#001–004 at (12,4), (12,5), (11,7), (11,8) as active: true, although the level file has them hidden: true and the PRTS 下半 screenshot shows none, so the top road is blocked; (b) even without crates, the 8-dir metric makes col 6 (10.41) shorter than the top road (11). |
| act1 m04 | 12,10 → 12,9 → 11,9 → 10,8 → row 10 | row 11 (upper row of the 2-wide band) | Upper-gate enemies join the lower-gate lane at once. |
| act1 m03 | … (10,6) → (9,5) → row 9 | row 10 → (10,3) (if platforms are walkable) | joins the lower lane 3 tiles early (platform rule [ASSUMED]) |
| act1 m01 (lower gate) | (9,9) → (10,8) → col 8 up | col 9 up | small visual difference |
| act2 m01–m04 | same lanes as official (diagonal corner cuts only) | — | OK |
The historical note on act1 m02 [DATA]:
- The 上半 PRTS image shows crates at (12,4) and (12,5).
- The 下半
ver2image shows none. The current level file has all m02 crates and platformshidden. - The terrain effects
map_m02_1/2("阻隔工事变为射击台" / "移除全部阻隔工事") still exist in the 下半effectInfoDataDict. The 战术决策 card pools are server-side, so they may still be offered.- On 下半 m02, applying them can only remove crates that are already hidden, or show the hidden 射击台 via the platform aliases [ASSUMED].
- They are no reason to start with crates.
- The 下半 m02 has no crates at start.
3.4 Pathing spec to implement (replaces Grid.findPath for ground enemies)
flowField(dest, {obstacleCost = 1000, allowDiagonal = true}): // cache per (dest, grid.version)
dist = Int32Array(ROWS*COLS).fill(-1); parent = Int32Array(...).fill(-1); inQ = Uint8Array
q = FIFO; dist[dest] = 0; q.push(dest)
while q: cur = q.shift(); inQ[cur] = 0
for (dr,dc) of [[+1,0],[0,+1],[-1,0],[0,-1]]: // UP, RIGHT, DOWN, LEFT — do not reorder
nb = cur+(dr,dc); if !inMap(nb) || tile(nb).pass !== 'ALL': continue
nd = dist[cur] + (isCrate(nb) ? obstacleCost : 1)
if dist[nb] < 0 || nd < dist[nb]: dist[nb] = nd; parent[nb] = cur; if !inQ[nb]: q.push(nb); inQ[nb]=1
// smoothing, row-major, in place
for r in 0..ROWS-1: for c in 0..COLS-1: n=(r,c); if dist[n]<0 || parent[n]<0: continue
b = parent[n]; while parent[b] >= 0 && los(n, parent[b]): b = parent[b]; parent[n] = b
los(a,b): Bresenham from a to b; every visited tile passable && !crate; on a diagonal step also both
orthogonal neighbours passable && !crate (allowDiagonal=false ⇒ same row or same col only)
- A ground enemy at tile t with a leg to D moves straight toward
parent_D[t]. It re-reads the field on entering a new tile, or whengrid.versionchanges. - The whole 19×21 map may be used as the official does. Keeping the field rect is harmless for normal rounds, because no shorter path leaves it.
remainingDistance(targeting "closest to goal") should usedist_D[t]plus the leg tail. The official exposesTryCalculatePathFindingDistanceon the same field.- Performance. One BFS over 399 tiles per destination and grid version replaces one A* per enemy leg. It is also deterministic across clients.
4. Enemy preview (prep phase) [DATA, client AutoChessEnemyPreviewManager]
4.1 Which enemies use which gate
The gate is the start of the action's route. R1–R3 use only the lower gate. Notation: class count @ preDelay / interval (game seconds). E.g. N3@7/5 = 3 enemies of the N slot, the first at 7 s, then one every 5 s. Counts are template counts before replacement (§2.3).
| R | tpl | lower gate (9,10) | upper gate (12,10) |
|---|---|---|---|
| 1 | 01 | 源石虫2@3/5 N2@12/3 NF2@12/3 | — |
| 2 | 02 | N3@3/5 N3@15/7 NF4@3/7 NF5@15/7 | — |
| 3 | 03 | N10@6/6 E1@18/10 NF9@6/7 EF2@10/10 | — |
| 4 | 04 | N2@12/4 E1@13/12 S1@29/7 NF2@3/6 NF3@26/6.5 EF3@17/9 SF2@18/9 | N2@3/6 NF3@7/8 |
| 5 | 05 | N3@7/5 N3@27/5 E2@14/7 S1@25/9 NF3@3/6 NF4@16/5 EF3@17/1 SF2@31/6 | N2@3/10 N3@22/8 NF3@5/6 NF3@22/6 |
| 6 | 06 | N4@7/8 N3@16/8 E2@20/12 S2@28/14 NF4@2/5 NF4@20/7 NF4@27/5 EF3@17/1 SF3@29/5 | N3@2/8 N3@19/8 NF4@7/8 |
| 7 | 07 | N4@8/5 N4@22/9 E4@5/17 S2@12/14 S1@40/14 EF3@17/11 SF1@9/12 SF1@13/12 EF2@41/11 NF2@2/10 NF8@27/4 | N3@2/8 N3@18/10 NF4@5/5 NF1@30/5 |
| 8 | h01 | N3@2/3 N3@28/5 N2@62/5 E1@37/9 S3@42/9 NF2@3/5 NF2@25/5 EF2@17/5 SF1@10/20 | N3@11/1 E2@17/10 S1@9/12 NF2@5/7 NF2@13/5 EF3@39/5 SF3@28/12 |
| 9 | h02 | N3@2/5 N3@42/7 N3@65/5 E2@17/10 S1@13/16 S1@40/5 NF2@3/3 EF3@28/5 | N3@3/5 N3@39/5 E1@41/10 NF1@3/1 NF2@13/5 NF3@20/5 EF2@41/5 SF1@7/7 SF3@21/12 |
| 10 | h03 | N3@3/5 N3@25/5 N3@49/5 E3@17/10 E1@41/10 S1@57/5 NF2@7/5 EF3@28/6 | N2@3/4 N4@5/5 S3@17/13 NF2@3/4 NF3@5/5 NF2@13/5 EF2@41/5 SF1@10/10 SF3@25/10 |
| 11 | h04 | N2@5/2 N3@22/2 N2@45/5 N2@50/5 E1@10/9 E3@54/7 S2@17/5 S4@42/5 NF3@27/2 EF3@28/2 EF2@52/2 SF2@17/2 | N2@5/5 N2@10/5 S1@6/20 NF2@3/3 NF2@5/5 NF4@9/5 EF1@20/5 SF1@7/10 SF2@21/10 |
| 12 | h05 | N3@5/3 N2@30/3 N2@10/3 N3@35/5 N3@51/4 E2@12/10 E2@37/9 E2@53/7 S3@2/10 S3@10/10 S2@31/10 NF3@10/3 NF3@27/2 EF3@28/2 EF3@47/6 SF2@12/5 SF3@22/6 SF2@35/5 | N2@5/3 E3@10/10 S2@21/12 NF2@5/1 NF3@8/4 NF3@21/5 NF3@43/5 EF2@13/5 EF2@15/5 SF2@17/10 |
| 13 | h06 | N3@7/3 N3@10/5 N3@39/5 N3@62/4 E3@11/9 E4@37/9 E3@55/9 S4@2/10 S4@10/7 S3@32/10 NF3@27/2 EF4@28/2 EF3@55/7 SF2@10/5 SF3@22/5 SF2@35/5 | N1@7/1 E2@3/10 S2@21/15 NF2@5/1 NF3@21/4 NF3@50/5 EF2@13/5 EF1@15/5 SF3@17/11 |
- FLY actions also start at a gate (their routes start at (9,10) or (12,10)), so they are previewed at that gate.
- Only the valid actions of the round's movement class appear (§2.3).
- Leader rounds use the boss field gates (2,10) and (5,10) with a row offset of 13, which lands in the same pen.
4.2 Placement algorithm
- Preview entries. For each valid round action there is one entry
{preDelay, startPosition = routes[action.routeIndex].startPosition, enemyKey, count}. The boss action is added in leader rounds. Entries are sorted bypreDelay(_CompareActionByTime). - Thinning. Let
total = Σ count.showRatio = min(1, 50/total)andtotalShowCnt = min(50, total).MAX_PREVIEW_CNT= 50.- Per entry,
n = max(1, round(count·showRatio)). - An enemy whose handbook
enemyLevel≠ NORMAL (elites, bosses) always uses ratio 1. - Each enemy is shown as its own model. The preview has no count badge.
- Pen and zones.
- The pen is
enemy_place_rect ((14,7),(18,13))(stageconfigBlackBoard). - The zone anchors are the
tile_starttiles in the rect: (15,7) and (18,7). - Every other pen tile with
passableMaskALL and withoutpreviewNotAlloedbelongs to the Manhattan-nearest anchor. Row 16 is excluded. - Lower zone = rows 14–15 (13 tiles). Upper zone = rows 17–18 (13 tiles). Each list is in row-major order (low row first, col 7→13).
- The pen is
- Zone choice.
- Target =
(start.row + enemy_preview_row_offset, start.col). The offset is 6 by default (13 in leader rounds); neither key is set in the stages, so the defaults apply. - The zone is the anchor with the smallest
|anchor.row − target.row| + |anchor.col − target.col|among anchors withanchor.row ≤ target.row. - Result: lower gate (9,10) → the (15,7) zone; upper gate (12,10) → the (18,7) zone.
- Target =
- Tile choice. The k-th model sent to a zone goes to
zoneTiles[clamp(round(k/totalShowCnt·len) + Random.Range(−1,0))]. Several models may share a tile. When no zone fits, a random pen tile is used. - Camera. In the official 3D camera the pen sits top-right, beyond the gates.
- For our 2D board, render the pen rows 14–18 (the stage already has them).
- Alternatively, render two gate-anchored zones next to the gates: the upper zone beside (12,10) and the lower zone beside (9,10). That is what the user remembers ("右侧上下红门后方").
- Keep the grouping exact: one zone per gate.
5. Secondary: bounty and 联防 spawn mapping [DATA, client AutoChessLevelEnemyManager; partially decoded]
Bounty (悬赏) and extra enemies (_InsertSpActionToNormal, _GetSpEnemyActionData):
- Each added enemy is attached to the first action of the round's original template whose key is
templateEnemyNormal(walker) ortemplateEnemyNormalFly(flyer). Summoned tokens usetemplateEnemyToken/templateEnemyTokenFlyinstead. - If no action matches, it uses action 0.
- All units attached to that action are then spread evenly over its window:
preDelay + i·max(window/n, 0.05·window). - So a walking bounty enters at the round's first N action gate. That is the lower gate in every round, because action N#1 always starts at (9,10).
联防:
- Each leaked enemy maps to the escaped template's action as follows:
- walkers → the
lrsldraction (route 3: WALK from (9,18) via (9,10), or from (9,10) inescaped_single; preDelay 3, interval 8), - flyers → the
yokaiaction (route 0), - tokens →
gopro_2/lazerd.
- walkers → the
- Elite and special leakers are not routed through the E/S actions (the class is ignored).
- Timing is recomputed per owner uid (
_CalculateActionPredelayConsiderUid; constantsACTION_INTERVAL_FOR_UID0.5 s,MAX_ACTION_INTERVAL_FOR_UID5 s). The exact formula was not decoded [ASSUMED: 0.5 s spacing within an owner, capped at 5 s]. - Helper order [WIKI 帮助]: most units on the field (downed included) > has an active bond (?) > highest bond layers (?) > most undowned units. The first helper takes the right-hand field. With 3 perfect players, the top 2 by the same order help.
6. Deviations from official numbers (custom balance and assumptions)
| # | Where (ours) | Current | Official | Source | Action |
|---|---|---|---|---|---|
| 1 | server/match/waves.js replaceSlot / templateSpawns; data/factions.json generation.kFormula |
each template enemy → k copies (k ≤ 5), 0.5 s stagger | each template action → clamp(roundHalfEven(n·BE_t/BE_new),1,5) enemies, interval n·iv/n' |
[DATA] | replace (§7-1) |
| 2 | setupMatchWaves / pickEntries |
3 factions per match; every round: a ground entry from the chosen types + SPECIAL, and a FLY entry | 15-slot schedule (3 types × 3 + 6 SPECIAL, shuffled); one entry per round | [DATA] | replace |
| 3 | flyPlaceholders 'keep' (BALANCE §2.2) |
寒霜 / 暴鸰 / 妖怪 stay in ground rounds (the #1 leak source) | the other movement class is not spawned (isValid=false) |
[DATA] + [VIDEO] | drop the knob; implement the rule |
| 4 | pickEntries inactiveIn, pickReplacement filters attached keys |
inactive list also filters N/E replacements | only specialEnemyKey is filtered |
[DATA], [WIKI] 上半 note | change |
| 5 | data/tuning.json → modes[*].enemyHpMul / enemyAtkMul (all 8 modes, e.g. multi 险境 R13 HP ×0.55) |
custom per-round multipliers from tools/balance.mjs |
none; only the PRTS table (config.enemyScale) |
[WIKI] | delete (keep titles.comment_3, which is unrelated) |
| 6 | data/tuning.json → mode_single_funny.bossHpMul 0.6 |
leader pool ×0.6 in solo 标准 | none | — | delete |
| 7 | config.bossHpScale bloodPoint × alive/4, solo ×0.25 [ASSUMED] |
pool scales with alive players | co-op: split leaders share HP, "敌方领袖的总生命值不变" → pool = bloodPoint (no alive scaling found); solo: unknown. Re-read 2026-10-01 (DESIGN §20.10): notice 5114's full sentence is "在联合模拟的最终攻势阶段,部分敌方领袖会同时出现在战场的左右两侧,两侧的敌方领袖共享生命值(敌方领袖的总生命值不变)" — the left / right copies of one pair field share the HP; it says nothing about the number of players. The one note on that is community (巴哈姆特 12294 "聯機隊友(撤退/死掉)變少,最後boss血條也會變少", no proportion); it is kept as the config switch bossHpScale.aliveScaling (× alive / 4), off until the user confirms it from official play — it would shorten fights after eliminations, the opposite of the playtest report |
[WIKI] notice 5114 / PRTS 下半 | co-op: bloodPoint whatever the alive count (× alive / 4 behind the switch, [ASSUMED proportion]); solo: keep a config value, flagged unknown |
| 8 | waves.js bountySpawns soloMul 0.7 for "perfect" bounties |
bounty HP/ATK/DEF ×0.7 on top of the solo 0.7 base | no bounty-specific rule; the "70 %" note is the global solo base, already in enemyScale |
[WIKI] 上半 11/18 log | remove soloMul |
| 9 | bountySpawns timing |
t = 4 s, +5 s per bounty, interval 3, routeByMotion |
attached to the template's first N / NF action window (§5) | [DATA] | change |
| 10 | buildUniteWave |
grouped by slot class N/E/S/…, compressed into 40 % of the time limit | walkers → lrsldr action, flyers → yokai, tokens → T/TF; per-owner 0.5 s spacing |
[DATA] (partial) | change |
| 11 | config.unite.helperOrder highestLpThenSeat [ASSUMED] |
by LP | most units on the field > active bond > layers > undowned units | [WIKI] 帮助 | change |
| 12 | gamedata.combatTimeLimit ×2 (COMBAT_TIME_SCALE) |
maxPlayTime read as real seconds |
unit not stated. The official count rule keeps each action's window, e.g. FLY R2 still spawns its last flyer at ≈ 43 s of "45", so ×2 remains necessary. | [ASSUMED], supported by spawn schedules | keep, but re-check after the count fix |
| 13 | server/sim/grid.js 8-dir A*, √2 |
see §3.3 | 4-dir SPFA flow field + LOS smoothing | [DATA] | replace (§7-3) |
| 14 | data/stages.json act1 m02 crates trap_1105_accrate#001–004 active:true |
top road blocked | hidden in the 下半 level; absent in the 下半 screenshot | [DATA] + [VIDEO] | set active:false |
| 15 | crates impassable + ignoreObstacles fallback |
— | passable at cost 1000, block LOS; blocked enemy destroys the crate | [DATA] | change |
| 16 | 射击台 / mounds block ground movement [ASSUMED] | — | unknown (crate and platform token data are identical; behaviour is in the prefab) | — | keep, flagged |
| 17 | config.enemyScale (HP/ATK only) |
— | 下半 text: HP/ATK; 上半 11/18 also named DEF | [WIKI] | keep; optional DEF |
| 18 | economy.income min(3+r, 12) |
R4+ assumed | BWIKI (hidden text, older season): "从第2回合开始每回合获得资金数+1,上限12" | [WIKI], weak | keep |
| 19 | previewOf |
one list per key, no gate | per-action models placed in a gate zone (§4) | [DATA] | change |
Everything in docs/BALANCE.md §4 (the tuned tables) and the tools/balance.mjs --tuning path becomes obsolete once #1–#6 are applied. The "competent board" model can still be used to measure difficulty, but no longer to tune it.
7. Changes required in our implementation (precise diffs vs current behaviour)
server/match/waves.js+data/factions.json(and the data generator intools/):- Match setup.
setupMatchWavesreturns{stageId, factions: [3 types], typeSlots: [15 types], bossId, hiddenBossId}.- Shuffle the 6
involveRandomtypes and take 3. - Push each
count(3) times, fill tomaxLevelCnt(15) with SPECIAL, then Fisher-Yates-shuffle with the match rng. - Store
typeSlotsin match state. It is visible in the briefing as the 3 types, and the slot order stays hidden.
- Shuffle the 6
- Round pick. Add
pickRoundEntry(gd, rng, typeSlots, r):- half =
r <= 7; - candidates =
specialEnemyInfoDictof that type and half, whose special key is not inmode.inactiveEnemyKeys; - weighted by
randomWeight; normal = attachedNormal[randInt],elite = attachedElite[randInt].- Generate the picks for all 15 rounds at match start (like the client), so the preview and the server agree.
- half =
- Replacement. Replace
templateSpawns/replaceSlotas follows:- For each template SPAWN action whose key is a template placeholder: if
isFly(newKey) !== isFlySlot, skip the action (no spawn, no preview). - Otherwise
n' = clamp(roundHalfEven(f32(f32(n·P(t))/f(t)) / f32(P(new)/f(new))), 1, 5), and each unit i spawns attime + i·max(n·iv/n', 0.05·n·iv). P = hp + 5·atk + 3·def + 3·resfrom theenemy_databaselevel-0 attributes (not the autochess overrides).roundHalfEven(x) = (x − floor(x) === 0.5) ? (floor(x) % 2 ? floor(x)+1 : floor(x)) : Math.floor(x + 0.5).- Keep literal keys and the T/TF tokens unchanged.
- Remove
COPY_STAGGER, thekfields (kS,N[].k,E[].k),flyPlaceholders,groundRouteLikeand theinactiveInfiltering of attached keys.
- For each template SPAWN action whose key is a template placeholder: if
- Leader rounds.
buildBossWaveuses the pick of slot 14 (or 15, or 9 in solo 标准) with the same rule, so only E or EF escorts spawn. - Unit tests (fixtures from §2.6 / §2.7):
- R1 高级军用猎狗 → {源石虫 2, 猎狗pro 4};
- R1 炽焰源石虫 → {源石虫 2, 灼热源石虫 5};
- R1 萨卡兹大剑组长 → {源石虫 2, 萨卡兹刀兵 3};
- R3 FLY 护障 → {妖怪 5, 妖怪MKII 4} and no walkers;
- an elite action "2 × 灵幛" → 隐形弩手组长 ×4 (the tie case);
- multi 险境 R3 total ≤ 10 for every entry.
- Match setup.
- Stat scaling and balance.
- Delete every
enemyHpMul,enemyAtkMulandbossHpMulindata/tuning.json. Keep thetitlesblock. - Remove the
soloMulinbountySpawns. config.bossHpScale: co-op pool =bloodPoint[difficulty]with no alive-player factor; solo stays a flagged config value. (2026-10-01: × alive / 4 available behindaliveScaling, off, see §6 #7.) (2026-10-06: replaced by the owner's decision adopting PR #209 —bloodPoint× the players alive at the fight's start, solo × 1;perPlayer: falserestores the fixed pool; DESIGN §25.13.4.)- Keep
enemyScale(PRTS table) and the 终极 speed ×1.15 from R3. - Re-run
tools/balance.mjs --tuning offonly to report, not to tune.
- Delete every
- Pathing (
server/sim/grid.js,server/sim/ai.js,server/sim/Battle.js— since 0.2.0server/sim/battle/spawns.js/tiles.js):- Add
Grid.flowField(dest)as in §3.4, cached per(dest, version). planLegfor WALK: follow the smoothed parents from the enemy's tile to the leg target and re-plan on version change. That is the same trigger as today.- Crates:
setObstaclemarks a cost-1000 obstacle-like tile, not an impassable one. The existing "blocked enemy attacks the crate" logic stays. data/stages.json: set act1 m02trap_1105_accrate#001…#004toactive:false.- Keep platforms and mounds blocking [ASSUMED].
- Add tests: the upper gate on act1 m02 reaches E through (12,3)→(9,3), and on act1 m04 it stays on row 11 down to col 4.
- Add
- Preview (
previewOfand the client prep view):- Emit one entry per valid action:
{enemyKey, count, gate: route.start, preDelay, elite}. - The client places models with §4.2: two zones, ≤ 50 models, elites always shown.
- Show upper-gate enemies in the upper zone and lower-gate enemies in the lower zone.
- The HUD count stays Σ count.
- Emit one entry per valid action:
- Bounty and 联防:
bountySpawns: attach bounties to the round template's firstlrsldr/yokaiaction (gate, preDelay, window spread).buildUniteWave: walkers → the escaped template'slrsldraction route; flyers →yokai; tokens →gopro_2/lazerd; 0.5 s spacing per owner.- Change
unite.helperOrderto the PRTS rule (§5).
- Performance (user item 4).
- Items 1 and 3 cut enemies per board by ≈ 55–70 % (R12: 80 → 35) and replace per-enemy A* with one BFS per destination.
- Measure again before adding other optimisations.
8. Open questions
| # | Question | Default |
|---|---|---|
| 1 | Do 射击台 (act1 m03 (10,3)/(10,4)) and 土石结构 block ground movement? | Block [ASSUMED] |
| 2 | Leader HP pool in solo, and in co-op with fewer than 4 alive players | co-op bloodPoint (× alive / 4 behind aliveScaling, off — ask the user, 2026-10-01); solo config value (flagged). Answered 2026-10-06 (the owner, PR #209): bloodPoint × the players alive at the fight's start, solo × 1 (DESIGN §25.13.4) |
| 3 | Unit of maxPlayTime |
Real seconds (×2 game) [ASSUMED] |
| 4 | Exact 联防 predelay formula (_CalculateActionPredelayConsiderUid) |
0.5 s per owner [ASSUMED] |
| 5 | DEF reduction in the 70 % / 80 % bases | HP/ATK only (下半 text) |
Everything else in §2–§4 is decoded from official data or client code.
Appendix A. Official client/server split for battles (relevant to user item 5) [DATA, metadata names]
- The protocol types show that each client simulates its own battle locally and reports events to the server:
AutoChessBattleSceneSelfBattleKillEnemyUp {enemyInstId, attackerInstId, damageSrc}…SelfBattleEnemyEscapeUp {enemyInstId, isToken}…SelfBattleAddBondUp {charInstId, layerDelta, bondIndexList}…SelfBattleFinishUp {round, escapedInstIds, escapedTokenInstIds, killedInsts, charBattleStatusList}…SelfBattleInfoUp {round, charBattleStatusList, charBattleInfoList}
- 联防 uses the same pattern:
…HelpBattleKillEnemyUp {enemyInstId, isToken, killedByPlayer, attackerInstId, damageSrc}…HelpBattleEnemyEscapeUp…HelpBattleFinishUp {round, escapedEnemies, killInfos}
- The server sends:
AutoChessBattleSelfBattleInfo {enemyInfos: [{enemyId, actionIndex, instIdList}]}AutoChessBattleHelpBattleInfo {escapedEnemies: [{ownerPlayerIndex, enemyId, enemyInstId, isToken}], players: [{deployment, charBattleStatusList}]}
- The helper client receives the partner's deployment and end-of-battle status and replays 联防 locally.
- Round enemy data is generated client-side with a shared seed (
RandomEnemyGenerater). Actions go to the server only ifShouldActionUpToServer.
Appendix B. Client method RVAs (Windows build in the user's CrossOver bottle, metadata v29) for re-verification
| method | RVA |
|---|---|
RandomEnemyGenerater._DoReplaceActionDataClient |
0x180b640f0 |
_CalculateEnemyCountByBattleEffectiveness |
0x180b63e70 |
_CalculateActionBattleEffectiveness |
0x180b63d50 |
_GetEnemyAttrPower |
0x180b65e10 |
_GenerateSpecialEnemyTypesLocal |
0x180b655c0 |
_GenerateRandomEnemyData |
0x180b64870 |
_DoReplaceLevelDataClient |
0x180b64580 |
_InitData |
0x180b663f0 |
SPFA._GenerateNextMapImpl |
0x180893720 |
SPFA._PostprocessAndMakeNextMapSmoothly |
0x1808940c0 |
SPFA._RaycastSegmentLine |
0x180894c80 |
Tile.get_moveCost |
0x18089cdc0 (obstacle-like ? 1000 : 1) |
GridPosition..cctor |
0x18210a800 (FOUR_WAYS = UP, RIGHT, DOWN, LEFT) |
AutoChessEnemyPreviewManager._GeneratePreviewData |
0x180b3cad0 |
AutoChessEnemyPreviewManager._GetBetterGridToSpawn |
0x180b3d180 |
AutoChessEnemyPreviewManager._InitIfNot |
0x180b3d710 |
AutoChessEnemyPreviewManager..ctor |
0x180b3e190 (row offsets 6 / 13) |
<_SpawnPreviewEnemies>d__16.MoveNext |
0x180b4fc40 (cap 50) |
AutoChessLevelEnemyManager._CalculateActionPredelay |
0x180b453c0 (ratio 0.05) |
AutoChessLevelEnemyManager._GetSpEnemyActionData |
0x180b466f0 |
Il2CppCodeGenModule for Assembly-CSharp is at VA 0x186e69af0 (217 790 method pointers at 0x188216460).