"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
@@ -57,6 +57,23 @@
|
|||||||
/** A mind YOU read: the stolen glimpse arrives once, in an event — hold
|
/** A mind YOU read: the stolen glimpse arrives once, in an event — hold
|
||||||
* it up until dismissed, or it is lost to the scrollback. */
|
* it up until dismissed, or it is lost to the scrollback. */
|
||||||
let mindRead = $state<{ player: string; cards: CardInstance[] } | null>(null);
|
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]) {
|
function playFx(events: Parameters<typeof scheduleFx>[0]) {
|
||||||
if (!view) return;
|
if (!view) return;
|
||||||
for (const e of events) {
|
for (const e of events) {
|
||||||
@@ -104,7 +121,7 @@
|
|||||||
/** Leafing through the face-up discard pile. */
|
/** Leafing through the face-up discard pile. */
|
||||||
let showDiscards = $state(false);
|
let showDiscards = $state(false);
|
||||||
let chatDraft = $state("");
|
let chatDraft = $state("");
|
||||||
let botTier = $state("adept");
|
let botTier = $state<string>(prefs.botTier);
|
||||||
/** Card whose official FAQ rulings are open. */
|
/** Card whose official FAQ rulings are open. */
|
||||||
let faqCardId = $state<string | null>(null);
|
let faqCardId = $state<string | null>(null);
|
||||||
/** A discard-pile card enlarged above the pile. */
|
/** A discard-pile card enlarged above the pile. */
|
||||||
@@ -669,7 +686,7 @@
|
|||||||
for (const side of SIDES) {
|
for (const side of SIDES) {
|
||||||
const t = stepTarget(view.board, me.position, side);
|
const t = stepTarget(view.board, me.position, side);
|
||||||
if (t.kind !== "blocked" && cellKey(t.to) === cellKey(cell)) {
|
if (t.kind !== "blocked" && cellKey(t.to) === cellKey(cell)) {
|
||||||
dispatch({ type: "move", direction: side });
|
tryMove(side);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -685,7 +702,7 @@
|
|||||||
illusionPrompt = illusionEdgeParts(k);
|
illusionPrompt = illusionEdgeParts(k);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dispatch({ type: "move", direction: side });
|
tryMove(side);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -699,7 +716,7 @@
|
|||||||
const hazard = view.squareContents[cellKey(mid)]?.kind;
|
const hazard = view.squareContents[cellKey(mid)]?.kind;
|
||||||
if (cellKey(far) === cellKey(cell) &&
|
if (cellKey(far) === cellKey(cell) &&
|
||||||
(hazard === "pit" || hazard === "tacks" || hazard === "ooze")) {
|
(hazard === "pit" || hazard === "tacks" || hazard === "ooze")) {
|
||||||
dispatch({ type: "move", direction: side, over: true });
|
tryMove(side, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -769,7 +786,7 @@
|
|||||||
if (creature && creature.position.x === cell.x && creature.position.y === cell.y) {
|
if (creature && creature.position.x === cell.x && creature.position.y === cell.y) {
|
||||||
dispatch({ type: "moveCreature", creatureId: creature.id, direction: side });
|
dispatch({ type: "moveCreature", creatureId: creature.id, direction: side });
|
||||||
} else if (me.position.x === cell.x && me.position.y === cell.y) {
|
} else if (me.position.x === cell.x && me.position.y === cell.y) {
|
||||||
dispatch({ type: "move", direction: side });
|
tryMove(side);
|
||||||
} else {
|
} else {
|
||||||
clickCell(cell);
|
clickCell(cell);
|
||||||
}
|
}
|
||||||
@@ -885,6 +902,25 @@
|
|||||||
|
|
||||||
function endTurn() {
|
function endTurn() {
|
||||||
clearSelection();
|
clearSelection();
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
withCaution(reasons, () => {
|
||||||
// Preference: a lone grabbable treasure underfoot is picked up on the
|
// Preference: a lone grabbable treasure underfoot is picked up on the
|
||||||
// way out (pickup ends actions; ending the turn is still legal after).
|
// way out (pickup ends actions; ending the turn is still legal after).
|
||||||
if (prefs.autoGrab && view && me && !me.carriedTreasureId &&
|
if (prefs.autoGrab && view && me && !me.carriedTreasureId &&
|
||||||
@@ -893,6 +929,7 @@
|
|||||||
dispatch({ type: "pickUpTreasure" });
|
dispatch({ type: "pickUpTreasure" });
|
||||||
}
|
}
|
||||||
dispatch({ type: "endTurn", draw: drawCount });
|
dispatch({ type: "endTurn", draw: drawCount });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// SWAP MEET: pick one of your carried items, then one of theirs you can
|
// 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)} />
|
onchange={(e) => setPref("flourishes", e.currentTarget.checked)} />
|
||||||
<span>Spell flourishes (the animated effects)</span>
|
<span>Spell flourishes (the animated effects)</span>
|
||||||
</label>
|
</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">
|
<label class="pref-row">
|
||||||
<span>Wizard name</span>
|
<span>Wizard name</span>
|
||||||
<input class="setup-input pref-name" maxlength="20" value={prefs.wizardName}
|
<input class="setup-input pref-name" maxlength="20" value={prefs.wizardName}
|
||||||
@@ -1338,6 +1387,23 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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}
|
{#if mindRead}
|
||||||
<div class="scrim attack-scrim" role="alertdialog" aria-label="a mind read">
|
<div class="scrim attack-scrim" role="alertdialog" aria-label="a mind read">
|
||||||
<div class="attack-notice mind-read">
|
<div class="attack-notice mind-read">
|
||||||
@@ -1679,7 +1745,8 @@
|
|||||||
{#if net.players.length < 6}
|
{#if net.players.length < 6}
|
||||||
<span class="bot-row">
|
<span class="bot-row">
|
||||||
<span class="bot-label">⚙ seat a</span>
|
<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="apprentice">apprentice</option>
|
||||||
<option value="adept">adept</option>
|
<option value="adept">adept</option>
|
||||||
<option value="archmage">archmage</option>
|
<option value="archmage">archmage</option>
|
||||||
@@ -2289,6 +2356,8 @@
|
|||||||
.mast-room b { color: #e9e1cb; letter-spacing: 0.12em; }
|
.mast-room b { color: #e9e1cb; letter-spacing: 0.12em; }
|
||||||
.mast-audience { font-size: 0.85rem; color: #a49c86; white-space: nowrap; }
|
.mast-audience { font-size: 0.85rem; color: #a49c86; white-space: nowrap; }
|
||||||
.mind-read { max-width: min(92vw, 46rem); }
|
.mind-read { max-width: min(92vw, 46rem); }
|
||||||
|
.caution-reason { padding: 0.15rem 0; }
|
||||||
|
.caution-reason + .caution-reason { border-top: 1px dotted #4a4536; }
|
||||||
.mind-cards {
|
.mind-cards {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@@ -14,12 +14,19 @@ export interface Prefs {
|
|||||||
wizardName: string;
|
wizardName: string;
|
||||||
/** Preferred wizard color (0-5), claimed in lobbies when free. */
|
/** Preferred wizard color (0-5), claimed in lobbies when free. */
|
||||||
color: number | null;
|
color: number | null;
|
||||||
|
/** "Are you sure?" prompts before easy-to-regret plays. */
|
||||||
|
cautions: boolean;
|
||||||
|
/** Default skill for automatons seated from the lobby. */
|
||||||
|
botTier: "apprentice" | "adept" | "archmage";
|
||||||
}
|
}
|
||||||
|
|
||||||
const KEY = "wizwar-prefs";
|
const KEY = "wizwar-prefs";
|
||||||
|
|
||||||
function load(): Prefs {
|
function load(): Prefs {
|
||||||
const fallback: Prefs = { art: "photo", autoGrab: false, flourishes: true, wizardName: "", color: null };
|
const fallback: Prefs = {
|
||||||
|
art: "photo", autoGrab: false, flourishes: true, wizardName: "", color: null,
|
||||||
|
cautions: true, botTier: "adept",
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(KEY);
|
const raw = localStorage.getItem(KEY);
|
||||||
if (!raw) return fallback;
|
if (!raw) return fallback;
|
||||||
@@ -30,6 +37,8 @@ function load(): Prefs {
|
|||||||
flourishes: p.flourishes !== false,
|
flourishes: p.flourishes !== false,
|
||||||
wizardName: typeof p.wizardName === "string" ? p.wizardName.slice(0, 20) : "",
|
wizardName: typeof p.wizardName === "string" ? p.wizardName.slice(0, 20) : "",
|
||||||
color: typeof p.color === "number" && p.color >= 0 && p.color <= 5 ? p.color : null,
|
color: typeof p.color === "number" && p.color >= 0 && p.color <= 5 ? p.color : null,
|
||||||
|
cautions: p.cautions !== false,
|
||||||
|
botTier: p.botTier === "apprentice" || p.botTier === "archmage" ? p.botTier : "adept",
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return fallback;
|
return fallback;
|
||||||
|
|||||||
Reference in New Issue
Block a user