Files
ark-stronghold-protocol/test/ui/feedback7-orchid-freeze.test.js
T
sganggs 16df0bf466 feedback7 R22-HUD: the card 梓兰's 猎头顾问 freezes shows its frost — every shop card draws its own slot's freeze, not the toggle's (GitHub #354 by @DuolaCCCCat); golden: unchanged
Seen (reproduced on the parent, notes repro-354.mjs): band_orchid's active refresh copies the top-tier operator into another
slot and freezes the copy (bands/meta.js band_shop_refresh_copy_max_lv_char, slot.frozen = true) while shop.frozen — the
toggle — stays false; m.private sends both (views.js: shop.frozen and slots[i].frozen). The shop bar passed the toggle
(`frozen = !!shop.frozen`) to every ChessCard / ItemCard, so no card showed the frost: the server kept the card to the next
prep, the player could not see which one.

Now: shopBar.js slotFrozen(slot, toggle) = the slot's own flag, or the toggle (a frame without per-slot flags — the dev
mock — and the old behaviour for the 冻结 button, which still copies the toggle onto every unsold slot); both card kinds
use it. The bar's frame (is-frozen) and the 冻结 / 解冻 button stay on the toggle. The rule is not touched (bands.test.js,
and the new test): a second manual refresh re-rolls the frozen card too — like the toggle, "再手动刷新会重抽" — and
freezes one of the new pair; with no further refresh the frozen card stays in the next prep (the flags are cleared
then, round.js startRound).

- Tests: test/ui/feedback7-orchid-freeze.test.js (4: the wire — exactly one frozen card of the pair, toggle off — and the
  cards drawn from it; the toggle still freezes every slot; the rule unchanged across two refreshes and a round; the
  ShopBar source pin); fails on the parent (no slotFrozen, the cards passed `frozen`).
- Docs: PLAYING.md (冻结 row), design/network.md (m.private slot.frozen). Golden: unchanged (client render only).
2026-10-08 19:22:08 +08:00

110 lines
6.0 KiB
JavaScript

