Files
ark-stronghold-protocol/test/sim/feedback5-bounty-payee.test.js
T
sganggs 75202776cc feedback6 R21B: 联防 plays on the round's battlefield again — its terrain, crates, water, devices and runes, as in 0.1.4 (the owner's decision of 2026-10-07, 「官服保留地形,改回去」; GitHub #244). 0.2.0's escaped-level map (8a436249, §25.6.4) is withdrawn: that map is the placeholder grid all 39 wave template levels carry, tile for tile; only the 11 stage levels have terrain. match/unitePhase.js _uniteOpts fields this.stageId in both combat modes, unite.js uniteStageId is removed, the client draws the 联防 on the board's stage again (screens/game.js: 0.1.4's ownStage / baseStage, no map switch, nothing to restore on leaving), the dev mock likewise. Kept from 0.2.0: own-board 突袭 / 乌尔比安 S3 landings (onOwnBoard, raidTile untouched), the bounty payee, the 联防 BGM, the SETTLE result box. data/stages.json keeps the two kind 'unite' records (no field uses them; engine tests use them as a plain two-halves road); tools/build-data.mjs comments only, an offline rebuild reproduces data/ byte for byte. The 联防 battle input is byte-identical to v0.1.4's in 20 cases (5 stages × 1 / 2 helpers × both combat modes); GitHub #41 item 3 (野鬃 pushing walkers back across the gate onto the other half) is back with v0.1.4's numbers, none off the map. Tests: test/match/feedback5-unite-map.test.js rewritten (6, all fail on the parent), test/e2e/coop.e2e.mjs checks the 联防 field's drawn stage; docs: DATA §12, META §4, PLAYING §5, design/geometry-time, I18N, the §25.6.4 pointer; golden: fields unite-1 (escaped_single → act1autochess_m02) and unite-2 (escaped_multi → act1autochess_m07), the stages of 0.1.4, and the co-op matches whose 联防 rounds come out differently on the round's map — coop2-NORMAL-3, coop2-HARD-4, coop3-ABYSS-5, coop4-ABYSS-7, coop2-NORMAL-8-serverrun, coop2-NORMAL-14-standins (now a victory)
2026-10-07 17:40:03 +08:00

101 lines
5.9 KiB
JavaScript

