"Hold on —": cautions before easy-to-regret plays, and a house bot tier
An optional confirmation (on by default, one checkbox to silence) catches the classic table groans before they land: ending the turn with movement unspent, ending it with a hand too full to take the draw, and walking off — or ending on — your own home with a deliverable treasure still in your arms. Preferences also learn a default automaton skill, remembered from either the prefs panel or the lobby's tier picker. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
bde68aa85c
commit
6e1ccb0195
+82
-13
@@ -57,6 +57,23 @@
|
||||
/** A mind YOU read: the stolen glimpse arrives once, in an event — hold
|
||||
* it up until dismissed, or it is lost to the scrollback. */
|
||||
let mindRead = $state<{ player: string; cards: CardInstance[] } | null>(null);
|
||||
/** An easy-to-regret play awaiting its "are you sure?" (see prefs.cautions). */
|
||||
let caution = $state<{ reasons: string[]; go: () => void } | null>(null);
|
||||
function withCaution(reasons: string[], go: () => void) {
|
||||
if (!prefs.cautions || reasons.length === 0) go();
|
||||
else caution = { reasons, go };
|
||||
}
|
||||
/** A move with the delivery check: stepping off your home still laden. */
|
||||
function tryMove(direction: Side, over?: boolean) {
|
||||
const reasons: string[] = [];
|
||||
if (view && me && me.carriedTreasureId && cellKey(me.position) === cellKey(me.home)) {
|
||||
const t = view.treasures.find((t) => t.id === me!.carriedTreasureId);
|
||||
reasons.push(t && t.owner !== view.you
|
||||
? `you stand on your home with ${t.owner}'s treasure in your arms — drop it first and it scores`
|
||||
: "you stand on your home with your own treasure in your arms — drop it first and it is safe");
|
||||
}
|
||||
withCaution(reasons, () => dispatch({ type: "move", direction, ...(over ? { over: true } : {}) }));
|
||||
}
|
||||
function playFx(events: Parameters<typeof scheduleFx>[0]) {
|
||||
if (!view) return;
|
||||
for (const e of events) {
|
||||
@@ -104,7 +121,7 @@
|
||||
/** Leafing through the face-up discard pile. */
|
||||
let showDiscards = $state(false);
|
||||
let chatDraft = $state("");
|
||||
let botTier = $state("adept");
|
||||
let botTier = $state<string>(prefs.botTier);
|
||||
/** Card whose official FAQ rulings are open. */
|
||||
let faqCardId = $state<string | null>(null);
|
||||
/** A discard-pile card enlarged above the pile. */
|
||||
@@ -669,7 +686,7 @@
|
||||
for (const side of SIDES) {
|
||||
const t = stepTarget(view.board, me.position, side);
|
||||
if (t.kind !== "blocked" && cellKey(t.to) === cellKey(cell)) {
|
||||
dispatch({ type: "move", direction: side });
|
||||
tryMove(side);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -685,7 +702,7 @@
|
||||
illusionPrompt = illusionEdgeParts(k);
|
||||
return;
|
||||
}
|
||||
dispatch({ type: "move", direction: side });
|
||||
tryMove(side);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -699,7 +716,7 @@
|
||||
const hazard = view.squareContents[cellKey(mid)]?.kind;
|
||||
if (cellKey(far) === cellKey(cell) &&
|
||||
(hazard === "pit" || hazard === "tacks" || hazard === "ooze")) {
|
||||
dispatch({ type: "move", direction: side, over: true });
|
||||
tryMove(side, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -769,7 +786,7 @@
|
||||
if (creature && creature.position.x === cell.x && creature.position.y === cell.y) {
|
||||
dispatch({ type: "moveCreature", creatureId: creature.id, direction: side });
|
||||
} else if (me.position.x === cell.x && me.position.y === cell.y) {
|
||||
dispatch({ type: "move", direction: side });
|
||||
tryMove(side);
|
||||
} else {
|
||||
clickCell(cell);
|
||||
}
|
||||
@@ -885,14 +902,34 @@
|
||||
|
||||
function endTurn() {
|
||||
clearSelection();
|
||||
// Preference: a lone grabbable treasure underfoot is picked up on the
|
||||
// way out (pickup ends actions; ending the turn is still legal after).
|
||||
if (prefs.autoGrab && view && me && !me.carriedTreasureId &&
|
||||
!view.turn.actionsEnded && treasuresHere.length === 1 &&
|
||||
cellKey(me.position) !== cellKey(me.home)) {
|
||||
dispatch({ type: "pickUpTreasure" });
|
||||
const reasons: string[] = [];
|
||||
if (view && me) {
|
||||
const left = view.turn.movementAllowance - view.turn.movementUsed;
|
||||
if (left > 0) reasons.push(`${left} movement point${left === 1 ? "" : "s"} unspent`);
|
||||
const limit = me.displayed.some((c) => c.cardId === "brainstone") ? 9 : 7;
|
||||
const room = limit - view.yourHand.length;
|
||||
if (drawCount > room) {
|
||||
reasons.push(room <= 0
|
||||
? "your hand is full — the draw will bring nothing"
|
||||
: `your hand only has room for ${room} of the ${drawCount}-card draw`);
|
||||
}
|
||||
if (me.carriedTreasureId && cellKey(me.position) === cellKey(me.home)) {
|
||||
const t = view.treasures.find((t) => t.id === me!.carriedTreasureId);
|
||||
reasons.push(t && t.owner !== view.you
|
||||
? `${t.owner}'s treasure is still in your arms — drop it here and it scores`
|
||||
: "your own treasure is still in your arms — drop it here and it is safe");
|
||||
}
|
||||
}
|
||||
dispatch({ type: "endTurn", draw: drawCount });
|
||||
withCaution(reasons, () => {
|
||||
// Preference: a lone grabbable treasure underfoot is picked up on the
|
||||
// way out (pickup ends actions; ending the turn is still legal after).
|
||||
if (prefs.autoGrab && view && me && !me.carriedTreasureId &&
|
||||
!view.turn.actionsEnded && treasuresHere.length === 1 &&
|
||||
cellKey(me.position) !== cellKey(me.home)) {
|
||||
dispatch({ type: "pickUpTreasure" });
|
||||
}
|
||||
dispatch({ type: "endTurn", draw: drawCount });
|
||||
});
|
||||
}
|
||||
|
||||
// SWAP MEET: pick one of your carried items, then one of theirs you can
|
||||
@@ -1273,6 +1310,18 @@
|
||||
onchange={(e) => setPref("flourishes", e.currentTarget.checked)} />
|
||||
<span>Spell flourishes (the animated effects)</span>
|
||||
</label>
|
||||
<label class="pref-row">
|
||||
<input type="checkbox" checked={prefs.cautions}
|
||||
onchange={(e) => setPref("cautions", e.currentTarget.checked)} />
|
||||
<span>Ask "are you sure?" before easy-to-regret plays (unspent movement, capped draws, undropped deliveries)</span>
|
||||
</label>
|
||||
<div class="pref-row">
|
||||
<span>Automatons default to</span>
|
||||
{#each ["apprentice", "adept", "archmage"] as t (t)}
|
||||
<button class="stamp tiny" class:lit={prefs.botTier === t}
|
||||
onclick={() => { setPref("botTier", t as typeof prefs.botTier); botTier = t; }}>{t}</button>
|
||||
{/each}
|
||||
</div>
|
||||
<label class="pref-row">
|
||||
<span>Wizard name</span>
|
||||
<input class="setup-input pref-name" maxlength="20" value={prefs.wizardName}
|
||||
@@ -1338,6 +1387,23 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if caution}
|
||||
<div class="scrim attack-scrim" role="alertdialog" aria-label="are you sure?">
|
||||
<div class="attack-notice">
|
||||
<div class="attack-headline">Hold on —</div>
|
||||
<div class="attack-power">
|
||||
{#each caution.reasons as r (r)}
|
||||
<div class="caution-reason">{r}</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="attack-actions">
|
||||
<button class="stamp big" onclick={() => { const go = caution!.go; caution = null; go(); }}>Do it anyway</button>
|
||||
<button class="hint-cancel" onclick={() => (caution = null)}>never mind</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if mindRead}
|
||||
<div class="scrim attack-scrim" role="alertdialog" aria-label="a mind read">
|
||||
<div class="attack-notice mind-read">
|
||||
@@ -1679,7 +1745,8 @@
|
||||
{#if net.players.length < 6}
|
||||
<span class="bot-row">
|
||||
<span class="bot-label">⚙ seat a</span>
|
||||
<select class="tier-pick" bind:value={botTier} aria-label="automaton difficulty">
|
||||
<select class="tier-pick" bind:value={botTier} aria-label="automaton difficulty"
|
||||
onchange={() => setPref("botTier", botTier as typeof prefs.botTier)}>
|
||||
<option value="apprentice">apprentice</option>
|
||||
<option value="adept">adept</option>
|
||||
<option value="archmage">archmage</option>
|
||||
@@ -2289,6 +2356,8 @@
|
||||
.mast-room b { color: #e9e1cb; letter-spacing: 0.12em; }
|
||||
.mast-audience { font-size: 0.85rem; color: #a49c86; white-space: nowrap; }
|
||||
.mind-read { max-width: min(92vw, 46rem); }
|
||||
.caution-reason { padding: 0.15rem 0; }
|
||||
.caution-reason + .caution-reason { border-top: 1px dotted #4a4536; }
|
||||
.mind-cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
Reference in New Issue
Block a user