// test/ui/feedback7-orchid-freeze.test.js — GitHub #354 (DuolaCCCCat): 「梓兰的主动刷新不会触发主动冻结」. 梓兰's 猎头顾问 (band_orchid) makes
// every active refresh a special one: two operators of the same name, and ONE of them frozen — kept to the next prep. The
// server did freeze it (slot.frozen, sent in m.private shop.slots[i].frozen) while the shop toggle stayed off; the shop bar
// drew the frost from the toggle alone, so the player never saw which card was frozen. Each card now reads its own slot.
// The rule itself (a second manual refresh re-rolls the frozen card too, like the toggle) is unchanged.
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { makeMatch, DATA } from '../match/harness.js';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
// the browser data store reads the real data files from disk
globalThis.fetch = async (url) => {
const name = String(url).split('/').pop();
try {
const body = readFileSync(path.join(ROOT, 'data', name), 'utf8');
return { ok: true, status: 200, json: async () => JSON.parse(body) };
} catch {
return { ok: false, status: 404, json: async () => ({}) };
}
};
const { ChessCard, ItemCard, slotFrozen } = await import('../../public/js/ui/shopBar.js');
const { data } = await import('../../public/js/data.js');
await data.loadAll('items', 'effects', 'choices', 'assets', 'chess', 'bonds', 'garrisons');
/** Every vnode of a preact tree (htm output), depth first. */
function* walk(v) {
if (Array.isArray(v)) { for (const x of v) yield* walk(x); return; }
if (!v || typeof v !== 'object') return;
yield v;
yield* walk(v.props?.children);
}
const hasClass = (v, c) => typeof v?.props?.class === 'string' && v.props.class.split(/\s+/).includes(c);
const iced = (card) => hasClass(card, 'is-frozen') && [...walk(card)].some((n) => hasClass(n, 'scard__ice'));
const bare = (card) => !hasClass(card, 'is-frozen') && ![...walk(card)].some((n) => hasClass(n, 'scard__ice'));
/** A solo match in its first prep with 梓兰's strategy, level 4 shop, funds for a few refreshes. */
function orchid(seed = 12) {
const h = makeMatch({ mode: 'solo', seed, fake: true }).start();
h.toPrep(1);
const ps = h.ps('p_0');
ps.bandId = 'band_orchid';
ps.shop.level = 4;
ps.funds = 50;
return { h, ps };
}
describe('#354: the card the 猎头顾问 freezes shows its frost', () => {
test('the wire: the active refresh freezes one copied card and leaves the shop toggle off; the cards read their own slot', () => {
const { h, ps } = orchid();
assert.deepEqual(h.m.handle('p_0', { t: 'g.refresh' }), { ok: true });
const priv = ps.privateView();
assert.equal(priv.shop.frozen, false, 'the 冻结 toggle is off');
const chess = priv.shop.slots.filter((s) => s && s.kind === 'chess');
const frozen = chess.filter((s) => s.frozen);
assert.equal(frozen.length, 1, 'exactly one card is frozen');
assert.equal(chess.filter((s) => s.id === frozen[0].id).length, 2, 'one of the two operators of the same name');
// the bar's frame and the button follow the toggle (off), each card follows its own slot
for (const s of priv.shop.slots.filter(Boolean)) {
const props = { slot: s, idx: 0, priv, frozen: slotFrozen(s, priv.shop.frozen), onBuy() {}, onDetail() {} };
const card = s.kind === 'item' ? ItemCard(props) : ChessCard(props);
assert.equal(s.frozen ? iced(card) : bare(card), true, `${s.id}: ${s.frozen ? 'frost' : 'no frost'}`);
}
h.m.dispose();
});
test('the toggle still freezes every unsold card, and the helper reads the slot and the toggle', () => {
const { h, ps } = orchid(13);
h.m.handle('p_0', { t: 'g.freeze' });
const priv = ps.privateView();
assert.equal(priv.shop.frozen, true);
assert.ok(priv.shop.slots.filter(Boolean).every((s) => s.frozen), 'every slot copies the toggle');
assert.equal(slotFrozen(priv.shop.slots[0], priv.shop.frozen), true);
assert.equal(slotFrozen({ frozen: false }, true), true, 'a frame without per-slot flags: the toggle');
assert.equal(slotFrozen({ frozen: true }, false), true, 'a slot of its own: its flag');
assert.equal(slotFrozen({ frozen: false }, false), false);
assert.equal(slotFrozen(null), false);
h.m.dispose();
});
test('the rule stays as it was: the next manual refresh re-rolls the frozen card too and freezes one of the new pair; one refresh then the next prep keeps that card', () => {
const { h, ps } = orchid();
h.m.handle('p_0', { t: 'g.refresh' });
const first = ps.shop.slots.find((s) => s && s.frozen);
h.m.handle('p_0', { t: 'g.refresh' });
const frozenNow = ps.shop.slots.filter((s) => s && s.frozen);
assert.equal(frozenNow.length, 1, 'again exactly one frozen card');
assert.equal(ps.shop.frozen, false);
assert.ok(first && frozenNow[0], 'both refreshes froze a card');
const kept = frozenNow[0].id;
h.toPrep(2);
assert.ok(ps.shop.slots.some((s) => s && s.id === kept), 'the frozen card is still in the shop in the next prep');
assert.equal(ps.shop.frozen, false);
h.m.dispose();
});
test('ShopBar passes slotFrozen(slot, toggle) to the operator and item cards; the frame and the 冻结 button stay on the toggle', () => {
const src = readFileSync(path.join(ROOT, 'public/js/ui/shopBar.js'), 'utf8');
assert.match(src, /<\$\{ChessCard\} key=\$\{`c\$\{i\}:\$\{s\.id\}`\} slot=\$\{s\} idx=\$\{i\} priv=\$\{priv\} frozen=\$\{slotFrozen\(s, frozen\)\}/);
assert.match(src, /<\$\{ItemCard\} key=\$\{`i\$\{i\}:\$\{s\.id\}`\} slot=\$\{s\} idx=\$\{i\} frozen=\$\{slotFrozen\(s, frozen\)\}/);
assert.match(src, /const frozen = !!shop\.frozen;/);
assert.match(src, /cx\('shopbar', frozen && 'is-frozen'/, 'the bar\'s frame is the toggle');
assert.match(src, /cx\('toolbtn', 'toolbtn--ice', frozen && 'is-on'\)/, 'the button is the toggle');
assert.ok(DATA.bands.band_orchid, 'the band exists in the data');
});
});