feedback7: merge fb7-dobermann — 教鞭 is the official three-choice (PR #353 by @Sukvii, the owner's decision), the Final Assault / Hidden Core pay a 战术特训 card's perfect coins (guarded by b.progress leaksBy), the bots put zero-range heavy guards on the road first (PR #339 by @IceCodeNew, placement only)

- Conflicts (the branch started at ae22335c): spec.js / protocol.js b.progress (resolved from fb7-ui-backlog + leaksBy),
  gameLogic.js / game.js exports (device tips + normalizePersonalChoice), docs/PLAYING.md / design/network.md lines
  merged against the base, META.md (画卷 note + 教鞭), test/docs-consistency.test.js pin (both fields).
- Grok review: MERGE AFTER FIXES — the HIGH (a forged boss result paid the cards) fixed on the branch (66a228c6), the
  META wording (1ab12763).
- Golden: matches 10 (the 杜宾 bot picks and the guard placement); union: coop3-ABYSS-5 differs from HEAD only in ai_2's
  dmgDealt (1238011 → 1186269), no outcome change — HEAD's side had moved it, the new placement applies on that path.
- Broad node --test 5623 (5621 pass, 0 fail, 2 skipped); lint 0 errors; typecheck; i18n strict clean.
This commit is contained in:
sganggs committed 2026-10-08 22:11:49 +08:00
commit 2de2feee3c
57 files changed
+1339 -276

No files matched your search

+27 -1
View File
@@ -11,7 +11,7 @@ import {
phaseMode, phaseBanner, isCombatPhase, isBossPhase, countdownState, phaseTotalSeconds, sortBonds, bondTier, nextThreshold,
battleOverSfx, ownRoundLoss, uniteResultBox, battleResultBox, roundResultBox, RESULT_BOX_MS,
bondMembers, memberHeadCount, bannedPerBond, priceTone, mergeProgress, shopBlockReason, deploySets, indexPieces, placementContext, canPlace,
boardTargets, dropIntent, normalizeDraft, normalizeSp, groupEnemies, factionTypes, snapHud, bossFrac, attackInterval, fmtNum,
boardTargets, dropIntent, normalizeDraft, normalizeSp, normalizePersonalChoice, groupEnemies, factionTypes, snapHud, bossFrac, attackInterval, fmtNum,
rangeGridBox, shortcutFor, sanitizeSettings, DEFAULT_SETTINGS, normalizeResult, cycleField, fieldLabel, homeFieldId,
activeBubbles, sortedPlayers, tileKey, prepCapsuleLabel, prepCamera, dropFailureReason, terrainInfo,
} from '../../public/js/ui/gameLogic.js';
@@ -584,6 +584,32 @@ describe('drafts', () => {
assert.equal(normalizeSp(null), null);
assert.equal(normalizeSp({ cards: new Array(9).fill({}) }).cards.length, 6, 'at most 6 cards');
});
test('personal choice: only the living recipient in the current PREP, independent of the public draft', () => {
const pub = { phase: PHASE.PREP, round: 14, sp: { family: 'supply', cards: ['global'] } };
const priv = { playerId: 'a', alive: true, canReady: false, temp: [], personalChoice: {
id: 'seed.choice.3', round: 14, sourceItemId: 'chess_item_6_03_m',
cards: [{ id: 'e1', kind: 'bounty', name: '战术特训', coin: 2 }, { id: 'e2', kind: 'bounty' }],
} };
const before = JSON.stringify({ pub, priv });
const sp = normalizePersonalChoice(pub, priv, 'a');
assert.equal(sp.id, priv.personalChoice.id);
assert.equal(sp.family, 'bounty');
assert.equal(sp.name, '教鞭 · 战术特训');
assert.equal(sp.turnPid, 'a');
assert.equal(sp.pickOf.size, 0);
assert.deepEqual(sp.cards.map((c) => c.idx), [0, 1]);
assert.ok(sp.cards.every((c) => c.takenBy === null));
assert.equal(JSON.stringify({ pub, priv }), before);
assert.equal(shopBlockReason('ready', { priv, editable: true }), '请先完成教鞭选择');
assert.equal(shopBlockReason('ready', { priv: { ...priv, temp: [{}] }, editable: true }), '请先完成教鞭选择');
assert.match(shopBlockReason('ready', { priv: { ...priv, personalChoice: null }, editable: true }), /临时整备区/);
for (const phase of [PHASE.SP_DRAFT, PHASE.COMBAT, PHASE.ROUND_START]) assert.equal(normalizePersonalChoice({ ...pub, phase }, priv, 'a'), null);
assert.equal(normalizePersonalChoice(pub, null, 'a'), null, 'spectator has no private state');
assert.equal(normalizePersonalChoice(pub, priv, 'b'), null);
assert.equal(normalizePersonalChoice(pub, { ...priv, alive: false }, 'a'), null);
assert.equal(normalizePersonalChoice({ ...pub, round: 15 }, priv, 'a'), null);
assert.equal(normalizePersonalChoice(pub, { ...priv, personalChoice: null }, 'a'), null);
});
});
describe('fields, players, emotes', () => {