This commit is contained in:
pb-coding 2023-09-23 12:18:34 +02:00
parent f5d58c1541
commit 1e549de4fb
15 changed files with 479 additions and 32 deletions

View file

@ -1,15 +1,19 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { socket } from "./socket"; import { socket } from "./socket";
import ThreeScene from "./components/ThreeScene"; import { Object3D, Mesh } from "three";
import { Canvas } from "@react-three/fiber";
import { OrbitControls, useGLTF, PerspectiveCamera } from "@react-three/drei";
import { ConnectionState } from "./components/ConnectionState"; import { ConnectionState } from "./components/ConnectionState";
import { ConnectionManager } from "./components/ConnectionManager"; import { ConnectionManager } from "./components/ConnectionManager";
import { JoinSession } from "./components/JoinSession"; import { JoinSession } from "./components/JoinSession";
import { Events } from "./components/Events"; import { Events } from "./components/Events";
import { Game } from "./types/gameTypes"; import { Game } from "./types/gameTypes";
import Action from "./components/Action"; import Action from "./components/Action";
import CardStack from "./components/CardStack"; import CardStack from "./components/CardStackOld";
import DepositCards from "./components/DepositCards"; import DepositCards from "./components/DepositCardsOld";
import CardCache from "./components/CardCache"; import CardCache from "./components/CardCacheOld";
import { extractCurrentPlayer } from "./helpers";
import PlayArea from "./components/PlayArea";
export default function App() { export default function App() {
const [isConnected, setIsConnected] = useState(socket.connected); const [isConnected, setIsConnected] = useState(socket.connected);
@ -22,7 +26,7 @@ export default function App() {
const showStartGameButton = session !== "" && clientsInRoom >= 2; const showStartGameButton = session !== "" && clientsInRoom >= 2;
const showNextGameButton = gameData?.phase === "new round"; const showNextGameButton = gameData?.phase === "new round";
const playersData = extractMyData(gameData); const playersData = extractCurrentPlayer(gameData);
function startGame() { function startGame() {
socket.emit("new-game", { sessionId: session }); socket.emit("new-game", { sessionId: session });
@ -37,11 +41,6 @@ export default function App() {
socket.emit("click-card", cardPosition); socket.emit("click-card", cardPosition);
} }
function extractMyData(gameData: Game | null) {
if (!gameData) return undefined;
return gameData.players.find((player) => player.socketId === socket.id);
}
function setTempMessage(message: string) { function setTempMessage(message: string) {
setMessageDisplay(message); setMessageDisplay(message);
setTimeout(() => { setTimeout(() => {
@ -49,16 +48,6 @@ export default function App() {
}, 3000); }, 3000);
} }
useEffect(() => {
if (gameData?.phase === "game-over") {
setTempMessage("Game Over");
} else if (gameData?.phase === "game-started") {
setTempMessage("Game Started");
} else if (gameData?.phase === "waiting-for-players") {
setTempMessage("Waiting for players");
}
}, [gameData]);
useEffect(() => { useEffect(() => {
function onConnect() { function onConnect() {
setIsConnected(true); setIsConnected(true);
@ -96,9 +85,57 @@ export default function App() {
}; };
}, []); }, []);
// Three-Fiber
const tableModel = useGLTF("/models/table.glb");
const heightProportion = 1.25;
return ( return (
<div className="App"> <div className="App">
<ThreeScene gameData={gameData} /> <div
style={{
width: window.innerWidth,
height: window.innerHeight / heightProportion,
}}
>
<Canvas>
<PerspectiveCamera
makeDefault
manual
fov={75}
aspect={window.innerWidth / (window.innerHeight / heightProportion)}
near={0.1}
far={1000}
position={[0, 45, 10]}
// lookAt={() => new Vector3(0, 0, 0)}
/>
<ambientLight color={0xa3a3a3} intensity={0.1} />
<directionalLight
color={0xffffff}
position={[0, 50, 20]}
castShadow
shadow-mapSize={[1024, 1024]}
/>
<mesh>
<boxGeometry args={[2, 2, 2]} />
<meshStandardMaterial color={0x00ff00} />
</mesh>
<primitive
object={tableModel.scene}
position={[0, 1.8, 0]}
scale={[2, 2, 2]}
traverse={(node: Object3D) => {
if (node instanceof Mesh) {
// node.castShadow = true;
node.receiveShadow = true;
}
}}
/>
<gridHelper args={[100, 100]} />
<axesHelper args={[5]} />
<OrbitControls />
<PlayArea gameData={gameData} />
</Canvas>
</div>
<ConnectionState <ConnectionState
isConnected={isConnected} isConnected={isConnected}
session={session} session={session}

View file

@ -1,21 +1,42 @@
import { FC } from "react"; import { FC, useEffect, useState } from "react";
import { Vector3, Object3D } from "three";
import { socket } from "../socket";
import { createCard } from "../objects/cards";
import { Player } from "../types/gameTypes"; import { Player } from "../types/gameTypes";
type CardCacheProps = { type CardCacheProps = {
playersData: Player | undefined; playersData: Player[];
}; };
const CardCache: FC<CardCacheProps> = ({ playersData }) => { const CardCache: FC<CardCacheProps> = ({ playersData }) => {
if (!playersData?.cardCache) { const [cardCacheCard, setCardCacheCard] = useState<Object3D | null>(null);
return null;
}
return ( const updateCardCache = (playerData: Player) => {
<div> if (playerData.cardCache == null) {
<p>Card Cache: {playersData.cardCache.value}</p> setCardCacheCard(null);
</div> return;
}
const showFaceUp = true;
const card = createCard(
playerData.cardCache,
new Vector3(9, 20, 4),
showFaceUp
); );
setCardCacheCard(card);
};
useEffect(() => {
const playerData = playersData.find(
(player) => player.socketId === socket.id
);
if (!playerData) return;
updateCardCache(playerData);
}, [playersData]);
if (!cardCacheCard) return null;
return <primitive object={cardCacheCard} />;
}; };
export default CardCache; export default CardCache;

View file

@ -0,0 +1,21 @@
import { FC } from "react";
import { Player } from "../types/gameTypes";
type CardCacheProps = {
playersData: Player | undefined;
};
const CardCache: FC<CardCacheProps> = ({ playersData }) => {
if (!playersData?.cardCache) {
return null;
}
return (
<div>
<p>Card Cache: {playersData.cardCache.value}</p>
</div>
);
};
export default CardCache;

View file

@ -0,0 +1,27 @@
import { FC, useState, useEffect } from "react";
import { Object3D, Object3DEventMap } from "three";
import { socket } from "../socket";
type CardStackCardProps = {
card: Object3D<Object3DEventMap>;
isUppermostCard: boolean;
};
const CardStackCard: FC<CardStackCardProps> = ({ card, isUppermostCard }) => {
const [cardObject, setCardObject] =
useState<Object3D<Object3DEventMap>>(card);
useEffect(() => {
if (cardObject.name === card.name) return;
setCardObject(card);
}, [card, cardObject]);
const clickCard = () => {
if (!isUppermostCard) return;
console.log("Draw card");
socket.emit("draw-from-card-stack", "draw card");
};
return <primitive object={cardObject} onClick={() => clickCard()} />;
};
export default CardStackCard;

View file

@ -0,0 +1,45 @@
import { FC, useEffect, useState } from "react";
import { Vector3, Object3D } from "three";
import { CardStack } from "../types/gameTypes";
import { createCardStaple } from "../objects/cards";
import CardStackCard from "./CardStackCard";
type CardStackProps = {
cardStackData: CardStack | null;
};
const CardStackStaple: FC<CardStackProps> = ({ cardStackData }) => {
const [cardStack, setCardStack] = useState<Object3D[]>([]);
const updateCardStack = (cardStackData: CardStack) => {
const stack = createCardStaple(
cardStackData.cards,
new Vector3(-1.2, 20, 0)
);
setCardStack(stack);
};
const checkIfUppermostCard = (index: number) => {
return index === cardStack.length - 1;
};
useEffect(() => {
if (!cardStackData) return;
updateCardStack(cardStackData);
}, [cardStackData]);
if (!cardStackData) return null;
return (
<>
{cardStack.map((card, index) => (
<CardStackCard
key={index}
card={card}
isUppermostCard={checkIfUppermostCard(index)}
/>
))}
</>
);
};
export default CardStackStaple;

View file

@ -0,0 +1,48 @@
import { FC, useState, useEffect } from "react";
import { Vector3, Object3D } from "three";
import { Card } from "../types/gameTypes";
import { createCardStaple } from "../objects/cards";
import DiscardPileCard from "./DiscardPileCard";
type DiscardPileProps = {
discardPileData: Card[] | null;
};
const DiscardPile: FC<DiscardPileProps> = ({ discardPileData }) => {
const [discardPile, setDiscardPile] = useState<Object3D[]>([]);
const updateDiscardPile = (discardPileData: Card[]) => {
const showFaceUp = true;
const pile = createCardStaple(
discardPileData,
new Vector3(1.2, 20, 0),
showFaceUp
);
setDiscardPile(pile);
};
const checkIfUppermostCard = (index: number) => {
return index === discardPile.length - 1;
};
useEffect(() => {
if (!discardPileData) return;
updateDiscardPile(discardPileData);
}, [discardPileData]);
if (!discardPile) return null;
return (
<>
{discardPile.map((card, index) => (
<DiscardPileCard
key={index}
card={card}
isUppermostCard={checkIfUppermostCard(index)}
/>
))}
</>
);
};
export default DiscardPile;

View file

@ -0,0 +1,30 @@
import { FC, useState, useEffect } from "react";
import { Object3D, Object3DEventMap } from "three";
import { socket } from "../socket";
type DiscardPileCardProps = {
card: Object3D<Object3DEventMap>;
isUppermostCard: boolean;
};
const DiscardPileCard: FC<DiscardPileCardProps> = ({
card,
isUppermostCard,
}) => {
const [cardObject, setCardObject] =
useState<Object3D<Object3DEventMap>>(card);
useEffect(() => {
if (cardObject.name === card.name) return;
setCardObject(card);
}, [card, cardObject]);
const clickCard = () => {
if (!isUppermostCard) return;
console.log("Draw card");
socket.emit("draw-from-card-stack", "draw card");
};
return <primitive object={cardObject} onClick={() => clickCard()} />;
};
export default DiscardPileCard;

View file

@ -0,0 +1,26 @@
import { FC } from "react";
import PlayerCards from "../components/PlayerCards";
import { Game } from "../types/gameTypes";
import CardStackStaple from "./CardStackStaple";
import DiscardPile from "./DiscardPile";
import CardCache from "./CardCache";
type PlayAreaProps = {
gameData: Game | null;
};
const PlayArea: FC<PlayAreaProps> = ({ gameData }) => {
if (!gameData) return null;
return (
<>
<PlayerCards playersData={gameData.players} />
<CardStackStaple cardStackData={gameData.cardStack} />
<DiscardPile discardPileData={gameData.discardPile} />
<CardCache playersData={gameData.players} />
</>
);
};
export default PlayArea;

View file

@ -0,0 +1,69 @@
import { FC, useState, useEffect } from "react";
import { Object3D, Object3DEventMap } from "three";
// import { useFrame } from "@react-three/fiber";
import { socket } from "../socket";
import { PlayerWithVisualCards } from "../types/gameTypes";
type PlayerCardProps = {
card: Object3D<Object3DEventMap>;
index: number;
isCurrentPlayer: boolean;
playerWithCards: PlayerWithVisualCards;
};
const PlayerCard: FC<PlayerCardProps> = ({
card,
index,
isCurrentPlayer,
playerWithCards,
}) => {
const isCardRevealed = playerWithCards.player.knownCardPositions[index];
const [cardObject, setCardObject] =
useState<Object3D<Object3DEventMap>>(card);
useEffect(() => {
if (cardObject.name === card.name) return;
setCardObject(card);
}, [card, cardObject]);
// TODO: Fix stuttering card rotation
/*const [rotationGoal, setRotationGoal] = useState(0);
const rotationSpeed = 0.05; // Adjust for faster/slower flip
const currentRotation = useRef<number>(0);
useFrame(() => {
if (currentRotation.current < rotationGoal) {
card.rotation.x += rotationSpeed;
currentRotation.current += rotationSpeed;
if (currentRotation.current >= rotationGoal) {
card.rotation.x = rotationGoal;
}
} else if (currentRotation.current > rotationGoal) {
card.rotation.x -= rotationSpeed;
currentRotation.current -= rotationSpeed;
if (currentRotation.current <= rotationGoal) {
card.rotation.x = rotationGoal;
}
}
});*/
if (isCardRevealed) {
card.rotation.x = Math.PI;
}
const clickCard = () => {
if (!isCurrentPlayer) return;
console.log("Clicked on one of my cards");
socket.emit("click-card", index, (response: string) => {
console.log("Response:", response);
/*if (rotationGoal === 0) {
setRotationGoal(Math.PI);
} else {
setRotationGoal(0);
}*/
});
};
return <primitive object={cardObject} onClick={() => clickCard()} />;
};
export default PlayerCard;

View file

@ -0,0 +1,73 @@
import { FC, useState, useEffect } from "react";
import { socket } from "../socket";
import { Vector3 } from "three";
import { createPlayerCards } from "../objects/cards";
import PlayerCard from "./PlayerCard";
import { PlayerWithVisualCards, Player } from "../types/gameTypes";
type PlayerCardsProps = {
playersData: Player[];
};
const PlayerCards: FC<PlayerCardsProps> = ({ playersData }) => {
const [playersWithCards, setPlayersWithCards] = useState<
PlayerWithVisualCards[]
>([]);
const updatePlayerCards = (playersData: Player[]) => {
const playersWithCards: PlayerWithVisualCards[] = [];
const currentPlayerWithCards: PlayerWithVisualCards[] = [];
let nonCurrentPlayerIndex = 1;
playersData.forEach((player) => {
const positionOffset = 12;
const playerWithCards: PlayerWithVisualCards = {
player,
cards: [],
};
if (player.socketId === socket.id) {
playerWithCards.cards = createPlayerCards(
player.cards,
new Vector3(0, 20, positionOffset)
);
currentPlayerWithCards.push(playerWithCards);
} else {
const playerOffset = positionOffset + nonCurrentPlayerIndex * -20;
playerWithCards.cards = createPlayerCards(
player.cards,
new Vector3(0, 20, playerOffset)
);
playersWithCards.push(playerWithCards);
nonCurrentPlayerIndex++;
}
});
setPlayersWithCards([...currentPlayerWithCards, ...playersWithCards]);
};
useEffect(() => {
if (!playersData) return;
updatePlayerCards(playersData);
}, [playersData]);
if (!playersWithCards) return null;
return (
<>
{playersWithCards.map((playerWithCards, playerIndex) => (
<>
{playerWithCards.cards.map((card, index) => (
<PlayerCard
key={index}
card={card}
playerWithCards={playerWithCards}
index={index}
isCurrentPlayer={playerIndex === 0}
/>
))}
</>
))}
</>
);
};
export default PlayerCards;

7
src/helpers.ts Normal file
View file

@ -0,0 +1,7 @@
import { Game } from "./types/gameTypes";
import { socket } from "./socket";
export function extractCurrentPlayer(gameData: Game | null) {
if (!gameData) return undefined;
return gameData.players.find((player) => player.socketId === socket.id);
}

View file

@ -77,7 +77,11 @@ const getCardTexture = (value: number | string) => {
return cardTexture; return cardTexture;
}; };
export const createCard = (cardData: Card, position: Vector3) => { export const createCard = (
cardData: Card,
position: Vector3,
faceUp: boolean = false
) => {
const cardMaterial = [ const cardMaterial = [
new MeshBasicMaterial(), new MeshBasicMaterial(),
new MeshBasicMaterial(), new MeshBasicMaterial(),
@ -89,6 +93,12 @@ export const createCard = (cardData: Card, position: Vector3) => {
const card = new Mesh(cardGeometry, cardMaterial); const card = new Mesh(cardGeometry, cardMaterial);
card.name = cardData.name; card.name = cardData.name;
card.position.copy(position); card.position.copy(position);
if (faceUp) {
card.rotation.x = Math.PI;
console.log("faceUp");
}
return card; return card;
}; };
@ -117,3 +127,29 @@ export const createPlayerCards = (
}); });
return playerCards; return playerCards;
}; };
export const createCardStaple = (
cards: Card[],
positionReference: Vector3,
faceUp: boolean = false
) => {
const cardStackCards: Mesh<
BoxGeometry,
MeshBasicMaterial[],
Object3DEventMap
>[] = [];
cards.forEach((card: Card, index: number) => {
const cardPositionX = positionReference.x;
const cardPositionY = positionReference.y + index * 0.01;
const cardPositionZ = positionReference.z;
const cardPosition = new Vector3(
cardPositionX,
cardPositionY,
cardPositionZ
);
const cardStackCard = createCard(card, cardPosition, faceUp);
cardStackCards.push(cardStackCard);
});
return cardStackCards;
};

View file

@ -1,3 +1,5 @@
import { Object3D } from "three";
export type Player = { export type Player = {
id: number; id: number;
socketId: string; socketId: string;
@ -12,6 +14,11 @@ export type Player = {
closedRound: boolean; closedRound: boolean;
}; };
export type PlayerWithVisualCards = {
player: Player;
cards: Object3D[];
};
export type CardValue = export type CardValue =
| -2 | -2
| -1 | -1