Attacks announce themselves; the response moves beside the hand
Being attacked was announced only by a slip in the paper rail, easy to miss mid-turn — the playtest complaint was real. Now an attack on you (or a counter to your spell) opens a modal: the attacker's name in red Oswald caps, the actual card face enlarged, the number backing it if any, a bare 👊 for a punch, and a single "To arms!" stamp. Dismissing it leaves the actionable prompt in the hint strip beside your hand — where the counteraction cards you would tap actually live — with the "let it resolve" escape. The rail keeps a one-line reminder. Each new decision point in a counter-chain re-announces. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
f9eff3341b
commit
a0d862ca10
+115
-6
@@ -15,6 +15,8 @@
|
|||||||
let joinCode = $state("");
|
let joinCode = $state("");
|
||||||
let claimPhrase = $state("");
|
let claimPhrase = $state("");
|
||||||
let showHelp = $state(false);
|
let showHelp = $state(false);
|
||||||
|
/** Which pending attack the player has already acknowledged (modal dismissed). */
|
||||||
|
let attackNoticeSeen = $state<string | null>(null);
|
||||||
let helpTab = $state<"play" | "rules" | "cards" | "about">("play");
|
let helpTab = $state<"play" | "rules" | "cards" | "about">("play");
|
||||||
let hotseatCount = $state(2);
|
let hotseatCount = $state(2);
|
||||||
let setupName = $state("");
|
let setupName = $state("");
|
||||||
@@ -70,6 +72,11 @@
|
|||||||
const view = $derived(local.active ? local.view : net.view);
|
const view = $derived(local.active ? local.view : net.view);
|
||||||
const isYourTurn = $derived(view !== null && view.activePlayerId === view.you && !view.stack);
|
const isYourTurn = $derived(view !== null && view.activePlayerId === view.you && !view.stack);
|
||||||
const youMustRespond = $derived(view?.stack != null && view.stack.waitingOn === view.you);
|
const youMustRespond = $derived(view?.stack != null && view.stack.waitingOn === view.you);
|
||||||
|
const attackNoticeKey = $derived(
|
||||||
|
youMustRespond && view?.stack
|
||||||
|
? `${view.stack.attackerId}:${view.stack.attackCard?.instanceId ?? "punch"}:${view.stack.counters.length}`
|
||||||
|
: null,
|
||||||
|
);
|
||||||
const youMustDiscard = $derived(view != null && view.pendingDiscard === view.you);
|
const youMustDiscard = $derived(view != null && view.pendingDiscard === view.you);
|
||||||
|
|
||||||
const selectedDef = $derived(selectedCard ? cardDef(selectedCard.cardId) : null);
|
const selectedDef = $derived(selectedCard ? cardDef(selectedCard.cardId) : null);
|
||||||
@@ -557,6 +564,31 @@
|
|||||||
<Help initialTab={helpTab} onclose={() => (showHelp = false)} />
|
<Help initialTab={helpTab} onclose={() => (showHelp = false)} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if view?.stack && attackNoticeKey && attackNoticeSeen !== attackNoticeKey}
|
||||||
|
<div class="scrim attack-scrim" role="alertdialog" aria-label="you are under attack">
|
||||||
|
<div class="attack-notice">
|
||||||
|
<div class="attack-headline">
|
||||||
|
{#if view.stack.defenderId === view.you}
|
||||||
|
<strong>{view.stack.attackerId}</strong> attacks you!
|
||||||
|
{:else}
|
||||||
|
<strong>{view.stack.counters[view.stack.counters.length - 1]?.player}</strong> counters your spell!
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if view.stack.defenderId === view.you && view.stack.attackCard}
|
||||||
|
<div class="attack-card"><Card card={view.stack.attackCard} /></div>
|
||||||
|
{#if view.stack.numberValue != null}
|
||||||
|
<div class="attack-power">powered by a {view.stack.numberValue}</div>
|
||||||
|
{/if}
|
||||||
|
{:else if view.stack.defenderId === view.you}
|
||||||
|
<div class="attack-fist">👊 a bare-knuckled punch</div>
|
||||||
|
{:else if view.stack.counters.length > 0}
|
||||||
|
<div class="attack-card"><Card card={view.stack.counters[view.stack.counters.length - 1]!.card} /></div>
|
||||||
|
{/if}
|
||||||
|
<button class="stamp big" onclick={() => (attackNoticeSeen = attackNoticeKey)}>To arms!</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if net.catchUp && net.catchUp.length > 0}
|
{#if net.catchUp && net.catchUp.length > 0}
|
||||||
<Replay steps={net.catchUp} onclose={() => net.closeCatchUp()} />
|
<Replay steps={net.catchUp} onclose={() => net.closeCatchUp()} />
|
||||||
{/if}
|
{/if}
|
||||||
@@ -790,13 +822,10 @@
|
|||||||
{:else if youMustRespond}
|
{:else if youMustRespond}
|
||||||
<div class="slip urgent">
|
<div class="slip urgent">
|
||||||
{#if view.stack?.defenderId === view.you}
|
{#if view.stack?.defenderId === view.you}
|
||||||
<strong>{view.stack.attackerId}</strong> attacks you
|
Under attack — respond by your hand.
|
||||||
{#if view.stack.attackCard}with <strong>{cardDef(view.stack.attackCard.cardId).name}</strong>{:else}with a punch{/if}!
|
|
||||||
Play a counteraction, or
|
|
||||||
{:else}
|
{:else}
|
||||||
Respond to the counteraction, or
|
Your spell is countered — respond by your hand.
|
||||||
{/if}
|
{/if}
|
||||||
<button class="stamp tiny" onclick={pass}>let it resolve</button>
|
|
||||||
</div>
|
</div>
|
||||||
{:else if view.stack}
|
{:else if view.stack}
|
||||||
<div class="slip">Waiting on {view.stack.waitingOn}…</div>
|
<div class="slip">Waiting on {view.stack.waitingOn}…</div>
|
||||||
@@ -864,8 +893,20 @@
|
|||||||
<button class="hint-cancel" onclick={clearSelection}>cancel</button>
|
<button class="hint-cancel" onclick={clearSelection}>cancel</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if selectedDef || youMustDiscard || discardMode || discardSelection.size > 0}
|
{#if youMustRespond || selectedDef || youMustDiscard || discardMode || discardSelection.size > 0}
|
||||||
<div class="hint-strip">
|
<div class="hint-strip">
|
||||||
|
{#if youMustRespond && view.stack}
|
||||||
|
<span class="hint-alert">
|
||||||
|
{#if view.stack.defenderId === view.you}
|
||||||
|
{view.stack.attackerId} attacks with
|
||||||
|
{view.stack.attackCard ? cardDef(view.stack.attackCard.cardId).name : "a punch"} —
|
||||||
|
tap a counteraction card, or
|
||||||
|
{:else}
|
||||||
|
{view.stack.waitingOn === view.you ? "They counter your spell — counter back, or" : ""}
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
<button class="stamp tiny" onclick={pass}>let it resolve</button>
|
||||||
|
{/if}
|
||||||
{#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}
|
{:else if discardMode && discardSelection.size === 0}
|
||||||
@@ -1470,6 +1511,40 @@
|
|||||||
.actions { display: flex; align-items: center; gap: 0.55rem; flex-wrap: wrap; min-height: 2rem; }
|
.actions { display: flex; align-items: center; gap: 0.55rem; flex-wrap: wrap; min-height: 2rem; }
|
||||||
.draw-pick { color: #a49c86; font-size: 0.9rem; }
|
.draw-pick { color: #a49c86; font-size: 0.9rem; }
|
||||||
|
|
||||||
|
.attack-scrim {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(10, 12, 16, 0.72);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
z-index: 45;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.attack-notice {
|
||||||
|
background: #efe8d4;
|
||||||
|
border: 2px solid #43331f;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.6);
|
||||||
|
padding: 1.2rem 1.6rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.8rem;
|
||||||
|
max-width: min(22rem, 92vw);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.attack-headline {
|
||||||
|
font-family: "Oswald", sans-serif;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: #b3372b;
|
||||||
|
}
|
||||||
|
.attack-card :global(.card) { transform: scale(1.35); margin: 1.2rem 0; }
|
||||||
|
.attack-card :global(.card:hover) { transform: scale(1.35); }
|
||||||
|
.attack-power { font-family: "Courier Prime", monospace; color: #6b5a41; font-size: 0.9rem; }
|
||||||
|
.attack-fist { font-size: 1.1rem; color: #43331f; }
|
||||||
|
|
||||||
.inspector {
|
.inspector {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 0.9rem;
|
right: 0.9rem;
|
||||||
@@ -1503,6 +1578,40 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
|
.attack-scrim {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(10, 12, 16, 0.72);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
z-index: 45;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.attack-notice {
|
||||||
|
background: #efe8d4;
|
||||||
|
border: 2px solid #43331f;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.6);
|
||||||
|
padding: 1.2rem 1.6rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.8rem;
|
||||||
|
max-width: min(22rem, 92vw);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.attack-headline {
|
||||||
|
font-family: "Oswald", sans-serif;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: #b3372b;
|
||||||
|
}
|
||||||
|
.attack-card :global(.card) { transform: scale(1.35); margin: 1.2rem 0; }
|
||||||
|
.attack-card :global(.card:hover) { transform: scale(1.35); }
|
||||||
|
.attack-power { font-family: "Courier Prime", monospace; color: #6b5a41; font-size: 0.9rem; }
|
||||||
|
.attack-fist { font-size: 1.1rem; color: #43331f; }
|
||||||
|
|
||||||
.inspector { right: 0.5rem; bottom: 0.5rem; }
|
.inspector { right: 0.5rem; bottom: 0.5rem; }
|
||||||
.inspector-card { transform: scale(1.3); }
|
.inspector-card { transform: scale(1.3); }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user