// test/sim/feedback5-bounty-payee.test.js — community report of 2026-10-06 「被源石地板烫死的悬赏没给赏金」. The bounty pays
// whenever the body dies (the owner's decision, relayed 2026-10-06; 0.1.1 "a bounty coin is paid once, at the final
// death"; 0.1.3 §23.25 "Killing the body pays once"); PRTS 卫戍协议:盟约 决策: "该敌人于对应玩家所属区域倒下时,使相应玩家获得
// 额外资金". Battle._bountyPayee: an operator of a player in the battle that dealt the blow pays its player; any other
// death pays the card's owner when that player fights there — on its own field a 活性源石 death always paid (pinned) —
// and in 联防, where the owner is the leaker (unite.js planUnite) and fights elsewhere, the helper whose half it fell on.
// Until 0.2.0 that last case fell back to the leaker, who has no per-player entry in the 联防 battle: the coins were
// dropped (in 0.1.x the 联防 field was the round's map, as it is again since 0.2.1, so 战场#04's 活性源石 burned leaked
// bounty enemies for nothing).
// Run: node --test test/sim/feedback5-bounty-payee.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { makeBattle, enemyRec, chessRec, checkInvariants } from '../helpers/battleHarness.js';
const coins = (h) => {
const out = {};
for (const e of h.eventsOf('bounty')) out[e[1]] = (out[e[1]] || 0) + e[2];
return out;
};
const done = (h) => { checkInvariants(h.b); assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0])); };
const helper = (playerId, seat, side, colOffset, units = []) => ({ playerId, seat, side, colOffset, units, bonds: {}, layers: {} });
// a 联防 field: helpers A (left half) and B (right half), a bounty enemy the leaker L sent in (planUnite's shape)
function unite(players, pos) {
return makeBattle({
kind: 'unite', content: 'none', autoFinish: false, timeLimit: 60, players,
defs: { enemies: { enemy_b: enemyRec({ key: 'enemy_b', hp: 100, speed: 0 }), enemy_x: enemyRec({ key: 'enemy_x', hp: 100, speed: 0 }) } },
enemies: [{ key: 'enemy_b', pos, sourcePlayerId: 'L', mods: { bountyId: 'card1' }, bounty: { coins: 2, ownerPlayerId: 'L' }, tag: 'bounty' }],
});
}
const target = (h) => h.b.enemies.find((e) => e.defId === 'enemy_b');
test('own field (战场#04): a bounty enemy the 活性源石 alone burns to death pays its owner — as before', () => {
const h = makeBattle({
stageId: 'act1autochess_m04', autoFinish: false, timeLimit: 60,
defs: { enemies: { enemy_b: enemyRec({ key: 'enemy_b', hp: 100, speed: 0.5 }) } },
enemies: [{ key: 'enemy_b', route: { motion: 'WALK', start: [10, 7], end: [9, 2], checkpoints: [] }, mods: { bountyId: 'card1' }, bounty: { coins: 2, ownerPlayerId: 'p1' }, tag: 'bounty', ownerPlayerId: 'p1' }],
});
assert.ok(h.runUntil(() => !h.b.enemies.some((e) => e.alive) && h.b.time > 1, 30));
assert.deepEqual(h.eventsOf('die').map((d) => d[2]), ['killed'], 'it died, no leak');
assert.deepEqual(coins(h), { p1: 2 });
assert.equal(h.result().perPlayer.p1.coins, 2);
done(h);
});
test('联防: a leaked bounty enemy that dies with no operator behind it pays the helper of the half it fell on', () => {
const ways = [
['killed by nobody', (h, e) => h.b.kill(e, null)],
['无来源 terrain-tagged damage', (h, e) => h.b.dealDamage(null, e, { amount: 500, type: 'true', canDodge: false, tags: ['terrain'] })],
['another enemy\'s hit', (h, e) => h.b.dealDamage(h.spawn('enemy_x', { pos: [10, 3] }), e, { amount: 500, type: 'true' })],
['its own HP loss', (h, e) => h.b.loseHp(e, 500, { source: e })],
];
for (const [label, how] of ways) {
// one helper: the whole field is hers
const h1 = unite([helper('A', 0, 'L', 0)], [10, 6]);
h1.step();
how(h1, target(h1));
assert.ok(!target(h1).alive, label);
assert.deepEqual(coins(h1), { A: 2 }, `one helper, ${label}`);
assert.equal(h1.result().perPlayer.A.coins, 2);
done(h1);
// two helpers: by the half it fell on (the right-hand helper's half from column 11)
for (const [col, who] of [[5, 'A'], [15, 'B']]) {
const h2 = unite([helper('A', 0, 'L', 0), helper('B', 1, 'R', 8)], [10, col]);
h2.step();
how(h2, target(h2));
assert.deepEqual(coins(h2), { [who]: 2 }, `two helpers, column ${col}, ${label}`);
done(h2);
}
}
});
test('联防: a helper\'s operator that knocks it out still takes the coins, whatever half it fell on', () => {
const GUN = chessRec({ id: 't_gun', profession: 'SNIPER', projectile: 'none', stats: { atk: 50, maxHp: 1e7, bat: 1, blockCnt: 0 }, rangeGrid: [[0, 0]], skill: null });
const h = makeBattle({
kind: 'unite', content: 'none', autoFinish: false, timeLimit: 60,
players: [helper('A', 0, 'L', 0, [{ uid: 1, kind: 'chess', chessId: 't_gun', row: 10, col: 4 }]), helper('B', 1, 'R', 8)],
defs: { chess: { t_gun: GUN }, enemies: { enemy_b: enemyRec({ key: 'enemy_b', hp: 100, speed: 0 }) } },
enemies: [{ key: 'enemy_b', pos: [10, 15], sourcePlayerId: 'L', mods: { bountyId: 'card1' }, bounty: { coins: 2, ownerPlayerId: 'L' }, tag: 'bounty' }],
});
h.step();
const gun = h.b.allies().find((u) => u.ownerId === 'A');
assert.ok(gun, 'A\'s operator');
h.b.dealDamage(gun, target(h), { amount: 500, type: 'phys' });
assert.deepEqual(coins(h), { A: 2 }, 'the killer\'s player, on B\'s half');
done(h);
});
test('Final Assault pair field: the card\'s owner fights there, so a death nobody caused pays the owner on either half', () => {
const h = makeBattle({
kind: 'boss', content: 'none', autoFinish: false, timeLimit: 60,
players: [helper('A', 0, 'L', 0), helper('B', 1, 'R', 8)],
defs: { enemies: { enemy_b: enemyRec({ key: 'enemy_b', hp: 100, speed: 0 }) } },
enemies: [{ key: 'enemy_b', pos: [3, 15], ownerPlayerId: 'A', mods: { bountyId: 'card1' }, bounty: { coins: 2, ownerPlayerId: 'A' }, tag: 'bounty' }],
});
h.step();
h.b.kill(target(h), null);
assert.deepEqual(coins(h), { A: 2 });
done(h);
});