Feature: public rooms, mobile UX, reconnection, and gameplay fixes

Rooms & lobby
- Rename docker-compose.yml → compose.yml
- Public/Private toggle on room creation; public rooms assign random seats
  to prevent team collusion
- GET /api/rooms API — lists open public rooms; Join tab shows live list
  with one-tap join
- Room creator: swap any two seats by tapping (select-to-swap UI); ▶ Start
  Game button force-starts with bots filling empty seats

Reconnection
- Session moved from sessionStorage → localStorage (survives browser close)
- Socket handlers split: socket.once for one-shot callbacks, persistent
  socket.on('connect') for auto-rejoin on network drops
- Server rejoin accepts userId match as fallback (cross-device rejoin for
  authenticated users); re-issues token on success
- Server emits hasActiveGame on connect so auth'd users on a new device are
  pulled back into their game automatically
- Explicit leave nulls seat/token/userIds so hasActiveGame never re-drags a
  player back in after they chose to leave

Mobile UX
- Remove all opponent/partner card backs; replace with compact card-count
  badge — frees ~120px of vertical space on small phones
- Screen height: 100dvh (dynamic viewport) instead of 100vh — fixes the
  "only top 1/5 visible" issue on phones with browser chrome
- Table grid side columns shrunk to 36px on touch devices; player names
  rotated vertically
- Bidding overlay: transparent non-blocking top panel on touch; hand stays
  visible and interactive; auto fan-mode during bidding
- touch-action: pan-x on hand scroll, none in fan/drag mode — suppresses
  Android back-gesture and Google Gemini conflicts
- user-select: none on game screen prevents long-press selection menus

Gameplay & notifications
- Center trick area now shows whose turn it is instead of trump (trump is
  already in the info bar); flashes gold when it's the player's turn
- Turn reminder after 5 s of inaction: gold glow pulse on hand area
  + Android vibration OR two-note Web Audio chime on iOS (vibrate API not
  supported by Apple)
- Fix: turn reminder was never triggered after winning a trick — justWon
  branch blocked myTurnNow from being set even when currentTurn === mySeat
- Waiting room ☰ menu: Reload and Exit accessible without entering the game
- Prevent duplicate room joins (same socket, same userId, or same name)

Service worker
- Bump to shelem-v2; pre-cache all 55 card SVGs at install time so cards
  are available instantly from the very first hand, including offline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
goyban
2026-05-19 20:42:00 +00:00
parent 8e8478e45b
commit a4fefd92f1
7 changed files with 636 additions and 78 deletions
+32 -5
View File
@@ -57,6 +57,13 @@
<button class="score-btn" data-score="1005">1005</button>
</div>
</div>
<div class="field">
<label>Visibility</label>
<div class="visibility-row">
<button class="visibility-btn active" data-public="false">🔒 Private</button>
<button class="visibility-btn" data-public="true">🌐 Public</button>
</div>
</div>
<button id="btn-create" class="btn-primary">Create Game</button>
</div>
@@ -67,6 +74,14 @@
</div>
<button id="btn-join" class="btn-primary">Join Game</button>
<hr class="divider">
<div class="public-rooms-section">
<div class="rooms-header">
<span class="rooms-label">Public Rooms</span>
<button id="btn-refresh-rooms" class="btn-text"></button>
</div>
<div id="public-rooms-list"><p class="hint">Loading…</p></div>
</div>
<hr class="divider">
<div class="field">
<label>Watch as spectator</label>
<input id="input-spectate-code" type="text" maxlength="8" placeholder="Room code" style="text-transform:uppercase" autocomplete="off">
@@ -81,7 +96,16 @@
<!-- ══════════════ WAITING ROOM ══════════════ -->
<div id="screen-waiting" class="screen">
<div class="waiting-box">
<button id="btn-leave-waiting" class="btn-leave-screen">← Leave</button>
<div class="waiting-top-row">
<button id="btn-leave-waiting" class="btn-leave-screen">← Leave</button>
<div class="game-menu-wrap" style="margin-left:auto">
<button id="btn-waiting-menu" class="btn-leave-screen" title="Options"></button>
<div id="waiting-menu-dropdown" class="game-menu-dropdown hidden">
<button id="btn-waiting-reload" class="game-menu-item">↺ Reload</button>
<button id="btn-waiting-exit" class="game-menu-item game-menu-exit">🚪 Exit</button>
</div>
</div>
</div>
<h2>Waiting for Players</h2>
<div class="room-code-box">
<span class="label">Room Code</span>
@@ -103,7 +127,10 @@
<span class="waiting-opt-label" id="waiting-mode-label"></span>
<span class="waiting-opt-label" id="waiting-score-label"></span>
</div>
<button id="btn-fill-bots" class="btn-fill-bots hidden">🤖 Fill empty seats with bots</button>
<div id="waiting-creator-controls" class="waiting-creator-controls hidden">
<button id="btn-fill-bots" class="btn-fill-bots">🤖 Fill with bots</button>
<button id="btn-start-game" class="btn-start-game">▶ Start Game</button>
</div>
</div>
</div>
@@ -149,8 +176,8 @@
<span id="top-turn" class="turn-dot hidden"></span>
<span id="top-partner" class="partner-badge">partner</span>
<span id="top-score" class="player-score-badge"></span>
<span id="top-count" class="card-count-badge"></span>
</div>
<div id="top-cards" class="opp-cards"></div>
</div>
<!-- Left player (seat 1) -->
@@ -159,8 +186,8 @@
<span id="left-name"></span>
<span id="left-turn" class="turn-dot hidden"></span>
<span id="left-score" class="player-score-badge"></span>
<span id="left-count" class="card-count-badge"></span>
</div>
<div id="left-cards" class="opp-cards vertical"></div>
</div>
<!-- Center trick area -->
@@ -182,8 +209,8 @@
<span id="right-name"></span>
<span id="right-turn" class="turn-dot hidden"></span>
<span id="right-score" class="player-score-badge"></span>
<span id="right-count" class="card-count-badge"></span>
</div>
<div id="right-cards" class="opp-cards vertical"></div>
</div>
</div><!-- /table-grid -->