From 2ae4c6a77b2b174221be895fb00378723449af0b Mon Sep 17 00:00:00 2001 From: Gavin McDonald Date: Fri, 27 Jun 2025 18:14:06 -0400 Subject: [PATCH] teletilting --- app/AppContext.tsx | 23 ++++++++++++++++------- components/Card.tsx | 2 +- components/TiltCard.tsx | 37 +++++++++++++++++++++++++------------ hooks/useSocket.ts | 23 ++++++++++++++++------- lib/GameStore.ts | 36 +++++++++++++++++++++++++++++------- server.ts | 6 +++--- types/index.ts | 4 +++- 7 files changed, 93 insertions(+), 38 deletions(-) diff --git a/app/AppContext.tsx b/app/AppContext.tsx index 712aef7..44a62e8 100644 --- a/app/AppContext.tsx +++ b/app/AppContext.tsx @@ -1,6 +1,6 @@ 'use client'; -import { createContext, useContext, useState, ReactNode } from 'react'; +import { createContext, useContext, useEffect, useState, ReactNode } from 'react'; import useSocket from '@/hooks/useSocket'; import type { GameUpdate, Tilt } from '@/types'; @@ -17,12 +17,13 @@ const gameStart: GameUpdate = { notes: false, cardStyle: 'color', }, + tilts: Array.from({ length: 5 }, () => []), }; export interface AppContext { gameData: GameUpdate; noGame: boolean; - tilts: Tilt[]; + tilt: Tilt[]; selectCardIndex: number; flipCard: (cardIndex: number) => void; handleSettings: (gameData: GameUpdate) => void; @@ -30,7 +31,7 @@ export interface AppContext { select: (cardID: string) => void; setGameID: (gameID: string) => void; setSelectCardIndex: (cardIndex: number) => void; - setTilts: (tilts: Tilt[]) => void; + setTilt: (tilt: Tilt[]) => void; } export function AppProvider({ children }: { children: ReactNode }) { @@ -38,14 +39,22 @@ export function AppProvider({ children }: { children: ReactNode }) { const [gameID, setGameID] = useState(''); const [noGame, setNoGame] = useState(false); const [selectCardIndex, setSelectCardIndex] = useState(-1); - const [tilts, setTilts] = useState([]); + const [tilt, setTilt] = useState([]); - const { flipCard, redraw, select, handleSettings } = useSocket({ + const { flipCard, redraw, select, handleSettings, emitTilt } = useSocket({ gameID, setGameData, setNoGame, }); + useEffect(() => { + const cardIndex = tilt.findIndex((tilt) => !!tilt); + + if (tilt[cardIndex]) { + emitTilt(cardIndex, tilt[cardIndex]); + } + }, [tilt]); + const handleSelect = (cardID: string) => { setSelectCardIndex(-1); @@ -55,7 +64,7 @@ export function AppProvider({ children }: { children: ReactNode }) { const appInterface = { gameData, noGame, - tilts, + tilt, selectCardIndex, flipCard, handleSettings, @@ -63,7 +72,7 @@ export function AppProvider({ children }: { children: ReactNode }) { select: handleSelect, setGameID, setSelectCardIndex, - setTilts, + setTilt, }; return {children}; diff --git a/components/Card.tsx b/components/Card.tsx index 57c11f8..4b12f86 100644 --- a/components/Card.tsx +++ b/components/Card.tsx @@ -55,7 +55,7 @@ export default function Card({ card, cardIndex }: CardProps) {
(null); - const { tilts, setTilts } = useAppContext(); - - const card = cardRef.current; + const { gameData, setTilt } = useAppContext(); useEffect(() => { + const card = cardRef.current; if (!card) return; - const tilt = tilts.find((tilt) => tilt.cardID === cardID); + const tilt = gameData.tilts[cardIndex]; if (!tilt) { card.style.transform = `rotateX(0deg) rotateY(0deg)`; return; } - const { rotateX, rotateY } = tilt; + const tilted = tilt.filter(({ rotateX, rotateY }) => rotateX || rotateY); + + const { totalX, totalY } = tilted.reduce( + ({ totalX, totalY }, { rotateX, rotateY }) => ({ + totalX: totalX + rotateX, + totalY: totalY + rotateY, + }), + { totalX: 0, totalY: 0 }, + ); + + const rotateX = totalX / tilted.length; + const rotateY = totalY / tilted.length; if (rotateX || rotateY) { card.style.transform = `rotateX(${rotateX}deg) rotateY(${rotateY}deg)`; } else { card.style.transform = `rotateX(0deg) rotateY(0deg)`; } - }, [tilts]); + }, [gameData]); const handleMouseMove = (e: React.MouseEvent) => { + const card = cardRef.current; if (!card) return; const rect = card.getBoundingClientRect(); @@ -45,13 +57,14 @@ export default function TiltCard({ const rotateX = ((y - centerY) / centerY) * -20; const rotateY = ((x - centerX) / centerX) * 20; - setTilts([...tilts.filter((tilt) => tilt.cardID !== cardID), { cardID, rotateX, rotateY }]); + const newTilt: Tilt[] = []; + newTilt[cardIndex] = { rotateX, rotateY }; + + setTilt(newTilt); }; const handleMouseLeave = () => { - if (!card) return; - - setTilts(tilts.filter((tilt) => tilt.cardID !== cardID)); + setTilt([]); }; return ( diff --git a/hooks/useSocket.ts b/hooks/useSocket.ts index 794c8fe..8af6b3e 100644 --- a/hooks/useSocket.ts +++ b/hooks/useSocket.ts @@ -1,7 +1,8 @@ import { useEffect } from 'react'; import { socket } from '@/socket'; +import throttle from '@/tools/throttle'; -import type { GameUpdate } from '@/types'; +import type { GameUpdate, Tilt } from '@/types'; interface UseSocketProps { gameID: string; @@ -44,6 +45,13 @@ export default function useSocket({ gameID, setGameData, setNoGame }: UseSocketP }); }; + const handleSettings = (gameData: GameUpdate) => { + socket.emit('settings', { + gameID, + gameData, + }); + }; + const redraw = (cardIndex: number) => { socket.emit('redraw', { gameID, @@ -59,17 +67,18 @@ export default function useSocket({ gameID, setGameData, setNoGame }: UseSocketP }); }; - const handleSettings = (gameData: GameUpdate) => { - socket.emit('settings', { - gameID, - gameData, + const emitTilt = throttle((cardIndex: number, tilt: Tilt) => { + socket.emit('tilt', { + cardIndex, + tilt, }); - }; + }, 33.3); return { flipCard, + handleSettings, redraw, select, - handleSettings, + emitTilt, }; } diff --git a/lib/GameStore.ts b/lib/GameStore.ts index de08e2a..c13d114 100644 --- a/lib/GameStore.ts +++ b/lib/GameStore.ts @@ -2,7 +2,7 @@ import Deck from '@/lib/TarokkaDeck'; import generateID from '@/tools/simpleID'; import parseMilliseconds from '@/tools/parseMilliseconds'; import { HOUR, DAY } from '@/constants/time'; -import { GameState, GameUpdate, Settings } from '@/types'; +import { GameState, GameUpdate, Settings, Tilt } from '@/types'; const deck = new Deck(); @@ -91,6 +91,7 @@ export default class GameStore { notes: true, cardStyle: 'color', }, + tilts: Array.from({ length: 5 }, () => []), }; this.totalCreated++; @@ -157,6 +158,21 @@ export default class GameStore { return this.gameUpdate(game); } + tilt(playerID: string, cardIndex: number, { rotateX, rotateY }: Tilt) { + const game = this.getGameByPlayerID(playerID); + const cardTilts = game.tilts[cardIndex]; + + if (!cardTilts) throw new Error(`Card tilts ${cardIndex} not found`); + + game.tilts[cardIndex] = [ + ...cardTilts.filter((tilt) => tilt.playerID !== playerID), + { playerID, rotateX, rotateY }, + ]; + game.lastUpdated = Date.now(); + + return this.gameUpdate(game); + } + updateSettings(gameID: string, settings: Settings) { const game = this.getGame(gameID); @@ -173,10 +189,18 @@ export default class GameStore { return game; } - gameUpdate(game: GameState): GameUpdate { - const { dmID, spectatorID, cards, settings } = game; + getGameByPlayerID(playerID: string): GameState { + const game = this.players.get(playerID); - return { dmID, spectatorID, cards, settings }; + if (!game) throw new Error(`Player ${playerID} not found`); + + return game; + } + + gameUpdate(game: GameState): GameUpdate { + const { dmID, spectatorID, cards, settings, tilts } = game; + + return { dmID, spectatorID, cards, settings, tilts }; } playerExit(playerID: string): GameState | null { @@ -185,9 +209,7 @@ export default class GameStore { return null; } else { - const game = this.players.get(playerID); - - if (!game) throw new Error(`Player ${playerID} not found`); + const game = this.getGameByPlayerID(playerID); this.players.delete(playerID); return this.leaveGame(game, playerID); diff --git a/server.ts b/server.ts index 8be15ff..eb8e062 100644 --- a/server.ts +++ b/server.ts @@ -119,10 +119,10 @@ app.prepare().then(() => { } }); - socket.on('tilt', ({ gameID, tilt }: { gameID: string; tilt: Tilt }) => { + socket.on('tilt', ({ cardIndex, tilt }: { cardIndex: number; tilt: Tilt }) => { try { - const gameState = gameStore.getGame(gameID); - broadcast('tilt', gameState); + const gameState = gameStore.tilt(socket.id, cardIndex, tilt); + broadcast('game-update', gameState); } catch (e) { const error = e instanceof Error ? e.message : e; console.error(Date.now(), 'Error[tilt]', error); diff --git a/types/index.ts b/types/index.ts index 2af9ab4..b98319f 100644 --- a/types/index.ts +++ b/types/index.ts @@ -82,6 +82,7 @@ export interface GameState { cards: TarokkaGameCard[]; lastUpdated: number; settings: Settings; + tilts: Tilt[][]; } export interface GameUpdate { @@ -89,6 +90,7 @@ export interface GameUpdate { spectatorID: string; cards: TarokkaGameCard[]; settings: Settings; + tilts: Tilt[][]; } export interface ClientUpdate { @@ -105,7 +107,7 @@ export interface Layout { } export interface Tilt { - cardID: string; + playerID?: string; rotateX: number; rotateY: number; }