Files
sganggs 620f88c0d8 Playtest #6 follow-up (v2.5.1): leader HP review — 直接乘算, one debuff instance, 999 layer cap, 限伤; elite to the board; 假想敌:胄 kit
- co-op leaders died within seconds: leader HP is the official data (40 values pinned); bond / strategy / item /
  机变 / 特质 "+X%" bonuses now add up (PRTS 直接乘算) instead of compounding; a same-named enemy debuff keeps one
  (strongest) instance per target (奥术 across a pair field); leader parts pass damage 1:1; fixed co-op pool (the user's call)
- official limits read from the client: each bond stops at 999 layers (MAX_GARRISON_STACK); a leader hit of 300000+ in
  boss / hidden battles is cancelled (MAX_BATTLE_DAMAGE, 限伤) — shared/constants.js switches, research 11
- a merge that consumes a deployed copy puts the elite on that tile and facing (user-confirmed); gold target highlight,
  promotion effect
- 假想敌:胄 per PRTS: one shell, shells within range, any drone death costs 2 % max HP, 剑/锤 sorties and 瘫痪
- DESIGN §20.10–§20.14; 2842 unit tests, all 28 browser suites green; Chrome = Node on boss fields
2026-10-02 04:19:28 +08:00

85 lines
5.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// "终极 leaders die in seconds" (user report after playtest #6): the percentage attribute bonuses of the 卫戍 systems
// compounded. PRTS 卫戍协议:盟约 下半/PRTS盟约记录: "盟约效果,策略效果,装备效果提供的属性加成均为直接乘算"; PRTS 游戏数据基础
// 属性基本公式: 直接乘算 values are SUMMED — D_t = t₁ + … + tₙ, A = (A₀ + D_p)(1 + D_t) — and PRTS 作战机制 X₂ = X₁(1 + b₁% +
// b₂%) with a skill's "攻击力+X%" in the same sum. v2.5 gave every bond / strategy / item its own ×(1 + x) (atkMul), so
// with many layers a ranged operator under 精准 + 独行 + 卡西米尔 + an item + a 特质 dealt ×74 instead of ×12 of its ATK.
// Now: content/support directMods → atkPct / defPct / hpPct (constants.js DIRECT_BONUS_STACKING 'add'); the 炎佑 ×1.5
// ("提升至1.5倍") and the char_attribute_mul 特质 (a rune on the chess's base attributes) stay multipliers.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { makeBattle, chessRec, enemyRec, flatStage, checkInvariants } from '../helpers/battleHarness.js';
import { directMods } from '../../server/sim/content/support/index.js';
import { bondBb } from '../../server/sim/content/bonds/addon/battle.js';
import { DIRECT_BONUS_STACKING } from '../../server/sim/constants.js';
import { SharedBossPool } from '../../server/match/finalAssault.js';
const close = (a, b, msg, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg ?? ''} expected ${b}, got ${a}`);
const bond = (tier, layers = 0, count = 2) => ({ count, active: tier > 0, tier, layers });
const HAMMER = 'chess_item_1_01_e_a'; // 维式重锤 攻击力+15%
const BLADE = 'chess_item_3_07_e_a'; // 阿戈尔重刃 攻击力+40%
test('directMods: 直接乘算 ratios go to the additive percentage bucket (zeros dropped, extras merged)', () => {
assert.equal(DIRECT_BONUS_STACKING, 'add');
assert.deepEqual(directMods({ atk: 0.3, def: 0.2, hp: -0.1 }), { atkPct: 0.3, defPct: 0.2, hpPct: -0.1 });
assert.deepEqual(directMods({ atk: 0, hp: NaN }), {});
assert.deepEqual(directMods({ def: 0.5 }, { redeployMul: 0.7 }), { defPct: 0.5, redeployMul: 0.7 });
});
test('bonds + equipment + a skill ATK%: one sum (精准 L100 +130 %, 独行 +60 %, two items +55 %, skill +50 %)', () => {
const pre = bondBb('preciShip'), solo = bondBb('soloShip');
const preci = pre.base_atk + pre.atk_per_stack * 100;
const defs = { chess: { s_op: chessRec({ id: 's_op', bonds: ['preciShip', 'soloShip'], profession: 'SNIPER', skill: null }) } };
const h = makeBattle({
defs, units: [{ chessId: 's_op', row: 10, col: 3, items: [HAMMER, BLADE] }],
bonds: { preciShip: bond(1, 100), soloShip: bond(1, 0, 1) },
});
h.step();
const u = h.unit('s_op');
const sum = 1 + preci + solo.atk + 0.15 + 0.4;
close(u.s.atk, 500 * sum, 'bonds and items add up');
h.b.addBuff(u, { key: 'test:skill', mods: { atkPct: 0.5 } }); // a skill's "攻击力+50%" (直接乘算 too)
close(u.s.atk, 500 * (sum + 0.5), 'the skill joins the same sum');
const v25 = 500 * (1 + preci) * (1 + solo.atk) * 1.15 * 1.4 * 1.5; // the v2.5 product (each source its own multiplier)
assert.ok(u.s.atk < v25 / 1.8, `far below the v2.5 product ${v25}`);
close(u.s.maxHp, 2000 * (1 + solo.max_hp), '独行 HP');
checkInvariants(h.b);
});
test('strategy + bonds add up: 阿米娅 众志合一 (3 active bonds: +20 % ATK / HP) with 精准 and 绝技 (elite)', () => {
const pre = bondBb('preciShip'), sunt = bondBb('suntShip');
const defs = { chess: { e_op: chessRec({ id: 'e_op', bonds: ['preciShip'], profession: 'SNIPER', skill: null, golden: true }) } };
const h = makeBattle({
defs, bandId: 'band_amiya', units: [{ chessId: 'e_op', row: 10, col: 3 }],
bonds: { preciShip: bond(1, 50), suntShip: bond(1, 0, 2), steadShip: bond(1, 0) },
});
h.step();
const u = h.unit('e_op');
const want = 1 + pre.base_atk + pre.atk_per_stack * 50 + sunt.power_atk + 0.2;
close(u.s.atk, 500 * want, 'one 直接乘算 sum');
checkInvariants(h.b);
});
test('a leader sharing the pool takes the summed ATK: one normal hit of a stacked operator (精准 L600 + 独行 + 2 items)', () => {
const pre = bondBb('preciShip'), solo = bondBb('soloShip');
const defs = {
chess: { s_op: chessRec({ id: 's_op', bonds: ['preciShip', 'soloShip'], profession: 'SNIPER', skill: null, rangeGrid: [[0, 0], [0, 1], [0, 2], [0, 3]] }) },
enemies: { enemy_stack_leader: { ...enemyRec({ key: 'enemy_stack_leader', hp: 600000, speed: 0, dmgType: 'none' }), rank: 'BOSS', hitArea: { w: 4.95, h: 2.95, dx: 0, dy: 1 } } },
};
const pool = new SharedBossPool(3600000);
const h = makeBattle({
defs, kind: 'boss', stage: flatStage(), sharedBoss: pool, autoFinish: false,
units: [{ chessId: 's_op', row: 10, col: 6, items: [HAMMER, BLADE] }],
bonds: { preciShip: bond(1, 600), soloShip: bond(1, 0, 1) },
enemies: [{ key: 'enemy_stack_leader', pos: [3, 10], route: { motion: 'WALK', start: [3, 10], end: [3, 10], checkpoints: [] }, tag: 'boss' }],
});
const hits = [];
h.b.on('damaged', (c) => { if (c.target.bossPool && c.dmg.isAttack) hits.push(c.amount); }, { priority: -1e9 });
h.runUntil(() => hits.length >= 1, 10);
const atk = 500 * (1 + pre.base_atk + pre.atk_per_stack * 600 + solo.atk + 0.15 + 0.4);
assert.ok(hits.length >= 1, 'the operator hits the leader');
close(hits[0], atk, 'DEF 0: one hit = the summed ATK');
close(pool.maxHp - pool.hp, hits.reduce((s, x) => s + x, 0), 'the pool lost exactly the hits');
assert.ok(hits[0] < 500 * (1 + pre.base_atk + pre.atk_per_stack * 600) * (1 + solo.atk) * 1.15 * 1.4 / 2, 'v2.5 compounded this to more than twice as much');
checkInvariants(h.b);
});