Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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);
Expand Down
89 changes: 76 additions & 13 deletions src/components/GameBoard.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -12,6 +12,27 @@ interface GameBoardProps {
devMode?: boolean;
}

const collectBoardUids = (state: GameState) => {
const uids = new Set<number>();
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<HTMLDivElement | null>(null);
const [size, setSize] = useState({ width: 0, height: 0 });
Expand Down Expand Up @@ -46,6 +67,52 @@ const GameBoard: React.FC<GameBoardProps> = ({
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<Set<number>>(new Set());
const prevBoardUidsRef = useRef<Set<number>>(new Set());
const playedTimeoutsRef = useRef<Map<number, number>>(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) => {
Expand All @@ -66,12 +133,7 @@ const GameBoard: React.FC<GameBoardProps> = ({
) => (
<div
key={card.uid}
className={`game-board__card${options?.selected ? ' game-board__card--selected' : ''}${options?.highlight ? ' game-board__card--corruption' : ''}`}
options?: { selected?: boolean; showActivate?: boolean; onActivate?: () => void }
) => (
<div
key={card.uid}
className={`game-board__card${options?.selected ? ' game-board__card--selected' : ''}`}
className={`game-board__card${options?.selected ? ' game-board__card--selected' : ''}${options?.highlight ? ' game-board__card--corruption' : ''}${playedUids.has(card.uid) ? ' game-board__card--played' : ''}`}
style={style}
onClick={() => onCardClick(data)}
onMouseEnter={(event) => handleHover(card, event)}
Expand Down Expand Up @@ -105,7 +167,6 @@ const GameBoard: React.FC<GameBoardProps> = ({
key={key}
type="button"
className={`game-board__slot${highlight ? ' game-board__slot--corruption' : ''}`}
className="game-board__slot"
style={style}
onClick={onClick}
onMouseLeave={() => onCardHover(null)}
Expand Down Expand Up @@ -137,10 +198,12 @@ const GameBoard: React.FC<GameBoardProps> = ({
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 },
);
});
};

Expand Down
77 changes: 21 additions & 56 deletions src/hooks/useGameEffects.ts
Original file line number Diff line number Diff line change
@@ -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<React.SetStateAction<GameState>>,
log: (msg: string) => void,
loggers: GameEffectLoggers = {},
) {
const {
logFunctionCall = () => {},
logCardEffect = () => {},
Expand All @@ -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 };

Expand Down Expand Up @@ -272,71 +287,32 @@ 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'];

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 => {
Expand All @@ -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[];
Expand Down
4 changes: 2 additions & 2 deletions src/ui/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -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;
}
}
4 changes: 3 additions & 1 deletion src/utils/effectUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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':
Expand Down