feedback3: merge WH — ground operators are melee-position, 叙拉古 rolls on every normal hit, off bonds show as disabled, prep-end items merge next prep

This commit is contained in:
sganggs committed 2026-10-04 21:26:10 +08:00
commit 1005754bf3
28 files changed
+746 -98

No files matched your search

+10 -7
View File
@@ -3,8 +3,9 @@
// of the modes whose official modeDataDict inactiveBondIdList switches 奥术 off (with 拉特兰 阿戈尔 卡西米尔 灵巧 奇迹 投资人
// 突袭 独行 绝技), yet 深靛 洛洛 阿罗玛 夕 圣聆初雪 stay in the pool through their other bond (server pool.js) and their cards
// showed 奥术 like any bond: the detail card's chip read "奥术 0/2 未激活" with three 奥术 operators deployed (the server
// leaves an inactive bond out of m.private.bonds) and the bond popup "在场 0/2 未激活". The cards, chips and popup now say
// 本局禁用 for a bond the mode never activates (gameLogic modeOffBonds over config.modes[modeId].inactiveBondIds).
// used to leave an inactive bond out of m.private.bonds) and the bond popup "在场 0/2 未激活". The cards, chips and popup
// say 本局禁用 for a bond the mode never activates (gameLogic modeOffBonds over config.modes[modeId].inactiveBondIds);
// since 0.1.3 the view also lists it (`off: true`) so the strip can draw the grey disc — still never in ps.bonds.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
@@ -63,13 +64,15 @@ test('a real 标准 match: 奥术 operators are buyable, three on the board neve
const used = new Set();
for (const id of members.slice(0, 3)) { const t = legalTileFor(m, ps, id, used); used.add(tileKey(t[0], t[1])); give(m, ps, id, 'board', t); }
ps.recompute();
assert.equal(ps.bonds.arcaneShip, undefined, 'official: the mode never activates 奥术');
assert.equal(ps.bonds.arcaneShip, undefined, 'official: the mode never activates 奥术 — not in the battle state');
const priv = ps.privateView();
assert.equal(priv.bonds.find((b) => b.bondId === 'arcaneShip'), undefined, 'no 奥术 entry in m.private.bonds');
const arc = priv.bonds.find((b) => b.bondId === 'arcaneShip');
assert.ok(arc && arc.off === true && arc.active === false && arc.tier === 0 && arc.count === 3, 'the view lists the members as a grey 本局禁用 disc');
assert.ok(priv.bonds.slice(priv.bonds.findIndex((b) => b.off)).every((b) => b.off), 'off entries come last');
const off = modeOffBonds(getMode(m.modeId));
// before: the chip computed 0 members from the missing entry — "奥术:在场 0/2,未激活" with 3 deployed
const before = chipOf(BondChips({ bondIds: DATA.chess[members[0]].bonds, bonds: priv.bonds }), 'arcaneShip');
assert.match(before.props.title, /在场 0\/2,未激活/);
// without the mode's off set the chip would read the new entry as an ordinary inactive bond (在场 3/3,未激活)
const bare = chipOf(BondChips({ bondIds: DATA.chess[members[0]].bonds, bonds: priv.bonds }), 'arcaneShip');
assert.match(bare.props.title, /在场 3\/3,未激活/);
const chip = chipOf(BondChips({ bondIds: DATA.chess[members[0]].bonds, bonds: priv.bonds, off }), 'arcaneShip');
assert.ok(hasClass(chip, 'is-off') && !hasClass(chip, 'is-active'));
assert.equal(chip.props.title, '奥术:本局禁用(该盟约不会激活)');
+38 -1
View File
@@ -9,7 +9,7 @@ import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
phaseMode, phaseBanner, isCombatPhase, isBossPhase, countdownState, phaseTotalSeconds, sortBonds, bondTier, nextThreshold,
bondMembers, bannedPerBond, priceTone, mergeProgress, shopBlockReason, deploySets, indexPieces, placementContext, canPlace,
bondMembers, memberHeadCount, bannedPerBond, priceTone, mergeProgress, shopBlockReason, deploySets, indexPieces, placementContext, canPlace,
boardTargets, dropIntent, normalizeDraft, normalizeSp, groupEnemies, factionTypes, snapHud, bossFrac, attackInterval, fmtNum,
rangeGridBox, shortcutFor, sanitizeSettings, DEFAULT_SETTINGS, normalizeResult, cycleField, fieldLabel, homeFieldId,
activeBubbles, sortedPlayers, tileKey, prepCapsuleLabel, prepCamera, dropFailureReason,
@@ -138,9 +138,46 @@ describe('bonds', () => {
assert.equal(byId.get(m2).owned, true, 'golden in hand counts as owned base');
assert.equal(byId.get(m2).onBoard, false);
assert.equal(byId.get(m3).banned, true);
assert.equal(byId.get(m2).inHand, true, 'a golden copy in the hand marks the base in hand');
assert.equal(rows[0].id, m1, 'on-board first');
assert.equal(rows.length, b.visibleMembers.length);
});
test('memberHeadCount: the hand counts only for a bond that counts the hand, once per row', () => {
const rows = [
{ id: 'a', onBoard: true, inHand: true },
{ id: 'b', onBoard: false, inHand: true },
{ id: 'c', onBoard: false, inHand: false },
null,
];
assert.equal(memberHeadCount(rows, false), 1, 'board only');
assert.equal(memberHeadCount(rows, true), 2, 'board or hand, one row once');
assert.equal(memberHeadCount(null, true), 0);
assert.equal(memberHeadCount(undefined), 0);
const invest = bonds.investShip;
assert.equal(invest.countsHand, true);
const [m1, m2] = invest.visibleMembers;
const priv = privWith({
board: [{ ...piece(m1), row: 9, col: 3 }],
hand: [piece(m2)],
temp: [piece(invest.visibleMembers[2] || m2)],
});
const got = bondMembers(invest, priv, [], getChess);
const byId = new Map(got.map((r) => [r.id, r]));
assert.equal(byId.get(m1).onBoard, true);
assert.equal(byId.get(m2).inHand, true);
assert.equal(byId.get(m2).onBoard, false);
if (invest.visibleMembers[2]) assert.equal(byId.get(invest.visibleMembers[2]).inHand, false, 'a temporary-slot copy is not in hand');
assert.equal(memberHeadCount(got, true), 2, 'the board member and the hand member, not the temporary slot');
assert.equal(memberHeadCount(got, false), 1, 'without countsHand the hand member is left out');
});
test('sortBonds puts a mode-off bond after every live bond', () => {
const sorted = sortBonds([
{ bondId: 'investShip', off: true, active: false, layers: 99, count: 3, tier: 0 },
{ bondId: 'yanShip', active: true, layers: 1, count: 3, tier: 1 },
{ bondId: 'raidShip', off: true, layers: 0, count: 1 },
], (id) => bonds[id]).map((b) => b.bondId);
assert.deepEqual(sorted, ['yanShip', 'investShip', 'raidShip']);
});
test('bannedPerBond counts banned visible members', () => {
const b = bonds.deputShip;
const per = bannedPerBond(Object.values(bonds), [b.visibleMembers[0], b.visibleMembers[1], 'nope']);