diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 36dfcc2..93e5f30 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -1756,6 +1756,20 @@ view && me ? view.treasures.filter((t) => t.owner !== me!.id && t.position && t.position.x === me!.home.x && t.position.y === me!.home.y).length : 0, ); + /** Chests you carried home THIS turn: the drop is the game's biggest + * moment, and the turn bar's quiet tally alone let it pass unremarked. */ + let scoredThisTurn = $state(0); + let scoreTurnKey = ""; + let homeAtTurnStart = 0; + $effect(() => { + const key = view && view.activePlayerId === view.you ? `${view.turn.round}:${view.activePlayerId}` : ""; + if (key !== scoreTurnKey) { scoreTurnKey = key; homeAtTurnStart = treasuresHome; } + scoredThisTurn = key ? Math.max(0, treasuresHome - homeAtTurnStart) : 0; + }); + /** Steps spent, actions not: the turn waits on a spell or End turn, + * and a still board with no word reads as a game that froze. */ + const movesSpent = $derived(!!view && view.activePlayerId === view.you && !view.turn.actionsEnded && + view.turn.movementAllowance - view.turn.movementUsed <= 0); const paBurnable = $derived(me ? Array.from({ length: Math.max(0, me.life - 1) }, (_, i) => i + 1) : []); /** Two treasures can share a square: one dispatches, more ask whose. */ const treasuresHere = $derived( @@ -3291,8 +3305,16 @@ {/if} {:else} {view.turn.movementAllowance - view.turn.movementUsed} moves left{view.turn.attackUsed ? " ยท attack spent" : ""} ยท treasures home {treasuresHome} of 2 + {#if movesSpent} +
Out of steps โ€” cast a spell, or End turn.
+ {/if} {/if} + {#if scoredThisTurn > 0 && treasuresHome < 2} +
+ ๐Ÿ† Scored โ€” {treasuresHome} of 2. One more stolen treasure carried home wins the game. +
+ {/if} {#if !prefs.coached && !view.turn.actionsEnded} @@ -3658,7 +3680,7 @@ {:else if net.confirmed} โœ“ {net.confirmed} confirmed {/if} - + {/if} @@ -4401,6 +4423,14 @@ .stamp:disabled { opacity: 0.45; cursor: default; } .stamp.primary { background: #43331f; color: #e9e1cb; } .stamp.primary:hover:not(:disabled) { background: #5a4429; } + .stamp.beckon:not(:disabled) { animation: beckon 1.6s ease-in-out infinite; } + @keyframes beckon { + 0%, 100% { box-shadow: 2px 2px 0 rgba(0,0,0,0.35), 0 0 0 0 rgba(201, 167, 42, 0); } + 50% { box-shadow: 2px 2px 0 rgba(0,0,0,0.35), 0 0 0 4px rgba(201, 167, 42, 0.55); } + } + @media (prefers-reduced-motion: reduce) { + .stamp.beckon:not(:disabled) { animation: none; box-shadow: 0 0 0 3px rgba(201, 167, 42, 0.7); } + } .stamp.big { font-size: 0.95rem; padding: 0.6rem 1.3rem; } .stamp.tiny { font-size: 0.62rem; padding: 0.22rem 0.5rem; box-shadow: 1px 1px 0 rgba(0,0,0,0.3); } .prefs-slip { text-align: left; } @@ -4598,6 +4628,7 @@ .slip.urgent { border-left: 4px solid #b3372b; transform: rotate(0.4deg); } .slip.ambush-note { border-left: 4px solid #43331f; font-size: 0.85rem; } .slip.catchup { border-left: 4px solid #5b3f9e; } + .slip.scored { border-left: 4px solid #c9a72a; background: #f3e7b8; } .catchup-head { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; } .catchup-facts { list-style: none; margin: 0.45rem 0 0; padding: 0; font-size: 0.9rem; } .catchup-facts li { margin: 0.15rem 0; }