fix(online): rotate server state to viewer's seat — non-seat-0 players can play
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 1m1s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m12s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m7s

The server is authoritative with ABSOLUTE seats and tells each client its own
seat via mySeat, but the client copied seats verbatim — so any player not at
absolute seat 0 had their hand at players[mySeat] while the table read players[0]
and "your turn" checked turn===0. Result: they couldn't play (server auto-played
after the timeout → "hang"), and the turn highlight was identical for everyone
instead of rotating per viewer.

Fix (client-only; server was correct): new viewerRot(mySeat) rotates every
seat-indexed value into the viewer's frame (viewer → local seat 0): players/hands,
turn, hakem, leadSeat, lastTrickWinner, currentTrick, hakemDraw, seat roster,
disconnectedSeat, and the team arrays (matchScore/roundTricks/lastRoundResult/
matchWinner — odd seats swap team order). Store mySeat and rotate reaction bubbles
too (they carried absolute seats).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-19 08:59:03 +03:30
parent 0790ad6fe0
commit 2aac6257d6
2 changed files with 81 additions and 40 deletions
+5 -3
View File
@@ -3,7 +3,7 @@
import { AnimatePresence, motion } from "framer-motion";
import { Crown, Flag, LogOut, SmilePlus, Volume2, VolumeX, Zap } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { useGameStore } from "@/lib/game-store";
import { useGameStore, viewerRot } from "@/lib/game-store";
import { useSoundStore } from "@/lib/sound-store";
import { Avatar } from "@/components/online/Avatar";
import { legalMoves } from "@/lib/hokm/engine";
@@ -776,8 +776,10 @@ function Reactions() {
useEffect(() => {
const unsub = getService().onReaction((seat, emoji) => {
const id = `${seat}-${Date.now()}-${Math.random()}`;
setBubbles((b) => [...b, { id, seat, emoji }]);
// Reactions carry the sender's ABSOLUTE seat — rotate into this viewer's frame.
const local = viewerRot(useGameStore.getState().mySeat).seat(seat) ?? seat;
const id = `${local}-${Date.now()}-${Math.random()}`;
setBubbles((b) => [...b, { id, seat: local, emoji }]);
setTimeout(() => setBubbles((b) => b.filter((x) => x.id !== id)), 2600);
});
return unsub;