- Shared card pool and item stock as in the official mode (the owner's request, from the 路标档案馆 video and public notes): solo low-tier exceptions, co-op operators x2, shared item stock with special-item exceptions, normal / advanced occupancy 1 / 2, re-check on buy, 机变 exhaustion, release on consume / destroy / elimination; 整备 never exceeds stock; invariant held <= cap. - 突袭: one taunt-first reselect right after a revive (the owner's rule); ordinary polls and redeploys keep the 0.2.3 rule; a revive on an enemy's tile stays. #475: online boss / hidden-boss Raid may land on the empty half (owner-limited scope). - 浊心斯卡蒂 S1 x 坚守 no longer bounce redirected damage, and the two shares conserve the hit (idea of PR #464 by @HappyTanuki). - Operator fixes adapted from PRs by @Xinglan233 (#459 哈洛德 S2 elemental trigger, #460 涤火杰西卡 shield on the four adjacent tiles, #461 家族徽章 first hit after stealth, #462 松果 S2 regrow per deploy); 克莱门莎 S2 splash 1.1 / 100% ATK and air auto-trigger (#467); 白面 copying 歌蕾蒂娅 / 余 skips the three-member condition (#473, the owner's video). - 香槟炸弹 is not chosen by friendly auras / heals (PR #468 / #476 by @Siornya); 琳琅诗怀雅 coin balance (PR #450 by @IceCodeNew); Touch prep display (PR #451 by @YinqiMargatroid); lobby header (PR #471 by @YuriRestia); Japanese UI terms (PR #448 by @scutkane). - AI-only boss countdown grace, render-failure cache / recovery without restarting a battle, device units kept on a same-field re-entry, DIY detail / skill previews, large-text result buttons. - golden: battle families equal 0.2.3 except diy-135 / diy-136 (克莱门莎); all 18 release matches move through the shared pool, every flip named in docs/history/0.2.4.md §29.12; two coverage scenarios added.
134 lines
7.0 KiB
JavaScript
134 lines
7.0 KiB
JavaScript
// Jessica's shield placement uses the real Match actions and the client's drag-preview mirror. The only fixture
|
|
// grant is the selected operator; its shield is generated by normal deployment. No battle/UI is simulated here.
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { makeMatch } from './harness.js';
|
|
import { placementContext, canPlace, boardTargets } from '../../public/js/ui/gameLogic/placement.js';
|
|
import { planLayout } from '../../server/match/bot.js';
|
|
|
|
const SLOT = 'chess_char_5_diy1_a';
|
|
const JESCA = 'char_1034_jesca2';
|
|
const SHIELD = 'token_10032_jesca2_jckshd';
|
|
const board = (row, col) => ({ area: 'board', row, col });
|
|
const move = (h, uid, to, dir) => h.m.handle('p_0', { t: 'g.move', uid, to, ...(dir ? { dir } : {}) });
|
|
const stackOf = (ps, owner) => [...ps.hand, ...ps.temp].find((p) => p?.kind === 'token' && p.ownerUid === owner.uid);
|
|
const ctx = (h, ps) => placementContext({ priv: ps.privateView(), stage: h.m.stage, editable: true,
|
|
getChess: (id) => ps.gd.chess(id), getToken: (id) => ps.gd.token(id), backups: ps.gd.raw.backups });
|
|
|
|
function prep(t, { row = 10, col = 4, skillIndex = 0, uniEquipId = null } = {}) {
|
|
const h = makeMatch({ mode: 'solo', seed: 11, seats: [{ seat: 0, playerId: 'p_0', name: 'P0', isBot: false,
|
|
connected: true, diy: { [SLOT]: { charId: JESCA, skillIndex, uniEquipId } } }] }).start();
|
|
t.after(() => h.m.dispose());
|
|
h.toPrep(1);
|
|
assert.equal(h.m.stageId, 'act2autochess_m04');
|
|
const ps = h.ps('p_0');
|
|
const owner = ps.acquireChess(SLOT, { source: 'grant' });
|
|
assert.ok(owner);
|
|
assert.deepEqual(move(h, owner.uid, board(row, col)), { ok: true });
|
|
assert.equal(stackOf(ps, owner)?.id, SHIELD);
|
|
return { h, ps, owner };
|
|
}
|
|
|
|
test('Jessica: hand shield refuses diagonal and distant tiles, with unchanged state and matching drag highlights', (t) => {
|
|
const { h, ps, owner } = prep(t, { row: 9, col: 5 });
|
|
const shield = stackOf(ps, owner);
|
|
for (const [row, col] of [[9, 7], [10, 4], [11, 5], [12, 3]]) {
|
|
const before = structuredClone(ps.privateView());
|
|
assert.equal(canPlace(ctx(h, ps), shield.uid, board(row, col)).code, 'BAD_TILE', `client ${row},${col}`);
|
|
assert.equal(move(h, shield.uid, board(row, col)).error, 'BAD_TILE', `server ${row},${col}`);
|
|
assert.deepEqual(ps.privateView(), before, 'rejected placement changes no inventory or board state');
|
|
}
|
|
assert.deepEqual(boardTargets(ctx(h, ps), shield.uid).legal.sort(), [[9, 4], [9, 6], [10, 5]].sort());
|
|
h.invariants();
|
|
});
|
|
|
|
test('Jessica: the server rejects distant and diagonal placements even when the client sends them directly', (t) => {
|
|
const { h, ps, owner } = prep(t, { row: 9, col: 5 });
|
|
const shield = stackOf(ps, owner);
|
|
for (const [row, col] of [[9, 7], [10, 4]]) {
|
|
assert.equal(move(h, shield.uid, board(row, col)).error, 'BAD_TILE');
|
|
assert.equal(stackOf(ps, owner), shield);
|
|
assert.ok(!ps.board.has(`${row},${col}`));
|
|
}
|
|
h.invariants();
|
|
});
|
|
|
|
test('Jessica: all four orthogonal tiles are legal regardless of her facing, skill or selected module', (t) => {
|
|
for (const [skillIndex, uniEquipId] of [[0, null], [1, 'uniequip_002_jesca2'], [2, 'uniequip_003_jesca2']]) {
|
|
const { h, ps, owner } = prep(t, { skillIndex, uniEquipId });
|
|
for (const dir of ['UP', 'RIGHT', 'DOWN', 'LEFT']) {
|
|
assert.deepEqual(move(h, owner.uid, board(10, 4), dir), { ok: true });
|
|
for (const [row, col] of [[9, 4], [11, 4], [10, 3], [10, 5]]) {
|
|
const shield = stackOf(ps, owner);
|
|
assert.equal(canPlace(ctx(h, ps), shield.uid, board(row, col)).ok, true);
|
|
assert.deepEqual(move(h, shield.uid, board(row, col)), { ok: true });
|
|
assert.deepEqual(move(h, ps.board.get(`${row},${col}`).uid, { area: 'hand', idx: ps.hand.findIndex((p) => !p) }), { ok: true });
|
|
}
|
|
}
|
|
h.invariants();
|
|
}
|
|
});
|
|
|
|
test('Jessica: a deployed shield cannot move or be swapped to a distant tile; adjacent moves and owner swaps work', (t) => {
|
|
const { h, ps, owner } = prep(t);
|
|
assert.deepEqual(move(h, stackOf(ps, owner).uid, board(10, 5)), { ok: true });
|
|
const shield = ps.board.get('10,5');
|
|
for (const [row, col] of [[11, 5], [9, 7]]) {
|
|
assert.equal(canPlace(ctx(h, ps), shield.uid, board(row, col)).code, 'BAD_TILE');
|
|
assert.equal(move(h, shield.uid, board(row, col)).error, 'BAD_TILE');
|
|
assert.equal(ps.board.get('10,5'), shield);
|
|
}
|
|
const other = ps.acquireChess('chess_char_1_02_a', { source: 'grant' });
|
|
assert.deepEqual(move(h, other.uid, board(9, 7)), { ok: true });
|
|
for (const [uid, target] of [[shield.uid, board(9, 7)], [other.uid, board(10, 5)]]) {
|
|
assert.equal(canPlace(ctx(h, ps), uid, target).code, 'BAD_TILE');
|
|
assert.equal(move(h, uid, target).error, 'BAD_TILE');
|
|
}
|
|
assert.equal(canPlace(ctx(h, ps), shield.uid, board(11, 4)).ok, true);
|
|
assert.deepEqual(move(h, shield.uid, board(11, 4)), { ok: true });
|
|
assert.equal(canPlace(ctx(h, ps), shield.uid, board(10, 4)).ok, true);
|
|
assert.deepEqual(move(h, shield.uid, board(10, 4)), { ok: true });
|
|
assert.equal(ps.board.get('11,4'), owner);
|
|
assert.equal(ps.board.get('10,4'), shield);
|
|
h.invariants();
|
|
});
|
|
|
|
test('Jessica: owner reorientation keeps the shield; moving her returns it and withdrawal removes it', (t) => {
|
|
const { h, ps, owner } = prep(t);
|
|
assert.deepEqual(move(h, stackOf(ps, owner).uid, board(10, 5)), { ok: true });
|
|
const shield = ps.board.get('10,5');
|
|
assert.equal(canPlace(ctx(h, ps), shield.uid, board(10, 5)).action, 'orient');
|
|
assert.deepEqual(move(h, shield.uid, board(10, 5), 'LEFT'), { ok: true });
|
|
assert.deepEqual(move(h, owner.uid, board(10, 4), 'UP'), { ok: true });
|
|
assert.equal(ps.board.get('10,5'), shield);
|
|
assert.deepEqual(move(h, owner.uid, board(9, 4)), { ok: true });
|
|
assert.ok(!ps.board.has('10,5'));
|
|
const returned = stackOf(ps, owner);
|
|
assert.equal(returned.count, 1);
|
|
assert.equal(canPlace(ctx(h, ps), returned.uid, board(10, 5)).code, 'BAD_TILE');
|
|
assert.equal(move(h, returned.uid, board(10, 5)).error, 'BAD_TILE');
|
|
assert.deepEqual(move(h, returned.uid, board(9, 5)), { ok: true });
|
|
assert.deepEqual(move(h, owner.uid, { area: 'hand', idx: ps.hand.findIndex((p) => !p) }), { ok: true });
|
|
assert.ok(![...ps.board.values(), ...ps.hand, ...ps.temp].some((p) => p?.ownerUid === owner.uid));
|
|
h.invariants();
|
|
});
|
|
|
|
test('Jessica: bots plan her shield on an orthogonal tile', (t) => {
|
|
const { h, ps, owner } = prep(t);
|
|
const shield = stackOf(ps, owner);
|
|
const plan = planLayout(h.m, ps, [shield], undefined, { occupied: new Set(ps.board.keys()) });
|
|
assert.ok(['9,4', '11,4', '10,3', '10,5'].includes(plan.get(shield.uid)));
|
|
h.invariants();
|
|
});
|
|
|
|
test('other summons keep their own placement rules, including a medical drone far from its owner', (t) => {
|
|
const { h, ps } = prep(t);
|
|
const silence = ps.acquireChess('chess_char_2_02_a', { source: 'grant' });
|
|
assert.deepEqual(move(h, silence.uid, board(9, 3)), { ok: true });
|
|
const drone = stackOf(ps, silence);
|
|
assert.ok(drone);
|
|
assert.equal(canPlace(ctx(h, ps), drone.uid, board(12, 8)).ok, true);
|
|
assert.deepEqual(move(h, drone.uid, board(12, 8)), { ok: true });
|
|
h.invariants();
|
|
});
|