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 claimPhrase = $state("");
|
||||
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 hotseatCount = $state(2);
|
||||
let setupName = $state("");
|
||||
@@ -70,6 +72,11 @@
|
||||
const view = $derived(local.active ? local.view : net.view);
|
||||
const isYourTurn = $derived(view !== null && view.activePlayerId === view.you && !view.stack);
|
||||
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 selectedDef = $derived(selectedCard ? cardDef(selectedCard.cardId) : null);
|
||||
@@ -557,6 +564,31 @@
|
||||
<Help initialTab={helpTab} onclose={() => (showHelp = false)} />
|
||||
{/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}
|
||||
<Replay steps={net.catchUp} onclose={() => net.closeCatchUp()} />
|
||||
{/if}
|
||||
@@ -790,13 +822,10 @@
|
||||
{:else if youMustRespond}
|
||||
<div class="slip urgent">
|
||||
{#if view.stack?.defenderId === view.you}
|
||||
<strong>{view.stack.attackerId}</strong> attacks you
|
||||
{#if view.stack.attackCard}with <strong>{cardDef(view.stack.attackCard.cardId).name}</strong>{:else}with a punch{/if}!
|
||||
Play a counteraction, or
|
||||
Under attack — respond by your hand.
|
||||
{:else}
|
||||
Respond to the counteraction, or
|
||||
Your spell is countered — respond by your hand.
|
||||
{/if}
|
||||
<button class="stamp tiny" onclick={pass}>let it resolve</button>
|
||||
</div>
|
||||
{:else if view.stack}
|
||||
<div class="slip">Waiting on {view.stack.waitingOn}…</div>
|
||||
@@ -864,8 +893,20 @@
|
||||
<button class="hint-cancel" onclick={clearSelection}>cancel</button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if selectedDef || youMustDiscard || discardMode || discardSelection.size > 0}
|
||||
{#if youMustRespond || selectedDef || youMustDiscard || discardMode || discardSelection.size > 0}
|
||||
<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}
|
||||
<span class="hint-alert">Hand over the limit — mark cards and discard.</span>
|
||||
{: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; }
|
||||
.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 {
|
||||
position: fixed;
|
||||
right: 0.9rem;
|
||||
@@ -1503,6 +1578,40 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
@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-card { transform: scale(1.3); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user