diff --git a/src/App.css b/src/App.css index 9ab784040..71cd9ea5f 100644 --- a/src/App.css +++ b/src/App.css @@ -54,7 +54,7 @@ .game-board__background { position: absolute; inset: 0; - background-size: cover; + background-size: 100% 100%; background-position: center; filter: saturate(1.05) contrast(1.05); } @@ -93,6 +93,7 @@ box-shadow: 0 12px 24px rgba(0, 0, 0, 0.35); cursor: pointer; overflow: hidden; + background: rgba(9, 15, 26, 0.9); } .game-board__card img { @@ -101,6 +102,54 @@ object-fit: cover; } +.game-board__card::before { + content: ''; + position: absolute; + inset: 0; + border-radius: 14px; + border: 2px solid rgba(148, 163, 184, 0.35); + box-shadow: 0 0 18px rgba(15, 23, 42, 0.35); + pointer-events: none; +} + +.game-board__card--played::after { + content: ''; + position: absolute; + inset: -12px; + border-radius: 18px; + background: + linear-gradient(120deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 42%, rgba(255, 255, 255, 0) 58%), + radial-gradient(circle at 30% 30%, rgba(59, 130, 246, 0.55), rgba(59, 130, 246, 0) 60%), + radial-gradient(circle at 70% 70%, rgba(251, 191, 36, 0.65), rgba(251, 191, 36, 0) 65%); + opacity: 0; + filter: blur(6px); + animation: card-lightning 900ms ease-out; + pointer-events: none; +} + +@keyframes card-lightning { + 0% { + opacity: 0; + transform: scale(0.96) rotate(-6deg); + } + 15% { + opacity: 0.9; + transform: scale(1.02) rotate(2deg); + } + 35% { + opacity: 0.65; + transform: scale(1.01) rotate(0deg); + } + 60% { + opacity: 0.35; + transform: scale(1.04) rotate(1deg); + } + 100% { + opacity: 0; + transform: scale(1.08) rotate(0deg); + } +} + .game-board__card--selected { outline: 3px solid #38bdf8; box-shadow: 0 0 0 4px rgba(56, 189, 248, 0.3), 0 12px 24px rgba(0, 0, 0, 0.4); diff --git a/src/components/GameBoard.tsx b/src/components/GameBoard.tsx index 416509d99..ce097a036 100644 --- a/src/components/GameBoard.tsx +++ b/src/components/GameBoard.tsx @@ -1,5 +1,5 @@ -import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; -import { Card, GameState } from '../types/game'; +import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { Card, GameState, Player } from '../types/game'; import { getCardImagePath } from '../data/gameData'; import { LAYOUT, UI_BASE, computeSlotRects, getGovernmentRects, getPublicRects, getSofortRect, getUiTransform, getZone } from '../ui/layout'; import { sortHandCards } from '../utils/gameUtils'; @@ -12,6 +12,27 @@ interface GameBoardProps { devMode?: boolean; } +const collectBoardUids = (state: GameState) => { + const uids = new Set(); + const addCard = (card?: Card | null) => { + if (card?.uid !== undefined) { + uids.add(card.uid); + } + }; + + const players: Player[] = [1, 2]; + players.forEach((player) => { + state.board[player].innen.forEach(addCard); + state.board[player].aussen.forEach(addCard); + addCard(state.permanentSlots[player].government); + addCard(state.permanentSlots[player].public); + addCard(state.board[player].sofort[0]); + (state.traps[player] || []).forEach(addCard); + }); + + return uids; +}; + const useBoardSize = () => { const ref = useRef(null); const [size, setSize] = useState({ width: 0, height: 0 }); @@ -46,6 +67,52 @@ const GameBoard: React.FC = ({ const transform = useMemo(() => getUiTransform(size.width, size.height), [size.height, size.width]); const corruptionActive = (gameState as any).pendingAbilitySelect?.type === 'corruption_steal'; const corruptionTargetPlayer = gameState.current === 1 ? 2 : 1; + const [playedUids, setPlayedUids] = useState>(new Set()); + const prevBoardUidsRef = useRef>(new Set()); + const playedTimeoutsRef = useRef>(new Map()); + const hasInitializedRef = useRef(false); + + useEffect(() => { + const currentUids = collectBoardUids(gameState); + const prevUids = prevBoardUidsRef.current; + if (!hasInitializedRef.current) { + prevBoardUidsRef.current = currentUids; + hasInitializedRef.current = true; + return; + } + const newUids = Array.from(currentUids).filter((uid) => !prevUids.has(uid)); + + if (newUids.length > 0) { + setPlayedUids((prevSet) => { + const next = new Set(prevSet); + newUids.forEach((uid) => next.add(uid)); + return next; + }); + newUids.forEach((uid) => { + const existingTimeout = playedTimeoutsRef.current.get(uid); + if (existingTimeout) { + window.clearTimeout(existingTimeout); + } + const timeoutId = window.setTimeout(() => { + setPlayedUids((prevSet) => { + if (!prevSet.has(uid)) return prevSet; + const next = new Set(prevSet); + next.delete(uid); + return next; + }); + playedTimeoutsRef.current.delete(uid); + }, 900); + playedTimeoutsRef.current.set(uid, timeoutId); + }); + } + + prevBoardUidsRef.current = currentUids; + }, [gameState]); + + useEffect(() => () => { + playedTimeoutsRef.current.forEach((timeoutId) => window.clearTimeout(timeoutId)); + playedTimeoutsRef.current.clear(); + }, []); const handleHover = useCallback( (card: Card | null, event?: React.MouseEvent) => { @@ -66,12 +133,7 @@ const GameBoard: React.FC = ({ ) => (
void } - ) => ( -
onCardClick(data)} onMouseEnter={(event) => handleHover(card, event)} @@ -105,7 +167,6 @@ const GameBoard: React.FC = ({ key={key} type="button" className={`game-board__slot${highlight ? ' game-board__slot--corruption' : ''}`} - className="game-board__slot" style={style} onClick={onClick} onMouseLeave={() => onCardHover(null)} @@ -137,10 +198,12 @@ const GameBoard: React.FC = ({ isCorruptionTarget, ); } - return renderCard(card, style, { type: 'board_card', player, lane, index, card }, { highlight: isCorruptionTarget }); - ); - } - return renderCard(card, style, { type: 'board_card', player, lane, index, card }); + return renderCard( + card, + style, + { type: 'board_card', player, lane, index, card }, + { highlight: isCorruptionTarget }, + ); }); }; diff --git a/src/hooks/useGameEffects.ts b/src/hooks/useGameEffects.ts index f9d0e45c6..f1cc3ecb8 100644 --- a/src/hooks/useGameEffects.ts +++ b/src/hooks/useGameEffects.ts @@ -1,5 +1,21 @@ import { useCallback } from 'react'; -import { GameState, Card, Player, PoliticianCard, SpecialCard } from '../types/game'; +import { Card, GameState, Player, PoliticianCard, SpecialCard } from '../types/game'; +import { ActiveAbilitiesManager, EffectQueueManager, adjustInfluence, drawCards, hasDiplomatCard, sumRow } from '../utils/gameUtils'; +import { getCardDetails } from '../data/cardDetails'; + +type GameEffectLoggers = { + logFunctionCall?: (functionName: string, params: any, context: string) => void; + logCardEffect?: (cardName: string, message: string) => void; + logDataFlow?: (from: string, to: string, data: any, action: string) => void; + logWarning?: (warning: string, context: string) => void; +}; + +export function useGameEffects( + gameState: GameState, + setGameState: React.Dispatch>, + log: (msg: string) => void, + loggers: GameEffectLoggers = {}, +) { const { logFunctionCall = () => {}, logCardEffect = () => {}, @@ -10,8 +26,7 @@ import { GameState, Card, Player, PoliticianCard, SpecialCard } from '../types/g const executeCardEffect = useCallback(( card: Card, player: Player, - state: GameState, - logFunc: (msg: string) => void + state: GameState ): GameState => { let newState = { ...state }; @@ -272,32 +287,14 @@ import { GameState, Card, Player, PoliticianCard, SpecialCard } from '../types/g if (!flags || flags.diplomatInfluenceTransferUsed || flags.influenceTransferBlocked) return prev; if (!hasDiplomatCard(player, prev)) return prev; - const governmentCards = prev.board[player].aussen; - const fromGovCard = governmentCards.find(c => c.uid === fromCardUid && c.kind === 'pol') as PoliticianCard; - const toGovCard = governmentCards.find(c => c.uid === toCardUid && c.kind === 'pol') as PoliticianCard; - // Finde beide Karten in der Regierungsreihe - const govCards = prev.board[player].aussen; - const fromCard = govCards.find(c => c.uid === fromCardUid && c.kind === 'pol') as PoliticianCard; - const toCard = govCards.find(c => c.uid === toCardUid && c.kind === 'pol') as PoliticianCard; - - if (!fromGovCard || !toGovCard || fromGovCard.influence < amount) return prev; - - adjustInfluence(fromGovCard, -amount, 'Diplomat-Transfer'); - adjustInfluence(toGovCard, amount, 'Diplomat-Transfer'); - // Transfer durchführen - adjustInfluence(fromCard, -amount, 'Diplomat-Transfer'); - adjustInfluence(toCard, amount, 'Diplomat-Transfer'); - - if (!hasDiplomatCard(player, prev)) return prev; - const governmentCards = prev.board[player].aussen; const fromCard = governmentCards.find(c => c.uid === fromCardUid && c.kind === 'pol') as PoliticianCard; const toCard = governmentCards.find(c => c.uid === toCardUid && c.kind === 'pol') as PoliticianCard; if (!fromCard || !toCard || fromCard.influence < amount) return prev; - adjustInfluence(fromGovCard, -amount, 'Diplomat-Transfer'); - adjustInfluence(toGovCard, amount, 'Diplomat-Transfer'); + adjustInfluence(fromCard, -amount, 'Diplomat-Transfer'); + adjustInfluence(toCard, amount, 'Diplomat-Transfer'); const newFlags = { ...flags, diplomatInfluenceTransferUsed: true }; const newEffectFlags = { ...prev.effectFlags, [player]: newFlags } as GameState['effectFlags']; @@ -305,38 +302,17 @@ import { GameState, Card, Player, PoliticianCard, SpecialCard } from '../types/g const diplomat = governmentCards.find(c => c.kind === 'pol' && (c as PoliticianCard).tag === 'Diplomat') as PoliticianCard | undefined; if (diplomat) diplomat._activeUsed = true; - log(`P${player} transferiert ${amount} Einfluss von ${fromGovCard.name} zu ${toGovCard.name} (Diplomat).`); + log(`P${player} transferiert ${amount} Einfluss von ${fromCard.name} zu ${toCard.name} (Diplomat).`); return { ...prev, effectFlags: newEffectFlags }; }); - }, [log]); - - const resetActiveAbilities = useCallback((state: GameState): GameState => { - const newState = { ...state }; - [1, 2].forEach(player => { - const allCards = [...newState.board[player as Player].innen, ...newState.board[player as Player].aussen].filter(c => c.kind === 'pol') as PoliticianCard[]; - allCards.forEach(card => { - card._activeUsed = false; - }); - }); - - return newState; - }, []); - - const executePutinDoubleIntervention = useCallback((interventionCardIds: number[]) => { - setGameState(prev => { - const player = prev.current; - return ActiveAbilitiesManager.executePutinDoubleIntervention(prev, player, interventionCardIds, log); - }); }, [log, setGameState]); const resetActiveAbilities = useCallback((state: GameState): GameState => { const newState = { ...state }; - - // Reset _activeUsed für alle Politikerkarten [1, 2].forEach(player => { const allCards = [...newState.board[player as Player].innen, ...newState.board[player as Player].aussen].filter(c => c.kind === 'pol') as PoliticianCard[]; allCards.forEach(card => { @@ -355,17 +331,6 @@ import { GameState, Card, Player, PoliticianCard, SpecialCard } from '../types/g }); }, [log, setGameState]); - const canUsePutinDoubleIntervention = useCallback((player: Player): boolean => { - const board = gameState.board[player]; - const allCards = [...board.innen, ...board.aussen].filter(c => c.kind === 'pol') as PoliticianCard[]; - const putin = allCards.find(c => c.name === 'Vladimir Putin'); - - if (!putin || putin.deactivated || putin._activeUsed) return false; - - const interventions = gameState.hands[player].filter(c => c.kind === 'spec'); - return interventions.length >= 2; - }, [gameState]); - const canUsePutinDoubleIntervention = useCallback((player: Player): boolean => { const board = gameState.board[player]; const allCards = [...board.innen, ...board.aussen].filter(c => c.kind === 'pol') as PoliticianCard[]; diff --git a/src/ui/layout.ts b/src/ui/layout.ts index 6ce77706c..37921326e 100644 --- a/src/ui/layout.ts +++ b/src/ui/layout.ts @@ -55,7 +55,7 @@ export function getZone(id: string): UiZone { export function getUiTransform(canvasW: number, canvasH: number) { const sx = canvasW / UI_BASE.width; const sy = canvasH / UI_BASE.height; - const scale = Math.min(sx, sy); + const scale = Math.min(sx, sy, 1); const offsetX = Math.floor((canvasW - UI_BASE.width * scale) / 2); const offsetY = Math.floor((canvasH - UI_BASE.height * scale) / 2); return { scale, offsetX, offsetY }; @@ -126,4 +126,4 @@ export function getInterventionsRect(side: 'player' | 'opponent'): Rect { // capacities used by canvas/utils export function getLaneCapacity(lane: 'public' | 'government'): number { return lane === 'government' ? 5 : 3; -} \ No newline at end of file +} diff --git a/src/utils/effectUtils.ts b/src/utils/effectUtils.ts index 7042763c8..114a59543 100644 --- a/src/utils/effectUtils.ts +++ b/src/utils/effectUtils.ts @@ -208,10 +208,12 @@ export class ActiveAbilitiesManager { ...state, actionPoints: { ...state.actionPoints } }; + let applied = false; switch (ability.type) { case 'hardliner': if (select.targetCard) { + applied = true; const loc = findCardLocation(select.targetCard, state); if (loc && loc.lane === 'innen' && loc.player !== select.actorPlayer) { const updatedLane = state.board[loc.player][loc.lane].filter(c => c.uid !== select.targetCard!.uid); @@ -240,13 +242,13 @@ export class ActiveAbilitiesManager { const isGovTarget = ownGov.some(card => card.uid === select.targetCard?.uid); if (isGovTarget) { adjustInfluence(select.targetCard, 2, 'Oligarchen-Einfluss'); + applied = true; } } break; case 'diplomat_transfer': // Handled separately in useGameEffects - applied = true; break; case 'putin_double_intervention':