132 lines
7.3 KiB
JavaScript
132 lines
7.3 KiB
JavaScript
// 芬's 信标 gift (server/match/builtinMeta.js use_equip_recruit_new_char_and_give_char_to_player_most_bond →
|
|
// effect:builtin_gift): act2autochess eff_acarm109 "下个休整期向相应盟约人数最多的队友发送1个原干员"; band_fang
|
|
// "原干员在下回合传递给对应盟约人数最多的队友".
|
|
// * community report #6 after 0.1.2: an ELITE carrier arrived at the teammate as the normal card — the gift is the
|
|
// original operator, an elite with its 3 pool copies;
|
|
// * GitHub #86: a sender eliminated before the next round start got no onRoundStart, so the gift was lost — it is
|
|
// delivered anyway (afterElimination); a failed grant keeps the gift for the next round start; a receiver
|
|
// eliminated meanwhile is replaced by the living teammate with the most members of the bonds [ASSUMED].
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { makeMatch, give, giveItem, DATA, legalTileFor } from './harness.js';
|
|
import { FakeBattle } from './fakeBattle.js';
|
|
import { createRegistry } from '../../server/match/effectsMeta.js';
|
|
|
|
const QUIET = { warn() {}, error() {}, info() {} };
|
|
const REG = createRegistry({ log: QUIET });
|
|
const OK = { ok: true };
|
|
const BEACON = 'chess_item_5_04_e_a';
|
|
|
|
/** Co-op match of `humans` humans at PREP R1, every board / hand emptied (pool copies returned), no strategies. */
|
|
function setup({ humans = 2, seed = 31 } = {}) {
|
|
const h = makeMatch({ mode: 'coop', humans, seed, registry: REG, fake: true }).start();
|
|
h.toPrep(1);
|
|
const m = h.m;
|
|
for (const ps of m.players.values()) {
|
|
for (const p of [...ps.board.values(), ...ps.hand.filter(Boolean), ...ps.temp.filter(Boolean)]) if (p.kind === 'chess') ps.returnCopies(p);
|
|
ps.board.clear(); ps.hand.fill(null); ps.temp.fill(null); ps.offers.length = 0;
|
|
ps.funds = 50; ps.layers = {}; ps.pendingFunds = 0; ps.shop.freeRefreshes = 0;
|
|
ps.bandId = null;
|
|
ps.recompute();
|
|
}
|
|
const equip = (item, target, pid = 'p_0') => m.handle(pid, { t: 'g.equip', itemUid: item.uid, targetUid: target.uid });
|
|
return { h, m, equip };
|
|
}
|
|
/** Visible normal chess (with an elite) whose 特质 are all IN_BATTLE, sorted. */
|
|
const plain = (pred = () => true) => Object.values(DATA.chess)
|
|
.filter((c) => c.visible && !c.isGolden && c.goldenId && (c.garrisonIds || []).every((g) => DATA.garrisons[g].eventType === 'IN_BATTLE') && pred(c))
|
|
.map((c) => c.chessId).sort();
|
|
const owned = (ps) => [...ps.board.values(), ...ps.hand, ...ps.temp].filter((p) => p && p.kind === 'chess');
|
|
const ofBase = (m, ps, base) => owned(ps).filter((p) => m.gd.baseIdOf(p.id) === base);
|
|
const gifts = (ps) => ps.effects.filter((e) => e.key === 'effect:builtin_gift');
|
|
/** `ps` drops to 1 LP and every field leaks one enemy (nobody perfect: no 联防), so `ps` is eliminated at the settle. */
|
|
const knockOut = (ps) => { ps.lp = 1; FakeBattle.script = (b) => ({ leaks: Object.fromEntries(b.players.map((id) => [id, 1])) }); };
|
|
const deploy = (m, ps, id) => give(m, ps, id, 'board', legalTileFor(m, ps, id));
|
|
|
|
test('信标 on an elite: the teammate receives that elite with its 3 pool copies, not the normal card (community report #6)', () => {
|
|
const { h, m, equip } = setup({ seed: 31 });
|
|
const p0 = h.ps('p_0'), p1 = h.ps('p_1');
|
|
const cid = plain((c) => c.tier === 3 && c.bonds.length)[0];
|
|
const gid = DATA.chess[cid].goldenId;
|
|
const t = give(m, p0, gid, 'hand');
|
|
assert.equal(t.poolCopies, m.gd.goldenCopies);
|
|
const left = m.pool.left(cid);
|
|
assert.deepEqual(equip(giveItem(m, p0, BEACON), t), OK);
|
|
assert.equal(p0.find(t.uid), null, 'carrier destroyed');
|
|
assert.equal(m.pool.left(cid), left + 3, 'its 3 copies are back in the pool');
|
|
for (const s of p0.offers[0].slots) assert.equal(DATA.chess[s.id].tier, 3, 'the pick of two stays at the carrier\'s tier');
|
|
assert.equal(gifts(p0)[0].params.chessId, gid, 'the gift carries the elite id');
|
|
h.toPrep(2);
|
|
const got = ofBase(m, p1, cid);
|
|
assert.equal(got.length, 1, 'gift arrived');
|
|
assert.equal(got[0].id, gid, 'as the elite');
|
|
assert.equal(got[0].poolCopies, 3, 'holding the 3 copies');
|
|
assert.equal(m.pool.left(cid), left, 'taken from the pool again');
|
|
assert.equal(gifts(p0).length, 0, 'removed once granted');
|
|
h.invariants();
|
|
});
|
|
|
|
test('信标 on a normal operator still sends the normal card', () => {
|
|
const { h, m, equip } = setup({ seed: 35 });
|
|
const p0 = h.ps('p_0'), p1 = h.ps('p_1');
|
|
const cid = plain((c) => c.tier === 2 && c.bonds.length)[0];
|
|
assert.deepEqual(equip(giveItem(m, p0, BEACON), give(m, p0, cid, 'hand')), OK);
|
|
h.toPrep(2);
|
|
assert.deepEqual(ofBase(m, p1, cid).map((p) => [p.id, p.poolCopies]), [[cid, 1]]);
|
|
h.invariants();
|
|
});
|
|
|
|
test('信标: the gift still arrives when its sender is eliminated before the next round start (GitHub #86)', () => {
|
|
const { h, m, equip } = setup({ seed: 33 });
|
|
const p0 = h.ps('p_0'), p1 = h.ps('p_1');
|
|
const cid = plain((c) => c.tier === 3 && c.bonds.length)[1];
|
|
const gid = DATA.chess[cid].goldenId;
|
|
assert.deepEqual(equip(giveItem(m, p0, BEACON), give(m, p0, gid, 'hand')), OK);
|
|
knockOut(p0);
|
|
h.toPrep(2);
|
|
assert.equal(p0.alive, false, 'the sender was eliminated at the R1 settle');
|
|
assert.equal(p0.eliminatedRound, 1);
|
|
assert.deepEqual(ofBase(m, p1, cid).map((p) => p.id), [gid], 'the teammate got the elite anyway');
|
|
assert.equal(gifts(p0).length, 0, 'delivered, then removed');
|
|
const line = h.allTo('p_1', 'm.ticker').find((t) => t.type === 'CHAR_GIFT');
|
|
assert.ok(line && line.text.includes('P0') && line.text.includes(DATA.chess[gid].name), `CHAR_GIFT names the sender: ${line && line.text}`);
|
|
h.invariants();
|
|
});
|
|
|
|
test('信标: a grant that fails (no copy left in the pool) keeps the gift for the next round start', () => {
|
|
const { h, m, equip } = setup({ seed: 37 });
|
|
const p0 = h.ps('p_0'), p1 = h.ps('p_1');
|
|
const cid = plain((c) => c.tier === 2 && c.bonds.length)[1];
|
|
assert.deepEqual(equip(giveItem(m, p0, BEACON), give(m, p0, cid, 'hand')), OK);
|
|
// every copy taken by "other players" before the round start
|
|
const held = m.pool.take(cid, 99);
|
|
assert.ok(held > 0);
|
|
h.drive(() => m.phase === 'PREP' && m.round === 2);
|
|
assert.equal(ofBase(m, p1, cid).length, 0, 'nothing granted');
|
|
assert.equal(gifts(p0).length, 1, 'the gift is kept');
|
|
m.pool.give(cid, held); // the copies come back (sold)
|
|
h.toPrep(3);
|
|
assert.deepEqual(ofBase(m, p1, cid).map((p) => p.id), [cid], 'delivered at the next round start');
|
|
assert.equal(gifts(p0).length, 0);
|
|
h.invariants();
|
|
});
|
|
|
|
test('信标: a receiver eliminated before the round start is replaced by the living teammate with the most bond members [ASSUMED]', () => {
|
|
const { h, m, equip } = setup({ humans: 3, seed: 39 });
|
|
const p0 = h.ps('p_0'), p1 = h.ps('p_1'), p2 = h.ps('p_2');
|
|
const cid = plain((c) => c.tier === 3 && c.bonds.length === 1)[0];
|
|
const bond = DATA.chess[cid].bonds[0];
|
|
const mates = plain((c) => c.bonds.includes(bond) && c.chessId !== cid);
|
|
deploy(m, p1, mates[0]);
|
|
deploy(m, p1, mates[1]); // p1 fields the most members → picked at equip time
|
|
deploy(m, p2, mates[2]);
|
|
assert.deepEqual(equip(giveItem(m, p0, BEACON), give(m, p0, cid, 'hand')), OK);
|
|
assert.equal(gifts(p0)[0].params.toPlayerId, 'p_1');
|
|
knockOut(p1);
|
|
h.toPrep(2);
|
|
assert.equal(p1.alive, false);
|
|
assert.deepEqual(ofBase(m, p2, cid).map((p) => p.id), [cid], 'the next teammate got it');
|
|
assert.equal(gifts(p0).length, 0);
|
|
h.invariants();
|
|
});
|