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
+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 = '';
|
||||
|
||||
Reference in new issue
Block a user