Files
ark-stronghold-protocol/test/sim/feedback1b-arcane.test.js
sganggs b4fa21a0a6 feedback1b WG: 奥术 re-measured; bonds the mode never activates read 本局禁用
Player report after 0.1.0: "奥术盟约不生效". Measured on a real 险境 battle
(Match._normalOpts) and the same battle from its BattleSpec (browser path):
the bond works - member arts hits debuff the target x(1.2 + 0.01*L) for 3 s,
later arts hits land multiplied, the 3-member tier gives x1.4 below 50 % HP,
in-battle layer gains apply live, browser = server.

What a player sees instead: 标准模拟 switches 奥术 off (official
inactiveBondIdList, with 9 other bonds) while 深靛 洛洛 阿罗玛 夕 圣聆初雪 stay in
the pool through their other bond; their cards showed 奥术 like any bond and
the detail chip read "0/2 未激活" with three deployed. Shop / reward cards,
the detail card's bond chips and the bond popup now mark such a bond
本局禁用 (gameLogic modeOffBonds over config.modes[modeId].inactiveBondIds).

The PRTS 盟约记录 奥术 note (one instance per unit and target) is documented
next to the one-effective-instance rule it fits (SIM.md, bond header).
2026-10-03 02:31:35 +08:00

87 lines
5.2 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.
// Player report after the public 0.1.0 release (relayed, untested by the user): "奥术盟约不生效". Measured on the real
// product path — a 险境 match's own normal battle (Match._normalOpts: the player's board, bonds, loadouts, the round's
// wave) and the same battle built from its BattleSpec as the browser does (spec.js createBattleFromSpec) — the bond
// works: every member's arts damage puts the 奥术 debuff on its target (×(1.2 + 0.01·L) arts damage taken for 3 s, one
// instance per target, DESIGN §5.3 / §20.10), later arts hits land multiplied, the 3-member tier raises it ×1.4 on
// targets below 50 % HP, and the browser battle is identical. (What a player could see instead — 标准 never activates
// 奥术 — is test/ui/feedback1b-bonds.test.js.)
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { makeMatch, give, legalTileFor } from '../match/harness.js';
import { tileKey } from '../../server/match/board.js';
import { buildBattleSpec, createBattleFromSpec } from '../../server/sim/spec.js';
import { bondBb } from '../../server/sim/content/bonds/addon/battle.js';
const BB = bondBb('arcaneShip');
const MEMBERS = ['chess_char_1_17_a', 'chess_char_2_10_a', 'chess_char_3_09_a']; // 深靛, 洛洛, 海霓
function setup(ids, seed = 7, round = 5) {
const h = makeMatch({ mode: 'solo', difficulty: 'NORMAL', humans: 1, seed }).start();
h.toPrep(1);
h.ps('p_0').lp = 9999;
h.toPrep(round);
const m = h.m, ps = h.ps('p_0');
for (const p of [...ps.board.values()]) if (p.kind === 'chess') ps.returnCopies(p);
ps.board.clear();
const used = new Set();
for (const id of ids) { const t = legalTileFor(m, ps, id, used); used.add(tileKey(t[0], t[1])); give(m, ps, id, 'board', t); }
ps.recompute();
return { m, ps };
}
/** Run a battle; per arts hit of an operator: the 奥术 multiplier on the target when it landed (0 = none). */
function run(b, pid) {
const hits = [];
b.on('hit', (c) => {
if (!c.dmg || c.dmg.type !== 'arts' || !c.source || c.source.kind !== 'op') return;
const x = c.target.buffs.find((y) => y.key === 'bond:arcaneShip');
c.dmg.arcane = x ? x.data.value : 0;
c.dmg.taken = c.target.s.artsTakenMul;
});
b.on('damaged', (c) => { if (c.type === 'arts' && c.source && c.source.kind === 'op') hits.push({ mul: c.dmg.arcane, taken: c.dmg.taken, amount: c.amount }); });
for (let n = 0; !b.finished && n < 30 * 400; n++) b.step();
const pp = b.result().perPlayer[pid];
return { hits, killed: pp.killed, dealt: Math.round(pp.damageDealt) };
}
test('奥术 on a real 险境 battle: two members debuff their targets and later arts hits land ×(1.2 + 0.01·L); the browser battle is identical', () => {
const { m, ps } = setup(MEMBERS.slice(0, 2));
assert.deepEqual(ps.bonds.arcaneShip, { count: 2, active: true, tier: 1, layers: 0 });
for (const L of [0, 50]) {
const opts = m._normalOpts(ps);
opts.players[0].bonds.arcaneShip.layers = L;
const srv = run(m.newBattle(opts), ps.playerId);
const want = BB.base_damage_scale + BB.damage_scale_per_stack * L;
const boosted = srv.hits.filter((x) => x.mul > 0);
assert.ok(srv.hits.length >= 30 && boosted.length >= srv.hits.length * 0.6, `L${L}: ${boosted.length}/${srv.hits.length} arts hits on a debuffed target`);
for (const x of boosted) {
assert.ok(Math.abs(x.mul - want) < 1e-9, `L${L}: debuff ×${x.mul}, want ×${want}`);
assert.ok(x.taken >= want - 1e-9, `L${L}: the target took arts ×${x.taken}`);
}
// the browser path: the same spec, the same battle
const spec = buildBattleSpec({ ...opts, stageId: m.stageId, battleId: 't', boss: null });
const cli = run(createBattleFromSpec(spec, m.ds), ps.playerId);
assert.deepEqual([cli.killed, cli.dealt, cli.hits.length], [srv.killed, srv.dealt, srv.hits.length], `L${L}: browser = server`);
// without the bond the same battle has no debuffed hit
const offOpts = m._normalOpts(ps);
offOpts.players[0].bonds.arcaneShip = { ...offOpts.players[0].bonds.arcaneShip, active: false, tier: 0 };
assert.equal(run(m.newBattle(offOpts), ps.playerId).hits.filter((x) => x.mul > 0).length, 0);
}
m.dispose();
});
test('奥术 with 3 different members: targets below 50 % HP take ×1.4·(1.2 + 0.01·L) (+68 % at 0 layers); 海霓 adds layers live', () => {
const { m, ps } = setup(MEMBERS);
assert.equal(ps.bonds.arcaneShip.tier, 2);
const res = run(m.newBattle(m._normalOpts(ps)), ps.playerId);
const base = (L) => BB.base_damage_scale + BB.damage_scale_per_stack * L;
const seen = res.hits.filter((x) => x.mul > 0).map((x) => x.mul);
// L = 0 at the start; 海霓 "<战斗中>首次击倒敌人…【奥术】层数+3" raises it during the fight (bonds/addon live layers)
const match = (v) => [0, 3].flatMap((L) => [['base', L, base(L)], ['low', L, BB.power_weak_scale * base(L)]]).find(([, , w]) => Math.abs(v - w) < 1e-9);
for (const v of seen) assert.ok(match(v), `unexpected multiplier ×${v}`);
const kinds = new Set(seen.map((v) => match(v).slice(0, 2).join(':')));
for (const k of ['base:0', 'low:0', 'base:3', 'low:3']) assert.ok(kinds.has(k), `${k} seen (${[...kinds]})`);
assert.ok(Math.abs(BB.power_weak_scale * base(0) - 1 - BB.base_damage_scale_show_ex) < 1e-9, 'the shown +68 %');
m.dispose();
});