feedback5 WY: the Final Assault / Hidden Core leader pool is bloodPoint × the players alive when the fight starts (solo × 1), replacing 「保持固定血量」; 胄's drone share passes 限伤 (from PR #209 by @qingjingshenghuo; the owner's decision of 2026-10-06)
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.
This commit is contained in:
1 parent
661f1c074e
commit
4e58481706
33 files changed
+778
-531
No files matched your search
+1
-1
@@ -1 +1 @@
|
||||
{"season":"act2autochess","seasonName":"卫戍协议:盟约","modes":{"mode_training_1":{"modeId":"mode_training_1","name":"入门协议","code":"AC-TR-1","sortId":1,"type":"LOCAL","difficulty":"TRAINING","inScope":false,"color":"#4ed8af","iconId":"mode_training_icon","backgroundId":"background_training","desc":"用以熟悉协议内容的模式","effectDescList":["·仅可使用预置的调配干员","·无奖励","·无法用于解锁策略"],"unlockText":null,"specialPhaseTime":150,"activeBondIds":["yanShip","sargonShip","victoriaShip","kjeragShip","lateranoShip","egirShip","siracusaShip","kazimierzShip","preciShip","swiftShip","skillfulShip","arcaneShip","steadShip","deputShip","visiShip","miraShip","investShip","raidShip","indomShip","maniShip","emptyShip","soloShip","suntShip"],"inactiveBondIds":[],"inactiveEnemyKeys":[],"lastRound":4,"bossRound":4,"hiddenRound":null,"rounds":{"1":{"template":"act1autochess_tr01","bossTemplates":null,"combatTimeLimit":40,"levelMaxPlayTime":40,"prepTime":999,"prepTimeData":999,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"2":{"template":"act1autochess_tr02","bossTemplates":null,"combatTimeLimit":45,"levelMaxPlayTime":45,"prepTime":999,"prepTimeData":999,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"3":{"template":"act1autochess_tr02","bossTemplates":null,"combatTimeLimit":45,"levelMaxPlayTime":45,"prepTime":999,"prepTimeData":999,"isSpPrepare":true,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"4":{"template":null,"bossTemplates":{"boss_7":"act1autochess_tr04"},"combatTimeLimit":null,"levelMaxPlayTime":180,"prepTime":999,"prepTimeData":999,"isSpPrepare":false,"isBoss":true,"isHidden":false,"bossOvertimeAfter":150}},"spRounds":[3],"combatTimeLimit":{"1":40,"2":45,"3":45,"4":null},"enemyScale":{"1":{"atk":1,"hp":1,"speed":1,"assumed":true},"2":{"atk":1,"hp":1,"speed":1,"assumed":true},"3":{"atk":1,"hp":1,"speed":1,"assumed":true},"4":{"atk":1,"hp":1,"speed":1,"assumed":true}},"bossHpScale":{"bloodPointKey":null,"coop":1,"aliveScaling":false,"aliveFull":4,"aliveAssumed":true,"unaffectedByEnemyScale":true},"upgradePrices":[4,4,6,9,12],"maxShopLevel":6,"shopSlots":{"1":{"chess":3,"item":0},"2":{"chess":3,"item":0},"3":{"chess":3,"item":0},"4":{"chess":3,"item":0},"5":{"chess":3,"item":1},"6":{"chess":3,"item":1}},"levelTagColors":{"1":"#434343","2":"#626654","3":"#445760","4":"#615B74","5":"#6C5E41","6":"#5d341e"},"stages":["act1autochess_m01"],"bossWeights":{"boss_7":5},"hiddenBossWeights":{}},"mode_single_funny":{"modeId":"mode_single_funny","name":"标准模拟","code":"AC-1","sortId":2,"type":"SINGLE","difficulty":"FUNNY","inScope":true,"color":"#f6a329","iconId":"mode_funny_icon","backgroundId":"background_funny","desc":"时长较短的模拟训练","effectDescList":["·可以快速完成作战","·常规奖励"],"unlockText":"通关【入门协议】后解锁","specialPhaseTime":150,"activeBondIds":["yanShip","sargonShip","victoriaShip","kjeragShip","siracusaShip","preciShip","swiftShip","steadShip","deputShip","visiShip","indomShip","maniShip","emptyShip"],"inactiveBondIds":["lateranoShip","egirShip","kazimierzShip","skillfulShip","arcaneShip","miraShip","investShip","raidShip","soloShip","suntShip"],"inactiveEnemyKeys":["enemy_1112_emppnt","enemy_1207_sfji","enemy_1275_dwlock_2","enemy_1234_dsubrl_2","enemy_10122_uacann_2","enemy_1272_nhtank_2","enemy_9008_acbunn","enemy_1174_duholy_2","enemy_1355_mrfly_2","enemy_1197_sfshu_2","enemy_10067_ftsjc","enemy_1270_nhstlk_2","enemy_1283_sgkill","enemy_1175_dushdo_2","enemy_1254_lypa_2","enemy_1425_lrcmra_2","enemy_1329_cbshld_2","enemy_1072_dlancer"],"lastRound":9,"bossRound":9,"hiddenRound":null,"rounds":{"1":{"template":"act1autochess_01","bossTemplates":null,"combatTimeLimit":45,"levelMaxPlayTime":45,"prepTime":null,"prepTimeData":300,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"2":{"template":"act1autochess_02","bossTemplates":null,"combatTimeLimit":45,"levelMaxPlayTime":45,"prepTime":null,"prepTimeData":300,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"3":{"template":"act1autochess_03","bossTemplates":null,"combatTimeLimit":55,"levelMaxPlayTime":55,"prepTime":null,"prepTimeData":300,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"4":{"template":"act1autochess_04","bossTemplates":null,"combatTimeLimit":55,"levelMaxPlayTime":55,"prepTime":null,"prepTimeData":300,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"5":{"template":"act1autochess_05","bossTemplates":null,"combatTimeLimit":55,"levelMaxPlayTime":55,"prepTime":null,"prepTimeData":300,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"6":{"template":"act1autochess_06","bossTemplates":null,"combatTimeLimit":70,"levelMaxPlayTime":70,"prepTime":null,"prepTimeData":300,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfLine truncated
|
||||
{"season":"act2autochess","seasonName":"卫戍协议:盟约","modes":{"mode_training_1":{"modeId":"mode_training_1","name":"入门协议","code":"AC-TR-1","sortId":1,"type":"LOCAL","difficulty":"TRAINING","inScope":false,"color":"#4ed8af","iconId":"mode_training_icon","backgroundId":"background_training","desc":"用以熟悉协议内容的模式","effectDescList":["·仅可使用预置的调配干员","·无奖励","·无法用于解锁策略"],"unlockText":null,"specialPhaseTime":150,"activeBondIds":["yanShip","sargonShip","victoriaShip","kjeragShip","lateranoShip","egirShip","siracusaShip","kazimierzShip","preciShip","swiftShip","skillfulShip","arcaneShip","steadShip","deputShip","visiShip","miraShip","investShip","raidShip","indomShip","maniShip","emptyShip","soloShip","suntShip"],"inactiveBondIds":[],"inactiveEnemyKeys":[],"lastRound":4,"bossRound":4,"hiddenRound":null,"rounds":{"1":{"template":"act1autochess_tr01","bossTemplates":null,"combatTimeLimit":40,"levelMaxPlayTime":40,"prepTime":999,"prepTimeData":999,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"2":{"template":"act1autochess_tr02","bossTemplates":null,"combatTimeLimit":45,"levelMaxPlayTime":45,"prepTime":999,"prepTimeData":999,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"3":{"template":"act1autochess_tr02","bossTemplates":null,"combatTimeLimit":45,"levelMaxPlayTime":45,"prepTime":999,"prepTimeData":999,"isSpPrepare":true,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"4":{"template":null,"bossTemplates":{"boss_7":"act1autochess_tr04"},"combatTimeLimit":null,"levelMaxPlayTime":180,"prepTime":999,"prepTimeData":999,"isSpPrepare":false,"isBoss":true,"isHidden":false,"bossOvertimeAfter":150}},"spRounds":[3],"combatTimeLimit":{"1":40,"2":45,"3":45,"4":null},"enemyScale":{"1":{"atk":1,"hp":1,"speed":1,"assumed":true},"2":{"atk":1,"hp":1,"speed":1,"assumed":true},"3":{"atk":1,"hp":1,"speed":1,"assumed":true},"4":{"atk":1,"hp":1,"speed":1,"assumed":true}},"bossHpScale":{"bloodPointKey":null,"unaffectedByEnemyScale":true},"upgradePrices":[4,4,6,9,12],"maxShopLevel":6,"shopSlots":{"1":{"chess":3,"item":0},"2":{"chess":3,"item":0},"3":{"chess":3,"item":0},"4":{"chess":3,"item":0},"5":{"chess":3,"item":1},"6":{"chess":3,"item":1}},"levelTagColors":{"1":"#434343","2":"#626654","3":"#445760","4":"#615B74","5":"#6C5E41","6":"#5d341e"},"stages":["act1autochess_m01"],"bossWeights":{"boss_7":5},"hiddenBossWeights":{}},"mode_single_funny":{"modeId":"mode_single_funny","name":"标准模拟","code":"AC-1","sortId":2,"type":"SINGLE","difficulty":"FUNNY","inScope":true,"color":"#f6a329","iconId":"mode_funny_icon","backgroundId":"background_funny","desc":"时长较短的模拟训练","effectDescList":["·可以快速完成作战","·常规奖励"],"unlockText":"通关【入门协议】后解锁","specialPhaseTime":150,"activeBondIds":["yanShip","sargonShip","victoriaShip","kjeragShip","siracusaShip","preciShip","swiftShip","steadShip","deputShip","visiShip","indomShip","maniShip","emptyShip"],"inactiveBondIds":["lateranoShip","egirShip","kazimierzShip","skillfulShip","arcaneShip","miraShip","investShip","raidShip","soloShip","suntShip"],"inactiveEnemyKeys":["enemy_1112_emppnt","enemy_1207_sfji","enemy_1275_dwlock_2","enemy_1234_dsubrl_2","enemy_10122_uacann_2","enemy_1272_nhtank_2","enemy_9008_acbunn","enemy_1174_duholy_2","enemy_1355_mrfly_2","enemy_1197_sfshu_2","enemy_10067_ftsjc","enemy_1270_nhstlk_2","enemy_1283_sgkill","enemy_1175_dushdo_2","enemy_1254_lypa_2","enemy_1425_lrcmra_2","enemy_1329_cbshld_2","enemy_1072_dlancer"],"lastRound":9,"bossRound":9,"hiddenRound":null,"rounds":{"1":{"template":"act1autochess_01","bossTemplates":null,"combatTimeLimit":45,"levelMaxPlayTime":45,"prepTime":null,"prepTimeData":300,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"2":{"template":"act1autochess_02","bossTemplates":null,"combatTimeLimit":45,"levelMaxPlayTime":45,"prepTime":null,"prepTimeData":300,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"3":{"template":"act1autochess_03","bossTemplates":null,"combatTimeLimit":55,"levelMaxPlayTime":55,"prepTime":null,"prepTimeData":300,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"4":{"template":"act1autochess_04","bossTemplates":null,"combatTimeLimit":55,"levelMaxPlayTime":55,"prepTime":null,"prepTimeData":300,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"5":{"template":"act1autochess_05","bossTemplates":null,"combatTimeLimit":55,"levelMaxPlayTime":55,"prepTime":null,"prepTimeData":300,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"6":{"template":"act1autochess_06","bossTemplates":null,"combatTimeLimit":70,"levelMaxPlayTime":70,"prepTime":null,"prepTimeData":300,"isSpPrepare":false,"isBoss":false,"isHidden":false,"bossOvertimeAfter":null},"7":{"template":"act1autochess_07","bossTemplates":nuLine truncated
|
||||
+12
-8
@@ -13,9 +13,11 @@ kinds / counts and routes must follow the official game; research 08 §6–§7,
|
||||
`k` copies, no kept fly placeholders; verified against all 429 official entry × round compositions and the official
|
||||
count distribution (同盟 险境 R3 ≤ 10 enemies, R13 ≈ 37 — the old generator averaged 28 / 81);
|
||||
* stats = the PRTS per-round `enemyScale` table only (`data/config.json`, 终极 ×1.15 speed from R3);
|
||||
* leader pool = `bloodPoint[difficulty]` in co-op — one pool for every boss field, whatever the number of alive players
|
||||
(config `bossHpScale.aliveScaling` would make it × alive / 4; off — the user chose the fixed pool, DESIGN §20.10); solo keeps ×0.25,
|
||||
flagged [ASSUMED] in data;
|
||||
* leader pool = `bloodPoint[difficulty]` × the players alive when the fight starts — one pool for every boss field; solo
|
||||
× 1 (the owner's decision of 2026-10-06, adopting PR #209: it replaces the fixed pool of DESIGN §20.10, co-op
|
||||
bloodPoint whatever the count and solo × 0.25 [ASSUMED] — config `bossHpScale.perPlayer: false`, `solo: 0.25` restores
|
||||
it; DESIGN §25.13.4). The bots' solo matches got 4× the leader HP: in the golden corpus 4 of the 6 solo matches that
|
||||
reached and won the Final Assault now lose it (team LP 0 with 93k–176k of the pool left);
|
||||
* leader parts and drones (DESIGN §20.10): a 剑 / 锤 / 碎铳之簧 passes every damage it takes to its leader 1:1 (PRTS "等量的
|
||||
无来源生命流失"; until 2026-10-01 half, and a 剑 / 锤 only while grounded — the dive hits [ASSUMED], the handbook says
|
||||
"被击落时"); a 胄 drone that dies (whoever kills it, DESIGN §20.13) costs 2 % of the leader's shown max HP = the pool (unchanged from v2.5; which "最大生命值" the
|
||||
@@ -68,7 +70,7 @@ and runs the real
|
||||
`Battle` with full content (kits, talents, bonds with layers, IN_BATTLE 特质, equipment, bands, summons). Co-op rounds
|
||||
field **4 boards** against the same wave and then run **联防** exactly like the match (`unite.js`: ≤ 2 perfect helpers,
|
||||
only the survivors cost their source LP). Boss rounds build the Final Assault fields exactly like `Match.startFinalAssault`
|
||||
(pairs / `_s` for a lone player, shared pool `GameData.bossPoolHp` = `bloodPoint` (solo ×0.25), merged team LP of 15 per player, leaks' `lpr`,
|
||||
(pairs / `_s` for a lone player, shared pool `GameData.bossPoolHp` = `bloodPoint` × the players alive (solo × 1, §25.13.4), merged team LP of 15 per player, leaks' `lpr`,
|
||||
−1 LP/s after 150 s) and record the pool damage by 150 s, the kill time and the win rate.
|
||||
|
||||
Metrics: **capped leaks** = min(leaks, 10) per board and round (what that board alone would lose; the targets below
|
||||
@@ -199,8 +201,8 @@ earlier commit (a current run differs for every leader) and were not refreshed h
|
||||
|
||||
Reading: the early game is easy everywhere (official R1–R3 bring 3–10 enemies); leaks concentrate on the second-half
|
||||
specials (深池逐火 TIMES embers, stealth 隐形弩手组长 / 重弩突袭者, 疯狂的逐腐兽, 掠海漂移体) and 终极 R6–R11, where co-op
|
||||
boards still leak 3–6 and 联防 halves the LP cost. Solo leaders (bloodPoint × 0.25 [ASSUMED]) are the hard part of the
|
||||
solo modes; the solo pool factor is the one open number here (research 08 §8 #2).
|
||||
boards still leak 3–6 and 联防 halves the LP cost. Solo leaders (bloodPoint × 0.25 [ASSUMED] when these tables were
|
||||
measured; × 1 since the owner's decision of 2026-10-06, §25.13.4) are the hard part of the solo modes.
|
||||
|
||||
Reproduce: `node tools/balance.mjs --mode all --difficulty ALL --tuning off [--bots 5] [--json]`.
|
||||
|
||||
@@ -422,6 +424,8 @@ on a leader is cancelled (`MAX_BATTLE_DAMAGE`: 0 damage, nothing to the pool —
|
||||
`--profile` above ≈ 4.9 would have pushed the 170-layer R15 core curve, +20 % jitter, past 999) and
|
||||
`matchrun.mjs --layers N` adds at most the room left under 999, so `--check` stays clean on a boosted run.
|
||||
- **Pool size and the line**: a drone that dies (whoever kills it, DESIGN §20.13) costs 假想敌:胄 0.02 × the pool
|
||||
max (【死亡集群】, boss_1 / boss_8; `bosses.js DRONE_LINK_BASE 'pool'`). Today that is at most 144000 (boss_8 ABYSS 7.2M) and lands; a pool above 14999950 would make every
|
||||
drone kill a cancelled hit. Re-check this whenever the pool size changes (research 11 §6).
|
||||
max (【死亡集群】, boss_1 / boss_8; `bosses.js DRONE_LINK_BASE 'pool'`). With the pool per player alive (§25.13.4) the
|
||||
hidden 胄 终极 pool is 21.6M / 28.8M at 3 / 4 players, so a drone is 432000 / 576000 — above the line; the link is a
|
||||
share, no hit, and passes it (`Battle.loseHp noHitLimit` [ASSUMED]: research 11 §2.1 checks every damage modifier, but
|
||||
which max HP the official link reads is not documented). Re-check this whenever the pool size changes (research 11 §6).
|
||||
|
||||
+8
-7
@@ -71,7 +71,7 @@ Top level: `{ season, seasonName, modes, economy, lpCapPerRound, bossOvertimeAft
|
||||
| `spRounds` | `[3,9,11]` | rounds whose prep opens with a 机变 draft |
|
||||
| `combatTimeLimit[r]` | `{"1":45,…,"14":null}` | = `rounds[r].combatTimeLimit`: **real** seconds of the forced-2× battle (DESIGN §4) |
|
||||
| `enemyScale[r]` | `{"atk":1.1,"hp":1.2,"speed":1,"kAtk":1,"kHp":1}` | non-boss enemy multipliers (research 01 A3): `atk = atkBase·1.1^kAtk`, `hp = hpBase·1.2^kHp·extra`, `speed` 1.15 on ABYSS from R3. Apply to level-0 stats after special-enemy replacement, also to bounty/special enemies. **Leader HP pools excluded.** |
|
||||
| `bossHpScale` | multi `{"bloodPointKey":"bloodPointAbyss","coop":1,"aliveScaling":false,"aliveFull":4,"aliveAssumed":true,…}` · solo `{"bloodPointKey":"bloodPoint","solo":0.25,"soloAssumed":true,…}` | one pool for every boss field: co-op = `bosses[id].bloodPoint[difficulty]` whatever the number of alive players; `aliveScaling: true` would scale it × alive / `aliveFull` (巴哈姆特 12294 "隊友變少,最後boss血條也會變少" — one community note, no proportion: off until confirmed, the proportion [ASSUMED, flagged `aliveAssumed`]); solo = × `solo` [ASSUMED, flagged]. `GameData.bossPoolHp(bossId, aliveCount)` / `bossPoolShare` implement it (DESIGN §20.10) |
|
||||
| `bossHpScale` | `{"bloodPointKey":"bloodPointAbyss","unaffectedByEnemyScale":true}` | which `bloodPoint` column the mode reads; the pool rule's keys live in the global `bossHpScale` below — one set here would override it for this mode (`gamedata.js bossPoolShareOf`) |
|
||||
| `upgradePrices` | `[5,8,11,12,13]` | base price L1→2 … L5→6 (−1 per round start, floor 0, reset after upgrade) |
|
||||
| `maxShopLevel` | `6` | |
|
||||
| `shopSlots[level]` | `{"1":{"chess":3,"item":1},…}` | operator + item slots per shop level |
|
||||
@@ -119,7 +119,7 @@ Top level: `{ season, seasonName, modes, economy, lpCapPerRound, bossOvertimeAft
|
||||
|---|---|---|
|
||||
| `lpCapPerRound` | `10` | max LP lost per player per normal round |
|
||||
| `bossOvertimeAfter`, `bossOvertimeDrainPerSec` | `150`, `1` | official `bossTurnHpReduceTime`: from 150 **real** s of a boss round the merged team LP loses 1 per whole real second (`gamedata.js bossOvertimeDue`; first point at 151 s) |
|
||||
| `bossHpScale` | `{"formula":"co-op: bloodPoint[difficulty] — one pool for every field …","coop":1,"solo":0.25,"soloAssumed":true,"aliveScaling":false,"aliveFull":4,"aliveAssumed":true,…}` | global form of the mode rule |
|
||||
| `bossHpScale` | `{"formula":"one pool for every boss field …","perPlayer":true,"coop":1,"solo":1,"aliveFull":4,"aliveScaling":false,"aliveAssumed":true,…}` | the leader pool (one for every boss field) = `bosses[id].bloodPoint[difficulty]` × share: `perPlayer` true (the owner's decision of 2026-10-06, adopting PR #209 — it replaces the fixed pool of 「保持固定血量」) — co-op share = `coop` × the players alive when the fight starts (bots and AI 托管 seats count, eliminated and departed seats do not; at most `aliveFull`), solo share = `solo` (1). `perPlayer: false` with `solo: 0.25` restores the fixed pool of 0.1.x (co-op × `coop` whatever the count; `aliveScaling: true` would make it × alive / `aliveFull`, the proportion [ASSUMED, flagged `aliveAssumed`]). `GameData.bossPoolHp(bossId, aliveCount)` / `bossPoolShare` implement it (DESIGN §20.10, §25.13.4) |
|
||||
| `hiddenCore` | `{"single":350,"multi":1200,"minTeamLpExclusive":1,"difficulties":["NORMAL","HARD","ABYSS"],"checkedAfterRound":14}` | Σ activated layers must be **>** threshold and LP **>** 1 |
|
||||
| `dp` | `{"init":10,"perSec":1,"max":99}` | |
|
||||
| `unite` | `{"maxHelpers":2,"helperOrder":"unitsOnField>activeBond>undownedUnits; pair: unitsOnField>activeBond>activeLayers>undownedUnits, first = right field (PRTS 帮助)","layerGainsEnabled":false,"templates":{"1":"act1autochess_escaped_single","2":"act1autochess_escaped_multi"},…}` | 联防; `helperOrder` documents the rule `server/match/unite.js helperOrder` implements (research 08 §5; ties → seat) |
|
||||
@@ -538,11 +538,12 @@ Glyph legend (`rows`):
|
||||
15. 炎佑 name in the enemy DB is `"炎佑"` with literal ASCII quotes.
|
||||
16. Research-only fields (null without `docs/research`): stage `name`, item `category/kind/family/implFormula/
|
||||
requiresBondId/rangeGrid/flavor`, bond `spec`, E2 art availability (falls back to "char has an E2 phase").
|
||||
17. [ASSUMED] content (flagged in data): 机变 family schedule and server pools, the solo leader pool factor
|
||||
(`bossHpScale.solo` 0.25), title criteria, income cap 12, per-turn band-draft timer 30 s (`timers.bandTurn`, the
|
||||
step's only countdown — user playtest #4), the shop-only item list (`SHOP_EXCLUDED_ITEMS`, from play), the alive / 4
|
||||
proportion of the optional co-op alive scaling (`bossHpScale.aliveAssumed`; `aliveScaling` off). The special-enemy
|
||||
generator, the co-op leader pool (`bloodPoint`, one pool for every field) and the 联防 timing are official.
|
||||
17. [ASSUMED] content (flagged in data): 机变 family schedule and server pools, title criteria, income cap 12, per-turn
|
||||
band-draft timer 30 s (`timers.bandTurn`, the step's only countdown — user playtest #4), the shop-only item list
|
||||
(`SHOP_EXCLUDED_ITEMS`, from play), the alive / 4 proportion of the fixed pool's optional alive scaling
|
||||
(`bossHpScale.aliveAssumed`; `aliveScaling` off). The special-enemy generator, the leader table (`bloodPoint`, one
|
||||
pool for every field) and the 联防 timing are official; the pool's × players alive is the owner's decision of
|
||||
2026-10-06 (PR #209: players' observation, no official text).
|
||||
18. **Module parts flagged `isToken`** (伺夜, 浊心斯卡蒂, 缪尔赛思, 耀骑士临光 golden) upgrade the summon only; they are
|
||||
applied to `tokens.json` variants, never to the operator's talents/trait.
|
||||
19. **Undefined enemy-database fields** (`m_defined:false`): a zero `m_value` means "never set" and falls back to the
|
||||
|
||||
+16
-13
@@ -183,8 +183,8 @@ A `choice:<effectId>` registry handler overrides the default application (§2.4)
|
||||
* `g.autoplay { on }` ("AI 托管"): the bot plays the seat (drafts, buying, placement, ready) until turned off.
|
||||
* `onLeave` (quit / reconnect window expired): 中途退出 counts as elimination (research 00-INDEX §3, 01 §9, 06 §7 /
|
||||
§10.3): every copy the seat holds returns to the shared pool at once; the seat leaves the round loop and the Final
|
||||
Assault pairing (re-planned when it quits before the boss fight; the boss pool stays bloodPoint — it would shrink to
|
||||
× alive / 4 only with config `bossHpScale.aliveScaling`, off — the user chose the fixed pool, DESIGN §20.10); its
|
||||
Assault pairing (re-planned when it quits before the boss fight) and the boss pool (bloodPoint per player alive when
|
||||
the fight starts — a departed seat no longer counts, DESIGN §25.13.4); its
|
||||
running normal battle is force-ended; a pending band pick
|
||||
becomes the default band and a 机变 turn passes on. Status `left`, LP 0, rounds passed = the rounds it had survived.
|
||||
When no human is left at all the match ends immediately (`reason: 'abandoned'`); when only eliminated spectators are
|
||||
@@ -626,13 +626,14 @@ Any `choice:` handler whose EffectRef reuses its own key must guard like this (o
|
||||
(own phase perfect) go to pending funds; bounty rounds decrement; LP ≤ 0 ⇒ eliminated (all copies back to the pool).
|
||||
* **Final Assault / Hidden Core**: finalAssault.js header. Boards are passed in board coordinates; the sim maps board
|
||||
rows 9–12 onto boss rows 2–5 (`BOSS_ROW_OFFSET` −7, matching every stage's boss rows) and mirrors the right side.
|
||||
Pool (`finalAssault.js bossPoolHp` → `GameData.bossPoolShare`, DESIGN §20.10): one pool shared by every boss field
|
||||
(official tip "最终攻势中,所有人将一起对敌方领袖造成伤害"); co-op = `bloodPoint[difficulty]` whatever the number of alive
|
||||
players (notice 5114's "敌方领袖的总生命值不变" is about the mirrored copies of a pair field sharing the pool, not about that
|
||||
number); `bossHpScale.aliveScaling` true (default false) would scale it × alive / `aliveFull` (4) — 巴哈姆特 12294
|
||||
"聯機隊友(撤退/死掉)變少,最後boss血條也會變少" is one community note without a proportion, kept off until the user confirms
|
||||
it (it would shorten the fight after eliminations, the opposite of the playtest report); solo = ×
|
||||
`bossHpScale.solo` (0.25 = one player of four [ASSUMED]); leaders are never scaled by `enemyScale`. The merged team LP loses leaks (`lpr`), the overtime drain
|
||||
Pool (`finalAssault.js bossPoolHp` → `GameData.bossPoolShare` → `gamedata.js bossPoolShareOf`, DESIGN §20.10,
|
||||
§25.13.4): one pool shared by every boss field (official tip "最终攻势中,所有人将一起对敌方领袖造成伤害"; notice 5114's
|
||||
"敌方领袖的总生命值不变" is about the mirrored copies of a pair field sharing it) = `bloodPoint[difficulty]` × the players
|
||||
alive when the fight starts (bots and AI 托管 seats count, eliminated and departed seats do not; solo × 1) — the owner's
|
||||
decision of 2026-10-06, adopting PR #209 by @qingjingshenghuo (players' observation: bloodPoint is one player's share),
|
||||
which replaces the fixed pool of 「保持固定血量」 (config `bossHpScale.perPlayer: false` with `solo: 0.25` restores it;
|
||||
its optional × alive / 4, `aliveScaling`, stays off); leaders are never scaled by `enemyScale`, their parts and escorts
|
||||
keep their own HP. The merged team LP loses leaks (`lpr`), the overtime drain
|
||||
(`bossTurnHpReduceTime` 150 counts REAL seconds, like the boss level's 120 s maxPlayTime that runs out first — the
|
||||
battle goes on — so 1 LP per real second from 150 real s = 300 game s on the 2× field clock; `gd.bossOvertimeDue`)
|
||||
and leader "扣除目标生命" effects (the sim's `lpLoss` hook: boss_7 Doom, 斥退 …); after every change it is written back
|
||||
@@ -659,13 +660,15 @@ Any `choice:` handler whose EffectRef reuses its own key must guard like this (o
|
||||
**限伤** (research 11 §2; client `AutoChessStepModeManager._OnBossEnemyTakeDamage`): in both boss rounds a single hit
|
||||
on a leader with ceil(damage) ≥ `BOSS_HIT_LIMIT` = 300000 (shared/constants.js; 0 / Infinity = off) is cancelled in the
|
||||
sim (`sim/damage.js leaderHitCancelled`, SIM.md §4) — it deals 0 and nothing reaches the shared pool, the per-player
|
||||
boss damage or the BOSS_HIT tickers; minions, parts, normal rounds and 联防 are unaffected. The server's own runs, the
|
||||
browsers' runs and the server's verification share the rule, so digests agree.
|
||||
boss damage or the BOSS_HIT tickers; minions, parts, normal rounds and 联防 are unaffected. 胄's drone link (2 % of the
|
||||
pool when a drone dies) is a share, no hit, and passes the limit (`Battle.loseHp noHitLimit`; with the per-player pool
|
||||
a hidden 胄 终极 drone is 432 000 / 576 000 at 3 / 4 players [ASSUMED], §25.13.4). The server's own runs, the browsers'
|
||||
runs and the server's verification share the rule, so digests agree.
|
||||
|
||||
### 3.1 Balance layer (data/tuning.json)
|
||||
`data/config.json` is generated and stays research-faithful. There is **no custom balance** any more (DESIGN §14
|
||||
corrections, research 08 §6): enemy numbers are the official ones (the PRTS `enemyScale` table, leader pool =
|
||||
`bloodPoint`). `data/tuning.json` (hand-maintained, loaded as `data.tuning`, layered on the config by gamedata.js only)
|
||||
corrections, research 08 §6): enemy numbers are the official ones (the PRTS `enemyScale` table, the leader table
|
||||
`bloodPoint` — × the players alive by the owner's decision of 2026-10-06, §25.13.4). `data/tuning.json` (hand-maintained, loaded as `data.tuning`, layered on the config by gamedata.js only)
|
||||
keeps just the result-title rules:
|
||||
|
||||
```jsonc
|
||||
|
||||
+3
-3
@@ -116,10 +116,10 @@
|
||||
|
||||
## 7. 最终攻势与隐秘核心
|
||||
|
||||
- **最终攻势**(第 14 回合;独立标准为第 9 回合):按座位两两配对,每对共享一个战场,一人守左半边、一人守右半边(右侧玩家的棋盘左右镜像)。所有人**共同削减同一条领袖血条**(血量 = 该难度的官方领袖生命值,取自当前版本的游戏数据,例如终极「假想敌:胄」3 600 000,不论存活几人),看不到另一组的战场。全队 LP 合并成一个池,不再有每回合 10 点上限。顶部的 120 秒倒计时结束后战斗**继续**(不会超时判负);从开战第 150 秒起(实际秒数)全队每秒扣 1 点 LP。领袖血量归零即胜利(血条显示 0 时领袖已倒下,战斗立即结束),LP 归零即失败——以先归零的一方为准,LP 耗尽后打出的伤害不再计入。此阶段盟约不叠层。
|
||||
- **限伤**(最终攻势与隐秘核心,与官方相同):对领袖的**单次伤害达到 300000 或以上时整次作废**——这一下造成 0 点伤害,不计入领袖血条,也不显示伤害数字;不是截到 300000,而是一点都不算(299999 照常生效)。只针对领袖本体(含两人战场里的另一个领袖),领袖的部件、护卫、无人机和其他小怪不受影响,普通回合和联防也不受影响;部件把伤害等量转给领袖、无人机死亡令领袖失去 2% 生命时,这一下也按一次伤害检查(目前最多 14.4 万,不会触线)。层数极高时领袖仍可能几秒内倒下(例如每名玩家的两个盟约都在 999 层,或所有激活的盟约都在 999 层),这来自官方按层数计算、没有上限的伤害倍率(谢拉格、奥术、精准等),官方社区也有「boss一秒死」的说法。
|
||||
- **最终攻势**(第 14 回合;独立标准为第 9 回合):按座位两两配对,每对共享一个战场,一人守左半边、一人守右半边(右侧玩家的棋盘左右镜像)。所有人**共同削减同一条领袖血条**(血量 = 该难度的官方领袖生命值 × 开战时存活的玩家人数:领袖生命值取自当前版本的游戏数据,例如终极「假想敌:胄」3 600 000,4 人存活时为 14 400 000、2 人为 7 200 000;AI 队友和掉线托管的玩家都算,已被淘汰或中途退出的不算;独立模拟按 1 人计。这是作者 2026-10-06 采纳社区 PR #209 的决定,取代之前「保持固定血量」的做法),看不到另一组的战场。全队 LP 合并成一个池,不再有每回合 10 点上限。顶部的 120 秒倒计时结束后战斗**继续**(不会超时判负);从开战第 150 秒起(实际秒数)全队每秒扣 1 点 LP。领袖血量归零即胜利(血条显示 0 时领袖已倒下,战斗立即结束),LP 归零即失败——以先归零的一方为准,LP 耗尽后打出的伤害不再计入。此阶段盟约不叠层。
|
||||
- **限伤**(最终攻势与隐秘核心,与官方相同):对领袖的**单次伤害达到 300000 或以上时整次作废**——这一下造成 0 点伤害,不计入领袖血条,也不显示伤害数字;不是截到 300000,而是一点都不算(299999 照常生效)。只针对领袖本体(含两人战场里的另一个领袖),领袖的部件、护卫、无人机和其他小怪不受影响,普通回合和联防也不受影响;部件把伤害等量转给领袖时,这一下也按一次伤害检查;无人机死亡令领袖失去的 2% 血条不算一次伤害,不受限伤影响(隐秘核心终极「假想敌:胄」3 或 4 人存活时每架为 43.2 万 / 57.6 万,按限伤会整次作废,本作仍然计入,属推断)。层数极高时领袖仍可能几秒内倒下(例如每名玩家的两个盟约都在 999 层,或所有激活的盟约都在 999 层),这来自官方按层数计算、没有上限的伤害倍率(谢拉格、奥术、精准等),官方社区也有「boss一秒死」的说法。
|
||||
- 最终攻势 / 隐秘核心的休整期按你在**关底战场上的半场**判定可部署地块(右侧玩家左右镜像),拖动时高亮的就是可放置的格子:例如「源石流发生装置」关底图中被 1×3 橙色管子围起的地块可以部署。在最终攻势放在这些格子上的干员进入隐秘核心时会留在原位。
|
||||
- **领袖的部件**:「假想敌:胄」的无人机死亡时(不论是谁击落;漏过去不算),胄失去领袖血条总量的 2%(例如终极 3 600 000 → 每架 72 000;官方没写明这个「最大生命值」指哪一个,本作按血条总量处理,属推断);隐秘核心的「斩胄之剑 / 破胄之锤」「碎铳之簧」受到的每一点伤害都会等量扣在领袖血条上(剑 / 锤被击落后一定算;俯冲途中挨的打也算,这一点官方两处文字不一致,属推断)。剑 / 锤平时悬浮在胄身边、无敌;掷出后闪现到固定路线,冲向攻击力最低的干员,途中被打中 15 次就会【瘫痪】(晕眩 10 秒、落地、受到的伤害 ×1.3);落地或瘫痪结束后,会以剩余生命值换成一把新的回到原位。「灭顶之灾」每次只发射 1 枚刺胄之弹(低于 20% 生命也一样),只打攻击范围内攻击力最高的干员。
|
||||
- **领袖的部件**:「假想敌:胄」的无人机死亡时(不论是谁击落;漏过去不算),胄失去领袖血条总量的 2%(例如终极 4 人存活 14 400 000 → 每架 288 000;官方没写明这个「最大生命值」指哪一个,本作按血条总量处理,属推断);隐秘核心的「斩胄之剑 / 破胄之锤」「碎铳之簧」受到的每一点伤害都会等量扣在领袖血条上(剑 / 锤被击落后一定算;俯冲途中挨的打也算,这一点官方两处文字不一致,属推断)。剑 / 锤平时悬浮在胄身边、无敌;掷出后闪现到固定路线,冲向攻击力最低的干员,途中被打中 15 次就会【瘫痪】(晕眩 10 秒、落地、受到的伤害 ×1.3);落地或瘫痪结束后,会以剩余生命值换成一把新的回到原位。「灭顶之灾」每次只发射 1 枚刺胄之弹(低于 20% 生命也一样),只打攻击范围内攻击力最高的干员。
|
||||
- **巨型领袖**(假想敌:胄 / 管、盐风主教昆图斯、阿利斯泰尔、“萨米的意志”)的受击判定是一片约 5×3 格的区域(整个管道区块):攻击范围碰到其中任意一格就能打到它;它们**自缚、无法被阻挡**,不会离开原地。溅射按敌人的中心判定,打在旁边小怪身上的溅射碰不到巨型领袖。
|
||||
- **隐秘核心**(第 15 回合,仅险境及以上):打倒领袖后,若全队激活的盟约层数总和 **> 1200**(独立 > 350)且 LP > 1,进入隐秘核心,挑战更强的领袖。失败不影响本局的通关。
|
||||
|
||||
|
||||
+2
-1
@@ -742,7 +742,8 @@ partner's too: PRTS 备注 "全场范围内的所有敌人类我方单位也会
|
||||
second however many carriers (PRTS 作战机制 "同名buff的默认叠加策略buff只能表现出一个"), while a carrier is on the field
|
||||
[ASSUMED]. 奥术法阵 has the same 备注 for its rider: while a carrier is on the field, every damage instance of such a unit
|
||||
silences its target for the item's 5 s ("造成伤害时使目标失去特殊能力5秒"; since 0.1.1). On a leader
|
||||
in a boss / hidden battle a loss of ≥ `BOSS_HIT_LIMIT` (a part's 传递, a drone's death) is cancelled like a hit. Every HP-damage kind
|
||||
in a boss / hidden battle a loss of ≥ `BOSS_HIT_LIMIT` (a part's 传递) is cancelled like a hit; 胄's drone link — 2 % of the
|
||||
pool, a share that is no hit — passes it (`loseHp` `noHitLimit`, DESIGN §25.13.4 [ASSUMED]). Every HP-damage kind
|
||||
meets the limit (phys / arts / true / 元素伤害 incl. element bursts, DoT ticks); element 损伤 (the gauge, `type: 'element'`)
|
||||
removes no HP and never does.
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ Engine helpers (content must use these, never touch internals): `battle.dealDama
|
||||
- **Operator attacks**: every `interval` s if a valid target exists; targets = up to `maxTargets` (default 1) from `unitsInGrid(rangeGrid)` (a `rangeAoe` profile — 阵法术师, 轰击术师 — takes every selectable enemy in range, §21.18; 「同时攻击阻挡的所有敌人」 — 强攻手 / 重剑手 / 推击手 and the skills worded so, `hitAllBlocked` — up to the block count, never fewer than 1, since 0.2.0: PRTS 分支特性信息 "普通攻击最大目标数等于阻挡数(不会低于1)"), priority: (1) the enemies it blocks (every blocker, in range or not, §20.3), (2) profession/trait priorities (e.g. fly-first snipers, `trait` text parsed by build-data into `targetPriority`), (3) enemy with the **least remaining path distance** to its goal, ties → earliest spawned. Melee (no ranged trait) and ground-only attacks cannot hit air units (`Unit.isFlying`: FLY, or a 近地悬浮 / 浮空 enemy — PRTS "算作空中单位", §19.1). Stealthed enemies can't be targeted unless blocked or revealed — or within 3 s of a block's end (§22.8). Ranged ops fire projectiles (speed ~12 tiles/s, instant for melee/arts-beam classes as data indicates); damage applies on impact; a 锁定攻击范围 AoE (`rangeAoe`) strikes every selectable enemy on its range at once with instant hits (§21.18).
|
||||
- **Healers** (`dmgType heal`): target the ally in range with the lowest HP% below 100%, never a 禁疗 / 孤立 unit (tokens.json `abnormal`, `noHeal`; §20.3; 史尔特尔 during 余烬, §22.7), and never the enemies they block — also while blocking (§21.17); do nothing otherwise (SP still regenerates).
|
||||
- **Enemy AI**: follow the route (checkpoints; WALK paths recomputed on the grid when devices change); when blocked stop & fight (`dmgType 'none'` enemies never attack unless content arms them through `e.profile` — 转译基底's armed forms, §21.4); ranged enemies (`rangeRadius > 0`) attack ops whose 0.25 collider touches their radius (centre distance ≤ `rangeRadius` + 0.25, §3, §20.4): the enemy's own target rule filters the candidates first (`profile.canTarget`: 只攻击地面单位 …), then priority blocker → special priority → highest taunt → **latest deployed** (`aggroSeq`) — PRTS 索敌 "阻挡→特殊优先级→仇恨值→最早出现"; a stealthed ally is attacked only by the enemy it blocks (§19.1), and an enemy's area effects — splash, blasts, area skills, zones, jumps — and buff auras skip it unless it blocks that enemy, while 迷彩 does not protect from splash and the other area effects (`targeting.js areaSelectable` / `auraSelectable`, §22.12); an airborne (起飞) ally is never a ground enemy's selection — picks, area damage, zones and statuses skip it, while 无视无法选择 abilities, direct picks, debuff ticks and flying units still reach it (§21.22); unblocked, they stand for each attack's clip (enemies.json `attackAnim`: its wind-up before the strike and the rest of the clip after it, shortened when the attacks come quicker than the clip; `ATTACK_PAUSE = 0.35 s` after the strike when no clip is known; a 「不停止移动」 attacker never stops; §22.14). Every enemy attack strikes at its clip's damage frame after its swing starts — the first one of a contact too —, and a stun / freeze / sleep / 浮空, hiding, 缴械, 恐惧 or 战栗 (blocked) before that frame cuts the swing: no strike, the next swing starts from its wind-up (0.2.0, `ai.js attackWindup`; PRTS 状态机 / 异常效果 STUNNED; no clip known: no wind-up). FLY enemies ignore ground obstacles (only blockFly units block them) and follow their checkpoint route. A feared enemy runs between random 恐惧可达地块 away from the source (PRTS 诱发移动; `fear.js`); 恐惧 outranks 诱导; the 恐惧 / 诱导 walks re-plan from where the enemy stands after a push or an obstacle change, and the route re-plans when they end (§20.4).
|
||||
- **Damage**: phys `max(A − max(0, D×(1−defIgnorePct) − defIgnoreFlat), 0.05·A)`; arts `max(A × (1 − R'/100), 0.05·A)` with `R' = max(0, R×(1−resIgnorePct) − resIgnoreFlat)`; true = A; 元素伤害 ('elemental') `max(A × (1 − 元素抗性/100), 0.05·A)`. Then × `dmgDealtMul` (source) × `dmgTakenMul` & type-specific taken multipliers (target) × `dmg.mul` — 元素伤害 takes 元素脆弱 (`elementalTakenMul`) alone, not `dmgTakenMul` (脆弱 = "受到的物理、法术、真实伤害提升"). Then **限伤**: on a leader (`isBoss` — the 10 bossInfoDict enemyIds and their mirrored copies; never parts, drones or escorts) in a `'boss'` / `'hidden'` battle a hit with `ceil(final) ≥ 300000` is cancelled — no HP or pool loss, no credit, no damage number, no `damaged` / kill (`shared/constants.js BOSS_HIT_LIMIT`, `damage.js leaderHitCancelled`, after every multiplier and the hit-step blocks, before HP shields; `loseHp` too — a part's 传递, a drone's death; the client's `MAX_BATTLE_DAMAGE`, §20.12). Then shields absorb; dodge checks for phys/arts. Element damage fills a gauge (1000; enemy leaders 2000) × `elemTakenMul` × max(0.05, 1 − 损伤抵抗/100), 损伤抵抗 = data `epResistance` (PRTS 元素; the 5 % floor of DMG_e, PRTS 游戏数据基础). An element fill on a target with no HP left is refused (damage.js `hasHp`: no burst on a corpse). A full gauge bursts with the side-specific official effects, as **无来源** damage (no damage-dealt multiplier or penetration of the unit that filled the gauge, which keeps the kill and the stats; §19.5) (sim/constants.js `ELEMENT`; operators: 灼燃 1200 arts + RES −20, 神经 stun 10 s then 1000 true, 凋亡 15 s of 阻回 (`noSp`: no SP gain of any kind) + 静默 with −1 SP/s and 100 arts/s, 侵蚀 DEF −100 then 800 phys; enemies: the "·我方" terms). Every burst starts a **爆发冷却** = its duration (灼燃 / 神经 / 侵蚀 10 s — enemies' 侵蚀 8 s — 凋亡 15 s) during which NO element of the unit fills or can be recovered; when it ends every gauge of the unit resets (§18.3).
|
||||
- **Damage**: phys `max(A − max(0, D×(1−defIgnorePct) − defIgnoreFlat), 0.05·A)`; arts `max(A × (1 − R'/100), 0.05·A)` with `R' = max(0, R×(1−resIgnorePct) − resIgnoreFlat)`; true = A; 元素伤害 ('elemental') `max(A × (1 − 元素抗性/100), 0.05·A)`. Then × `dmgDealtMul` (source) × `dmgTakenMul` & type-specific taken multipliers (target) × `dmg.mul` — 元素伤害 takes 元素脆弱 (`elementalTakenMul`) alone, not `dmgTakenMul` (脆弱 = "受到的物理、法术、真实伤害提升"). Then **限伤**: on a leader (`isBoss` — the 10 bossInfoDict enemyIds and their mirrored copies; never parts, drones or escorts) in a `'boss'` / `'hidden'` battle a hit with `ceil(final) ≥ 300000` is cancelled — no HP or pool loss, no credit, no damage number, no `damaged` / kill (`shared/constants.js BOSS_HIT_LIMIT`, `damage.js leaderHitCancelled`, after every multiplier and the hit-step blocks, before HP shields; `loseHp` too — a part's 传递, but not 胄's drone link, a share of the pool that is no hit (`noHitLimit`, §25.13.4); the client's `MAX_BATTLE_DAMAGE`, §20.12). Then shields absorb; dodge checks for phys/arts. Element damage fills a gauge (1000; enemy leaders 2000) × `elemTakenMul` × max(0.05, 1 − 损伤抵抗/100), 损伤抵抗 = data `epResistance` (PRTS 元素; the 5 % floor of DMG_e, PRTS 游戏数据基础). An element fill on a target with no HP left is refused (damage.js `hasHp`: no burst on a corpse). A full gauge bursts with the side-specific official effects, as **无来源** damage (no damage-dealt multiplier or penetration of the unit that filled the gauge, which keeps the kill and the stats; §19.5) (sim/constants.js `ELEMENT`; operators: 灼燃 1200 arts + RES −20, 神经 stun 10 s then 1000 true, 凋亡 15 s of 阻回 (`noSp`: no SP gain of any kind) + 静默 with −1 SP/s and 100 arts/s, 侵蚀 DEF −100 then 800 phys; enemies: the "·我方" terms). Every burst starts a **爆发冷却** = its duration (灼燃 / 神经 / 侵蚀 10 s — enemies' 侵蚀 8 s — 凋亡 15 s) during which NO element of the unit fills or can be recovered; when it ends every gauge of the unit resets (§18.3).
|
||||
- **Leak**: an enemy reaching its goal is removed, counted as leaked for its field owner (`sourcePlayerId` for unite). LP accounting is done by the match, not the battle.
|
||||
- **Results**: `damageDealt` / a unit's `dmg` count only HP removed from the other side — a unit's own drain or 流失 and friendly damage are the target's `taken` only (§21.12).
|
||||
- **End**: all spawns done and no enemies alive ⇒ `cleared`; `time ≥ timeLimit` ⇒ `timeout` (remaining enemies = leaked). Boss battles end when the shared boss pool reaches 0 (victory) or the match forces an end (team LP 0). A pool holding less than 1 HP is empty (`constants.js BOSS_POOL_MIN_HP`: the hit, or the per-player credit, that would leave less takes the rest; §20.5).
|
||||
|
||||
@@ -85,7 +85,7 @@ Piece = { uid, kind: 'chess'|'item'|'token', id, golden: bool, tier, items: [ {
|
||||
|
||||
**Result validation (server, `fields.js validateClientResult`).** A client result is accepted only if structurally valid and plausible for its spec: every spec player reported and nobody else; finite numbers; `killed ≤ total ≤ 4·spawns + 100`; every leaked `enemyKey` is in the spec's spawns (multiset bound) or a key the spawned enemies' data mentions (content summons/splits, bounded by the total) — boss fields excepted (their leaks cost team LP through `b.progress`); `counted: false` only for enemies that never count (data `notCountInTotal`, schedule `countInTotal: false`, boss/part entries, content spawns); 联防: leaks + `unspawned` per (enemy, leaker) ≤ what that leaker sent in (nobody's survivors are billed to a teammate); per-bond `layerGains ≤ min(60 + 4·round, 999 − the bond's starting layers)` (§20.12) — the flat part raised by what the player's layer 特质 can add to that bond (their per-battle caps; an uncapped one leaves only the 999 room, §21.26) —, none when the spec disables gains, bonds must exist and be named by the player's bond snapshot, band, effects, units or items; `coins ≤ Σ bounty coins of the spawns`; `perfect` consistent with the counted leaks; unit states only for the player's own board uids with hpPct ∈ [0, 1]. The accepted result is **rebuilt** from whitelisted fields (leak mods/tags/sources from the spec — a content-spawned enemy (split, summon) keeps the mods of the schedule entry they equal, i.e. its parent's round multipliers and bounty id, so its 联防 re-entry and bounty match the server run; unit names from server data). Otherwise (or on any exception) the server re-simulates (normal/联防); a boss field is **handed over** instead (below). `SP_VERIFY=all|sample|off` (default `off`): `all` re-simulates every accepted normal/联防 result before accepting it (the server result wins on a digest mismatch); `sample` checks ~1 battle in 8 in a later callback and logs mismatches. `Match.verifyStats = { checked, mismatches, rejected, takeovers }`. Desync between display-only clients and the authority is tolerated: the authoritative outcome is what counts and is broadcast at settlement.
|
||||
|
||||
**Boss rounds.** The shared pool (`SharedBossPool`, co-op size = `bloodPoint[difficulty]`, see corrections) lives on the server and is fed by the authorities' `b.progress` damage deltas (attributed per player through `by`; BOSS_HIT tickers from each player's damage to that round's pool, §21.10) and by server-run fields; a server clock (250 ms) applies the overtime drain (−`bossOvertimeDrainPerSec` team LP per whole **real** second past `bossOvertimeAfter` real s, measured on the field clock — `gamedata.js bossOvertimeDue`, §4), the silence watchdog and the end checks: pool 0 → `b.end cleared` (victory), team LP 0 → `b.end forced` (defeat) — the first one the server registers decides the verdict (`Match._finalEnding`) and after a forced end no report (an in-flight `b.progress`, the final `b.result`) credits the pool (§20.5); results are awaited ≤ 6 s (× timerScale) after a forced end. `b.pool` is the live channel for the pool and the team LP: a boss `b.progress` (4 Hz per field) and the team-LP losses never re-mark the whole public state — `m.public` (`bossHp`, `teamLp`, `fields[].progress`) and the players' merged-LP shares (`_syncTeamLp`) refresh at ~1 Hz (`Match._bossPublic`, `BOSS_PUBLIC_MS`), and at once when the round ends. A boss field's `b.result` ends it only when the shared pool is empty or after that `b.end` — except a `'cleared'` result whose cumulative report covers what the pool still holds (within its 1 HP floor) while the plausibility budget (one field credits ≤ the pool per `BOSS_MIN_CLEAR_GS` 5 game s) has not caught up: it is **held** (`f.heldResult`; 999-layer boards kill a leader in 2–4 game s, §20.14) — the boss clock keeps crediting the report at the budget's rate, the silence watchdog and handovers skip the field, a re-sent copy is ignored, and the pool's end (`_endFinal`) completes the field with it; a forged report gains nothing a `b.progress` could not already get. Any other result — `'forced'` / `'timeout'` at t = 0, `'cleared'` while the pool still holds more than the report covers — and any result failing validation would stop the pair's fight (and, with every field done, end the Final Assault as a defeat), so the field is **handed over** (`Match._bossHandover`) to the partner's display replica (promoted with `b.start authoritative: true`) or, without one, to the server (fast-forwarded to the field clock); the sender gets `b.end takeover` and is never that field's authority again (`f.demoted`). There is no synthetic zero result for a running boss field: a stats-only stand-in (`fields.js syntheticResult`, carrying the damage already credited, never charging LP) only fills a result that cannot be had otherwise — the Final Assault already ending (no result, or an invalid one, within the grace) or a server-run battle whose `result()` threw.
|
||||
**Boss rounds.** The shared pool (`SharedBossPool`, size = `bloodPoint[difficulty]` × the players alive when the fight starts, solo × 1 — the owner's decision of 2026-10-06 adopting PR #209, which replaces the fixed pool of 「保持固定血量」, §25.13.4) lives on the server and is fed by the authorities' `b.progress` damage deltas (attributed per player through `by`; BOSS_HIT tickers from each player's damage to that round's pool, §21.10) and by server-run fields; a server clock (250 ms) applies the overtime drain (−`bossOvertimeDrainPerSec` team LP per whole **real** second past `bossOvertimeAfter` real s, measured on the field clock — `gamedata.js bossOvertimeDue`, §4), the silence watchdog and the end checks: pool 0 → `b.end cleared` (victory), team LP 0 → `b.end forced` (defeat) — the first one the server registers decides the verdict (`Match._finalEnding`) and after a forced end no report (an in-flight `b.progress`, the final `b.result`) credits the pool (§20.5); results are awaited ≤ 6 s (× timerScale) after a forced end. `b.pool` is the live channel for the pool and the team LP: a boss `b.progress` (4 Hz per field) and the team-LP losses never re-mark the whole public state — `m.public` (`bossHp`, `teamLp`, `fields[].progress`) and the players' merged-LP shares (`_syncTeamLp`) refresh at ~1 Hz (`Match._bossPublic`, `BOSS_PUBLIC_MS`), and at once when the round ends. A boss field's `b.result` ends it only when the shared pool is empty or after that `b.end` — except a `'cleared'` result whose cumulative report covers what the pool still holds (within its 1 HP floor) while the plausibility budget (one field credits ≤ the pool per `BOSS_MIN_CLEAR_GS` 5 game s) has not caught up: it is **held** (`f.heldResult`; 999-layer boards kill a leader in 2–4 game s, §20.14) — the boss clock keeps crediting the report at the budget's rate, the silence watchdog and handovers skip the field, a re-sent copy is ignored, and the pool's end (`_endFinal`) completes the field with it; a forged report gains nothing a `b.progress` could not already get. Any other result — `'forced'` / `'timeout'` at t = 0, `'cleared'` while the pool still holds more than the report covers — and any result failing validation would stop the pair's fight (and, with every field done, end the Final Assault as a defeat), so the field is **handed over** (`Match._bossHandover`) to the partner's display replica (promoted with `b.start authoritative: true`) or, without one, to the server (fast-forwarded to the field clock); the sender gets `b.end takeover` and is never that field's authority again (`f.demoted`). There is no synthetic zero result for a running boss field: a stats-only stand-in (`fields.js syntheticResult`, carrying the damage already credited, never charging LP) only fills a result that cannot be had otherwise — the Final Assault already ending (no result, or an invalid one, within the grace) or a server-run battle whose `result()` threw.
|
||||
|
||||
**Solo pause (official `PauseUp` / `ResumeUp`; `Match.setPause`).** C→S `g.pause { on }`: solo matches only (co-op → `WRONG_PHASE 'co-op battles never pause'`), and `on: true` only while a battle runs (COMBAT / FINAL_ASSAULT / HIDDEN_CORE with a live field, not once the Final Assault is ending — else `WRONG_PHASE 'no battle running'`); `on: false` is always accepted. While `m.public.paused` is true everything of the battle is frozen: the field clock (`b.start elapsed`, the boss plausibility budgets and the overtime drain), the authority deadline and the server-run release timers, the boss clock, the HUD `deadline` / `overtimeAt` (shifted by the paused time on resume) and the server-run pacers (FieldRunner / HeadlessPacer skip their intervals); the browser's runner stops its local clock. In solo battles the top bar shows a 暂停 / 继续作战 button (`ui/hud.js PauseButton`, `ui/matchStatus.js pauseAvailable`; `Space` toggles it), a paused overlay offers 继续作战 / 放弃模拟, and the HUD clocks freeze at the pause moment. A disconnect or leave resumes (the server takes the field over — nobody is left to resume it); the battle phase ending drops the pause.
|
||||
|
||||
|
||||
+54
-2
@@ -23,7 +23,7 @@ what stays [ASSUMED] and the questions left to the owner → §25.10. The closin
|
||||
2026-10-06 (望's stock, skill triggers through summon areas, the 突袭 landing, GitHub #148) and the items the sources
|
||||
settled (泥岩's 闭锁; 忍冬, 深靛, 阿) → §25.11; release packaging and the asset manifest (the 自选 voices, the summons
|
||||
with no Spine model) → §25.12; round 18 — 拟态物质 at the copy cap (GitHub #207), 华法琳's 7 / 14 (GitHub #175, PR #192),
|
||||
a merged elite 拉普兰德 as a new one (GitHub #169) → §25.13.
|
||||
a merged elite 拉普兰德 as a new one (GitHub #169), the leader pool per player alive (PR #209) → §25.13.
|
||||
|
||||
Decisions of the owner: 2026-10-04 — the server stays on plain `node:http` + `ws`, no web framework (§25.1);
|
||||
2026-10-05 — the 补位 plan (§25.3); the 自选编队 rules, the collab operators out of the pool, ISW-A refused (§25.4);
|
||||
@@ -33,7 +33,8 @@ enemy in range, 纯烬艾雅法拉 S1 at full SP, 伺夜 S1 only with his pack o
|
||||
does not own shows as its stand-in everywhere (the owner's recall of the official mode, §25.3); the enemy damage frame
|
||||
kept (§25.7.2); 望's placed 棋子 count in her stock, skills that act through summons cast on an enemy in a summon's area,
|
||||
a melee 突袭 member lands where it can block (§25.11); 华法琳's granted trait at the data's 7 / 14, replacing the 12 / 24
|
||||
lock (§25.13.2); a newly merged elite 拉普兰德 counts as a new 拉普兰德 (§25.13.3).
|
||||
lock (§25.13.2); a newly merged elite 拉普兰德 counts as a new 拉普兰德 (§25.13.3); the leader pool = bloodPoint × the players
|
||||
alive at the fight's start, adopting PR #209 and replacing 「保持固定血量」 (§25.13.4).
|
||||
|
||||
### 25.1 Refactor and tooling: the big files split, golden results, lint, types, import boundaries (refactor plan; owner's decision of 2026-10-04: no web framework) — `server/http/`, `server/match/match/`, `server/match/player/`, `server/sim/battle/`, `content/enemies/`, `kits/ops/`, `public/js/ui/gameLogic/`, `render/fx/`, `render/app/`, `screens/game/`, `tools/golden.mjs`, `tools/check-imports.mjs`, `eslint.config.js`, `jsconfig.json`
|
||||
|
||||
@@ -663,6 +664,7 @@ its tile, §18.1; §25.9.2). Where a 0.1.4 change met 0.2.0 work:
|
||||
| A melee 突袭 member lands on a tile where it can block before one where it cannot (GitHub #148, §25.11.3) | the owner, 2026-10-06 |
|
||||
| 华法琳's granted trait stops at the data's 7 / 14 (PRTS 3/27: lowered from 12 / 24; GitHub #175, PR #192, §25.13.2) | the owner, 2026-10-06 (replaces the 12 / 24 lock) |
|
||||
| A newly merged elite 拉普兰德 is a new 拉普兰德: +8 on its first manual refresh of the round (GitHub #169, §25.13.3) | the owner, 2026-10-06 (was [ASSUMED]: the copies' count carried) |
|
||||
| The leader pool = bloodPoint × the players alive when the fight starts, solo × 1 (PR #209, §25.13.4) | the owner, 2026-10-06 (replaces 「保持固定血量」; config restores it) |
|
||||
|
||||
Settled by the sources in W5V, no longer questions: 泥岩 S3's dormancy is 闭锁 — she lets go and blocks nobody (PRTS,
|
||||
§25.11.4); 阿 S2 / S3 keep the data's DEFAULT, 忍冬 S2's and 深靛 S1's notes are what the kits do (§25.11.5).
|
||||
@@ -894,3 +896,53 @@ ported PR credits its author there and here.
|
||||
`test/match/feedback1-meta.test.js` (updated on purpose: the elite made from copies that already fired adds +8 on its
|
||||
next refresh, once; new: two white copies refreshed this round, merge, refresh → +8). Docs: docs/META.md (the counter
|
||||
API, the edge cases), docs/PLAYING.md, the code comments.
|
||||
|
||||
#### 25.13.4 The leader pool is bloodPoint per player alive when the fight starts (PR #209 by @qingjingshenghuo; the owner's decision of 2026-10-06); 胄's drone share passes 限伤 — `server/match/gamedata.js bossPoolShareOf`, `server/match/finalAssault.js bossPoolHp`, `tools/build-data.mjs` (config `bossHpScale`), `sim/battle/combat.js loseHp`, `sim/content/bosses.js` (死亡集群)
|
||||
|
||||
- **Seen** (PR #209): the author's play of the official mode — the leader pool grows with the number of players; the
|
||||
remake kept one fixed pool (co-op bloodPoint whatever the count, solo × 0.25 [ASSUMED]) since the owner's 「保持固定血量」
|
||||
(§20.9 / §20.10).
|
||||
- **Official**: activity_table bossInfoDict `bloodPoint` per difficulty, no per-player factor in the data; notice 5114's
|
||||
「两侧的敌方领袖共享生命值(敌方领袖的总生命值不变)」 is about the mirrored copies of a pair field; the PR's rule is the
|
||||
author's observation (实玩反馈), not an official text. 假想敌:胄 (PRTS): a drone that dies 「令假想敌:胄受到最大生命值2%的真实
|
||||
伤害」; 限伤 (research 11 §2.1) cancels any damage modifier of ceil ≥ 300000 on a leader.
|
||||
- **Decision**: the owner's decision of 2026-10-06 — adopt #209: the pool is the solo table value × the players alive
|
||||
when the fight starts; it replaces 「保持固定血量」.
|
||||
- **Now** (ported by hand onto 0.2.0's files; the PR's one-line `data/config.json` and its `docs/BOSS_HP_ZH.md` table
|
||||
were not taken — the data is regenerated, PLAYING §7 gives the examples):
|
||||
1. `bossPoolShareOf` (gamedata.js, 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 Final
|
||||
Assault's / Hidden Core's start (`alivePlayers()`: bots and AI 托管 seats count, eliminated and departed seats do
|
||||
not; at most `aliveFull` 4; a count left out = a full team). The global `config.bossHpScale` carries the rule
|
||||
(`perPlayer: true, solo: 1`); the modes' entries keep only `bloodPointKey` (one place restores the fixed pool:
|
||||
`perPlayer: false, solo: 0.25`, its optional × alive / 4 `aliveScaling` too). data/config.json regenerated offline:
|
||||
only `bossHpScale` (global and per mode) changed. Leaders still ignore `enemyScale`; parts and escorts keep their HP.
|
||||
2. #209 documented, without fixing, that 胄's drone link (2 % of the pool, `DRONE_LINK_BASE 'pool'`) meets 限伤: the
|
||||
hidden 胄 终极 pool is 21 600 000 / 28 800 000 at 3 / 4 players, a drone 432 000 / 576 000, which the limit would
|
||||
cancel — drones would stop counting exactly in the largest fights. The share is no hit: `Battle.loseHp` takes
|
||||
`noHitLimit` and the drone link passes it; a part's 传递 and every hit stay under the limit.
|
||||
- [ASSUMED]: the drone share past the limit (the official client checks every damage modifier on a leader and PRTS
|
||||
calls the 2 % 真实伤害, but which max HP it reads is not documented — the remake reads the pool and keeps the share).
|
||||
The count is taken at each boss round's start, as #209 states (a seat that leaves before the Hidden Core no longer
|
||||
counts there).
|
||||
- Golden: fields — the 20 boss / hidden scenarios (`boss-boss_1..7-pair` / `-solo`, `hidden-boss_8..10-pair` /
|
||||
`-solo`): a pair field's pool × 2 (two players), a solo template's × 4 (solo × 1 instead of × 0.25); seven of them
|
||||
(`boss-boss_1-pair`, `boss-boss_1-solo`, `boss-boss_3-pair`, `boss-boss_3-solo`, `boss-boss_5-solo`,
|
||||
`boss-boss_7-pair`, `hidden-boss_9-solo`) now end at the 200 s cap instead of clearing; the two 联防 scenarios are
|
||||
unchanged. matches — 11 of 18: `solo-FUNNY-2`, `solo-NORMAL-1`, `solo-HARD-1`, `solo-HARD-2` now lose the Final
|
||||
Assault (team LP 0 with 93 088 – 176 395 of the pool left; solo leaders have 4× the HP); `solo-FUNNY-1`,
|
||||
`solo-NORMAL-2`, `solo-HARD-9-boosted` (its Hidden Core too) win with more leader damage (team LP −6 / 0 / −1);
|
||||
`coop2-NORMAL-3`, `coop4-FUNNY-6`, `coop2-ABYSS-10-boosted`, `coop2-NORMAL-70-diy` win with more leader damage
|
||||
(the pool × the players alive). Unchanged: the matches that end before R14 and `coop2-NORMAL-8-serverrun` /
|
||||
`coop2-NORMAL-14-standins` (one player left at R14: × 1, as before); 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 PR's test said the opposite —, the plain-object fallback agrees; only the leader's HP scales),
|
||||
`test/content/enemies_bosses.test.js` (hidden 胄 终极 at 3 / 4 players: the drone's 432 000 / 576 000 land, a hit of
|
||||
the same size is cancelled), updated on purpose: `test/match/finalAssault.test.js` (the per-player pool, the fixed
|
||||
pool restored through config, a mode entry overriding the global one), `test/match/balance.test.js`,
|
||||
`test/match/bosshp.test.js`, `test/match/connection.test.js`, `test/docs-consistency.test.js` (the §20.9 record
|
||||
points here; the data carries `perPlayer: true`), `test/sim/playtest6_limits.test.js` (the drone share lands past the
|
||||
line), `test/match/playtest6-bosspool.test.js` (its 5 % knob halved to keep the two-player pool's size: that test is
|
||||
about the pool's arithmetic). Docs: PLAYING §7, DATA (`bossHpScale`), META, BALANCE, SIM, the
|
||||
design (engine.md 限伤, network.md boss rounds), the §20.9 / §20.10 records, research 00 / 01 / 08.
|
||||
@@ -142,7 +142,7 @@ Official since the follow-up — read from the client binary (research 11, §20.
|
||||
| Ground routes (§21.15) | the official flow field unless the playtest #2 route crosses strictly fewer floor tiles (a corner touch is no crossing) | `server/sim/grid.js _buildField` always `use = o` (reverses 战场#01's col-8 road) |
|
||||
| Leader parts (§20.10, §20.13) | every damage a 剑 / 锤 / 碎铳之簧 takes is lost by its leader 1:1, 无来源 | `server/sim/content/bosses.js PART_TRANSFER = 0.5` (v2.5's share; v2.5 also passed a 剑 / 锤's damage only while grounded) |
|
||||
|
||||
Settled by the user ("保持固定血量"): the co-op leader pool does not shrink when players are eliminated — `config bossHpScale.aliveScaling` is **false** (pool = bloodPoint whatever the number of alive players; one community note, 巴哈姆特 12294, said otherwise) (§20.10); flip: `aliveScaling: true` (× alive / 4).
|
||||
Settled by the user ("保持固定血量"): the co-op leader pool does not shrink when players are eliminated — `config bossHpScale.aliveScaling` is **false** (pool = bloodPoint whatever the number of alive players; one community note, 巴哈姆特 12294, said otherwise) (§20.10); flip: `aliveScaling: true` (× alive / 4). Replaced on 2026-10-06 by the owner's decision adopting PR #209: the pool is bloodPoint × the players alive when the fight starts, solo × 1 (§25.13.4; `bossHpScale.perPlayer: false` with `solo: 0.25` restores this fixed pool).
|
||||
|
||||
Settled by the user (2026-10-02, after v2.5.2: "共享阵地按你说的规则就行"): on a shared field (联防, Final Assault / Hidden Core) the bond strip follows the player of the half the ‹ › pill points at; on 全景 or an empty half it shows your own bonds when you fight there, else the teammate picked with 前往查看 or the field's first player — never your own (§20.15); rule: `public/js/ui/watchBonds.js bondOwnerId`.
|
||||
|
||||
@@ -152,7 +152,7 @@ Still [ASSUMED] (each in its section): the class rows covering every MANUAL skil
|
||||
|
||||
- **Seen**: co-op 终极 / 绝境 leaders died within seconds; with the user's layer counts the official leader never dies that fast.
|
||||
- **HP and pool — official, unchanged**: `bloodPoint[difficulty]` = activity_table act2autochess bossInfoDict `bloodPoint` / `bloodPointNormal` / `bloodPointHard` / `bloodPointAbyss` of the current data (the cache equals upstream ArknightsGameData master as of 2026-10-01; `test/match/bosshp.test.js` pins all 40 values — e.g. 胄 247 500 / 675 000 / 1 800 000 / 3 600 000, hidden 胄 750 000 / 937 500 / 3 600 000 / 7 200 000). PRTS 盟约记录's leader table is an older (11月18日) revision: 铳 险境 and 胄 / 铳 / 萨米 绝境 differ, no 终极 column, no 卢西恩 row. The data has no per-player or per-field factor; leader HP ignores `enemyScale` ("领袖单位于服务器的生命值加成不受上述加成影响"). One pool for every boss field ("最终攻势中,所有人将一起对敌方领袖造成伤害"); notice 5114's "敌方领袖的总生命值不变" means the two mirrored copies of a pair field share it.
|
||||
- **Pool size**: co-op = bloodPoint whatever the number of alive players — `bossHpScale.aliveScaling` (× alive / 4, one community note: 巴哈姆特 12294) stays **off** — the user chose the fixed pool (§20.9); solo × 0.25 [ASSUMED].
|
||||
- **Pool size**: co-op = bloodPoint whatever the number of alive players — `bossHpScale.aliveScaling` (× alive / 4, one community note: 巴哈姆特 12294) stays **off** — the user chose the fixed pool (§20.9); solo × 0.25 [ASSUMED]. (Replaced on 2026-10-06: bloodPoint per player alive at the fight's start, §25.13.4.)
|
||||
- **Fixed — why leaders fell too fast**:
|
||||
1. **直接乘算**: "+X%" ATK / DEF / max HP from bonds, strategies, equipment, 机变 cards and per-layer 特质 are summed with each other and with the skills' "+X%" (PRTS 盟约记录 "…属性加成均为直接乘算"; 游戏数据基础 D_t = Σtᵢ, A = (A₀ + D_p)(1 + D_t)). v2.5 compounded them (×74 instead of ×12 at 600 精准 layers with four more bonuses). `DIRECT_BONUS_STACKING 'add'`, content/support `directMods`; 炎佑 ×1.5 and the char_attribute_mul 特质 stay multipliers (§5.2).
|
||||
2. **Same-named enemy debuffs**: one instance per target, the strongest (PRTS 作战机制 "同名buff…只能表现出一个"; 巴哈姆特 12316 "共享型buff會跟對面搶") — 奥术, 灵知 坚冰, the 莱恩哈特 / 缄默德克萨斯 RES cuts (`Battle.applyStrongest`, §5.3). v2.5 kept one 奥术 per player, so a pair field multiplied it (×40 arts damage taken at +400 layers; ×7.6 now). "Strongest" rather than "latest" is [ASSUMED].
|
||||
|
||||
@@ -142,7 +142,7 @@ The server draws a disabled bond set **D**: **3 core + 4 add-on** in NORMAL/HARD
|
||||
| 3 | Item slot odds and server pools `pool_equip_*` | Same tier shares; pool contents as in 04 §8 |
|
||||
| 4 | Disabled-bond counts for NORMAL/HARD | 3 core + 4 add-on |
|
||||
| 5 | 机变 family per SP round, card pools, 道具补给 tiers | 01 A4 table: HARD+ R3/R9 悬赏, R11 道具补给/机密商店. 道具补给 tier windows R3 I–IV, R6 II–V, R9 III–VI, R11 IV–VI. |
|
||||
| 6 | Boss HP pool vs alive players | `bloodPoint × alive/4`; solo ×0.25 |
|
||||
| 6 | Boss HP pool vs alive players | `bloodPoint × alive/4`; solo ×0.25 (since 2026-10-06, the owner's decision adopting PR #209: `bloodPoint × alive` at the fight's start, solo × 1 — DESIGN §25.13.4) |
|
||||
| 7 | Pair order in the Final Assault | Alive players by seat: (1,2), (3,4); an odd player gets an `_s` map |
|
||||
| 8 | 联防 helper choice when >2 are perfect; leaked-enemy HP | Highest LP, then seat; leaked enemies re-enter at full HP |
|
||||
| 9 | Per-turn band-draft timer | 12 s, with the 50 s step cap |
|
||||
|
||||
@@ -173,6 +173,8 @@ Boss HP scaling:
|
||||
- The boss HP is one shared pool for the whole team.
|
||||
- It shrinks when teammates are eliminated or leave [COMM: "联机队友变少,最后boss血条也会变少"].
|
||||
- Proposal: `hp = bloodPoint × alivePlayers / 4` [ASSUMED]. Solo uses `bloodPoint × 0.25` [ASSUMED].
|
||||
- [2026-10-06: the owner adopted PR #209 instead — `hp = bloodPoint × the players alive when the fight starts`, solo
|
||||
`bloodPoint` (bloodPoint is one player's share; players' observation, no official text); DESIGN §25.13.4.]
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -551,7 +551,7 @@ Everything in `docs/BALANCE.md` §4 (the tuned tables) and the `tools/balance.mj
|
||||
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.)
|
||||
- `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.) (2026-10-06: replaced by the owner's decision adopting PR #209 — `bloodPoint` × the players alive at the fight's start, solo × 1; `perPlayer: false` restores the fixed pool; DESIGN §25.13.4.)
|
||||
- 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`):
|
||||
@@ -581,7 +581,7 @@ Everything in `docs/BALANCE.md` §4 (the tuned tables) and the `tools/balance.mj
|
||||
| # | 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) |
|
||||
| 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) |
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
// RIGHT side (the sim mirrors the right side: board col c → field col 20 − c with the piece direction RIGHT ↔
|
||||
// LEFT, UP / DOWN unchanged (DESIGN §3, research 09 §1.2 ConvertChessPositionInfoToBossMap); board rows 9–12 →
|
||||
// boss rows 2–5, sim/constants BOSS_ROW_OFFSET). `bossFieldPlacement` gives that mapping for UIs / tools.
|
||||
// * Shared boss HP pool (DESIGN §20.10, GameData.bossPoolShare): one pool shared by every boss field (official tip
|
||||
// "所有人将一起对敌方领袖造成伤害"); co-op = bloodPoint[difficulty] whatever the number of alive players (notice 5114's
|
||||
// "敌方领袖的总生命值不变" is about the mirrored copies of a pair field sharing it, not about that number); config
|
||||
// bossHpScale.aliveScaling true scales it × alive / 4 (巴哈姆特 12294 "聯機隊友(撤退/死掉)變少,最後boss血條也會變少" — one
|
||||
// community note, no proportion; off until the user confirms it); solo = bloodPoint × config bossHpScale.solo (0.25,
|
||||
// flagged unknown); × the tuning bossHpMul when data/tuning.json still has one (docs/BALANCE.md); bosses are never
|
||||
// scaled by enemyScale.
|
||||
// * Shared boss HP pool (DESIGN §20.10, §25.13.4, gamedata.js bossPoolShareOf): one pool shared by every boss field
|
||||
// (official tip "所有人将一起对敌方领袖造成伤害"; notice 5114's "敌方领袖的总生命值不变" is about the mirrored copies of
|
||||
// a pair field sharing it) = bloodPoint[difficulty] × the players alive when the fight starts (bots and AI 托管 seats
|
||||
// count, eliminated and departed seats do not; solo × 1) — the owner's decision of 2026-10-06, adopting PR #209 by
|
||||
// @qingjingshenghuo, which replaces the fixed pool of 「保持固定血量」 (config bossHpScale perPlayer false / solo 0.25
|
||||
// restores it); × the tuning bossHpMul when data/tuning.json still has one (docs/BALANCE.md); bosses are never scaled
|
||||
// by enemyScale and their parts / escorts keep their own HP.
|
||||
// * Overtime: bossTurnHpReduceTime counts REAL seconds like the level's 120 s maxPlayTime (which runs out first; the
|
||||
// battle goes on): from 150 real s (300 game s on the 2× field clock) the team loses bossOvertimeDrainPerSec (1) LP
|
||||
// per real second (gamedata.js bossOvertimeDue); m.public.deadline = the 120 s countdown, m.public.overtimeAt = the
|
||||
@@ -35,6 +35,7 @@
|
||||
|
||||
import { BOSS_ROW_OFFSET, COLS, BOSS_POOL_MIN_HP } from '../sim/constants.js';
|
||||
import { mirrorDir, normDir } from '../sim/dir.js';
|
||||
import { bossPoolShareOf } from './gamedata.js';
|
||||
|
||||
/**
|
||||
* BOSS_HIT ticker thresholds (activity_table autoChessData.broadcastList comment_boss_hit_1..3, paramList 0.2 / 0.5 /
|
||||
@@ -62,7 +63,7 @@ export function pairPlayers(alive) {
|
||||
return groups;
|
||||
}
|
||||
|
||||
/** Shared boss HP for a boss id with `aliveCount` alive players (GameData.bossPoolShare; omitted ⇒ a full team). */
|
||||
/** Shared boss HP for a boss id with `aliveCount` alive players (GameData.bossPoolShare / bossPoolShareOf; omitted ⇒ a full team). */
|
||||
export function bossPoolHp(gd, bossId, aliveCount) {
|
||||
const boss = gd.boss(bossId);
|
||||
const diff = gd.difficulty;
|
||||
@@ -70,13 +71,8 @@ export function bossPoolHp(gd, bossId, aliveCount) {
|
||||
if (base == null && boss && boss.bloodPoint) base = Object.values(boss.bloodPoint).find((v) => Number.isFinite(v)) ?? null;
|
||||
if (base == null) base = 500000;
|
||||
const tune = typeof gd.bossHpMul === 'function' ? gd.bossHpMul(bossId) : 1;
|
||||
let share;
|
||||
if (typeof gd.bossPoolShare === 'function') share = gd.bossPoolShare(aliveCount);
|
||||
else {
|
||||
const scale = gd.mode.bossHpScale && typeof gd.mode.bossHpScale === 'object' ? gd.mode.bossHpScale : {};
|
||||
const cfg = gd.config.bossHpScale && typeof gd.config.bossHpScale === 'object' ? gd.config.bossHpScale : {};
|
||||
share = gd.isSolo ? (Number.isFinite(scale.solo) ? scale.solo : Number.isFinite(cfg.solo) ? cfg.solo : 0.25) : 1;
|
||||
}
|
||||
const share = typeof gd.bossPoolShare === 'function' ? gd.bossPoolShare(aliveCount)
|
||||
: bossPoolShareOf(gd.mode && gd.mode.bossHpScale, gd.config && gd.config.bossHpScale, !!gd.isSolo, aliveCount);
|
||||
return Math.max(1, Math.round(base * share * tune));
|
||||
}
|
||||
|
||||
|
||||
+37
-20
@@ -5,8 +5,8 @@
|
||||
// key is missing, so a partial data set (tests, data being regenerated) still yields a working match.
|
||||
//
|
||||
// No custom balance (DESIGN §14 corrections, research 08 §6): enemy numbers are the official ones — the PRTS
|
||||
// per-round enemyScale table of data/config.json, the leader pool = bloodPoint. data/tuning.json only overrides result
|
||||
// titles:
|
||||
// per-round enemyScale table of data/config.json, the leader pool = bloodPoint per player alive at the fight's start
|
||||
// (bossPoolShareOf). data/tuning.json only overrides result titles:
|
||||
// titles[titleId] { stat?, rule? } merged over config.titles
|
||||
// (the former enemyHpMul / enemyAtkMul / enemySpeedMul / bossHpMul / flyPlaceholders knobs were removed; a tuning file
|
||||
// that still carries them is ignored).
|
||||
@@ -55,6 +55,34 @@ export const DEFAULTS = Object.freeze({
|
||||
/** Game seconds per real second of a battle (forced 2×): combat limits in data are real seconds (combatTimeLimit). */
|
||||
export const COMBAT_TIME_SCALE = 2;
|
||||
|
||||
/**
|
||||
* Multiplier of bloodPoint[difficulty] for the shared leader pool (DESIGN §20.10, §25.13.4), from config bossHpScale —
|
||||
* each key from the mode's entry first, then the global one; the defaults are the current rule:
|
||||
* solo `solo` (1: the table value — one player's share);
|
||||
* co-op `perPlayer` true (the owner's decision of 2026-10-06, adopting PR #209 by @qingjingshenghuo): `coop` (1) × the
|
||||
* players alive when the fight starts — bots and AI 托管 seats count, eliminated and departed seats do not —
|
||||
* at most `aliveFull` (4); a count left out means a full team;
|
||||
* `perPlayer` false: the fixed pool of 0.1.x (「保持固定血量」, restorable with `solo` 0.25): `coop` whatever the
|
||||
* count, × min(alive, aliveFull) / aliveFull with `aliveScaling`.
|
||||
* @param {object|null|undefined} modeScale config.modes[modeId].bossHpScale
|
||||
* @param {object|null|undefined} cfgScale config.bossHpScale
|
||||
* @param {boolean} isSolo
|
||||
* @param {number} [aliveCount] alive players at the Final Assault / Hidden Core start (co-op)
|
||||
* @returns {number}
|
||||
*/
|
||||
export function bossPoolShareOf(modeScale, cfgScale, isSolo, aliveCount) {
|
||||
const ms = modeScale && typeof modeScale === 'object' ? modeScale : {};
|
||||
const cs = cfgScale && typeof cfgScale === 'object' ? cfgScale : {};
|
||||
const pick = (k, d) => (Number.isFinite(ms[k]) && ms[k] > 0 ? ms[k] : Number.isFinite(cs[k]) && cs[k] > 0 ? cs[k] : d);
|
||||
const flag = (k, d) => (typeof ms[k] === 'boolean' ? ms[k] : typeof cs[k] === 'boolean' ? cs[k] : d);
|
||||
if (isSolo) return pick('solo', 1);
|
||||
const full = Math.max(1, Math.floor(pick('aliveFull', 4)));
|
||||
const n = Number(aliveCount);
|
||||
const alive = Number.isFinite(n) && n >= 1 ? Math.min(full, Math.floor(n)) : full;
|
||||
if (flag('perPlayer', true)) return pick('coop', 1) * alive;
|
||||
return pick('coop', 1) * (flag('aliveScaling', false) ? alive / full : 1);
|
||||
}
|
||||
|
||||
/** Strip the _a/_b suffix of an item id (the registry key of an item family). */
|
||||
export const itemKey = (id) => (typeof id === 'string' ? id.replace(/_[ab]$/, '') : '');
|
||||
|
||||
@@ -110,14 +138,12 @@ export class GameData {
|
||||
}
|
||||
|
||||
/**
|
||||
* Official shared leader HP pool (DESIGN §20.10): ONE pool for every boss field of the match (official tip "最终攻势中,
|
||||
* Shared leader HP pool (DESIGN §20.10, §25.13.4): ONE pool for every boss field of the match (official tip "最终攻势中,
|
||||
* 所有人将一起对敌方领袖造成伤害"; the mirrored copies of a pair field share it — notice 5114 "两侧的敌方领袖共享生命值
|
||||
* (敌方领袖的总生命值不变)", which is about those copies, not about the number of players). Co-op = bloodPoint
|
||||
* [difficulty]; with config bossHpScale.aliveScaling (default false) × alive / aliveFull (4) — 巴哈姆特 12294 "聯機隊友
|
||||
* (撤退/死掉)變少,最後boss血條也會變少" is one community note without a proportion, kept off until confirmed (it would
|
||||
* shorten fights after eliminations, the opposite of the playtest report); `aliveCount` omitted ⇒ a full team. Solo = bloodPoint ×
|
||||
* bossHpScale.solo (0.25 = one player of four, [ASSUMED]). Leaders are never scaled by enemyScale ("领袖单位于服务器的
|
||||
* 生命值加成不受上述加成影响").
|
||||
* (敌方领袖的总生命值不变)", which is about those copies). Size = bloodPoint[difficulty] × bossPoolShare: the table value
|
||||
* per player alive when the fight starts (the owner's decision of 2026-10-06, PR #209; it replaces the fixed pool of
|
||||
* 「保持固定血量」, which config bossHpScale restores — bossPoolShareOf); `aliveCount` omitted ⇒ a full team. Leaders are
|
||||
* never scaled by enemyScale ("领袖单位于服务器的生命值加成不受上述加成影响").
|
||||
* @param {string} bossId
|
||||
* @param {number} [aliveCount] alive players at the Final Assault / Hidden Core start (co-op)
|
||||
* @returns {number}
|
||||
@@ -132,20 +158,11 @@ export class GameData {
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiplier of bloodPoint for the leader pool (see bossPoolHp): solo = bossHpScale.solo (0.25); co-op = coop (1) ×
|
||||
* min(alive, aliveFull) / aliveFull when bossHpScale.aliveScaling (mode entry first, then the global one).
|
||||
* Multiplier of bloodPoint for the leader pool (see bossPoolHp; the rule and the config keys: bossPoolShareOf).
|
||||
* @param {number} [aliveCount]
|
||||
*/
|
||||
bossPoolShare(aliveCount) {
|
||||
const ms = this.mode.bossHpScale && typeof this.mode.bossHpScale === 'object' ? this.mode.bossHpScale : {};
|
||||
const cs = this.config.bossHpScale && typeof this.config.bossHpScale === 'object' ? this.config.bossHpScale : {};
|
||||
const pick = (k, d) => (Number.isFinite(ms[k]) && ms[k] > 0 ? ms[k] : Number.isFinite(cs[k]) && cs[k] > 0 ? cs[k] : d);
|
||||
if (this.isSolo) return pick('solo', 0.25);
|
||||
const scaling = typeof ms.aliveScaling === 'boolean' ? ms.aliveScaling : cs.aliveScaling === true;
|
||||
const full = Math.max(1, Math.floor(pick('aliveFull', 4)));
|
||||
const n = Number(aliveCount);
|
||||
const alive = scaling && Number.isFinite(n) && n >= 1 ? Math.min(full, Math.floor(n)) : full;
|
||||
return pick('coop', 1) * (alive / full);
|
||||
return bossPoolShareOf(this.mode.bossHpScale, this.config.bossHpScale, this.isSolo, aliveCount);
|
||||
}
|
||||
|
||||
/** config.titles with the tuning overrides (stat / rule per title id) merged in. */
|
||||
|
||||
@@ -144,7 +144,8 @@ export class MatchPlatform {
|
||||
/**
|
||||
* 中途退出 counts as elimination (research 00-INDEX §3, 01 §9, 06 §7 / §10.3): every copy the player holds goes back
|
||||
* to the shared pool at once, and the seat has no place in later rounds, the Final Assault pairing or the boss pool
|
||||
* (alive × 25 %). Rounds passed = the rounds the player had survived when leaving.
|
||||
* (bloodPoint per player alive at the fight's start, DESIGN §25.13.4). Rounds passed = the rounds the player had
|
||||
* survived when leaving.
|
||||
*/
|
||||
_quit(ps) {
|
||||
this.maybeEndInfo();
|
||||
|
||||
@@ -21,12 +21,13 @@ export class BattleCombat {
|
||||
* derived from 无来源 damage (`from.sourceless`, element bursts) is 无来源 too (hooks see no source, `source` is credited).
|
||||
* `sourceless: true` makes the loss itself 无来源 ("受到等量的无来源生命流失": hooks see no source; `source` keeps the
|
||||
* credit — the stats and the per-player shared-pool tally).
|
||||
* On a leader in a boss / hidden battle a loss of ≥ BOSS_HIT_LIMIT is cancelled like a hit (damage.js leaderHitCancelled).
|
||||
* On a leader in a boss / hidden battle a loss of ≥ BOSS_HIT_LIMIT is cancelled like a hit (damage.js leaderHitCancelled)
|
||||
* — except with `noHitLimit: true`: a share of the leader's pool that is no hit (胄's 死亡集群 drone link, content/bosses.js).
|
||||
*/
|
||||
loseHp(target, amount, { source = null, silent = false, tags = null, from = null, sourceless = false } = {}) {
|
||||
loseHp(target, amount, { source = null, silent = false, tags = null, from = null, sourceless = false, noHitLimit = false } = {}) {
|
||||
if (!target || !target.alive || !(amount > 0)) return 0;
|
||||
// 限伤 (shared/constants.js BOSS_HIT_LIMIT): a loss passed on to a leader (parts' 传递, 无人机) is one hit too
|
||||
if (leaderHitCancelled(this, target, amount)) return 0;
|
||||
// 限伤 (shared/constants.js BOSS_HIT_LIMIT): a loss passed on to a leader (the parts' 传递) is one hit too
|
||||
if (!noHitLimit && leaderHitCancelled(this, target, amount)) return 0;
|
||||
const t = ['hpLoss'];
|
||||
for (const list of [from && from.tags, tags]) if (Array.isArray(list)) for (const x of list) if (!t.includes(x)) t.push(x);
|
||||
return applyHpLoss(this, source, target, amount, { type: 'true', tags: t, noSp: true, silent, origin: from ?? null, sourceless: !!sourceless || !!(from && from.sourceless) });
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
// boss_1/8 假想敌:胄 random arts beam (range 8); 灭顶之灾 one 刺胄之弹 at the highest-ATK operator in range (8-hit
|
||||
// flying shell, 3×3 stun + phys DoT on arrival); <20 %: damage ×0.5 (no extra shell: PRTS
|
||||
// 能力修正); 死亡集群 drones — a drone that dies costs the leader 2 % of its max HP = the shared
|
||||
// pool's (DRONE_LINK_BASE, [ASSUMED] reading). boss_8 adds 斩胄之剑 / 破胄之锤.
|
||||
// pool's (DRONE_LINK_BASE, [ASSUMED] reading), past 限伤 (a share, no hit). boss_8 adds 斩胄之剑 /
|
||||
// 破胄之锤.
|
||||
// 斩胄之剑 / 破胄之锤 初始模式: hover invulnerable next to 胄, AoE attacks (锤 150 % ATK). 出击模式: blink to the
|
||||
// level's blink route, fly at the lowest-ATK operator (3×3 stun + DoT); 15 hits shoot it
|
||||
// down (瘫痪: stun 10 s, ground unit, damage ×1.3). Either way a new 初始模式 copy with its HP
|
||||
@@ -110,8 +111,12 @@ const BLADE_HAND = Object.freeze({ enemy_9014_acstma: 'left_hand', enemy_9015_ac
|
||||
* 'unit': the in-battle unit's data max HP (600 000 / hidden 1 200 000, 不死) — 12 000 / 24 000 per drone at every
|
||||
* difficulty: 0.33 % of the 终极 bar but 19 % of the solo 标准 bar (61 875), so drones decide solo fights.
|
||||
* Round 2 of the boss-HP review tried 'unit'; the review measured the solo regression, so the default is back to 'pool'.
|
||||
* 限伤 (shared/constants.js BOSS_HIT_LIMIT): the loss is one hit through Battle.loseHp — it lands up to the largest pool
|
||||
* today (boss_8 终极 7 200 000 → 144 000) and would be cancelled above a 14 999 950 pool (ceil(0.02 × pool) ≥ 300000).
|
||||
* 限伤 (shared/constants.js BOSS_HIT_LIMIT): the share is no hit and passes the limit (Battle.loseHp `noHitLimit`).
|
||||
* Since the pool counts every player alive at the fight's start (DESIGN §25.13.4, the owner's decision of 2026-10-06),
|
||||
* the hidden 胄 终极 pool reaches 21 600 000 / 28 800 000 with 3 / 4 players — a drone 432 000 / 576 000, which the
|
||||
* limit (ceil ≥ 300000) would cancel, so drones would stop counting exactly in the largest fights. [ASSUMED]: the
|
||||
* official client checks every damage modifier on a leader (research 11 §2.1) and PRTS calls the drone's 2 % 真实伤害,
|
||||
* but which max HP it reads is not documented; the remake reads the pool and keeps the 2 % share.
|
||||
*/
|
||||
export const DRONE_LINK_BASE = 'pool';
|
||||
/** 卢西恩 / 不祥幻影 AoE radius (PRTS "半径2"). */
|
||||
@@ -403,7 +408,7 @@ function kitHelm(ab, e, b, tpl) {
|
||||
if (c.reason !== 'killed' || !(ratio > 0)) return; // a leak is no death
|
||||
const boss = e2.alive ? e2 : b3.aliveEnemies().find((o) => o.isBoss && o.defId === e2.defId);
|
||||
const by = c.killer && c.killer.side === 'ally' ? c.killer : null; // credited to the killing operator
|
||||
if (boss) { b3.loseHp(boss, droneLinkBase(boss) * ratio, { source: by }); b3.fx('beam', { x: d.x, y: d.y, from: d.id, to: boss.id, kind: 'droneLink' }); }
|
||||
if (boss) { b3.loseHp(boss, droneLinkBase(boss) * ratio, { source: by, noHitLimit: true }); b3.fx('beam', { x: d.x, y: d.y, from: d.id, to: boss.id, kind: 'droneLink' }); }
|
||||
},
|
||||
}]);
|
||||
},
|
||||
|
||||
@@ -172,8 +172,8 @@ export function mitigate(amount, type, target, ign = {}) {
|
||||
* number. A 'hitCap' fx event `{ id, n: ceil(amount) }` marks it for the client, which draws nothing — the official
|
||||
* shows no number [ASSUMED]. Every HP-damage kind is checked as the official `modifier.isDamage` (phys, arts, true,
|
||||
* 元素伤害 incl. element bursts, DoT ticks — they all come through dealDamage — and losses passed on to a leader through
|
||||
* Battle.loseHp); element 损伤 (gauge fill, 'element') removes no HP and is never checked. Deterministic (Math.ceil of
|
||||
* the same double on every engine).
|
||||
* Battle.loseHp, except the 胄 drone link's pool share: `noHitLimit`, DESIGN §25.13.4); element 损伤 (gauge fill,
|
||||
* 'element') removes no HP and is never checked. Deterministic (Math.ceil of the same double on every engine).
|
||||
*/
|
||||
export function leaderHitCancelled(battle, target, amount) {
|
||||
if (!(BOSS_HIT_LIMIT > 0) || !target || !target.isBoss || (battle.kind !== 'boss' && battle.kind !== 'hidden')) return false;
|
||||
|
||||
+3
-2
@@ -112,8 +112,9 @@ export function layerGainRoom(before, n) {
|
||||
* = data/bosses.json `enemyKey`; in the sim the tag-'boss' units: those leaders and their mirrored copies, never parts,
|
||||
* escorts or drones) whose `ceil(final damage)` ≥ this is CANCELLED: 0 damage, nothing credited to the shared pool
|
||||
* (`AutoChessStepModeManager._OnBossEnemyTakeDamage` → `modifier.Cancel()`). It is not a clamp: a hit of 299999 lands.
|
||||
* Checked in server/sim/damage.js (dealDamage after DEF / RES and every multiplier, before shields; Battle.loseHp).
|
||||
* Minions, normal rounds and 联防 are unaffected. 0 / Infinity = off.
|
||||
* Checked in server/sim/damage.js (dealDamage after DEF / RES and every multiplier, before shields; Battle.loseHp — not
|
||||
* the 胄 drone link's pool share, `noHitLimit`, DESIGN §25.13.4). Minions, normal rounds and 联防 are unaffected.
|
||||
* 0 / Infinity = off.
|
||||
*/
|
||||
export const BOSS_HIT_LIMIT = 300000;
|
||||
|
||||
|
||||
@@ -2306,6 +2306,29 @@ test('假想敌:胄 死亡集群: the 2 % reads the leader\'s shown max HP (th
|
||||
assert.equal(droneLinkBase(hb, 'unit'), 1200000);
|
||||
});
|
||||
|
||||
test('假想敌:胄 (隐秘核心) 死亡集群 past 限伤: with 3 / 4 players alive the drone share (432 000 / 576 000) still lands — a share, no hit (DESIGN §25.13.4)', () => {
|
||||
// the pool counts every player alive at the fight's start (the owner's decision of 2026-10-06, PR #209): hidden 胄 终极
|
||||
// 7 200 000 × 3 / × 4 — 2 % is ≥ BOSS_HIT_LIMIT (300000), which cancels a hit; the drone link is no hit
|
||||
for (const [n, loss] of [[3, 432000], [4, 576000]]) {
|
||||
const h = bossArena({ kind: 'hidden', hp: 7200000 * n, units: [{ chessId: 't_gun', row: 12, col: 3 }], setup: setTpl('act1autochess_h08_01') });
|
||||
h.step();
|
||||
const boss = put(h, 'enemy_9013_acstmk_2', [3, 10], { tag: 'boss' });
|
||||
boss.profile.noAttack = true;
|
||||
h.run(skb('enemy_9013_acstmk_2', '2').initCooldown + 0.1);
|
||||
const d = alive(h, 'enemy_1005_yokai')[0];
|
||||
assert.ok(d, `${n} players: a drone`);
|
||||
const before = h.b.sharedBoss.hp;
|
||||
h.b.kill(d, h.unit('t_gun'));
|
||||
approx(before - h.b.sharedBoss.hp, loss, 1e-6, `${n} players: 2 % of ${7200000 * n}`);
|
||||
assert.ok(!h.eventsOf('fx').some((f) => f[1] === 'hitCap'), `${n} players: not cancelled`);
|
||||
// a hit of the same size is still cancelled (限伤)
|
||||
const hp = h.b.sharedBoss.hp;
|
||||
h.b.dealDamage(h.unit('t_gun'), boss, { amount: loss, type: 'true', canDodge: false });
|
||||
assert.equal(h.b.sharedBoss.hp, hp, `${n} players: a ${loss} hit is cancelled`);
|
||||
assert.ok(h.eventsOf('fx').some((f) => f[1] === 'hitCap'));
|
||||
}
|
||||
});
|
||||
|
||||
test('假想敌:胄 (隐秘核心) 死亡集群: summon.hp_ratio scales the drone HP', () => {
|
||||
const h = bossArena({ units: [{ chessId: 't_gun', row: 12, col: 3 }], setup: setTpl('act1autochess_h08_01') });
|
||||
h.step();
|
||||
|
||||
@@ -581,12 +581,15 @@ test('playtest6b follow-up (DESIGN §20.10–§20.13): leader HP, 直接乘算,
|
||||
assert.match(BALANCE, /superseded by that binary evidence/);
|
||||
assert.match(R11, /MAX_BATTLE_DAMAGE = 300000/);
|
||||
assert.match(R02, /MAX_GARRISON_STACK = 999/);
|
||||
// §20.9: elite settled, 999 / 限伤 official with their flips, the fixed leader pool settled (aliveScaling off in the data)
|
||||
// §20.9: elite settled, 999 / 限伤 official with their flips, the fixed leader pool settled then — replaced on 2026-10-06
|
||||
// by the owner's decision adopting PR #209 (a pool per player alive at the fight's start, §25.13.4), which the
|
||||
// record points to; the data carries the new rule
|
||||
assert.match(s209, /\| A merge's elite \(§20\.11\) \| takes the consumed deployed copy's tile/);
|
||||
assert.match(s209, /`shared\/constants\.js BOND_LAYER_CAP = 0`/);
|
||||
assert.match(s209, /`shared\/constants\.js BOSS_HIT_LIMIT = 0`/);
|
||||
assert.match(s209, /Settled by the user \("保持固定血量"\)[^\n]*aliveScaling` is \*\*false\*\*/);
|
||||
assert.equal(DATA.config.bossHpScale.aliveScaling, false, 'the user chose the fixed leader pool');
|
||||
assert.match(s209, /Settled by the user \("保持固定血量"\)[^\n]*aliveScaling` is \*\*false\*\*[^\n]*Replaced on 2026-10-06 by the owner's decision[^\n]*§25\.13\.4/);
|
||||
assert.equal(DATA.config.bossHpScale.perPlayer, true, 'the owner adopted the per-player pool (2026-10-06)');
|
||||
assert.equal(DATA.config.bossHpScale.aliveScaling, false, 'the fixed pool\'s optional × alive / 4 stays off');
|
||||
assert.equal(SIM_CONST.DIRECT_BONUS_STACKING, 'add');
|
||||
assert.match(s209, /DIRECT_BONUS_STACKING = 'multiply'/);
|
||||
// the normative lines
|
||||
|
||||
+362
-343
File diff suppressed because it is too large.
Load diff
+45
-45
@@ -16,7 +16,7 @@
|
||||
"combat": "client",
|
||||
"boost": null,
|
||||
"setup": {"stageId":"act1autochess_m01","bossId":"boss_5","hiddenBossId":null,"factions":["TIMES","DOT","INVISIBLE"]},
|
||||
"end": {"victory":true,"roundsPassed":9,"hiddenReached":false,"hiddenCleared":false,"reason":"victory","teamLp":27,"bossPoolLeft":0,"virtualMs":68100},
|
||||
"end": {"victory":true,"roundsPassed":9,"hiddenReached":false,"hiddenCleared":false,"reason":"victory","teamLp":21,"bossPoolLeft":0,"virtualMs":68100},
|
||||
"rng": {"setup":16,"shop":142,"waves":0,"draft":0,"bots":5205,"meta":0},
|
||||
"errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0},
|
||||
"rounds": [
|
||||
@@ -35,12 +35,12 @@
|
||||
"victory": true,
|
||||
"roundsPassed": 9,
|
||||
"eliminatedRound": null,
|
||||
"lp": 27,
|
||||
"lp": 21,
|
||||
"bandId": "band_ermengard",
|
||||
"title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
|
||||
"trophies": 0,
|
||||
"reward": 113,
|
||||
"stats": {"activatedLayers":78,"bossDamage":50000,"dmgDealt":545829,"gold":72,"itemsEquipped":10,"kills":160,"leaks":0,"lpLost":0,"merges":2,"perfectRounds":8,"refreshes":14},
|
||||
"stats": {"activatedLayers":78,"bossDamage":200000,"dmgDealt":841708,"gold":72,"itemsEquipped":10,"kills":175,"leaks":0,"lpLost":0,"merges":2,"perfectRounds":8,"refreshes":14},
|
||||
"lineup": ["4_14_a@12,7","2_08_a@12,8[4_05_e_a+2_04_e_b]","2_14_a@11,8","3_10_a@10,3","2_17_a@10,4[1_01_e_b+4_02_e_a]","4_01_b@10,7","1_12_b@10,8[3_05_e_a+5_02_e_a]","5_02_a@9,8[4_08_e_a]"],
|
||||
"bonds": ["deputShip 0/16","emptyShip 1/0","indomShip 0/7","preciShip 1/7","sargonShip 1/28","steadShip 2/38","swiftShip 1/5"]
|
||||
}
|
||||
@@ -56,7 +56,7 @@
|
||||
"combat": "client",
|
||||
"boost": null,
|
||||
"setup": {"stageId":"act1autochess_m01","bossId":"boss_5","hiddenBossId":null,"factions":["ELEMENT","DOT","REFLECTION"]},
|
||||
"end": {"victory":true,"roundsPassed":9,"hiddenReached":false,"hiddenCleared":false,"reason":"victory","teamLp":23,"bossPoolLeft":0,"virtualMs":68100},
|
||||
"end": {"victory":false,"roundsPassed":8,"hiddenReached":false,"hiddenCleared":false,"reason":"defeat","teamLp":0,"bossPoolLeft":124751,"virtualMs":68100},
|
||||
"rng": {"setup":16,"shop":123,"waves":0,"draft":0,"bots":5205,"meta":28},
|
||||
"errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0},
|
||||
"rounds": [
|
||||
@@ -75,15 +75,15 @@
|
||||
"players": {
|
||||
"ai_0": {
|
||||
"alive": true,
|
||||
"victory": true,
|
||||
"roundsPassed": 9,
|
||||
"victory": false,
|
||||
"roundsPassed": 8,
|
||||
"eliminatedRound": null,
|
||||
"lp": 23,
|
||||
"lp": 0,
|
||||
"bandId": "band_ducklord",
|
||||
"title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
|
||||
"title": {"id":"comment_2","name":"不朽盟约","picId":"comment_icon_2","text":"激活的盟约层数总和最高"},
|
||||
"trophies": 0,
|
||||
"reward": 113,
|
||||
"stats": {"activatedLayers":182,"bossDamage":50000,"dmgDealt":550945,"gold":68,"itemsEquipped":11,"kills":132,"leaks":6,"lpLost":6,"merges":3,"perfectRounds":5,"refreshes":11},
|
||||
"reward": 100,
|
||||
"stats": {"activatedLayers":182,"bossDamage":75249,"dmgDealt":642303,"gold":68,"itemsEquipped":11,"kills":140,"leaks":6,"lpLost":6,"merges":3,"perfectRounds":5,"refreshes":11},
|
||||
"lineup": ["3_20_a@12,3[3_11_e_b+1_05_e_a]","1_02_b@12,8[3_10_e_b+4_10_e_a]","1_06_a@11,3[5_02_e_a+1_05_e_b]","3_08_b@11,8","4_14_a@10,3","3_16_a@10,4","5_22_a@9,8","2_05_a@9,9"],
|
||||
"bonds": ["deputShip 1/22","emptyShip 0/12","indomShip 0/2","kjeragShip 1/34","steadShip 1/21","swiftShip 1/21","victoriaShip 1/84"]
|
||||
}
|
||||
@@ -99,7 +99,7 @@
|
||||
"combat": "client",
|
||||
"boost": null,
|
||||
"setup": {"stageId":"act1autochess_m03","bossId":"boss_5","hiddenBossId":"boss_8","factions":["TIMES","DOT","INVISIBLE"]},
|
||||
"end": {"victory":true,"roundsPassed":14,"hiddenReached":false,"hiddenCleared":false,"reason":"victory","teamLp":17,"bossPoolLeft":0,"virtualMs":106900},
|
||||
"end": {"victory":false,"roundsPassed":13,"hiddenReached":false,"hiddenCleared":false,"reason":"defeat","teamLp":0,"bossPoolLeft":93088,"virtualMs":106900},
|
||||
"rng": {"setup":25,"shop":207,"waves":0,"draft":10,"bots":8483,"meta":14},
|
||||
"errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0},
|
||||
"rounds": [
|
||||
@@ -144,15 +144,15 @@
|
||||
"players": {
|
||||
"ai_0": {
|
||||
"alive": true,
|
||||
"victory": true,
|
||||
"roundsPassed": 14,
|
||||
"victory": false,
|
||||
"roundsPassed": 13,
|
||||
"eliminatedRound": null,
|
||||
"lp": 17,
|
||||
"lp": 0,
|
||||
"bandId": "band_ermengard",
|
||||
"title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
|
||||
"title": {"id":"comment_2","name":"不朽盟约","picId":"comment_icon_2","text":"激活的盟约层数总和最高"},
|
||||
"trophies": 0,
|
||||
"reward": 400,
|
||||
"stats": {"activatedLayers":290,"bossDamage":97500,"dmgDealt":1744405,"gold":131,"itemsEquipped":23,"kills":371,"leaks":11,"lpLost":10,"merges":5,"perfectRounds":12,"refreshes":15},
|
||||
"reward": 320,
|
||||
"stats": {"activatedLayers":290,"bossDamage":296912,"dmgDealt":2081142,"gold":131,"itemsEquipped":23,"kills":383,"leaks":11,"lpLost":10,"merges":5,"perfectRounds":12,"refreshes":15},
|
||||
"lineup": ["2_14_a@12,5","5_10_a@12,7[4_07_e_a+3_08_e_b]","5_20_b@11,5[6_09_e_a+5_02_e_b]","3_20_b@11,8[3_06_e_a+4_08_e_a]","6_11_a@10,3[4_12_e_a+5_02_e_b]","3_18_a@10,8[2_04_e_b+4_05_e_b]","6_19_a@9,4[4_04_e_a+4_10_e_a]","1_08_a@9,5[1_01_e_a+1_02_e_a]"],
|
||||
"bonds": ["arcaneShip 0/2","deputShip 1/14","emptyShip 1/10","kazimierzShip 0/16","kjeragShip 1/24","maniShip 1/2","miraShip 1/15","preciShip 0/12","sargonShip 0/8","siracusaShip 1/151","soloShip 1/22","steadShip 0/6","suntShip 1/8","swiftShip 1/44"]
|
||||
}
|
||||
@@ -227,7 +227,7 @@
|
||||
"title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
|
||||
"trophies": 0,
|
||||
"reward": 400,
|
||||
"stats": {"activatedLayers":268,"bossDamage":97500,"dmgDealt":2254275,"gold":140,"itemsEquipped":13,"kills":270,"leaks":4,"lpLost":4,"merges":5,"perfectRounds":11,"refreshes":18},
|
||||
"stats": {"activatedLayers":268,"bossDamage":390000,"dmgDealt":2637590,"gold":140,"itemsEquipped":13,"kills":278,"leaks":4,"lpLost":4,"merges":5,"perfectRounds":11,"refreshes":18},
|
||||
"lineup": ["1_17_b@12,5","4_14_b@11,4","3_11_a@11,5[1_05_e_a+3_08_e_a]","5_07_a@11,6[4_02_e_a+4_05_e_a]","4_10_a@10,3","1_02_b@10,4[4_12_e_a+4_06_e_a]","5_14_a@10,5[1_05_e_b+1_05_e_b]","4_04_a@10,6"],
|
||||
"bonds": ["arcaneShip 2/40","kazimierzShip 0/2","kjeragShip 1/13","lateranoShip 0/3","preciShip 2/58","raidShip 1/84","sargonShip 0/2","steadShip 0/8","suntShip 1/0","swiftShip 1/53","visiShip 1/20"]
|
||||
}
|
||||
@@ -243,7 +243,7 @@
|
||||
"combat": "client",
|
||||
"boost": null,
|
||||
"setup": {"stageId":"act1autochess_m03","bossId":"boss_5","hiddenBossId":"boss_8","factions":["TIMES","DOT","INVISIBLE"]},
|
||||
"end": {"victory":true,"roundsPassed":14,"hiddenReached":false,"hiddenCleared":false,"reason":"victory","teamLp":10,"bossPoolLeft":0,"virtualMs":107800},
|
||||
"end": {"victory":false,"roundsPassed":13,"hiddenReached":false,"hiddenCleared":false,"reason":"defeat","teamLp":0,"bossPoolLeft":94716,"virtualMs":107800},
|
||||
"rng": {"setup":25,"shop":201,"waves":0,"draft":42,"bots":8376,"meta":2},
|
||||
"errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0},
|
||||
"rounds": [
|
||||
@@ -288,15 +288,15 @@
|
||||
"players": {
|
||||
"ai_0": {
|
||||
"alive": true,
|
||||
"victory": true,
|
||||
"roundsPassed": 14,
|
||||
"victory": false,
|
||||
"roundsPassed": 13,
|
||||
"eliminatedRound": null,
|
||||
"lp": 10,
|
||||
"lp": 0,
|
||||
"bandId": "band_ermengard",
|
||||
"title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
|
||||
"title": {"id":"comment_2","name":"不朽盟约","picId":"comment_icon_2","text":"激活的盟约层数总和最高"},
|
||||
"trophies": 0,
|
||||
"reward": 425,
|
||||
"stats": {"activatedLayers":268,"bossDamage":195000,"dmgDealt":1669187,"gold":140,"itemsEquipped":16,"kills":380,"leaks":26,"lpLost":17,"merges":3,"perfectRounds":10,"refreshes":15},
|
||||
"reward": 340,
|
||||
"stats": {"activatedLayers":268,"bossDamage":685284,"dmgDealt":2273515,"gold":140,"itemsEquipped":16,"kills":391,"leaks":26,"lpLost":17,"merges":3,"perfectRounds":10,"refreshes":15},
|
||||
"lineup": ["6_12_a@12,5[5_01_e_a+5_02_e_a]","5_10_a@12,7[4_07_e_a+5_02_e_b]","4_14_a@11,8","3_11_a@10,3","5_14_a@9,4[3_08_e_a+3_11_e_a]","1_08_a@9,5[1_01_e_a+4_05_e_a]","6_02_a@9,6","4_10_a@9,8[4_08_e_a+4_10_e_b]"],
|
||||
"bonds": ["arcaneShip 1/30","deputShip 0/14","emptyShip 1/10","kazimierzShip 0/16","kjeragShip 1/51","preciShip 2/50","sargonShip 0/8","siracusaShip 1/79","soloShip 1/15","steadShip 0/21","swiftShip 1/33","visiShip 1/0"]
|
||||
}
|
||||
@@ -312,7 +312,7 @@
|
||||
"combat": "client",
|
||||
"boost": null,
|
||||
"setup": {"stageId":"act1autochess_m03","bossId":"boss_5","hiddenBossId":"boss_10","factions":["ELEMENT","DOT","REFLECTION"]},
|
||||
"end": {"victory":true,"roundsPassed":14,"hiddenReached":false,"hiddenCleared":false,"reason":"victory","teamLp":3,"bossPoolLeft":0,"virtualMs":107800},
|
||||
"end": {"victory":false,"roundsPassed":13,"hiddenReached":false,"hiddenCleared":false,"reason":"defeat","teamLp":0,"bossPoolLeft":176395,"virtualMs":107800},
|
||||
"rng": {"setup":25,"shop":279,"waves":0,"draft":42,"bots":8378,"meta":87},
|
||||
"errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0},
|
||||
"rounds": [
|
||||
@@ -363,15 +363,15 @@
|
||||
"players": {
|
||||
"ai_0": {
|
||||
"alive": true,
|
||||
"victory": true,
|
||||
"roundsPassed": 14,
|
||||
"victory": false,
|
||||
"roundsPassed": 13,
|
||||
"eliminatedRound": null,
|
||||
"lp": 3,
|
||||
"lp": 0,
|
||||
"bandId": "band_ducklord",
|
||||
"title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
|
||||
"title": {"id":"comment_2","name":"不朽盟约","picId":"comment_icon_2","text":"激活的盟约层数总和最高"},
|
||||
"trophies": 0,
|
||||
"reward": 425,
|
||||
"stats": {"activatedLayers":163,"bossDamage":195000,"dmgDealt":2277788,"gold":141,"itemsEquipped":11,"kills":264,"leaks":26,"lpLost":26,"merges":6,"perfectRounds":9,"refreshes":25},
|
||||
"reward": 340,
|
||||
"stats": {"activatedLayers":163,"bossDamage":603605,"dmgDealt":2769206,"gold":141,"itemsEquipped":11,"kills":273,"leaks":26,"lpLost":26,"merges":6,"perfectRounds":9,"refreshes":25},
|
||||
"lineup": ["1_17_b@12,5","3_17_a@12,6","4_20_a@12,7","4_10_b@10,3","2_18_a@10,5[4_11_e_a+1_05_e_b]","1_19_b@10,7","6_17_a@10,8[3_07_e_a+3_04_e_a]","6_19_a@9,5[4_02_e_b+5_03_e_a]"],
|
||||
"bonds": ["arcaneShip 1/48","kazimierzShip 2/48","kjeragShip 0/37","lateranoShip 0/3","preciShip 1/29","sargonShip 0/2","soloShip 1/0","steadShip 0/8","suntShip 1/0","swiftShip 1/38"]
|
||||
}
|
||||
@@ -596,7 +596,7 @@
|
||||
"title": {"id":"comment_2","name":"不朽盟约","picId":"comment_icon_2","text":"激活的盟约层数总和最高"},
|
||||
"trophies": 4,
|
||||
"reward": 400,
|
||||
"stats": {"activatedLayers":250,"bossDamage":280840,"dmgDealt":2842128,"gold":139,"itemsEquipped":12,"kills":325,"leaks":11,"lpLost":9,"merges":5,"perfectRounds":11,"refreshes":23},
|
||||
"stats": {"activatedLayers":250,"bossDamage":490972,"dmgDealt":3069097,"gold":139,"itemsEquipped":12,"kills":331,"leaks":11,"lpLost":9,"merges":5,"perfectRounds":11,"refreshes":23},
|
||||
"lineup": ["3_19_b@11,7[2_04_e_b]","5_12_a@11,8","4_10_a@10,5[1_02_e_b+2_01_e_a]","6_06_a@9,4","1_08_b@9,5","6_16_a@9,7","5_20_a@9,8[3_05_e_a+3_02_e_b]","3_13_b@9,9"],
|
||||
"bonds": ["arcaneShip 2/86","miraShip 1/12","preciShip 0/10","sargonShip 0/10","siracusaShip 1/110","skillfulShip 1/36","soloShip 1/6","suntShip 1/0","swiftShip 0/32"]
|
||||
},
|
||||
@@ -610,7 +610,7 @@
|
||||
"title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
|
||||
"trophies": 4,
|
||||
"reward": 400,
|
||||
"stats": {"activatedLayers":199,"bossDamage":427910,"dmgDealt":3303988,"gold":137,"itemsEquipped":18,"kills":325,"leaks":6,"lpLost":6,"merges":5,"perfectRounds":12,"refreshes":13},
|
||||
"stats": {"activatedLayers":199,"bossDamage":926528,"dmgDealt":3823060,"gold":137,"itemsEquipped":18,"kills":325,"leaks":6,"lpLost":6,"merges":5,"perfectRounds":12,"refreshes":13},
|
||||
"lineup": ["4_20_a@12,8[3_06_e_a]","1_17_b@11,7[3_08_e_a]","4_02_b@11,8[3_02_e_a+4_03_e_b]","4_01_b@10,7[4_04_e_a+1_02_e_a]","6_17_a@10,8[4_06_e_a+4_08_e_a]","6_19_a@9,7[1_05_e_a+1_01_e_b]","4_14_a@9,8","5_07_a@9,9"],
|
||||
"bonds": ["arcaneShip 2/34","kazimierzShip 1/38","kjeragShip 0/8","lateranoShip 0/8","preciShip 2/55","raidShip 1/0","sargonShip 0/10","suntShip 1/0","swiftShip 1/72"]
|
||||
}
|
||||
@@ -976,7 +976,7 @@
|
||||
"title": {"id":"comment_2","name":"不朽盟约","picId":"comment_icon_2","text":"激活的盟约层数总和最高"},
|
||||
"trophies": 3,
|
||||
"reward": 250,
|
||||
"stats": {"activatedLayers":482,"bossDamage":46468,"dmgDealt":1661472,"gold":140,"itemsEquipped":25,"kills":311,"leaks":0,"lpLost":0,"merges":5,"perfectRounds":13,"refreshes":21},
|
||||
"stats": {"activatedLayers":482,"bossDamage":190683,"dmgDealt":1852681,"gold":140,"itemsEquipped":25,"kills":319,"leaks":0,"lpLost":0,"merges":5,"perfectRounds":13,"refreshes":21},
|
||||
"lineup": ["6_08_a@11,6[3_03_e_a+3_02_e_a]","6_12_a@11,7[4_08_e_a+6_07_e_b]","2_14_b@10,3","2_17_b@10,4[4_02_e_b+5_01_e_a]","2_08_a@9,3[4_04_e_a+1_01_e_b]","6_06_a@9,4[3_08_e_a+3_01_e_b]","1_06_b@9,8","5_02_b@9,9[5_02_e_a+3_06_e_b]"],
|
||||
"bonds": ["deputShip 1/61","emptyShip 1/6","kjeragShip 0/7","preciShip 2/157","sargonShip 1/170","steadShip 1/88"]
|
||||
},
|
||||
@@ -990,7 +990,7 @@
|
||||
"title": {"id":"comment_3","name":"坚若磐石","picId":"comment_icon_3","text":"目标生命值损失最少"},
|
||||
"trophies": 3,
|
||||
"reward": 250,
|
||||
"stats": {"activatedLayers":207,"bossDamage":11698,"dmgDealt":1301544,"gold":129,"itemsEquipped":20,"kills":238,"leaks":46,"lpLost":0,"merges":6,"perfectRounds":10,"refreshes":20},
|
||||
"stats": {"activatedLayers":207,"bossDamage":73033,"dmgDealt":1373511,"gold":129,"itemsEquipped":20,"kills":238,"leaks":46,"lpLost":0,"merges":6,"perfectRounds":10,"refreshes":20},
|
||||
"lineup": ["2_14_b@12,3","5_10_a@11,6[4_03_e_b+1_02_e_a]","6_01_a@11,7[3_05_e_a]","5_10_a@11,8[5_09_e_a+4_08_e_b]","6_14_a@10,3","4_16_a@10,8[4_11_e_a+4_04_e_b]","6_05_a@9,8[1_05_e_a+5_03_e_a]","2_16_a@9,9[5_02_e_a+4_05_e_b]"],
|
||||
"bonds": ["deputShip 1/96","emptyShip 1/23","preciShip 1/14","sargonShip 0/2","siracusaShip 1/74","steadShip 0/4"]
|
||||
},
|
||||
@@ -1004,7 +1004,7 @@
|
||||
"title": {"id":"comment_5","name":"万事俱备","picId":"comment_icon_5","text":"装备配发数量最多"},
|
||||
"trophies": 3,
|
||||
"reward": 250,
|
||||
"stats": {"activatedLayers":260,"bossDamage":81958,"dmgDealt":1639551,"gold":127,"itemsEquipped":20,"kills":283,"leaks":0,"lpLost":0,"merges":3,"perfectRounds":13,"refreshes":16},
|
||||
"stats": {"activatedLayers":260,"bossDamage":308467,"dmgDealt":1912652,"gold":127,"itemsEquipped":20,"kills":291,"leaks":0,"lpLost":0,"merges":3,"perfectRounds":13,"refreshes":16},
|
||||
"lineup": ["4_25_a@12,3","1_07_b@11,7[1_01_e_a+1_05_e_a]","5_20_a@11,8[3_02_e_a+2_01_e_b]","6_18_a@10,3[2_01_e_b+4_03_e_b]","4_10_a@10,7[4_03_e_b]","3_18_b@9,4[3_07_e_b+3_11_e_a]","6_05_a@9,8","2_16_b@9,9[5_01_e_a+4_02_e_a]"],
|
||||
"bonds": ["sargonShip 0/8","siracusaShip 2/236","swiftShip 1/24","yanShip 0/1"]
|
||||
},
|
||||
@@ -1018,7 +1018,7 @@
|
||||
"title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
|
||||
"trophies": 3,
|
||||
"reward": 250,
|
||||
"stats": {"activatedLayers":311,"bossDamage":144877,"dmgDealt":1819100,"gold":138,"itemsEquipped":36,"kills":300,"leaks":1,"lpLost":0,"merges":3,"perfectRounds":12,"refreshes":23},
|
||||
"stats": {"activatedLayers":311,"bossDamage":567817,"dmgDealt":2252972,"gold":138,"itemsEquipped":36,"kills":300,"leaks":1,"lpLost":0,"merges":3,"perfectRounds":12,"refreshes":23},
|
||||
"lineup": ["3_20_b@12,8[5_02_e_b+6_03_e_a]","6_14_a@11,3","4_13_b@11,6[5_02_e_b+4_07_e_a]","6_01_a@11,7[3_11_e_a+3_04_e_a]","4_14_a@11,8[4_03_e_a+1_02_e_a]","4_22_a@10,8[4_04_e_b+4_02_e_a]","3_11_b@9,8[4_09_e_a+3_07_e_a]","5_14_a@9,9[4_06_e_b+2_01_e_b]"],
|
||||
"bonds": ["deputShip 1/20","emptyShip 0/12","kjeragShip 1/120","preciShip 2/117","siracusaShip 0/3","swiftShip 1/54","yanShip 0/1"]
|
||||
}
|
||||
@@ -1303,7 +1303,7 @@
|
||||
"combat": "client",
|
||||
"boost": {"lp":400,"layers":300},
|
||||
"setup": {"stageId":"act1autochess_m02","bossId":"boss_5","hiddenBossId":"boss_10","factions":["TIMES","ELEMENT","DOT"]},
|
||||
"end": {"victory":true,"roundsPassed":15,"hiddenReached":true,"hiddenCleared":true,"reason":"victory","teamLp":399,"bossPoolLeft":0,"virtualMs":113700},
|
||||
"end": {"victory":true,"roundsPassed":15,"hiddenReached":true,"hiddenCleared":true,"reason":"victory","teamLp":398,"bossPoolLeft":0,"virtualMs":113700},
|
||||
"rng": {"setup":25,"shop":356,"waves":0,"draft":42,"bots":10350,"meta":3},
|
||||
"errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0},
|
||||
"rounds": [
|
||||
@@ -1366,12 +1366,12 @@
|
||||
"victory": true,
|
||||
"roundsPassed": 15,
|
||||
"eliminatedRound": null,
|
||||
"lp": 399,
|
||||
"lp": 398,
|
||||
"bandId": "band_paganini",
|
||||
"title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
|
||||
"trophies": 0,
|
||||
"reward": 425,
|
||||
"stats": {"activatedLayers":2279,"bossDamage":1145000,"dmgDealt":3042833,"gold":175,"itemsEquipped":26,"kills":366,"leaks":1,"lpLost":1,"merges":6,"perfectRounds":12,"refreshes":35},
|
||||
"stats": {"activatedLayers":2279,"bossDamage":4580000,"dmgDealt":6590189,"gold":175,"itemsEquipped":26,"kills":376,"leaks":1,"lpLost":1,"merges":6,"perfectRounds":12,"refreshes":35},
|
||||
"lineup": ["4_02_b@12,7[3_08_e_a+3_03_e_b]","6_01_a@12,8[3_02_e_a]","6_16_a@11,5[1_05_e_b+3_03_e_a]","3_18_a@11,8[4_11_e_a+4_05_e_b]","4_10_b@10,4[4_07_e_a+1_02_e_b]","6_05_a@10,5","4_01_b@10,6[6_03_e_a+4_06_e_a]","1_08_b@9,9"],
|
||||
"bonds": ["arcaneShip 2/395","deputShip 0/300","egirShip 0/300","emptyShip 0/300","indomShip 0/308","investShip 0/300","kazimierzShip 0/308","kjeragShip 0/300","lateranoShip 1/306","maniShip 0/300","miraShip 0/300","preciShip 1/302","raidShip 0/300","sargonShip 0/300","siracusaShip 1/353","skillfulShip 0/343","soloShip 1/307","steadShip 0/300","suntShip 1/300","swiftShip 1/316","victoriaShip 0/300","visiShip 0/300","yanShip 0/300"]
|
||||
}
|
||||
@@ -1468,7 +1468,7 @@
|
||||
"title": {"id":"comment_2","name":"不朽盟约","picId":"comment_icon_2","text":"激活的盟约层数总和最高"},
|
||||
"trophies": 8,
|
||||
"reward": 425,
|
||||
"stats": {"activatedLayers":2713,"bossDamage":883949,"dmgDealt":8912493,"gold":153,"itemsEquipped":19,"kills":273,"leaks":6,"lpLost":0,"merges":4,"perfectRounds":12,"refreshes":33},
|
||||
"stats": {"activatedLayers":2713,"bossDamage":1676605,"dmgDealt":10050362,"gold":153,"itemsEquipped":19,"kills":283,"leaks":6,"lpLost":0,"merges":4,"perfectRounds":12,"refreshes":33},
|
||||
"lineup": ["2_18_b@11,5[2_01_e_a+3_03_e_a]","3_13_b@11,8","4_09_a@10,3","3_05_a@10,8[1_01_e_b+4_05_e_b]","3_09_a@9,6","6_16_a@9,7[3_11_e_a+1_02_e_b]","4_20_a@9,8","3_12_a@9,9[4_03_e_a]"],
|
||||
"bonds": ["arcaneShip 1/387","deputShip 0/304","egirShip 1/353","emptyShip 0/300","indomShip 0/300","investShip 0/300","kazimierzShip 1/356","kjeragShip 0/302","lateranoShip 0/300","maniShip 0/300","miraShip 0/300","preciShip 0/318","raidShip 1/330","sargonShip 0/300","siracusaShip 0/300","skillfulShip 1/340","soloShip 1/304","steadShip 1/343","suntShip 1/300","swiftShip 0/302","victoriaShip 0/300","visiShip 0/300","yanShip 0/300"]
|
||||
},
|
||||
@@ -1482,7 +1482,7 @@
|
||||
"title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
|
||||
"trophies": 8,
|
||||
"reward": 425,
|
||||
"stats": {"activatedLayers":2334,"bossDamage":6063984,"dmgDealt":20631738,"gold":157,"itemsEquipped":29,"kills":286,"leaks":3,"lpLost":0,"merges":4,"perfectRounds":11,"refreshes":30},
|
||||
"stats": {"activatedLayers":2334,"bossDamage":12218936,"dmgDealt":34332028,"gold":157,"itemsEquipped":29,"kills":286,"leaks":3,"lpLost":0,"merges":4,"perfectRounds":11,"refreshes":30},
|
||||
"lineup": ["3_16_a@12,8[3_09_e_b+4_07_e_a]","2_10_a@11,5[3_11_e_a+2_03_e_b]","6_05_a@11,8[3_05_e_b+3_10_e_b]","6_08_a@10,3[3_01_e_b+1_01_e_b]","5_11_a@10,4[4_12_e_a+5_09_e_a]","6_14_a@9,7[4_09_e_a+4_04_e_b]","1_06_b@9,8[1_05_e_b+2_01_e_b]","1_17_b@9,9[2_03_e_b+2_03_e_a]"],
|
||||
"bonds": ["arcaneShip 1/330","deputShip 1/326","egirShip 0/300","emptyShip 0/300","indomShip 0/326","investShip 0/300","kazimierzShip 0/305","kjeragShip 0/300","lateranoShip 0/300","maniShip 0/300","miraShip 0/300","preciShip 2/366","raidShip 0/300","sargonShip 0/300","siracusaShip 0/300","skillfulShip 0/300","soloShip 1/316","steadShip 1/340","suntShip 1/310","swiftShip 0/300","victoriaShip 1/346","visiShip 0/300","yanShip 0/305"]
|
||||
}
|
||||
@@ -1696,7 +1696,7 @@
|
||||
"title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
|
||||
"trophies": 4,
|
||||
"reward": 400,
|
||||
"stats": {"activatedLayers":513,"bossDamage":591866,"dmgDealt":4855342,"gold":160,"itemsEquipped":22,"kills":346,"leaks":0,"lpLost":0,"merges":5,"perfectRounds":13,"refreshes":31},
|
||||
"stats": {"activatedLayers":513,"bossDamage":1147951,"dmgDealt":5456360,"gold":160,"itemsEquipped":22,"kills":352,"leaks":0,"lpLost":0,"merges":5,"perfectRounds":13,"refreshes":31},
|
||||
"lineup": ["6_05_a@12,8[4_08_e_a+2_01_e_b]","5_02_a@11,6[6_01_e_a]","6_05_a@11,7[3_04_e_a+3_03_e_a]","4_20_b@11,8","4_14_b@10,6[3_11_e_a+5_01_e_b]","5_21_b@10,8[1_01_e_a+6_03_e_a]","4_07_b@9,8[4_04_e_b+4_11_e_b]","4_23_b@9,9[4_06_e_a]"],
|
||||
"bonds": ["arcaneShip 0/2","indomShip 0/30","preciShip 2/203","sargonShip 1/187","steadShip 0/54","suntShip 2/0","swiftShip 1/25","victoriaShip 0/21","visiShip 1/98"]
|
||||
},
|
||||
@@ -1710,7 +1710,7 @@
|
||||
"title": {"id":"comment_3","name":"坚若磐石","picId":"comment_icon_3","text":"目标生命值损失最少"},
|
||||
"trophies": 4,
|
||||
"reward": 400,
|
||||
"stats": {"activatedLayers":108,"bossDamage":195634,"dmgDealt":2640559,"gold":134,"itemsEquipped":19,"kills":246,"leaks":47,"lpLost":0,"merges":6,"perfectRounds":8,"refreshes":12},
|
||||
"stats": {"activatedLayers":108,"bossDamage":427049,"dmgDealt":2882939,"gold":134,"itemsEquipped":19,"kills":246,"leaks":47,"lpLost":0,"merges":6,"perfectRounds":8,"refreshes":12},
|
||||
"lineup": ["5_20_a@12,8","5_10_a@11,7","6_05_a@11,8[5_03_e_a+1_01_e_a]","3_13_a@10,5","5_02_a@10,6[3_07_e_a+4_03_e_a]","3_03_b@10,8[3_11_e_b+2_04_e_a]","2_04_b@9,8","1_08_b@9,9[1_02_e_a+4_06_e_a]"],
|
||||
"bonds": ["preciShip 1/24","sargonShip 1/32","siracusaShip 1/28","skillfulShip 1/24","soloShip 1/0","suntShip 1/0","yanShip 0/33"]
|
||||
}
|
||||
|
||||
+10
-10
@@ -13,7 +13,7 @@ import {
|
||||
|
||||
const RAW = withoutTuning(DATA);
|
||||
|
||||
test('no custom balance: legacy tuning multipliers are ignored; enemy scale = the PRTS table; leader pool = bloodPoint', () => {
|
||||
test('no custom balance: legacy tuning multipliers are ignored; enemy scale = the PRTS table; leader pool = bloodPoint × alive players', () => {
|
||||
const tuning = {
|
||||
modes: {
|
||||
'*': { enemyHpMul: 0.5, enemyAtkMul: 0.9 },
|
||||
@@ -28,21 +28,21 @@ test('no custom balance: legacy tuning multipliers are ignored; enemy scale = th
|
||||
assert.deepEqual(hard.enemyScale(r), { hpMul: e.hp, atkMul: e.atk, speedMul: e.speed }, `R${r}: the config (PRTS) table`);
|
||||
}
|
||||
assert.equal(hard.bossHpMul('boss_1'), 1);
|
||||
// co-op: one pool = bloodPoint[difficulty] whatever the alive count (× alive / 4 only with config aliveScaling, off —
|
||||
// DESIGN §20.10); solo: ×0.25 [ASSUMED, flagged]
|
||||
// co-op: one pool = bloodPoint[difficulty] × the players alive at the fight's start; solo × 1 (DESIGN §25.13.4, the
|
||||
// owner's decision of 2026-10-06 adopting PR #209 — it replaces the fixed pool of DESIGN §20.10)
|
||||
for (const [modeId, key] of [['mode_multi_funny', 'FUNNY'], ['mode_multi_normal', 'NORMAL'], ['mode_multi_hard', 'HARD'], ['mode_multi_abyss', 'ABYSS']]) {
|
||||
const gd = new GameData(RAW, modeId);
|
||||
for (const b of ['boss_1', 'boss_5', 'boss_8']) {
|
||||
assert.equal(gd.bossPoolHp(b), DATA.bosses[b].bloodPoint[key], `${modeId} ${b}`);
|
||||
assert.equal(gd.bossPoolHp(b, 4), DATA.bosses[b].bloodPoint[key], `${modeId} ${b} four alive`);
|
||||
assert.equal(gd.bossPoolHp(b, 2), DATA.bosses[b].bloodPoint[key], `${modeId} ${b} two alive: still the data value`);
|
||||
assert.equal(gd.bossPoolHp(b), DATA.bosses[b].bloodPoint[key] * 4, `${modeId} ${b}: no count, a full team`);
|
||||
assert.equal(gd.bossPoolHp(b, 4), DATA.bosses[b].bloodPoint[key] * 4, `${modeId} ${b} four alive`);
|
||||
assert.equal(gd.bossPoolHp(b, 2), DATA.bosses[b].bloodPoint[key] * 2, `${modeId} ${b} two alive: twice the data value`);
|
||||
}
|
||||
}
|
||||
assert.equal(DATA.config.bossHpScale.aliveScaling, false);
|
||||
assert.equal(DATA.config.bossHpScale.aliveAssumed, true);
|
||||
assert.equal(DATA.config.bossHpScale.perPlayer, true);
|
||||
assert.equal(DATA.config.bossHpScale.aliveScaling, false, 'the fixed pool\'s optional scaling stays off');
|
||||
const solo = new GameData(RAW, 'mode_single_hard');
|
||||
assert.equal(solo.bossPoolHp('boss_2'), Math.round(DATA.bosses.boss_2.bloodPoint.HARD * 0.25));
|
||||
assert.equal(DATA.config.bossHpScale.soloAssumed, true);
|
||||
assert.equal(solo.bossPoolHp('boss_2'), DATA.bosses.boss_2.bloodPoint.HARD);
|
||||
assert.equal(DATA.config.bossHpScale.solo, 1);
|
||||
// the waves carry exactly the table
|
||||
const setup = setupMatchWaves(hard, createRng(3));
|
||||
const w = buildNormalWave(hard, createRng(3), setup.factions, 3);
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// The Final Assault / Hidden Core leader pool = bloodPoint[difficulty] × the players alive when the fight starts (solo
|
||||
// × 1) — the owner's decision of 2026-10-06, adopting PR #209 by @qingjingshenghuo (these tests are ported from it);
|
||||
// it replaces the fixed pool of 「保持固定血量」 (DESIGN §20.9 / §20.10), which config bossHpScale still restores
|
||||
// (DESIGN §25.13.4, server/match/gamedata.js bossPoolShareOf).
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeMatch, DATA } from './harness.js';
|
||||
import { GameData } from '../../server/match/gamedata.js';
|
||||
import { bossPoolHp, SharedBossPool } from '../../server/match/finalAssault.js';
|
||||
import { makeBattle } from '../helpers/battleHarness.js';
|
||||
|
||||
const DIFFS = ['FUNNY', 'NORMAL', 'HARD', 'ABYSS'];
|
||||
|
||||
test('every leader and difficulty: bloodPoint × the players alive (co-op), × 1 solo', () => {
|
||||
for (const difficulty of DIFFS) {
|
||||
const gd = new GameData(DATA, `mode_multi_${difficulty.toLowerCase()}`);
|
||||
const solo = new GameData(DATA, `mode_single_${difficulty.toLowerCase()}`);
|
||||
for (const id of Object.keys(DATA.bosses)) {
|
||||
const base = DATA.bosses[id].bloodPoint[difficulty];
|
||||
for (const n of [1, 2, 3, 4]) assert.equal(bossPoolHp(gd, id, n), base * n, `${id} ${difficulty} ×${n}`);
|
||||
assert.equal(bossPoolHp(solo, id, 1), base, `${id} ${difficulty} solo`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (const hidden of [false, true]) {
|
||||
test(`R${hidden ? 15 : 14}: the round's pool leaves out two departed players`, () => {
|
||||
const h = makeMatch({ humans: 4, difficulty: 'HARD', fake: true, instant: false }).start();
|
||||
h.toPrep(1);
|
||||
const m = h.m;
|
||||
m.onLeave('p_2');
|
||||
m.onLeave('p_3');
|
||||
assert.equal(m.alivePlayers().length, 2);
|
||||
assert.equal(h.ps('p_2').alive, false);
|
||||
m.round = hidden ? 15 : 14;
|
||||
m.teamLp = 50;
|
||||
m.bossId = 'boss_1';
|
||||
m.hiddenBossId = 'boss_9';
|
||||
m._planBossWaves();
|
||||
m.startFinalAssault(hidden);
|
||||
const id = hidden ? m.hiddenBossId : m.bossId;
|
||||
assert.equal(m.bossPool.maxHp, DATA.bosses[id].bloodPoint.HARD * 2);
|
||||
m.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
test('disconnected players (AI 托管) and bots count; an eliminated player does not', () => {
|
||||
const h = makeMatch({ humans: 3, bots: 1, fake: true, instant: false }).start();
|
||||
h.toPrep(1);
|
||||
h.m.onDisconnect('p_1');
|
||||
h.ps('p_2').eliminate(1);
|
||||
h.m.round = 14;
|
||||
h.m._planBossWaves();
|
||||
h.m.startFinalAssault(false);
|
||||
assert.equal(h.m.alivePlayers().length, 3);
|
||||
assert.equal(h.m.bossPool.maxHp, h.m.gd.boss(h.m.bossId).bloodPoint.NORMAL * 3);
|
||||
h.m.dispose();
|
||||
});
|
||||
|
||||
test('config restores the fixed pool of 0.1.x (perPlayer false, solo 0.25) — GameData and the plain-object fallback agree', () => {
|
||||
const data = structuredClone(DATA);
|
||||
data.config.bossHpScale = { ...data.config.bossHpScale, perPlayer: false, solo: 0.25 };
|
||||
for (const modeId of ['mode_single_abyss', 'mode_multi_abyss']) {
|
||||
const gd = new GameData(data, modeId);
|
||||
const base = data.bosses.boss_8.bloodPoint.ABYSS;
|
||||
const expected = gd.isSolo ? base * 0.25 : base;
|
||||
for (const n of [1, 2, 3, 4]) {
|
||||
assert.equal(gd.bossPoolHp('boss_8', n), expected, `${modeId} ×${n}`);
|
||||
assert.equal(bossPoolHp(gd, 'boss_8', n), expected);
|
||||
// a caller's game-data object without bossPoolShare reads the same config through bossPoolShareOf
|
||||
const plain = { boss: (id) => gd.boss(id), difficulty: gd.difficulty, mode: gd.mode, config: gd.config, isSolo: gd.isSolo };
|
||||
assert.equal(bossPoolHp(plain, 'boss_8', n), expected);
|
||||
}
|
||||
}
|
||||
// and the per-player rule through the same fallback
|
||||
const gd = new GameData(DATA, 'mode_multi_abyss');
|
||||
const plain = { boss: (id) => gd.boss(id), difficulty: gd.difficulty, mode: gd.mode, config: gd.config, isSolo: false };
|
||||
assert.equal(bossPoolHp(plain, 'boss_8', 3), DATA.bosses.boss_8.bloodPoint.ABYSS * 3);
|
||||
});
|
||||
|
||||
test('only the leader\'s pool scales: escorts and the Hidden Core\'s parts keep their stats', () => {
|
||||
for (const kind of ['boss', 'hidden']) {
|
||||
const bossKey = kind === 'boss' ? 'enemy_9013_acstmk' : 'enemy_9013_acstmk_2';
|
||||
const snapshot = (n) => {
|
||||
const h = makeBattle({
|
||||
kind, content: 'none', sharedBoss: new SharedBossPool(7200000 * n), autoFinish: false,
|
||||
enemies: [{ key: bossKey, tag: 'boss', pos: [3, 10] }, { key: 'enemy_1427_lrnazg', pos: [2, 10] }, { key: 'enemy_9014_acstma', pos: [4, 10] }],
|
||||
});
|
||||
h.step();
|
||||
return [bossKey, 'enemy_1427_lrnazg', 'enemy_9014_acstma'].map((key) => {
|
||||
const e = h.enemy(key);
|
||||
assert.ok(e, `spawned ${key}`);
|
||||
return { hp: e.s.maxHp, atk: e.s.atk, def: e.s.def };
|
||||
});
|
||||
};
|
||||
const one = snapshot(1), four = snapshot(4);
|
||||
assert.equal(four[0].hp, one[0].hp * 4, `${kind}: the leader shows the pool`);
|
||||
assert.equal(four[0].atk, one[0].atk);
|
||||
assert.equal(four[0].def, one[0].def);
|
||||
assert.deepEqual(four.slice(1), one.slice(1), `${kind}: escort and part unchanged`);
|
||||
}
|
||||
});
|
||||
@@ -2,8 +2,8 @@
|
||||
// seconds; with our layers the official leader never dies that fast"). DESIGN §20.10.
|
||||
// * Pool: one pool for every boss field (official tip "最终攻势中,所有人将一起对敌方领袖造成伤害"), bloodPoint[difficulty]
|
||||
// of data/bosses.json (= activity_table bossInfoDict bloodPoint / Normal / Hard / Abyss of the current data; PRTS
|
||||
// 盟约记录's leader table is the older 11月18日 revision, 铳 险境 and 胄 / 铳 / 萨米 绝境 differ, no 终极 column) whatever
|
||||
// the number of alive players (× alive / 4 only with config bossHpScale.aliveScaling, off until confirmed).
|
||||
// 盟约记录's leader table is the older 11月18日 revision, 铳 险境 and 胄 / 铳 / 萨米 绝境 differ, no 终极 column) per
|
||||
// player alive when the fight starts (DESIGN §25.13.4: the owner's decision of 2026-10-06, adopting PR #209).
|
||||
// * Damage: the 卫戍 systems' "+X%" attribute bonuses are 直接乘算 — summed, not compounded (PRTS 盟约记录 / 游戏数据基础);
|
||||
// v2.5 compounded them, which made stacked lineups kill the leaders 1.2–3× faster (more with more layers).
|
||||
// Real bot matches to the Final Assault (real sim, server-run fields): every operator fighting the leader carries its
|
||||
@@ -67,12 +67,12 @@ test('leader HP data = the current official bossInfoDict for every leader and di
|
||||
const SYSTEM_KEY = /^(bond|item|band|choice):/;
|
||||
const MUL_STATS = ['atkMul', 'defMul', 'hpMul'];
|
||||
|
||||
test('终极 Final Assault (+200 layers per active bond): one bloodPoint pool for both fields, bonuses additive, each hit once', () => {
|
||||
test('终极 Final Assault (+200 layers per active bond): one pool (bloodPoint × 4 alive) for both fields, bonuses additive, each hit once', () => {
|
||||
const h = toFinalAssault({ difficulty: 'ABYSS', seed: 2, bossId: 'boss_1', layers: 200 });
|
||||
const m = h.m;
|
||||
const pool = m.bossPool;
|
||||
assert.equal(m.alivePlayers().length, 4);
|
||||
assert.equal(pool.maxHp, DATA.bosses.boss_1.bloodPoint.ABYSS, 'full team: the data value (3 600 000)');
|
||||
assert.equal(pool.maxHp, DATA.bosses.boss_1.bloodPoint.ABYSS * 4, 'four alive: 4 × the data value (14 400 000)');
|
||||
const fields = m.fields.filter((f) => f.battle);
|
||||
assert.equal(fields.length, 2, 'two pair fields');
|
||||
// one second into the fight: every operator's stats
|
||||
@@ -99,8 +99,8 @@ test('终极 Final Assault (+200 layers per active bond): one bloodPoint pool fo
|
||||
test('绝境 Final Assault: both pair fields drain the one pool, every hit exactly once', () => {
|
||||
const h = toFinalAssault({ difficulty: 'HARD', seed: 3, bossId: 'boss_5' });
|
||||
const m = h.m;
|
||||
assert.equal(m.bossPool.maxHp, DATA.bosses.boss_5.bloodPoint.HARD, 'four alive: the data value');
|
||||
assert.equal(m.gd.bossPoolHp('boss_5', 2), DATA.bosses.boss_5.bloodPoint.HARD, 'two alive: the same pool (aliveScaling off)');
|
||||
assert.equal(m.bossPool.maxHp, DATA.bosses.boss_5.bloodPoint.HARD * 4, 'four alive: 4 × the data value');
|
||||
assert.equal(m.gd.bossPoolHp('boss_5', 2), DATA.bosses.boss_5.bloodPoint.HARD * 2, 'two alive: 2 × the data value');
|
||||
const pool = m.bossPool;
|
||||
const fields = m.fields.filter((f) => f.battle);
|
||||
assert.equal(fields.length, 2);
|
||||
@@ -118,7 +118,8 @@ test('绝境 Final Assault: both pair fields drain the one pool, every hit exact
|
||||
|
||||
test('终极 Final Assault vs 假想敌:胄 (seeded bot match): both players\' 奥术 never multiply on the leader; a drone costs it 2 % of the pool', () => {
|
||||
// DESIGN §20.10: one 奥术 instance per target (the strongest — PRTS 作战机制 同名buff, 巴哈姆特 12316 "共享型buff會跟對面搶");
|
||||
// 死亡集群's "最大生命值2%" = the leader's shown max HP, the pool (DRONE_LINK_BASE 'pool' [ASSUMED]): 72 000 at 终极
|
||||
// 死亡集群's "最大生命值2%" = the leader's shown max HP, the pool (DRONE_LINK_BASE 'pool' [ASSUMED]): 288 000 at 终极
|
||||
// with 4 alive (14 400 000)
|
||||
// the bots' boards follow every draw of the match (the elite-to-board merge, DESIGN §20.11, moved seed 7 to 12; the
|
||||
// 战术决策 drawn with replacement moved 12 on): the first of these seeds whose bots pair two 奥术 players
|
||||
const pairOf = (m) => m.fields.filter((f) => f.battle).find((f) => f.players.length === 2 && f.players.every((pid) => f.battle.getPlayer(pid).bonds.arcaneShip?.active));
|
||||
@@ -161,7 +162,8 @@ test('终极 Final Assault vs 假想敌:胄 (seeded bot match): both players\'
|
||||
}
|
||||
assert.ok(withArcane > 100, `the leader carried 奥术 (${withArcane} samples)`);
|
||||
assert.ok(links.length >= 1, 'drones were shot down');
|
||||
for (const x of links) assert.equal(x, DATA.bosses.boss_1.bloodPoint.ABYSS * 0.02, 'drone link = 2 % × the 3 600 000 pool');
|
||||
for (const x of links) assert.equal(x, m.bossPool.maxHp * 0.02, 'drone link = 2 % × the pool');
|
||||
assert.equal(m.bossPool.maxHp, DATA.bosses.boss_1.bloodPoint.ABYSS * 4, 'the 14 400 000 pool of 4 alive players');
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
@@ -194,7 +196,7 @@ test('绝境 Hidden Core vs 假想敌:铳 (隐秘核心): a 碎铳之簧 passe
|
||||
assert.equal(m.phase, PHASE.HIDDEN_CORE, 'reached the Hidden Core');
|
||||
assert.ok(specs.length >= 1);
|
||||
const pool = new SharedBossPool(m.bossPool.maxHp);
|
||||
assert.equal(pool.maxHp, DATA.bosses.boss_9.bloodPoint.HARD, 'the hidden 铳 pool = bloodPoint');
|
||||
assert.equal(pool.maxHp, DATA.bosses.boss_9.bloodPoint.HARD * m.alivePlayers().length, 'the hidden 铳 pool = bloodPoint × the players alive at its start');
|
||||
const b = createBattleFromSpec(specs[0], new DataSource(DATA, null), { sharedBoss: pool, recordEvents: false, quiet: true });
|
||||
// the springs stand from the start, 铳 enters ≈ 10 game s later
|
||||
for (let i = 0; i < 1200 && !b.enemies.some((e) => e.alive && e.isBoss); i++) b.step();
|
||||
|
||||
@@ -183,7 +183,7 @@ test('onLeave: a quitter has no place in the Final Assault pairing or the boss p
|
||||
h.drive(() => m.phase === PHASE.FINAL_ASSAULT);
|
||||
assert.deepEqual(m.fields.map((f) => f.players), [alive], 'one boss field for the two players left');
|
||||
assert.equal(m.bossPool.maxHp, bossPoolHp(m.gd, m.bossId, 2), 'the co-op boss pool of two alive players');
|
||||
assert.equal(m.bossPool.maxHp, m.gd.boss(m.bossId).bloodPoint[m.gd.difficulty], 'bloodPoint, no alive-player factor (DESIGN §20.10)');
|
||||
assert.equal(m.bossPool.maxHp, m.gd.boss(m.bossId).bloodPoint[m.gd.difficulty] * 2, 'bloodPoint × the two players left; the quitter does not count (DESIGN §25.13.4)');
|
||||
const end = h.runToEnd();
|
||||
assert.equal(end.victory, true);
|
||||
assert.equal(end.players.find((p) => p.playerId === first).roundsPassed, 0);
|
||||
|
||||
@@ -17,25 +17,35 @@ test('pairing by seat: (1,2), (3,4); an odd player alone', () => {
|
||||
assert.deepEqual(pairPlayers([p(3), p(1), p(0), p(2)]).map((g) => g.map((x) => x.seat)), [[0, 1], [2, 3]]);
|
||||
});
|
||||
|
||||
test('boss pool = bloodPoint[difficulty] in co-op whatever the alive count (× alive / 4 only with aliveScaling; solo × 0.25) × tuning; shared and never negative', () => {
|
||||
// research numbers (data/tuning.json left out); DESIGN §20.10: notice 5114's "敌方领袖的总生命值不变" is about the
|
||||
// mirrored copies, the one note on player count (巴哈姆特 12294 "聯機隊友(撤退/死掉)變少,最後boss血條也會變少") has no
|
||||
// proportion — config bossHpScale.aliveScaling (off) would apply × alive / 4
|
||||
test('boss pool = bloodPoint[difficulty] × the players alive at the fight\'s start (solo × 1) × tuning; config restores the fixed pool; shared and never negative', () => {
|
||||
// DESIGN §25.13.4: the owner's decision of 2026-10-06 adopts PR #209 by @qingjingshenghuo (bloodPoint is one player's
|
||||
// share) and replaces the fixed pool of 「保持固定血量」 — config bossHpScale (perPlayer false, solo 0.25) restores it.
|
||||
// Research numbers (data/tuning.json left out).
|
||||
const { tuning, ...RAW } = DATA; // eslint-disable-line no-unused-vars
|
||||
const gd = new GameData(RAW, 'mode_multi_hard');
|
||||
for (const n of [4, 3, 2, 1, undefined, 9]) assert.equal(bossPoolHp(gd, 'boss_1', n), 1800000, `${n} alive`);
|
||||
for (const n of [1, 2, 3, 4]) assert.equal(bossPoolHp(gd, 'boss_1', n), 1800000 * n, `${n} alive`);
|
||||
assert.equal(bossPoolHp(gd, 'boss_1'), 1800000 * 4, 'no count given: a full team');
|
||||
assert.equal(bossPoolHp(gd, 'boss_1', 9), 1800000 * 4, 'at most aliveFull (4) players');
|
||||
assert.equal(gd.bossPoolHp('boss_1', 2), bossPoolHp(gd, 'boss_1', 2), 'GameData agrees');
|
||||
// the flip: config bossHpScale.aliveScaling true scales the pool by alive / 4
|
||||
const scaled = new GameData({ ...RAW, config: { ...RAW.config, bossHpScale: { ...RAW.config.bossHpScale, aliveScaling: true },
|
||||
modes: { ...RAW.config.modes, mode_multi_hard: { ...RAW.config.modes.mode_multi_hard, bossHpScale: { ...RAW.config.modes.mode_multi_hard.bossHpScale, aliveScaling: true } } } } }, 'mode_multi_hard');
|
||||
assert.equal(bossPoolHp(new GameData(RAW, 'mode_single_abyss'), 'boss_5', 1), 3000000, 'solo: the table value');
|
||||
assert.equal(bossPoolHp(new GameData(RAW, 'mode_single_funny'), 'boss_2', 1), 225000);
|
||||
// the flip back: the fixed pool of 0.1.x (perPlayer false; solo 0.25), and its optional × alive / 4 (aliveScaling)
|
||||
const withScale = (over) => ({ ...RAW, config: { ...RAW.config, bossHpScale: { ...RAW.config.bossHpScale, ...over } } });
|
||||
const fixed = withScale({ perPlayer: false, solo: 0.25 });
|
||||
for (const n of [4, 3, 2, 1, undefined, 9]) assert.equal(bossPoolHp(new GameData(fixed, 'mode_multi_hard'), 'boss_1', n), 1800000, `fixed pool, ${n} alive`);
|
||||
assert.equal(bossPoolHp(new GameData(fixed, 'mode_single_abyss'), 'boss_5', 1), 750000);
|
||||
assert.equal(bossPoolHp(new GameData(fixed, 'mode_single_funny'), 'boss_2', 1), 56250);
|
||||
const scaled = new GameData(withScale({ perPlayer: false, aliveScaling: true }), 'mode_multi_hard');
|
||||
assert.equal(bossPoolHp(scaled, 'boss_1', 4), 1800000);
|
||||
assert.equal(bossPoolHp(scaled, 'boss_1', 3), 1350000);
|
||||
assert.equal(bossPoolHp(scaled, 'boss_1', 2), 900000);
|
||||
assert.equal(bossPoolHp(scaled, 'boss_1', 1), 450000);
|
||||
assert.equal(bossPoolHp(scaled, 'boss_1'), 1800000, 'no count given: a full team');
|
||||
assert.equal(bossPoolHp(scaled, 'boss_1', 9), 1800000, 'never above the data value');
|
||||
assert.equal(bossPoolHp(new GameData(RAW, 'mode_single_abyss'), 'boss_5', 1), 750000);
|
||||
assert.equal(bossPoolHp(new GameData(RAW, 'mode_single_funny'), 'boss_2', 1), 56250);
|
||||
// a mode's own entry overrides the global one
|
||||
const perMode = { ...RAW, config: { ...RAW.config, modes: { ...RAW.config.modes, mode_multi_hard: { ...RAW.config.modes.mode_multi_hard, bossHpScale: { ...RAW.config.modes.mode_multi_hard.bossHpScale, perPlayer: false } } } } };
|
||||
assert.equal(bossPoolHp(new GameData(perMode, 'mode_multi_hard'), 'boss_1', 3), 1800000, 'the mode entry first');
|
||||
assert.equal(bossPoolHp(new GameData(perMode, 'mode_multi_abyss'), 'boss_1', 3), 3600000 * 3, 'other modes keep the global rule');
|
||||
// the balance layer multiplies the pool (docs/BALANCE.md)
|
||||
for (const modeId of ['mode_single_funny', 'mode_multi_hard']) {
|
||||
const tuned = new GameData(DATA, modeId);
|
||||
|
||||
@@ -26,9 +26,10 @@ function realFinalAssault({ bossId, seed, clientCombat = true }) {
|
||||
h.setStage('act2autochess_m01');
|
||||
const m = h.m;
|
||||
m.bossId = bossId;
|
||||
// the autoplay lineups cannot clear a full 绝境 pool: 5 % of it (the tuning knob GameData.bossHpMul) — the pool's
|
||||
// the autoplay lineups cannot clear a full 绝境 pool: 5 % of the one-player table value (the tuning knob
|
||||
// GameData.bossHpMul; the pool counts both players alive since DESIGN §25.13.4, so 2.5 % of it) — the pool's
|
||||
// arithmetic, not its size, stalled the fight
|
||||
m.gd.bossHpMul = () => 0.05;
|
||||
m.gd.bossHpMul = () => 0.025;
|
||||
h.autoHumans();
|
||||
m.start();
|
||||
let last = '';
|
||||
|
||||
@@ -372,11 +372,12 @@ test('限伤 vs the real leader defences [ASSUMED order]: 阿利斯泰尔\'s 500
|
||||
assert.equal(pool10.hp, before10);
|
||||
});
|
||||
|
||||
test('限伤 vs 【死亡集群】 (real kit): a drone killed by an operator costs the leader 2 % of the pool max — a loss that lands up to a 14999950 pool and is cancelled above it (ceil)', () => {
|
||||
test('限伤 vs 【死亡集群】 (real kit): a drone killed by an operator costs the leader 2 % of the pool max — a share, no hit: it lands past the 300000 line too (DESIGN §25.13.4)', () => {
|
||||
// boss_1 / boss_8 skill 2: bb hp_ratio 0.02; the leader's max HP is the shared pool's max (Battle syncs it), so the
|
||||
// loss is 0.02 × pool max. Today's largest drone pool is boss_8 ABYSS 7.2M → 144000; a pool above 14999950 (≈ 2.08 ×
|
||||
// that), where ceil(0.02 × max) reaches 300000, would make every drone kill a cancelled hit — kept in view for any
|
||||
// change to the pool size (research 11 §6).
|
||||
// loss is 0.02 × pool max. With the pool per player alive (the owner's decision of 2026-10-06, PR #209) boss_8 ABYSS
|
||||
// reaches 21.6M / 28.8M at 3 / 4 players: 432000 / 576000 per drone, past the line where ceil(0.02 × max) ≥ 300000 —
|
||||
// until 0.2.0 the link was cancelled there like a hit; it is a share of the pool and lands (Battle.loseHp noHitLimit
|
||||
// [ASSUMED], research 11 §6).
|
||||
assert.equal(gd.enemy('enemy_9013_acstmk').skills.find((s) => s.prefabKey === '2').bb.hp_ratio, 0.02);
|
||||
assert.equal(gd.enemy('enemy_9013_acstmk_2').skills.find((s) => s.prefabKey === '2').bb.hp_ratio, 0.02);
|
||||
const withOp = [{ ...PAIR[0], units: [{ uid: 1, chessId: 'chess_char_1_01_a', row: 10, col: 2 }] }, PAIR[1]];
|
||||
@@ -399,16 +400,16 @@ test('限伤 vs 【死亡集群】 (real kit): a drone killed by an operator cos
|
||||
assert.equal(drone.alive, false, 'the drone died');
|
||||
return { lost: before - pool.hp, caps, id: L.id };
|
||||
};
|
||||
const today = run(7200000);
|
||||
assert.equal(today.lost, 144000, 'boss_8 ABYSS-sized pool: the loss lands');
|
||||
assert.deepEqual(today.caps, []);
|
||||
const solo = run(7200000);
|
||||
assert.equal(solo.lost, 144000, 'boss_8 ABYSS-sized pool (one player): the loss lands');
|
||||
assert.deepEqual(solo.caps, []);
|
||||
const under = run(14999950);
|
||||
assert.equal(under.lost, 299999, 'just under the line: lands');
|
||||
assert.deepEqual(under.caps, []);
|
||||
for (const m of [14999951, 15000000]) {
|
||||
for (const [m, want] of [[14999951, 299999.02], [15000000, 300000], [21600000, 432000], [28800000, 576000]]) {
|
||||
const over = run(m);
|
||||
assert.equal(over.lost, 0, `${m}: ceil(0.02 × max) = 300000, one hit at the line: cancelled`);
|
||||
assert.deepEqual(over.caps.map((c) => c[0]), [over.id]);
|
||||
assert.ok(Math.abs(over.lost - want) < 1e-6, `${m}: the share lands past the line (${over.lost})`);
|
||||
assert.deepEqual(over.caps, [], `${m}: no cancelled hit`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+3
-3
@@ -7,8 +7,8 @@
|
||||
// matches (stage, factions, bans, boss and lineups all vary with the seed), simulates the real wave of that round with
|
||||
// the real Battle and FULL content (kits, talents, bonds incl. layers, IN_BATTLE garrisons, items, bands, tokens) and
|
||||
// reports the leak distribution and clear time. Boss rounds (R14 / solo 标准 R9) and the Hidden Core (R15) run the
|
||||
// real Final Assault fields (pairs, `_s` template for a lone player, shared boss HP pool = bloodPoint (co-op; × alive / 4
|
||||
// only with config bossHpScale.aliveScaling; solo ×0.25 [ASSUMED]) — GameData.bossPoolHp —, team LP, overtime drain) and report the pool damage by 150 s, the kill time and the team LP spent.
|
||||
// real Final Assault fields (pairs, `_s` template for a lone player, shared boss HP pool = bloodPoint × the players alive
|
||||
// at the fight's start (solo × 1; DESIGN §25.13.4) — GameData.bossPoolHp —, team LP, overtime drain) and report the pool damage by 150 s, the kill time and the team LP spent.
|
||||
//
|
||||
// The competent board of round r (curves below, profile-scaled): shop level, deployed units, elites (精锐), equipment,
|
||||
// 2–3 active bonds built around a core bond (3 members early, 6 from R11 when the pool allows it) and the layers those
|
||||
@@ -378,7 +378,7 @@ export function runBoss({ data, mode, difficulty, round, seed, profile = 1, boss
|
||||
}
|
||||
}
|
||||
const alive = m.alivePlayers();
|
||||
// the official pool (co-op = bloodPoint, × alive / 4 only with aliveScaling, DESIGN §20.10; solo ×0.25) — the match's own rule
|
||||
// the match's own pool rule (bloodPoint × the players alive, solo × 1 — DESIGN §25.13.4)
|
||||
const pool = new SharedBossPool(bossPoolHp(gd, id, alive.length));
|
||||
let teamLp = alive.reduce((s, p) => s + p.lp, 0);
|
||||
const teamLp0 = teamLp;
|
||||
|
||||
@@ -3511,9 +3511,9 @@ function buildConfig(ctx, waves, stages, bands) {
|
||||
lastRound, bossRound, hiddenRound,
|
||||
rounds, spRounds: roundNums.filter((r) => rounds[r].isSpPrepare),
|
||||
combatTimeLimit, enemyScale,
|
||||
bossHpScale: type === 'SINGLE'
|
||||
? { bloodPointKey: DIFF_KEYS[m.modeDifficulty] || null, solo: 0.25, soloAssumed: true, unaffectedByEnemyScale: true }
|
||||
: { bloodPointKey: DIFF_KEYS[m.modeDifficulty] || null, coop: 1, aliveScaling: false, aliveFull: 4, aliveAssumed: true, unaffectedByEnemyScale: true },
|
||||
// the pool rule's numbers live in the global bossHpScale below (one place restores the fixed pool); a key set here
|
||||
// would override it for this mode (match/gamedata.js bossPoolShareOf)
|
||||
bossHpScale: { bloodPointKey: DIFF_KEYS[m.modeDifficulty] || null, unaffectedByEnemyScale: true },
|
||||
upgradePrices: levels.slice(0, -1).map((l) => shopLv[l].initialUpgradePrice),
|
||||
maxShopLevel: levels.length ? levels[levels.length - 1] : 6,
|
||||
shopSlots: Object.fromEntries(levels.map((l) => [l, { chess: shopLv[l].charChessCount, item: shopLv[l].itemCount }])),
|
||||
@@ -3577,9 +3577,11 @@ function buildConfig(ctx, waves, stages, bands) {
|
||||
},
|
||||
lpCapPerRound: act.constData.costPlayerHpLimit ?? 10,
|
||||
bossOvertimeAfter: 150, bossOvertimeDrainPerSec: 1,
|
||||
// DESIGN §25.13.4: the owner's decision of 2026-10-06 adopts PR #209 by @qingjingshenghuo — bloodPoint is one player's
|
||||
// share; it replaces the fixed pool of 「保持固定血量」 (perPlayer false + solo 0.25 restore it)
|
||||
bossHpScale: {
|
||||
formula: 'co-op: bloodPoint[difficulty] — one pool for every field ("所有人将一起对敌方领袖造成伤害"; the mirrored copies of a pair field share it, "两侧的敌方领袖共享生命值(敌方领袖的总生命值不变)"); aliveScaling true would scale it × alive players at the Final Assault / aliveFull (巴哈姆特 12294 "聯機隊友(撤退/死掉)變少,最後boss血條也會變少", one community note, no proportion: off until confirmed, the alive / 4 proportion [ASSUMED]); solo: bloodPoint[difficulty] × solo (0.25 = one player of four) [ASSUMED]',
|
||||
coop: 1, solo: 0.25, soloAssumed: true, aliveScaling: false, aliveFull: 4, aliveAssumed: true, unaffectedByEnemyScale: true,
|
||||
formula: 'one pool for every boss field ("所有人将一起对敌方领袖造成伤害"; the mirrored copies of a pair field share it, "两侧的敌方领袖共享生命值(敌方领袖的总生命值不变)") = bloodPoint[difficulty] × share. perPlayer true (the owner\'s decision of 2026-10-06, PR #209): co-op share = coop × the players alive when the fight starts (bots and AI 托管 seats count, eliminated and departed seats do not), at most aliveFull; solo share = solo (1). perPlayer false (the fixed pool of 0.1.x, 「保持固定血量」): co-op share = coop whatever the count (× alive / aliveFull with aliveScaling, the alive / 4 proportion [ASSUMED]: 巴哈姆特 12294); solo was 0.25 [ASSUMED]',
|
||||
perPlayer: true, coop: 1, solo: 1, aliveFull: 4, aliveScaling: false, aliveAssumed: true, unaffectedByEnemyScale: true,
|
||||
},
|
||||
hiddenCore: { single: 350, multi: 1200, minTeamLpExclusive: 1, difficulties: ['NORMAL', 'HARD', 'ABYSS'], checkedAfterRound: 14 },
|
||||
dp: { init: 10, perSec: 1, max: 99 },
|
||||
|
||||
Reference in new issue
Block a user