637 lines
55 KiB
Markdown
637 lines
55 KiB
Markdown
# 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 disassembling `GameAssembly.dll`. The IL2CPP method RVAs are listed in Appendix B.
|
||
- **[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
|
||
|
||
1. **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).
|
||
2. **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].
|
||
3. **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.
|
||
4. **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.
|
||
5. **Custom balance to remove:**
|
||
- All of `data/tuning.json` `enemyHpMul` / `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.
|
||
|
||
---
|
||
|
||
## 1. Sources and method
|
||
|
||
- **Game data [DATA]:**
|
||
- `autoChessData.constData`: `min/maxReplacedEnemyCount` 1/5, `templateEnemy*`, `maxLevelCnt` 15, `specialEnemyNum` 3, `enemyTypeIdentifierToFillRandom` 1, and the BE factors 1/5/3/3.
|
||
- `act2autochess.specialEnemyInfoDict` (67 entries) and `specialEnemyRandomTypeDict` (count 3 / weight 3 per random type; SPECIAL −1).
|
||
- `modeDataDict[*].inactiveEnemyKey`, the round templates `level_act1autochess_01…h06` and `h07_*`/`h08_*`/`escaped_*`, the stage files `level_act{1,2}autochess_m0X`, and `enemy_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`, the `GridPosition` static constructor, `AutoChessEnemyPreviewManager`, and `AutoChessLevelEnemyManager`.
|
||
- **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
|
||
```
|
||
|
||
- `inactiveEnemyKey` filters **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_database` values. The `level_autochess_enemy_data` overrides (灼藤 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 at `preDelay + i·step`.
|
||
- `step = max(window/n', 0.05·window)`, where `window = 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: `lrnazg` 1+1 walk and `frostd` 1+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^kH` table with base 0.7 / 0.8 / 1 is the **only** source [WIKI, PRTS 下半 §开始模拟, flagged "敌方属性加成情况由用户提供"].
|
||
- `data/config.json → modes[*].enemyScale` reproduces 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.js` over 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`]
|
||
|
||
1. **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**.
|
||
- `PathRequest` is `{targetPos, motionMode, allowDiagonalMove}`.
|
||
- It uses a FIFO queue (SPFA), seeded with the destination at `dist` 0.
|
||
2. **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**.
|
||
3. **Edge cost.** The cost is `Tile.moveCost` of the neighbour: **1**, or **1000** if the tile is "obstacle-like" (a tile holding an active 阻隔工事). FLY always costs 1.
|
||
4. **Passability.** A neighbour is used only if it is passable for the motion. For WALK that is `passableMask` ALL (floor, road, start, end, telin, telout, mire, smog, deepsea, infection). `fence_bound`, wall and forbidden tiles are **not** walkable.
|
||
5. **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.
|
||
6. **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`).
|
||
7. **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`).
|
||
8. **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.js` is **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 `ignoreObstacles` fallback) 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 **下半** `ver2` image shows none. The current level file has all m02 crates and platforms `hidden`.
|
||
- 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 when `grid.version` changes.
|
||
- 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 use `dist_D[t]` plus the leg tail. The official exposes `TryCalculatePathFindingDistance` on 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
|
||
|
||
1. **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 by `preDelay` (`_CompareActionByTime`).
|
||
2. **Thinning.** Let `total = Σ count`.
|
||
- `showRatio = min(1, 50/total)` and `totalShowCnt = 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.
|
||
3. **Pen and zones.**
|
||
- The pen is `enemy_place_rect ((14,7),(18,13))` (stage `configBlackBoard`).
|
||
- The zone anchors are the `tile_start` tiles in the rect: **(15,7)** and **(18,7)**.
|
||
- Every other pen tile with `passableMask` ALL and without `previewNotAlloed` belongs 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).
|
||
4. **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 with `anchor.row ≤ target.row`.
|
||
- Result: lower gate (9,10) → the (15,7) zone; upper gate (12,10) → the (18,7) zone.
|
||
5. **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.
|
||
6. **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) or `templateEnemyNormalFly` (flyer). Summoned tokens use `templateEnemyToken` / `templateEnemyTokenFly` instead.
|
||
- 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 `lrsldr` action (route 3: WALK from (9,18) via (9,10), or from (9,10) in `escaped_single`; preDelay 3, interval 8),
|
||
- flyers → the `yokai` action (route 0),
|
||
- tokens → `gopro_2` / `lazerd`.
|
||
- Elite and special leakers are **not** routed through the E/S actions (the class is ignored).
|
||
- Timing is recomputed per owner uid (`_CalculateActionPredelayConsiderUid`; constants `ACTION_INTERVAL_FOR_UID` 0.5 s, `MAX_ACTION_INTERVAL_FOR_UID` 5 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)
|
||
|
||
1. **`server/match/waves.js` + `data/factions.json`** (and the data generator in `tools/`):
|
||
- **Match setup.** `setupMatchWaves` returns `{stageId, factions: [3 types], typeSlots: [15 types], bossId, hiddenBossId}`.
|
||
- Shuffle the 6 `involveRandom` types and take 3.
|
||
- Push each `count` (3) times, fill to `maxLevelCnt` (15) with SPECIAL, then Fisher-Yates-shuffle with the match rng.
|
||
- Store `typeSlots` in match state. It is visible in the briefing as the 3 types, and the slot order stays hidden.
|
||
- **Round pick.** Add `pickRoundEntry(gd, rng, typeSlots, r)`:
|
||
- half = `r <= 7`;
|
||
- candidates = `specialEnemyInfoDict` of that type and half, whose **special** key is not in `mode.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.
|
||
- **Replacement.** Replace `templateSpawns` / `replaceSlot` as 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 at `time + i·max(n·iv/n', 0.05·n·iv)`.
|
||
- `P = hp + 5·atk + 3·def + 3·res` from the `enemy_database` level-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`, the `k` fields (`kS`, `N[].k`, `E[].k`), `flyPlaceholders`, `groundRouteLike` and the `inactiveIn` filtering of attached keys.
|
||
- **Leader rounds.** `buildBossWave` uses 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.
|
||
2. **Stat scaling and balance.**
|
||
- Delete every `enemyHpMul`, `enemyAtkMul` and `bossHpMul` in `data/tuning.json`. Keep the `titles` block.
|
||
- Remove the `soloMul` in `bountySpawns`.
|
||
- `config.bossHpScale`: co-op pool = `bloodPoint[difficulty]` with no alive-player factor; solo stays a flagged config value. (2026-10-01: × alive / 4 available behind `aliveScaling`, off, see §6 #7.)
|
||
- Keep `enemyScale` (PRTS table) and the 终极 speed ×1.15 from R3.
|
||
- Re-run `tools/balance.mjs --tuning off` only to report, not to tune.
|
||
3. **Pathing** (`server/sim/grid.js`, `server/sim/ai.js`, `server/sim/Battle.js` — since 0.2.0 `server/sim/battle/spawns.js` / `tiles.js`):
|
||
- Add `Grid.flowField(dest)` as in §3.4, cached per `(dest, version)`.
|
||
- `planLeg` for 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: `setObstacle` marks a cost-1000 obstacle-like tile, not an impassable one. The existing "blocked enemy attacks the crate" logic stays.
|
||
- `data/stages.json`: set act1 m02 `trap_1105_accrate#001…#004` to `active: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.
|
||
4. **Preview** (`previewOf` and 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.
|
||
5. **Bounty and 联防**:
|
||
- `bountySpawns`: attach bounties to the round template's first `lrsldr` / `yokai` action (gate, preDelay, window spread).
|
||
- `buildUniteWave`: walkers → the escaped template's `lrsldr` action route; flyers → `yokai`; tokens → `gopro_2` / `lazerd`; 0.5 s spacing per owner.
|
||
- Change `unite.helperOrder` to the PRTS rule (§5).
|
||
6. **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) |
|
||
| 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 if `ShouldActionUpToServer`.
|
||
|
||
## 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).
|