feedback7 dober: 教鞭 is the official personal choice of three 战术特训 cards again, not a random one (PR #353 by @Sukvii, the owner's decision of 2026-10-08); golden: coop2-NORMAL-8-serverrun and coop2-ABYSS-10-boosted move
Seen: the Art 教鞭 (杜宾's strategy) drew three 战术特训 cards and added a random one — a documented simplification (docs/META.md §2.5: a personal choice would need its own phase, protocol message, timer and AI takeover). Official: act2autochess choice_event hunter_band_1 is a PERSONAL_CHOOSE; PRTS 下半 记录 §法术 教鞭 "使用后销毁,于3个战术特训的悬赏任务中选择一项". The owner decided on 2026-10-08 to follow it. Taken from PR #353 (code and tests by @Sukvii), ported to the split 0.2.2 modules. Now: ctx.offerBountyChoice -> Match.offerBountyChoice keeps up to three different mode-eligible cards (15 of 20 in 标准 / 险境, 20 in 绝境 / 终极; one meta-rng shuffle) in PlayerState.personalChoice, m.private only (same cards after a reconnect); g.choice { idx, choiceId } confirms it (Match.pickPersonalChoice: owner, id, round, index; a request without a choiceId is still the public draft, a stale id never falls through to it). No new phase or timer: Ready is refused while it is open, the prep's deadline picks at random before the temp pieces and Ready, a bot or a 托管 seat picks with the bounty scorer (botPickCard, the bots' seeded rng; at the prep's start, after the cast, before Ready; the scheduler's fallback picks at random if a step fails). The client reuses ChoiceOverlay (personal: no order, no turns, the prep's countdown) over a locked prep, the Ready reasons, the mock harness; six UI strings in en / ja / ko / zh-TW. An eliminated player's offer goes with it; invariants.js flags an offer outside its owner's own open prep. Not taken from the PR: its §25.24 in the released docs/history/0.2.0.md (the 0.2.2 section is the integrator's), its edits of the Ready toggle beyond the personal-choice case, its tail placement of the i18n keys. Golden: coop2-NORMAL-8-serverrun (ai_0 杜宾 casts 教鞭 from R2 on and now scores the three cards: teamLp 39 -> 34, virtualMs 121400 -> 118400, rng shop 511 -> 483 / bots 18063 -> 18070 / meta 112 -> 103) and coop2-ABYSS-10-boosted (ai_1 杜宾: virtualMs 121650 -> 124650, shop 590 -> 589 / bots 22075 -> 22096 / meta 141 -> 154); both first differ in round 2, every other scenario and family is unchanged; node tools/golden.mjs --twice is identical. Tests: items.test.js (three distinct cards, the tapped one applied once, nothing before the confirm, a refused cast keeps the Art / count / rng / uid, an eliminated player's offer), economy.test.js (Ready refused, the deadline picks before endPrep, solo prep untimed), connection.test.js (two players' offers, reconnect, foreign / duplicate / old ids, an offline player's pick at the deadline), bot.test.js (托管 takeover incl. the failing-step fallback), bot-smarter.test.js (the scorer's pick, deterministic per seed, no rng draw when nothing is pending), followups.test.js (R14 / R15), draft.test.js, unitStats.test.js (protocol), hardening.test.js (invariant), gameLogic / playtest3 / playtest4 (view model, Ready reason, ChoiceView, actions), mock.e2e.test.js and playtest2.real.e2e.test.js (browser, SP_E2E=1).
This commit is contained in:
1 parent
05eb2b807f
commit
56a7b918a9
48 files changed
+901
-159
No files matched your search
@@ -127,7 +127,7 @@ export function armedCard(armed, sp, o) {
|
||||
|
||||
/**
|
||||
* The overlay: keeps the two-tap selection and renders ChoiceView. `onPick(idx)` sends the confirmed card (g.choice).
|
||||
* @param {{ pub:any, sp:any, myId:string, solo:boolean, onPick:(idx:number)=>void, busyIdx?:number|null, total?:number|null }} props
|
||||
* @param {{ pub:any, sp:any, myId:string, solo:boolean, personal?:boolean, onPick:(idx:number)=>void, busyIdx?:number|null, total?:number|null }} props
|
||||
*/
|
||||
export function ChoiceOverlay(props) {
|
||||
const { sp, myId, solo, busyIdx = null, onPick } = props;
|
||||
@@ -154,10 +154,10 @@ export function ChoiceOverlay(props) {
|
||||
|
||||
/**
|
||||
* The overlay's view (pure: no hooks — test/ui renders it as a function).
|
||||
* @param {{ pub:any, sp:any, myId:string, solo:boolean, busyIdx?:number|null, total?:number|null, armed?:number|null,
|
||||
* @param {{ pub:any, sp:any, myId:string, solo:boolean, personal?:boolean, busyIdx?:number|null, total?:number|null, armed?:number|null,
|
||||
* onTap?:(idx:number)=>void, onConfirm?:()=>void, onDisarm?:()=>void }} props
|
||||
*/
|
||||
export function ChoiceView({ pub, sp, myId, solo, busyIdx = null, total = null, armed = null, onTap = () => {}, onConfirm = () => {}, onDisarm = () => {} }) {
|
||||
export function ChoiceView({ pub, sp, myId, solo, personal = false, busyIdx = null, total = null, armed = null, onTap = () => {}, onConfirm = () => {}, onDisarm = () => {} }) {
|
||||
if (!sp) return null;
|
||||
const fam = data.get('choices')?.families?.[sp.family] || null;
|
||||
const rawFam = data.getRaw('choices')?.families?.[sp.family] || null;
|
||||
@@ -166,8 +166,8 @@ export function ChoiceView({ pub, sp, myId, solo, busyIdx = null, total = null,
|
||||
const mine = sp.pickOf.get(myId);
|
||||
const turnName = players.get(sp.turnPid)?.name || t('队友');
|
||||
const special = /_s$/.test(String(sp.family || ''));
|
||||
const order = solo ? [] : sp.order;
|
||||
const timed = !solo && !sp.untimed;
|
||||
const order = personal || solo ? [] : sp.order;
|
||||
const timed = personal ? pub?.deadline > 0 : !solo && !sp.untimed;
|
||||
const armedCardRec = armed != null ? sp.cards.find((c) => c && c.idx === armed) : null;
|
||||
const armedName = armedCardRec ? resolveSpCard(armedCardRec, sp.family).name : null;
|
||||
// a press anywhere but a card or the confirm button drops the selection
|
||||
@@ -177,17 +177,18 @@ export function ChoiceView({ pub, sp, myId, solo, busyIdx = null, total = null,
|
||||
if (t && typeof t.closest === 'function' && t.closest('.spcard, .spov__confirm')) return;
|
||||
onDisarm();
|
||||
};
|
||||
return html`<div class=${cx('spov', armed != null && 'has-armed')} role="dialog" aria-label=${t('机变阶段')} onPointerDown=${onDown}>
|
||||
return html`<div class=${cx('spov', armed != null && 'has-armed')} role="dialog" aria-label=${personal ? t('教鞭选择') : t('机变阶段')} onPointerDown=${onDown}>
|
||||
<div class="spov__veil" aria-hidden="true"></div>
|
||||
<div class="spov__inner">
|
||||
<header class="spov__head">
|
||||
<div class="spov__titles">
|
||||
<${MicroLabel} tone="mint">${t('CONTINGENCY // 机变阶段')}</${MicroLabel}>
|
||||
<${MicroLabel} tone="mint">${personal ? t('教鞭选择') : t('CONTINGENCY // 机变阶段')}</${MicroLabel}>
|
||||
<h2 class=${cx('spov__title', special && 'is-special')}>${sentText(sp.name, rawFam?.name, fam?.name) || fam?.name || t('机变')}<span class="spov__bar">|</span><span class="spov__desc"><${RichText} text=${sentText(sp.desc, rawFam?.desc, fam?.desc) || fam?.desc || t('选择一项')} /></span></h2>
|
||||
<p class="spov__sub">${timed ? t('倒计时结束后仍未选定将自动分配') : t('选择一项(无时间限制)')}${mine == null && myTurn ? t(' · 点击卡牌选中,再次点击确认') : ''}</p>
|
||||
<p class="spov__sub">${timed ? (personal ? t('休整期结束时未选择将自动选定') : t('倒计时结束后仍未选定将自动分配')) : t('选择一项(无时间限制)')}${mine == null && myTurn ? t(' · 点击卡牌选中,再次点击确认') : ''}</p>
|
||||
</div>
|
||||
<div class="spov__turn">
|
||||
${mine != null ? html`<span class="spov__turntxt is-done"><${Icon} name="check" />${t('已完成选择')}</span>`
|
||||
${personal ? html`<span class="spov__turntxt is-mine">${t('从候选中选择一项战术特训')}</span>`
|
||||
: mine != null ? html`<span class="spov__turntxt is-done"><${Icon} name="check" />${t('已完成选择')}</span>`
|
||||
: myTurn ? html`<span class="spov__turntxt is-mine">${t('当前轮到你决策')}</span>`
|
||||
: html`<span class="spov__turntxt">${t('{turnName} 正在决策…', { turnName })}<${Icon} name="hourglass" /></span>`}
|
||||
${armed != null ? html`<${Button} variant="primary" size="lg" icon="check" class="spov__confirm" data-testid="sp-confirm"
|
||||
|
||||
Reference in new issue
Block a user