The whole punch list: rev 8, general FAQ, and four promoted UX items

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 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-16 15:45:59 -04:00
co-authored by Claude Fable 5
parent f657144127
commit d6d34887e3
10 changed files with 409 additions and 25 deletions
@@ -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
+40 -12
View File
@@ -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 };
+82
View File
@@ -182,3 +182,85 @@ export function sightedCellsFor(view: GameView): Set<string> {
}
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<string> | 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<string>();
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<string>();
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<string>();
const dist = new Map<string, number>([[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;
}
+35
View File
@@ -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/);
});
});
+1 -1
View File
@@ -46,7 +46,7 @@ export interface Room {
const rooms = new Map<string, Room>();
/** 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";
+27 -9
View File
@@ -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<string | null>(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}
<div class="inspector" role="img" aria-label="displayed card, enlarged">
<button class="inspector-close" onclick={() => { peekCard = null; peekCreatureId = null; }} aria-label="hide card">×</button>
<div class="inspector-card"><Card card={peekCard} onfaq={(id) => (faqCardId = id)} /></div>
{#if peekNote}<div class="peek-note">{peekNote}</div>{/if}
<div class="big-peek-scrim" role="button" tabindex="-1"
onclick={() => { peekCard = null; peekCreatureId = null; }} onkeydown={() => {}}>
<div class="big-peek">
<Card card={peekCard} onfaq={(id) => (faqCardId = id)} />
{#if peekNote}<div class="peek-note big-peek-note">{peekNote}</div>{/if}
</div>
</div>
{/if}
@@ -813,7 +824,7 @@
</div>
{/if}
{#if view?.stack && attackNoticeKey && attackNoticeSeen !== attackNoticeKey}
{#if view?.stack && attackNoticeKey && attackNoticeSeen !== attackNoticeKey && !noFanfare}
<div class="scrim attack-scrim" role="alertdialog" aria-label="you are under attack">
<div class="attack-notice">
<div class="attack-headline">
@@ -844,6 +855,8 @@
{#if net.catchUp && net.catchUp.length > 0}
<Replay steps={net.catchUp} onclose={() => net.closeCatchUp()} />
{:else if local.replaySteps && local.replaySteps.length > 0}
<Replay steps={local.replaySteps} onclose={() => (local.replaySteps = null)} />
{/if}
{#if net.error}
@@ -967,6 +980,9 @@
notify me on my turn
</button>
{/if}
{#if noFanfare}
<button class="hint-cancel" onclick={() => setFanfare(true)}>announce attacks</button>
{/if}
</div>
{#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 @@
<div class="final-reveal" aria-label="all hands revealed">
<div class="reveal-head">
The hands, face-up
{#if !local.active}
<button class="stamp tiny" onclick={() => net.requestFullReplay()}> Watch the whole game</button>
{/if}
<button class="stamp tiny"
onclick={() => (local.active ? local.buildReplay() : net.requestFullReplay())}>
⟲ Watch the whole game</button>
</div>
{#each view.players as p (p.id)}
<div class="reveal-row">
@@ -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;
+20
View File
@@ -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 @@
<h3>{section.title}</h3>
{#each section.body as p, i (i)}<p>{p}</p>{/each}
{/each}
<h2 class="faq-divider">Official FAQ — general rulings</h2>
<p class="colophon">
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.
</p>
{#each FAQ_GENERAL as section (section.title)}
<h3>{section.title}</h3>
{#each section.body as p, i (i)}<p>{p}</p>{/each}
{/each}
{:else}
<div class="search-row">
<input
@@ -273,6 +284,15 @@
font-size: 0.98rem;
line-height: 1.5;
}
.faq-divider {
font-family: "Oswald", sans-serif;
font-size: 1rem;
letter-spacing: 0.1em;
text-transform: uppercase;
border-top: 3px double #43331f;
padding-top: 0.8rem;
margin: 1.6rem 0 0.2rem;
}
.booklet-body h3 {
font-family: "Oswald", sans-serif;
font-size: 0.85rem;
+67
View File
@@ -0,0 +1,67 @@
// Tom Jolly's official Rules FAQ (wizwar.com, September 2002): the
// general-topic rulings, verbatim. Card-specific rulings live on the
// cards themselves (cards.json faqRulings).
export const FAQ_GENERAL: { title: string; body: string[] }[] = [
{
title: "Attack-Neutral Combos",
body: [
"Some cards have mutiple spell designations in the corner (ATTACK, NEUTRAL, COUNTERACTION) with some combination of spell types shown. Since the rules stipulate that you can cast a COUNTERACTION out of turn or during your normal turn, the COUNTERACTION-NEUTRAL is redundant (and to be eliminated in later editions). All the cards with \"SELF\" marked on them were changed to \"LOS\" in later editions, primarily because you are obviously in LOS with yourself, and the card specifies that you cast the spell on yourself. These will be changed back to \"SELF\" in later additions, primarily due to the fact that it misleads people into believing that a \"SELF\" type spell can be cast on anyone. In the 8th edition, \"who\" the spell can be cast on will be very specifically defined. The cards that DO say ATTACK-NEUTRAL on them refer to the fact that they are a Neutral spell if cast on yourself and an Attack when cast on others.",
],
},
{
title: "Combat",
body: [
"You can't attack yourself. This is mentioned in the 6th edition rules, but no earlier editions. This is a very important rule! Also, the rule should read \"No ATTACKS can be used during the first round of turns\", not \"No COMBAT during the first round of turns\". It's perfectly okay to use a DESTROY WALL next to a person, thus damaging him on the first turn, since it is not an attack.",
],
},
{
title: "Creating Objects",
body: [
"More notes on creating objects on one's home base; the basic rules state that you may NOT create an object on a player's home base. This rule appears to get broken by such a card as TRADER, and some TELEPORTATIONS. This is alright as long as \"immobile objects\", such as Solid Stone or Thornbush, are not placed on the home base. The 8th edition clarifies this, since it limits what can and cannot be teleported on the cards. TRADER, for example, only works on \"Mobile Objects\".",
],
},
{
title: "Death",
body: [
"The second-hand killing of a Wizard, say by falling walls or controlled MONSTERS, does not get you his cards, nor does a MONSTER get cards by killing a player!",
"Dumping Cards and Physical Objects This is explained pretty well in the basic rules, but everybody seems to forget you can do it. If you want to discard a physical object, it is NOT necessary to dump it on the board, you can just discard it to the discard pile. The board would get very crowded otherwise. This includes ARTIFACTS, if you think someone is about to take one from you and there's nothing you can do about it. This can only be done during your own turn (this is not in the rules, but will be next printing).",
],
},
{
title: "Line of Sight",
body: [
"If you \"barely nick a corner\" of a wall, this is still NOT line-of-sight. A VISIONSTONE could see through the corner, but if you \"nicked two corners\", the VISIONSTONE would not work. The new Wiz-War set, 6th edition, has no dots in the center of the squares with which to measure line-of-sight, even though the rules say to use these non-existent dots. Our advice is to eyeball the center of the square, or put your own dots on the playing board. It might also be worthwhile to take a magic marker and draw the walls brown, as they do not stand out very well as it is.",
],
},
{
title: "Lost Turns",
body: [
"These will accumulate if you get hit by a \"lost turns\" type spell while under the effect of another \"lost turns\" spell. Also, if you lose a turn while under a duration spell, this still uses up a turn of the duration spell. If you happen to get hit by a reflected attack (yours), and you \"lose a turn\", this would be THAT turn, not your next turn. This could happen with Lightning Bolt or Hypnosis, for example. As the rules say, if you Lose a Turn, you can't do anything but play Counteractions until your next turn. This also means you can't draw cards!",
"Magic Sticks These don't have to be displayed. When you first charge it with a NUMBER card, the NUMBER card gets discarded, so it no longer counts as part of your hand.",
"Magic Stones They may be discarded as cards even after used and displayed. Nothing makes an object so permanent that it cannot be discarded. This is always a player option. MAGIC STONES are not \"created\" by the card, the card merely represents the physical object. It may be displayed and left displayed whenever you use it first, even if it is not your turn.",
],
},
{
title: "Monsters",
body: [
"These are creatures that do damage, generally. This means that ALTER EGO, SHADOW, FAMILIAR, and KILLER OOZE are considered MONSTERS, while the SLIME, ANIMATE OBJECT, DECOY and BUCK are not, even though they are mobile. Though some Monsters can carry objects, they cannot use them. You can't attack your own monsters. This is in line with the rule that you can't attack yourself, and prevents some rule conflicts. Monsters that can cast spells can cast \"SELF\" spells, too. A monster that changes itself into a different monster (like a Werewolf), assumes the attributes of that monster, as described on the card, but retains any other attributes not mentioned.",
],
},
{
title: "Picking Up Objects",
body: [
"No, you can't pick up 2 objects at the same time, even if they're in the same space.",
"Players as Objects There is some confusion in the rules which will probably not be resolved until next edition as to whether Wizards and MONSTERS can be treated as objects or not, for purposes of spells such as DRAG and a few others. Unfortunately, this is not well defined anywhere. The basic rules state that players are NOT objects, so spells such as TELEPORT OBJECT and ANIMATE OBJECT would have no effect on Wizards. Use common sense and house rules. You will rarely run into this as a problem, but you could. Obviously, if a card says a MONSTER can carry objects, it is NOT referring to other Wizards!",
"Spell Priority There is some question as to whether Counteractions or Attacks work first, especially in a case like SHIELDSTONE vs FIREBALL. Counteractions always work first. Thus, if someone was hit with a FIREBALL while they were carrying a BLOODSTONE, the BLOODSTONE would reduce the damage done by a point before it was destroyed. If you have 2 Counteractions you want to use, you decide for yourself which one activates first. If a spell gets counteracted, the number card (if any) that accompanied it is also discarded.",
"Throwing Objects The basic rules imply, without explicitly stating, that you can throw objects, but not treasures. That's true! You could take a Magic Stone and throw it inot a PIT 3 spaces away, if you wanted. Throwing objects at other players does not do any damage, unless a card, such as HEAVE-HO or DAGGER says so.",
],
},
{
title: "Treasures",
body: [
"If a player is eliminated, any treasure left on his home base does NOT count toward eliminating the player to whom that treasure belongs. Also, the 7th edition rules are rather badly worded and imply that you can carry 2 treasures as long as one of them is your own and the other belongs to another player. This is incorrect. You can carry only ONE treasure, and it can be EITHER one of your own or one of the opponent's.",
"Card FAQ's for Wiz-War",
],
},
];
+25 -1
View File
@@ -13,6 +13,8 @@ import {
type GameState,
type GameView,
type PlayerId,
type GameEvent,
redactEvent,
} from "@wizwar/engine";
import { humanize, net } from "./net.svelte";
@@ -46,6 +48,8 @@ class LocalGame {
/** Set while the device should be handed to the named player. */
handoffTo = $state<PlayerId | null>(null);
log = $state<string[]>([]);
/** 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;
+2 -2
View File
@@ -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. */