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
@@ -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`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user