- 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
85 lines
5.7 KiB
JavaScript
85 lines
5.7 KiB
JavaScript
// "终极 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);
|
||
});
|