Voluntary discards, and no card ever casts on the first click
The rulebook allows discarding cards purely to churn for new ones, but the client had no way to start it: a "Discard cards..." action now switches the hand into marking mode (with a "never mind" exit), and confirming sends the discard. Alongside it, the hair-trigger casts are gone: selecting a card never plays it anymore — untargeted spells and stone displays get a Cast button in the hint bar, and a bare number card gets an explicit "Play for +N movement" button. Every play is now a two-step, which is the correct cost in a game with no undo. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
d7afe3a5ca
commit
42ea9b8f2c
+37
-17
@@ -38,6 +38,8 @@
|
|||||||
let rotateCW = $state(true);
|
let rotateCW = $state(true);
|
||||||
/** Cards marked for discard. */
|
/** Cards marked for discard. */
|
||||||
let discardSelection = $state<Set<string>>(new Set());
|
let discardSelection = $state<Set<string>>(new Set());
|
||||||
|
/** Voluntary discard mode: hand clicks mark cards instead of playing them. */
|
||||||
|
let discardMode = $state(false);
|
||||||
|
|
||||||
const view = $derived(net.view);
|
const view = $derived(net.view);
|
||||||
const isYourTurn = $derived(view !== null && view.activePlayerId === view.you && !view.stack);
|
const isYourTurn = $derived(view !== null && view.activePlayerId === view.you && !view.stack);
|
||||||
@@ -61,9 +63,7 @@
|
|||||||
const TWO_CELL_CARDS = new Set(["trader", "dimensional-warp", "redirection"]);
|
const TWO_CELL_CARDS = new Set(["trader", "dimensional-warp", "redirection"]);
|
||||||
const CREATURE_TARGET_CARDS = new Set(["mega-monster"]);
|
const CREATURE_TARGET_CARDS = new Set(["mega-monster"]);
|
||||||
const MODIFIER_CARDS = new Set(["amplify", "add", "extend", "around-the-corner"]);
|
const MODIFIER_CARDS = new Set(["amplify", "add", "extend", "around-the-corner"]);
|
||||||
const SELF_CARDS = new Set([
|
|
||||||
"invisible", "shrink", "mist-body", "strength", "empathy", "big-man", "fear", "adrenaline",
|
|
||||||
]);
|
|
||||||
const NAMED_CARDS = new Set(["card-erasure", "drop-object", "deja-vu", "thief", "swap-meet", "remove-curse", "swarthmores-enchantment", "illusionary-attack"]);
|
const NAMED_CARDS = new Set(["card-erasure", "drop-object", "deja-vu", "thief", "swap-meet", "remove-curse", "swarthmores-enchantment", "illusionary-attack"]);
|
||||||
const edgeSelectMode = $derived(selectedCard != null && EDGE_CARDS.has(selectedCard.cardId));
|
const edgeSelectMode = $derived(selectedCard != null && EDGE_CARDS.has(selectedCard.cardId));
|
||||||
const cellSelectMode = $derived(
|
const cellSelectMode = $derived(
|
||||||
@@ -111,7 +111,7 @@
|
|||||||
net.command({ type: "counteract", instanceId: card.instanceId });
|
net.command({ type: "counteract", instanceId: card.instanceId });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (youMustDiscard || discardSelection.size > 0) {
|
if (youMustDiscard || discardMode || discardSelection.size > 0) {
|
||||||
const next = new Set(discardSelection);
|
const next = new Set(discardSelection);
|
||||||
next.has(card.instanceId) ? next.delete(card.instanceId) : next.add(card.instanceId);
|
next.has(card.instanceId) ? next.delete(card.instanceId) : next.add(card.instanceId);
|
||||||
discardSelection = next;
|
discardSelection = next;
|
||||||
@@ -135,24 +135,32 @@
|
|||||||
}
|
}
|
||||||
selectedCard = card;
|
selectedCard = card;
|
||||||
attachedNumber = null;
|
attachedNumber = null;
|
||||||
if (isNumberCard(card.cardId)) {
|
// Nothing casts on first click: every play is confirmed from the hint
|
||||||
// A bare number card: play it for movement.
|
// bar or by choosing a target. Misclicks cost nothing.
|
||||||
net.command({ type: "playNumberForMovement", instanceId: card.instanceId });
|
|
||||||
clearSelection();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
// Instant untargeted spells (and stone displays) cast immediately;
|
|
||||||
// duration self-spells wait so a number card can be attached.
|
/** Untargeted spells confirmed from the hint bar (with optional number). */
|
||||||
const INSTANT = new Set([
|
const CONFIRM_CAST = new Set([
|
||||||
"speed", "pass-through-wall", "reuse-spell", "ugly", "alter-ego", "lifesaver", "mad-dash",
|
"speed", "pass-through-wall", "reuse-spell", "ugly", "alter-ego", "lifesaver", "mad-dash",
|
||||||
"gift-from-above", "chaos", "interrupt", "opportunity-fire",
|
"gift-from-above", "chaos", "interrupt", "opportunity-fire",
|
||||||
"bloodstone", "brainstone", "powerstone", "shadowstone",
|
"bloodstone", "brainstone", "powerstone", "shadowstone",
|
||||||
"shieldstone", "soulstone", "speedstone", "visionstone",
|
"shieldstone", "soulstone", "speedstone", "visionstone",
|
||||||
|
"invisible", "shrink", "mist-body", "strength", "empathy", "big-man", "fear", "adrenaline",
|
||||||
]);
|
]);
|
||||||
if (INSTANT.has(card.cardId)) {
|
|
||||||
net.command({ type: "cast", instanceId: card.instanceId });
|
function playNumberForMovement() {
|
||||||
|
if (!selectedCard) return;
|
||||||
|
net.command({ type: "playNumberForMovement", instanceId: selectedCard.instanceId });
|
||||||
clearSelection();
|
clearSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function startDiscardMode() {
|
||||||
|
clearSelection();
|
||||||
|
discardMode = true;
|
||||||
|
}
|
||||||
|
function cancelDiscardMode() {
|
||||||
|
discardMode = false;
|
||||||
|
discardSelection = new Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
function castSelfWithNumber() {
|
function castSelfWithNumber() {
|
||||||
@@ -351,6 +359,7 @@
|
|||||||
function doDiscard() {
|
function doDiscard() {
|
||||||
net.command({ type: "discard", instanceIds: [...discardSelection] });
|
net.command({ type: "discard", instanceIds: [...discardSelection] });
|
||||||
discardSelection = new Set();
|
discardSelection = new Set();
|
||||||
|
discardMode = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function endTurn() {
|
function endTurn() {
|
||||||
@@ -519,14 +528,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-edge">
|
<div class="table-edge">
|
||||||
{#if selectedDef || youMustDiscard || discardSelection.size > 0}
|
{#if selectedDef || youMustDiscard || discardMode || discardSelection.size > 0}
|
||||||
<div class="hint-strip">
|
<div class="hint-strip">
|
||||||
{#if youMustDiscard}
|
{#if youMustDiscard}
|
||||||
<span class="hint-alert">Hand over the limit — mark cards and discard.</span>
|
<span class="hint-alert">Hand over the limit — mark cards and discard.</span>
|
||||||
|
{:else if discardMode && discardSelection.size === 0}
|
||||||
|
<span>Mark the cards to throw away, then confirm.</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if discardSelection.size > 0}
|
{#if discardSelection.size > 0}
|
||||||
<button class="stamp tiny" onclick={doDiscard}>Discard {discardSelection.size} marked</button>
|
<button class="stamp tiny" onclick={doDiscard}>Discard {discardSelection.size} marked</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if discardMode || (discardSelection.size > 0 && !youMustDiscard)}
|
||||||
|
<button class="hint-cancel" onclick={cancelDiscardMode}>never mind</button>
|
||||||
|
{/if}
|
||||||
{#if selectedDef}
|
{#if selectedDef}
|
||||||
<strong class="hint-name">{selectedDef.name}</strong>
|
<strong class="hint-name">{selectedDef.name}</strong>
|
||||||
{#if edgeSelectMode}<span>— click a wall line</span>{/if}
|
{#if edgeSelectMode}<span>— click a wall line</span>{/if}
|
||||||
@@ -571,9 +585,14 @@
|
|||||||
<label class="inline">life to trade <input class="num-input" type="number" min="1" max="10" bind:value={runPoints} /></label>
|
<label class="inline">life to trade <input class="num-input" type="number" min="1" max="10" bind:value={runPoints} /></label>
|
||||||
<button class="stamp tiny" onclick={castPowerRun}>Run!</button>
|
<button class="stamp tiny" onclick={castPowerRun}>Run!</button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if selectedCard && SELF_CARDS.has(selectedCard.cardId)}
|
{#if selectedCard && CONFIRM_CAST.has(selectedCard.cardId)}
|
||||||
<button class="stamp tiny" onclick={castSelfWithNumber}>
|
<button class="stamp tiny" onclick={castSelfWithNumber}>
|
||||||
Cast{attachedNumber ? ` with the ${numberTotal}` : " (duration 1)"}
|
Cast{attachedNumber ? ` with the ${numberTotal}` : ""}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{#if selectedCard && isNumberCard(selectedCard.cardId)}
|
||||||
|
<button class="stamp tiny" onclick={playNumberForMovement}>
|
||||||
|
Play for +{cardDef(selectedCard.cardId).value} movement
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
<button class="hint-cancel" onclick={clearSelection}>cancel</button>
|
<button class="hint-cancel" onclick={clearSelection}>cancel</button>
|
||||||
@@ -589,6 +608,7 @@
|
|||||||
<button class="stamp" disabled={!treasureHere || carryingTreasure || view.turn.actionsEnded}
|
<button class="stamp" disabled={!treasureHere || carryingTreasure || view.turn.actionsEnded}
|
||||||
onclick={pickUp}>Pick up treasure</button>
|
onclick={pickUp}>Pick up treasure</button>
|
||||||
<button class="stamp" disabled={!carryingTreasure} onclick={drop}>Drop treasure</button>
|
<button class="stamp" disabled={!carryingTreasure} onclick={drop}>Drop treasure</button>
|
||||||
|
<button class="stamp" onclick={startDiscardMode}>Discard cards…</button>
|
||||||
<label class="inline draw-pick">
|
<label class="inline draw-pick">
|
||||||
draw
|
draw
|
||||||
<select bind:value={drawCount}>
|
<select bind:value={drawCount}>
|
||||||
|
|||||||
Reference in New Issue
Block a user