From d6d34887e33484a53e53859ec124d5ab2decc70b Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Sun, 16 Aug 2026 15:45:59 -0400 Subject: [PATCH] The whole punch list: rev 8, general FAQ, and four promoted UX items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rules rev 8 closes the last two fidelity threads. A SPEED bonus turn burns a turn of duration spells on the hastened wizard — recipient- counted, per the FAQ's most obscure ruling — with self-cast durations already burning through the normal turn-start sweep (expiry extracted into expireEffect so both paths share the cleanup). And nothing can be created on a DIMENSIONAL WARP token, as the card face always said. The general-topic FAQ sections (Combat, Line of Sight, Monsters, Treasures, and five more) join the rules tab verbatim under a double- ruled divider, completing the FAQ's absorption: card rulings on the cards, general rulings in the rules. The four promoted UX items: cell-target spells now dim ineligible squares (creations mirror emptySquareTarget from the viewer's knowledge, summons want clear sighted squares, teleport BFSes its four spaces exactly as the engine does, stone-to-water lights only stone, dispel lights only creations); hotseat games get the full replay reel, each step seen from its actor's own seat; a "stop announcing attacks on this device" link in the fanfare modal (undo lives beside the notification toggle in the lobby); the corner inspector now serves only the selected casting card, with every examine-a-card peek unified on the centered treatment; and the dev socket URL keys on vite's port instead of hijacking every localhost. Co-Authored-By: Claude Fable 5 --- .../console-2026-08-16T19-34-16-116Z.log | 110 ++++++++++++++++++ packages/engine/src/game.ts | 52 +++++++-- packages/engine/src/view.ts | 82 +++++++++++++ packages/engine/test/casting.test.ts | 35 ++++++ packages/server/src/rooms.ts | 2 +- packages/web/src/App.svelte | 36 ++++-- packages/web/src/Help.svelte | 20 ++++ packages/web/src/faq-general.ts | 67 +++++++++++ packages/web/src/local.svelte.ts | 26 ++++- packages/web/src/net.svelte.ts | 4 +- 10 files changed, 409 insertions(+), 25 deletions(-) create mode 100644 packages/web/src/faq-general.ts diff --git a/.playwright-mcp/console-2026-08-16T19-34-16-116Z.log b/.playwright-mcp/console-2026-08-16T19-34-16-116Z.log index 907f2fb..9d011b7 100644 --- a/.playwright-mcp/console-2026-08-16T19-34-16-116Z.log +++ b/.playwright-mcp/console-2026-08-16T19-34-16-116Z.log @@ -49,3 +49,113 @@ [ 169477ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 [ 174008ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 [ 178040ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 184181ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 190486ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 193749ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 199856ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 203003ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 206637ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 210167ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 215950ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 221046ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 225043ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 229602ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 232212ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 235783ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 240726ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 243562ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 246869ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 251933ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 256811ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 262875ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 268069ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 274279ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 277187ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 281386ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 287570ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 292533ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 297630ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 300789ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 306371ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 309321ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 313317ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 318531ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 325018ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 329131ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 334398ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 339026ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 345304ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 348984ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 354153ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 360602ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 367042ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 371403ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 374946ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 378537ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 384543ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 389557ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 395542ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 399198ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 405437ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 409320ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 414671ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 418122ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 422762ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 428522ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 434933ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 440494ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 444233ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 450393ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 456150ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 461178ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 466151ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 470131ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 474193ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 479280ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 485693ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 488787ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 491348ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 495642ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 499529ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 504086ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 507420ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 510475ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 513984ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 517094ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 520899ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 525963ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 532473ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 538010ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 541731ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 546731ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 552391ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 556687ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 561523ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 566456ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 571832ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 576666ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 580876ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 586166ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 591690ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 595601ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 598801ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 604138ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 610022ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 613113ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 617769ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 622903ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 629006ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 634764ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 639842ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 646351ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 651246ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 656927ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 663128ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 666172ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 672227ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 674779ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 680821ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 683499ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 687108ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 693288ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 699344ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 diff --git a/packages/engine/src/game.ts b/packages/engine/src/game.ts index ca4cfd4..46c2f47 100644 --- a/packages/engine/src/game.ts +++ b/packages/engine/src/game.ts @@ -221,7 +221,8 @@ export interface GameConfig { * Rev 7 briefly restricted warp sight to aisle corners; the table's * reading prevailed and all board openings carry sight in every revision. * DIMENSIONAL WARP's tokens never do ("There is no L.O.S. through the - * warp." — the card face). + * warp." — the card face). Rev 8: nothing can be created on a warp token, + * and a SPEED bonus turn burns a turn of durations on the hastened wizard. */ deckRev?: number; } @@ -2401,6 +2402,11 @@ function emptySquareTarget( if (state.players.some((p) => p.alive && cellKey(p.position) === key)) return "someone is standing there"; if (state.treasures.some((t) => t.position && cellKey(t.position) === key)) return "a treasure rests there"; if ((state.groundObjects[key] ?? []).length > 0) return "an object lies there"; + // "You can't create an object on a DIMENSIONAL WARP" (rules rev 8). + if ((state.config.deckRev ?? 1) >= 8 && + state.dimWarps.some((w) => cellKey(w.a) === key || cellKey(w.b) === key)) { + return "the warp shimmers there — nothing can form on it"; + } if (!gameLos(state, caster.position, cell)) return "no line of sight"; return cell; } @@ -5266,17 +5272,7 @@ function beginTurnFor(state: GameState, events: GameEvent[], index: number): voi if (s.casterId === player.id) { s.remainingTurns--; if (s.remainingTurns <= 0) { - events.push({ type: "spellExpired", effectId: s.id, cardId: s.cardId, target: s.targetId }); - // Edge-bound spells clean up their edge (WALL OF FIRE burns out). - if (s.edge && state.edgeOverrides[s.edge] === "firewall") { - delete state.edgeOverrides[s.edge]; - delete state.createdEdges[s.edge]; - events.push({ type: "firewallExpired", edge: s.edge }); - } - // GLUE dries out: the cell key rides in the same field. - if (s.cardId === "glue" && s.edge) { - delete state.gluedCells[s.edge]; - } + expireEffect(state, events, s); continue; } } @@ -5339,6 +5335,21 @@ function beginTurnFor(state: GameState, events: GameEvent[], index: number): voi }; } +/** An effect's turns ran out: announce it and clean up what it built. */ +function expireEffect(state: GameState, events: GameEvent[], s: SustainedEffect): void { + events.push({ type: "spellExpired", effectId: s.id, cardId: s.cardId, target: s.targetId }); + // Edge-bound spells clean up their edge (WALL OF FIRE burns out). + if (s.edge && state.edgeOverrides[s.edge] === "firewall") { + delete state.edgeOverrides[s.edge]; + delete state.createdEdges[s.edge]; + events.push({ type: "firewallExpired", edge: s.edge }); + } + // GLUE dries out: the cell key rides in the same field. + if (s.cardId === "glue" && s.edge) { + delete state.gluedCells[s.edge]; + } +} + function doEndTurn(prev: GameState, draw: number): CommandResult { if (draw < 0 || draw > DRAW_PER_TURN) return err(`you may draw 0-${DRAW_PER_TURN} cards`); @@ -5408,6 +5419,23 @@ function doEndTurn(prev: GameState, draw: number): CommandResult { if (p.extraTurns > 0) { p.extraTurns--; beginTurnFor(state, events, state.turn.activeIndex); + // "If you have SPEED on you while under the influence of a duration + // spell, it uses up a turn of that duration" — recipient-counted (FAQ; + // rules rev 8). Self-cast durations already burned in beginTurnFor. + if ((state.config.deckRev ?? 1) >= 8) { + const surviving: SustainedEffect[] = []; + for (const s of state.sustained) { + if (s.targetId === p.id && s.casterId !== p.id) { + s.remainingTurns--; + if (s.remainingTurns <= 0) { + expireEffect(state, events, s); + continue; + } + } + surviving.push(s); + } + state.sustained = surviving; + } events.push({ type: "extraTurnStarted", player: p.id }); events.push({ type: "turnStarted", player: p.id, round: state.turn.round }); return { ok: true, state, events }; diff --git a/packages/engine/src/view.ts b/packages/engine/src/view.ts index f17ec72..2aafee8 100644 --- a/packages/engine/src/view.ts +++ b/packages/engine/src/view.ts @@ -182,3 +182,85 @@ export function sightedCellsFor(view: GameView): Set { } return out; } + +const CREATION_CARD_IDS = new Set([ + "fill-square-with-stone", "thornbush", "killer-ooze", "rosebush", "dust-cloud", + "fill-square-with-slime", "create-pit", "handful-of-tacks", "glue", "safe", "boobytrap", +]); +const SUMMON_CARD_IDS = new Set([ + "troll", "skeleton", "wraith", "fire-imp", "democratic-monster", "shadow", +]); + +/** + * Squares a cell-target card can legally aim at, for the client's dimming + * aid — mirroring the engine's own validation from the viewer's knowledge. + * Null = this card's eligibility is not modeled; light everything. + */ +export function eligibleCellsFor(view: GameView, cardId: string): Set | null { + const me = view.players.find((p) => p.id === view.you); + if (!me) return null; + const cells = Object.keys(view.board.cells); + const key = (x: number, y: number) => `${x},${y}`; + const sighted = sightedCellsFor(view); + + if (CREATION_CARD_IDS.has(cardId)) { + // emptySquareTarget: on the board, unoccupied by content, home, wizard, + // treasure, object, or a warp token — and in the caster's sight. + const out = new Set(); + for (const k of cells) { + if (view.squareContents[k]) continue; + const [x, y] = k.split(",").map(Number) as [number, number]; + if (view.board.homes.some((h) => key(h.x, h.y) === k)) continue; + if (view.players.some((p) => p.alive && key(p.position.x, p.position.y) === k)) continue; + if (view.treasures.some((t) => t.position && key(t.position.x, t.position.y) === k)) continue; + if ((view.groundObjects[k] ?? []).length > 0) continue; + if (view.dimWarps.some((w) => key(w.a.x, w.a.y) === k || key(w.b.x, w.b.y) === k)) continue; + if (!sighted.has(k)) continue; + out.add(k); + void x; void y; + } + return out; + } + + if (SUMMON_CARD_IDS.has(cardId)) { + // Summon: any sighted square free of content and creatures. + const out = new Set(); + for (const k of cells) { + if (view.squareContents[k]) continue; + if (view.creatures.some((c) => key(c.position.x, c.position.y) === k)) continue; + if (!sighted.has(k)) continue; + out.add(k); + } + return out; + } + + if (cardId === "teleport") { + // Up to four spaces, walls and objects ignored; not into solid stone. + // BFS over existing cells, matching the engine's wallIgnoringDistance. + const out = new Set(); + const dist = new Map([[key(me.position.x, me.position.y), 0]]); + const queue = [me.position]; + while (queue.length > 0) { + const cur = queue.shift()!; + const d = dist.get(key(cur.x, cur.y))!; + if (d >= 4) continue; + for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]] as const) { + const n = { x: cur.x + dx, y: cur.y + dy }; + const nk = key(n.x, n.y); + if (!view.board.cells[nk] || dist.has(nk)) continue; + dist.set(nk, d + 1); + queue.push(n); + if (view.squareContents[nk]?.kind !== "stone") out.add(nk); + } + } + return out; + } + + if (cardId === "stone-to-water") { + return new Set(cells.filter((k) => view.squareContents[k]?.kind === "stone")); + } + if (cardId === "dispel-creation") { + return new Set(cells.filter((k) => view.squareContents[k] != null)); + } + return null; +} diff --git a/packages/engine/test/casting.test.ts b/packages/engine/test/casting.test.ts index 701db28..38d539c 100644 --- a/packages/engine/test/casting.test.ts +++ b/packages/engine/test/casting.test.ts @@ -734,3 +734,38 @@ describe("answering counteractions (FAQ rulings)", () => { expect(cellKey(after.position)).toBe(cellKey(escape)); }); }); + +describe("rules revision 8", () => { + it("a SPEED bonus turn burns a turn of durations on the hastened wizard", () => { + let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic"], deckRev: 8 }); + state = toRound2(state); + const me = activePlayer(state); + const other = state.players.find((p) => p.id !== me.id)!; + state.sustained.push({ + id: "fx-blind", cardId: "blind", casterId: other.id, targetId: me.id, + remainingTurns: 2, data: {}, + }); + giveCard(state, me.id, "speed", "SP", 0); + state = must(state, me.id, { type: "cast", instanceId: "speed#SP" }); + state = must(state, me.id, { type: "endTurn", draw: 0 }); + // The bonus turn began: one duration turn burned, recipient-counted. + expect(activePlayer(state).id).toBe(me.id); + expect(state.sustained.find((s) => s.id === "fx-blind")!.remainingTurns).toBe(1); + state = must(state, me.id, { type: "endTurn", draw: 0 }); + expect(activePlayer(state).id).toBe(other.id); + }); + + it("nothing can be created on a dimensional warp token", () => { + let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"], deckRev: 8 }); + state = toRound2(state); + const me = activePlayer(state); + const spot = { x: me.position.x + 1, y: me.position.y }; + state.dimWarps.push({ a: spot, b: { x: 0, y: 0 } }); + const tb = giveCard(state, me.id, "thornbush", "T", 0); + const r = applyCommand(state, me.id, { + type: "cast", instanceId: tb.instanceId, target: { kind: "cell", cell: spot }, + }); + expect(r.ok).toBe(false); + if (!r.ok) expect(r.error).toMatch(/warp shimmers/); + }); +}); diff --git a/packages/server/src/rooms.ts b/packages/server/src/rooms.ts index a8590bd..5086a0f 100644 --- a/packages/server/src/rooms.ts +++ b/packages/server/src/rooms.ts @@ -46,7 +46,7 @@ export interface Room { const rooms = new Map(); /** Rules revision new games are dealt under (stored games keep their own). */ -const RULES_REV = 7; +const RULES_REV = 8; const ROOM_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 0101437..5cc50db 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -7,7 +7,7 @@ import Help from "./Help.svelte"; import Replay from "./Replay.svelte"; import { local } from "./local.svelte"; - import { allCardDefs, cardDef, isNumberCard, SIDES, stepTarget, cellKey, isMovableObject, sightedCellsFor, type GameView } from "@wizwar/engine"; + import { allCardDefs, cardDef, isNumberCard, SIDES, stepTarget, cellKey, isMovableObject, sightedCellsFor, type GameView, eligibleCellsFor } from "@wizwar/engine"; import type { CardInstance, Side } from "@wizwar/engine"; net.connect(); @@ -19,6 +19,12 @@ let showHelp = $state(false); /** The finished game whose victory fanfare has been dismissed. */ let victorySeen = $state(false); + /** Quiet mode: skip the attack announcement modal (device preference). */ + let noFanfare = $state(localStorage.getItem("wizwar-no-fanfare") === "1"); + function setFanfare(announce: boolean) { + noFanfare = !announce; + localStorage.setItem("wizwar-no-fanfare", announce ? "0" : "1"); + } /** Which pending attack the player has already acknowledged (modal dismissed). */ let attackNoticeSeen = $state(null); /** A face-up card being peeked at from the scoresheet (view-only). */ @@ -641,6 +647,9 @@ if (!view || !selectedDef || !isYourTurn) return null; if (ambushVia || ambushSpell || ambushTrigger) return null; // ambushes aim at the future if (!me) return null; + if (CELL_CARDS.has(selectedCard!.cardId)) { + return eligibleCellsFor(view, selectedCard!.cardId); + } if (selectedDef.adjacent === true) { const { x, y } = me.position; return new Set( @@ -770,10 +779,12 @@ {/if} {#if peekCard} - {/if} - {#if view?.stack && attackNoticeKey && attackNoticeSeen !== attackNoticeKey} + {#if view?.stack && attackNoticeKey && attackNoticeSeen !== attackNoticeKey && !noFanfare}
@@ -844,6 +855,8 @@ {#if net.catchUp && net.catchUp.length > 0} net.closeCatchUp()} /> + {:else if local.replaySteps && local.replaySteps.length > 0} + (local.replaySteps = null)} /> {/if} {#if net.error} @@ -967,6 +980,9 @@ notify me on my turn {/if} + {#if noFanfare} + + {/if}
{#each net.seats as seat (seat.roomId + seat.name)} {@const g = net.games.find((x) => x.roomId === seat.roomId && x.name === seat.name)} @@ -1357,9 +1373,9 @@
The hands, face-up - {#if !local.active} - - {/if} +
{#each view.players as p (p.id)}
@@ -1963,6 +1979,8 @@ box-shadow: 0 18px 50px rgba(0, 0, 0, 0.7); } .big-peek :global(.card:hover) { transform: scale(2.1); } + .big-peek { display: flex; flex-direction: column; align-items: center; } + .big-peek-note { margin-top: 6.8rem; max-width: 15rem; font-size: 0.8rem; } .discard-link { background: none; border: none; diff --git a/packages/web/src/Help.svelte b/packages/web/src/Help.svelte index 66642b5..17bd8b0 100644 --- a/packages/web/src/Help.svelte +++ b/packages/web/src/Help.svelte @@ -3,6 +3,7 @@ import Card from "./Card.svelte"; import Faq from "./Faq.svelte"; import { HOW_TO_PLAY, RULES_SECTIONS } from "./rules"; + import { FAQ_GENERAL } from "./faq-general"; let { onclose, @@ -172,6 +173,16 @@

{section.title}

{#each section.body as p, i (i)}

{p}

{/each} {/each} +

Official FAQ — general rulings

+

+ Tom Jolly's own answers (wizwar.com, September 2002), verbatim. Rulings + occasionally reference other editions. Card-specific rulings sit on the + cards themselves — look for the FAQ seal. +

+ {#each FAQ_GENERAL as section (section.title)} +

{section.title}

+ {#each section.body as p, i (i)}

{p}

{/each} + {/each} {:else}
(null); log = $state([]); + /** The finished game as a reel, each step from its actor's own seat. */ + replaySteps = $state<{ seq: number; actor: PlayerId; events: GameEvent[]; view: GameView }[] | null>(null); view = $derived( this.gameState && this.viewerId ? viewFor(this.gameState, this.viewerId) : null, ); @@ -56,6 +60,26 @@ class LocalGame { private activeMs = 0; private lastMoveAt = 0; + /** Rebuild the whole game as replay steps (finished games only). */ + buildReplay(): void { + if (!this.config || this.gameState?.phase !== "finished") return; + const { state } = createGame(this.config); + let current = state; + const steps: { seq: number; actor: PlayerId; events: GameEvent[]; view: GameView }[] = []; + this.commands.forEach((c, i) => { + const r = applyCommand(current, c.playerId, c.command); + if (!r.ok) return; + current = r.state; + steps.push({ + seq: i, + actor: c.playerId, + events: r.events.map((e) => redactEvent(e, c.playerId)).filter((e): e is GameEvent => e !== null), + view: viewFor(current, c.playerId), + }); + }); + this.replaySteps = steps; + } + hasSave(): boolean { return localStorage.getItem(SAVE_KEY) !== null; } @@ -101,7 +125,7 @@ class LocalGame { seed, sets: expansion ? ["basic", "expansion1"] : ["basic"], ...(colors ? { colors } : {}), - deckRev: 7, + deckRev: 8, }; const { state, events } = createGame(config); this.config = config; diff --git a/packages/web/src/net.svelte.ts b/packages/web/src/net.svelte.ts index 1e0e154..7a78434 100644 --- a/packages/web/src/net.svelte.ts +++ b/packages/web/src/net.svelte.ts @@ -5,8 +5,8 @@ import { cardDef } from "@wizwar/engine"; const SERVER_URL = import.meta.env.VITE_WIZWAR_SERVER ?? - (location.hostname === "localhost" - ? "ws://localhost:8787" + (location.port === "5173" // the vite dev server; anything else serves its own socket + ? `ws://${location.hostname}:8787` : `${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/ws`); /** Sustained-effect ids that are engine constructs, not printed cards. */