126 lines
7.1 KiB
JavaScript
126 lines
7.1 KiB
JavaScript
// test/match/feedback3-unitecarry.test.js — community report #34 / GitHub #82: 联防 carried a skill that was running at
|
||
// the end of a helper's own combat and restarted it for free (圣聆初雪's S2 on at the 联防 start with SP 0, an empty bar).
|
||
// Official (PRTS 卫戍协议/帮助 §联防阶段): "部署完成后,将对应单位的生命比例、技力修改至与上一阶段结束时相同(召唤物仅修改技力,
|
||
// 上一阶段为退场状态的干员强制退场)"; the official setup message carries `charBattleStatusList: [{ instId, hp, tech }]` only
|
||
// (research 09 §3 HelpBattleInfo). So: an operator's HP ratio and 技力, a summon's 技力, nothing of the skill state.
|
||
|
||
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { uniteBattleOpts } from '../../server/match/unite.js';
|
||
import { makeBattle, chessRec, enemyRec } from '../helpers/battleHarness.js';
|
||
import { makeMatch } from './harness.js';
|
||
|
||
const close = (a, b, eps = 1e-6, msg = '') => assert.ok(Math.abs(a - b) <= eps, `${msg} expected ${b}, got ${a}`);
|
||
|
||
let gdCache = null;
|
||
/** A real GameData (unite templates for buildUniteWave). */
|
||
function gd() {
|
||
if (!gdCache) gdCache = makeMatch({ mode: 'coop', humans: 3, seed: 1, fake: true }).start().m.gd;
|
||
return gdCache;
|
||
}
|
||
|
||
/**
|
||
* unite.js's carry for one helper, from the given end-of-combat report: board = [[uid, 'chess'|'token', chessId?]],
|
||
* the helper's battle input lists those pieces (board row 10, from col 3) with the carry unite.js attached.
|
||
*/
|
||
function uniteInput(board, unitsEnd) {
|
||
const ps = {
|
||
playerId: 'p1', seat: 0,
|
||
board: new Map(board.map(([uid, kind], i) => [`10,${3 + i}`, { uid, kind }])),
|
||
battleInput: ({ side, colOffset, carry }) => ({
|
||
playerId: 'p1', seat: 0, side, colOffset, bonds: {}, bandId: null, playerEffects: [],
|
||
units: board.map(([uid, kind, id, extra], i) => ({
|
||
uid, kind, ...(kind === 'token' ? { tokenId: id } : { chessId: id }), row: 10, col: 3 + i, dir: 'RIGHT', items: [],
|
||
...(extra || {}), ...(carry.has(uid) ? { carryState: carry.get(uid) } : {}),
|
||
})),
|
||
}),
|
||
};
|
||
const m = { gd: gd(), round: 3, lastResults: new Map([['p1', { unitsEnd }]]), dispatch() {} };
|
||
const { players } = uniteBattleOpts(m, { helpers: [ps], leaked: [] }, 60);
|
||
return players[0];
|
||
}
|
||
|
||
test('unite.js carry: HP ratio + SP for an operator (never skillActive), SP only for a summon on the field, down for a knocked-out operator', () => {
|
||
const input = uniteInput(
|
||
[[1, 'chess', 't_run'], [2, 'token', 'tok'], [3, 'chess', 't_down'], [4, 'token', 'tok2']],
|
||
[
|
||
{ uid: 1, hpPct: 0.4, sp: 0, skillActive: true, alive: true },
|
||
{ uid: 2, hpPct: 0.3, sp: 40, skillActive: false, alive: true },
|
||
{ uid: 3, hpPct: 0, sp: 7, skillActive: false, alive: false },
|
||
{ uid: 4, hpPct: 0, sp: 12, skillActive: false, alive: false },
|
||
],
|
||
);
|
||
const cs = (uid) => input.units.find((u) => u.uid === uid).carryState;
|
||
assert.deepEqual(cs(1), { hpPct: 0.4, sp: 0 }, 'the running skill is not carried');
|
||
assert.deepEqual(cs(2), { sp: 40 }, '召唤物仅修改技力');
|
||
assert.deepEqual(cs(3), { down: true });
|
||
assert.equal(cs(4), undefined, 'a summon off the field at the end enters fresh [ASSUMED]');
|
||
});
|
||
|
||
test('PlayerState.battleInput hands a summon piece its carryState', () => {
|
||
const h = makeMatch({ mode: 'coop', humans: 3, seed: 5, fake: true }).start();
|
||
h.toPrep(1);
|
||
const ps = h.ps('p_1');
|
||
const tok = ps.newPiece('token', 'token_10030_mlyss_wtrman', { ownerUid: 999 });
|
||
ps.board.set('10,4', tok);
|
||
const input = ps.battleInput({ carry: new Map([[tok.uid, { sp: 40 }]]) });
|
||
assert.deepEqual(input.units.find((u) => u.uid === tok.uid).carryState, { sp: 40 });
|
||
});
|
||
|
||
test('an operator whose skill runs at the end of its own combat enters 联防 with the skill off and the carried SP; stored charges come back', () => {
|
||
const defs = {
|
||
chess: {
|
||
// a 30 s skill (cost 10, ready at once) cast on the dummy in range: running when the own combat ends
|
||
t_run: chessRec({ id: 't_run', profession: 'SNIPER', rangeGrid: [[0, 0], [0, 1], [0, 2]], stats: { atk: 100 },
|
||
skill: { duration: 30, spCost: 10, initSp: 10, bb: { atk: 1 } } }),
|
||
// 2 charges of a cost-5 skill, nothing in range: both charges stored at the end (技力 10 = 2 × 5)
|
||
t_chg: chessRec({ id: 't_chg', profession: 'SNIPER', rangeGrid: [[0, 0]], stats: { atk: 100 },
|
||
skill: { duration: 5, spCost: 5, initSp: 0, maxChargeTime: 2, bb: { atk: 1 } } }),
|
||
},
|
||
enemies: { e_dummy: enemyRec({ key: 'e_dummy', hp: 1e9, speed: 0 }) },
|
||
};
|
||
const own = makeBattle({
|
||
defs, content: 'generic', autoFinish: false, timeLimit: 60,
|
||
units: [{ uid: 1, chessId: 't_run', row: 10, col: 3 }, { uid: 2, chessId: 't_chg', row: 12, col: 3 }],
|
||
enemies: [{ key: 'e_dummy', pos: [10, 5] }],
|
||
});
|
||
own.runUntil(() => own.unit('t_run').skill.active, 10);
|
||
own.run(12);
|
||
assert.ok(own.unit('t_run').skill.active, 'still running at the end');
|
||
const unitsEnd = own.result().perPlayer.p1.unitsEnd;
|
||
const end = (uid) => unitsEnd.find((u) => u.uid === uid);
|
||
assert.equal(end(1).skillActive, true);
|
||
assert.equal(end(1).sp, 0, 'spent at the activation (PRTS 技能 "触发技能后…消耗相应的技力")');
|
||
assert.equal(own.unit('t_chg').skill.charges, 2);
|
||
assert.equal(end(2).sp, 10, 'the official 技力: both charges (PRTS 技能 "当前技力上限等于该技能技力需求的X倍")');
|
||
|
||
const input = uniteInput([[1, 'chess', 't_run'], [2, 'chess', 't_chg']], unitsEnd);
|
||
input.units[1].row = 12; // as on the own field
|
||
const u = makeBattle({ kind: 'unite', defs, content: 'generic', autoFinish: false, players: [input] });
|
||
u.step();
|
||
const run = u.unit('t_run'), chg = u.unit('t_chg');
|
||
close(run.hpRatio, end(1).hpPct, 1e-6, 'HP ratio');
|
||
assert.equal(run.skill.active, false, 'the skill enters 联防 switched off (it used to restart for free)');
|
||
assert.equal(run.skill.activations, 0);
|
||
assert.ok(run.skill.sp < 0.1, `SP from 0, got ${run.skill.sp}`);
|
||
close(run.s.atk, 100, 1e-6, 'no skill ATK');
|
||
assert.equal(chg.skill.charges, 2, 'the stored charges are back');
|
||
});
|
||
|
||
test('a summon piece enters 联防 with its carried SP only (流形: SP set, HP full)', () => {
|
||
// 缪尔赛思 (chess_char_6_11_a) and her board 流形 (token_10030_mlyss_wtrman: cost 100, initial SP 95)
|
||
const players = [{
|
||
playerId: 'p1', seat: 0, side: 'L', colOffset: 0, bonds: {}, bandId: null, playerEffects: [],
|
||
units: [
|
||
{ uid: 1, kind: 'chess', chessId: 'chess_char_6_11_a', row: 10, col: 3, dir: 'RIGHT', items: [], carryState: { hpPct: 0.5, sp: 3 } },
|
||
{ uid: 2, kind: 'token', tokenId: 'token_10030_mlyss_wtrman', ownerUid: 1, row: 10, col: 4, dir: 'RIGHT', carryState: { sp: 40, hpPct: 0.3 } },
|
||
],
|
||
}];
|
||
const h = makeBattle({ kind: 'unite', players, autoFinish: false });
|
||
h.step();
|
||
const tok = h.b.allyUnits.find((x) => x.uid === 2);
|
||
assert.ok(tok && tok.alive && tok.deployed, 'the 流形 is on the field');
|
||
assert.ok(tok.skill.sp >= 40 && tok.skill.sp < 41, `its SP is the carried 40 (fresh it would be 95), got ${tok.skill.sp}`);
|
||
close(tok.hp, tok.s.maxHp, 1e-6, 'a summon keeps no HP ratio (召唤物仅修改技力)');
|
||
});
|