refactors cards to nested array

This commit is contained in:
pb-coding 2023-10-02 14:47:13 +02:00
parent ab1bad8842
commit 7b9e05b122
14 changed files with 1801 additions and 356 deletions

4
.env
View file

@ -1,5 +1,5 @@
VITE_ENVIRONMENT=dev
VITE_BACKEND_URL=https://skyjo-backend.voltvector.org
#VITE_ENVIRONMENT=local
#VITE_BACKEND_URL=http://localhost:3001
VITE_ENVIRONMENT=local
VITE_BACKEND_URL=http://localhost:3001

View file

@ -7,9 +7,9 @@
rel="stylesheet"
as="style"
/>
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/card-back.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Play Skyjo!</title>
<title>Play Skylo!</title>
</head>
<body>
<div id="root"></div>

1631
public/card-back.svg Normal file

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 93 KiB

View file

@ -62,8 +62,6 @@ export default function App() {
};
}, []);
console.log(messageDispaly);
return (
<div className="bg-gray-900 w-screen h-screen">
{!gameData && (

View file

@ -60,8 +60,8 @@ export const Footer: FC<Footer> = ({
</Button>
</div>
</div>
{gameData.players.map((player) => (
<div className="mb-3 pt-2 mt-2 border-t border-gray-600">
{gameData.players.map((player, index) => (
<div key={index} className="mb-3 pt-2 mt-2 border-t border-gray-600">
<div className="flex justify-between items-center">
<Text>
{player?.name} {player.socketId == socket.id && "👤"}{" "}

View file

@ -1,6 +1,6 @@
import { FC } from "react";
import PlayerCards from "../components/PlayerCards";
import PlayerDecks from "./PlayerDecks";
import { Game } from "../types/gameTypes";
import CardStackStaple from "./CardStackStaple";
import DiscardPile from "./DiscardPile";
@ -14,7 +14,7 @@ const PlayArea: FC<PlayAreaProps> = ({ gameData }) => {
return (
<>
<PlayerCards playersData={gameData.players} />
<PlayerDecks playersData={gameData.players} />
<CardStackStaple cardStackData={gameData.cardStack} />
<DiscardPile discardPileData={gameData.discardPile} />
</>

View file

@ -2,22 +2,25 @@ 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";
import { PlayerVisualDeck } from "../types/gameTypes";
type PlayerCardProps = {
card: Object3D<Object3DEventMap>;
index: number;
columnIndex: number;
cardIndex: number;
isCurrentPlayer: boolean;
playerWithCards: PlayerWithVisualCards;
visualPlayerDeck: PlayerVisualDeck;
};
const PlayerCard: FC<PlayerCardProps> = ({
card,
index,
columnIndex,
cardIndex,
isCurrentPlayer,
playerWithCards,
visualPlayerDeck,
}) => {
const isCardRevealed = playerWithCards.player.knownCardPositions[index];
const isCardRevealed =
visualPlayerDeck.player.knownCardPositions[columnIndex][cardIndex];
const [cardObject, setCardObject] =
useState<Object3D<Object3DEventMap>>(card);
@ -54,16 +57,15 @@ const PlayerCard: FC<PlayerCardProps> = ({
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);
}*/
});
socket.emit("click-card", [columnIndex, cardIndex]);
};
return <primitive object={cardObject} onClick={() => clickCard()} />;
return (
<primitive
key={columnIndex + cardIndex * 4}
object={cardObject}
onClick={() => clickCard()}
/>
);
};
export default PlayerCard;

View file

@ -1,81 +0,0 @@
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";
import CardCache from "./CardCache";
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 = 14;
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}
/>
))}
<CardCache
playerData={playerWithCards.player}
// current player is always at index 0
position={new Vector3(9, 20, 6 - playerIndex * 12)}
/>
</>
))}
</>
);
};
export default PlayerCards;
// ich (0) --> 4
// andere (1) --> -8

View file

@ -0,0 +1,86 @@
import { FC, useState, useEffect } from "react";
import { socket } from "../socket";
import { Vector3 } from "three";
import { createPlayerCards as createPlayerDeck } from "../objects/cards";
import PlayerCard from "./PlayerCard";
import { PlayerVisualDeck, Player } from "../types/gameTypes";
import CardCache from "./CardCache";
type PlayerDecksProps = {
playersData: Player[];
};
const PlayerDecks: FC<PlayerDecksProps> = ({ playersData }) => {
const [visualPlayerDecks, setVisualPlayerDecks] = useState<
PlayerVisualDeck[]
>([]);
const updatePlayerCards = (playersData: Player[]) => {
const visualPlayerDecks: PlayerVisualDeck[] = [];
const currentVisualPlayerDeck: PlayerVisualDeck[] = [];
let nonCurrentPlayerIndex = 1;
playersData.forEach((player) => {
const positionOffset = 14;
const playerVisualDeck: PlayerVisualDeck = {
player,
visualDeck: [],
};
if (player.socketId === socket.id) {
playerVisualDeck.visualDeck = createPlayerDeck(
player.deck,
new Vector3(0, 20, positionOffset)
);
currentVisualPlayerDeck.push(playerVisualDeck);
} else {
const playerOffset = positionOffset + nonCurrentPlayerIndex * -20;
playerVisualDeck.visualDeck = createPlayerDeck(
player.deck,
new Vector3(0, 20, playerOffset)
);
visualPlayerDecks.push(playerVisualDeck);
nonCurrentPlayerIndex++;
}
});
setVisualPlayerDecks([...currentVisualPlayerDeck, ...visualPlayerDecks]);
};
useEffect(() => {
if (!playersData) return;
updatePlayerCards(playersData);
}, [playersData]);
if (!visualPlayerDecks) return null;
return (
<>
{visualPlayerDecks.map((visualPlayerDeck, playerIndex) => (
<>
{visualPlayerDeck.visualDeck.map((column, columnIndex) => (
<>
{column.map((card, cardIndex) => (
<PlayerCard
key={columnIndex + cardIndex * 4}
card={card}
visualPlayerDeck={visualPlayerDeck}
columnIndex={columnIndex}
cardIndex={cardIndex}
isCurrentPlayer={playerIndex === 0}
/>
))}
</>
))}
<CardCache
playerData={visualPlayerDeck.player}
// current player is always at index 0
position={new Vector3(9, 20, 6 - playerIndex * 12)}
/>
</>
))}
</>
);
};
export default PlayerDecks;
// ich (0) --> 4
// andere (1) --> -8

View file

@ -13,6 +13,8 @@ type SessionManagerProps = {
showStartGameButton: boolean;
};
type SessionResponse = "success" | "error:full" | "error:running";
export const SessionManager: FC<SessionManagerProps> = ({
isConnected,
clientsInRoom,
@ -25,9 +27,11 @@ export const SessionManager: FC<SessionManagerProps> = ({
function joinSession(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
socket.emit("join-session", sessionField);
setSession(sessionField);
setSessionField("");
socket.emit("join-session", sessionField, (response: SessionResponse) => {
if (response !== "success") return;
setSession(sessionField);
setSessionField("");
});
}
function leaveSession(sessionName: string) {
@ -52,10 +56,10 @@ export const SessionManager: FC<SessionManagerProps> = ({
>
<div className="py-8 px-4 mx-auto max-w-screen-xl text-center lg:py-16 z-10 relative">
<h1 className="mb-4 text-4xl font-extrabold tracking-tight leading-none md:text-5xl lg:text-6xl text-white">
Skyjo
Skylo
</h1>
<p className="mb-8 text-lg font-normal lg:text-xl sm:px-16 lg:px-48 text-gray-200">
Play Skyjo online with your friends!
Play Skylo online with your friends!
</p>
<div className="p-4">
{!isActiveSession && (
@ -64,7 +68,7 @@ export const SessionManager: FC<SessionManagerProps> = ({
htmlFor="first_name"
className="block mb-2 text-sm font-medium text-white"
>
Join Skyjo Session
Join Skylo Session
</label>
<div className="flex space-x-1 items-center">
<input

View file

@ -1,198 +0,0 @@
import { useRef, useEffect, FC } from "react";
import {
Scene,
PerspectiveCamera,
WebGLRenderer,
Vector3,
DirectionalLight,
DirectionalLightHelper,
AmbientLight,
Object3D,
Mesh,
GridHelper,
AxesHelper,
Material,
} from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { useGLTF } from "@react-three/drei";
import { createCube } from "../objects/cube";
import { createPlayerCards, createCard } from "../objects/cards";
import { Game, Player } from "../types/gameTypes";
import { socket } from "../socket";
type ThreeSceneProps = {
gameData: Game | null;
};
const ThreeScene: FC<ThreeSceneProps> = ({ gameData }) => {
const containerRef = useRef<HTMLDivElement | null>(null);
const cardsRef = useRef<Mesh[]>([]);
const tableModel = useGLTF("/models/table.glb");
const heightProportion = 1.25;
function extractCurrentPlayer(gameData: Game | null): Player | undefined {
if (!gameData) return undefined;
return gameData.players.find((player) => player.socketId === socket.id);
}
function disposeMesh(mesh: THREE.Mesh) {
if (mesh.material instanceof Material) {
mesh.material.dispose();
} else if (Array.isArray(mesh.material)) {
for (const material of mesh.material) {
material.dispose();
}
}
mesh.geometry.dispose();
}
useEffect(() => {
const container = containerRef.current;
const scene = new Scene();
const camera = new PerspectiveCamera(
75,
window.innerWidth / (window.innerHeight / heightProportion),
0.1,
1000
);
const renderer = new WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight / heightProportion);
container && container.appendChild(renderer.domElement);
// Objects
const cubeCenter = createCube(new Vector3(1, 1, 1), 0x00ff00);
cubeCenter.position.set(0, 20, 0);
const cubeTL = createCube(new Vector3(1, 1, 1), 0x00ff00);
cubeTL.position.set(-10, 20, -15);
const cubeTR = createCube(new Vector3(1, 1, 1), 0x00ff00);
cubeTR.position.set(10, 20, -15);
const cubeBL = createCube(new Vector3(1, 1, 1), 0x00ff00);
cubeBL.position.set(-10, 20, 15);
const cubeBR = createCube(new Vector3(1, 1, 1), 0x00ff00);
cubeBR.position.set(10, 20, 15);
scene.add(cubeCenter, cubeBL, cubeBR, cubeTL, cubeTR);
// Cards
/*const initialCards: Card[] = [];
for (let i = 1; i < 13; i++) {
initialCards.push({ id: i, name: `${i} Card`, value: i as CardValue });
}
const playerCards = createPlayerCards(initialCards, new Vector3(0, 20, 0));
scene.add(...playerCards);*/
const currentPlayer = extractCurrentPlayer(gameData);
if (currentPlayer && !cardsRef.current.length) {
cardsRef.current = createPlayerCards(
currentPlayer.cards,
new Vector3(0, 20, 0)
);
scene.add(...cardsRef.current);
}
// Camera
camera.position.set(0, 45, 10);
camera.lookAt(0, 0, 0);
// Orbit Controls
const orbit = new OrbitControls(camera, renderer.domElement);
orbit.update();
// Lights
const directionalLight = new DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(0, 50, 20);
scene.add(directionalLight);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 1024;
directionalLight.shadow.mapSize.height = 1024;
const ambientLight = new AmbientLight(0xa3a3a3, 0.3);
scene.add(ambientLight);
// Import models
const table = tableModel.scene;
scene.add(table);
// table.rotateY(Math.PI / 2);
table.scale.set(2, 2, 2);
table.position.set(0, 1.8, 0);
table.traverse(function (node: Object3D) {
if (node instanceof Mesh) {
// node.castShadow = true;
node.receiveShadow = true;
}
});
// Helpers
const gridHelper = new GridHelper(100, 100);
scene.add(gridHelper);
const axesHelper = new AxesHelper(5);
scene.add(axesHelper);
const directionalLightHelper = new DirectionalLightHelper(
directionalLight,
5
);
scene.add(directionalLightHelper);
// Animation
const animate = () => {
requestAnimationFrame(animate);
// cube.rotation.x += 0.01;
// cube.rotation.y += 0.01;
const currentPlayer = extractCurrentPlayer(gameData);
if (currentPlayer) {
if (cardsRef.current.length === 0) {
cardsRef.current = createPlayerCards(
currentPlayer.cards,
new Vector3(0, 20, 0)
);
scene.add(...cardsRef.current);
} else {
cardsRef.current.forEach((card, index) => {
if (card.name !== currentPlayer.cards[index]?.name) {
disposeMesh(card);
scene.remove(card);
cardsRef.current[index] = createCard(
currentPlayer.cards[index],
card.position
);
scene.add(cardsRef.current[index]);
}
});
}
//const cards = createPlayerCards(currentPlayer.cards);
//scene.add(...cards);
}
renderer.render(scene, camera);
};
console.log("Animate");
animate();
return () => {
// Clean up on unmount
renderer.dispose();
// scene.dispose();
// material.dispose();
// geometry.dispose();
container && container.removeChild(renderer.domElement);
};
}, [gameData]);
return <div ref={containerRef}></div>;
};
export default ThreeScene;

View file

@ -6,9 +6,10 @@ import {
Vector3,
Mesh,
Object3DEventMap,
Object3D,
} from "three";
import { Card } from "../types/gameTypes";
import { Card, Deck, VisualColumn, VisualDeck } from "../types/gameTypes";
const textureLoader = new TextureLoader();
const cardSize = 5;
@ -18,7 +19,7 @@ const cardGeometry = new BoxGeometry(
cardSize * 0.6
);
const getCardTexture = (value: number | string) => {
const getCardTexture = (value: number | null) => {
let cardTexture;
switch (value) {
case -2:
@ -66,7 +67,7 @@ const getCardTexture = (value: number | string) => {
case 12:
cardTexture = textureLoader.load("/textures/card-12.png");
break;
case "X":
case null:
cardTexture = textureLoader.load("/textures/card-back.png");
break;
default:
@ -78,54 +79,53 @@ const getCardTexture = (value: number | string) => {
};
export const createCard = (
cardData: Card,
card: Card,
position: Vector3,
faceUp: boolean = false
) => {
const cardMaterial = [
new MeshBasicMaterial(),
new MeshBasicMaterial(),
new MeshBasicMaterial({ map: getCardTexture("X") }), // X = backside
new MeshBasicMaterial({ map: getCardTexture(cardData.value) }),
new MeshBasicMaterial({ map: getCardTexture(null) }), // X = backside
new MeshBasicMaterial({ map: getCardTexture(card) }),
new MeshBasicMaterial(),
new MeshBasicMaterial(),
];
const card = new Mesh(cardGeometry, cardMaterial);
card.name = cardData.name;
card.position.copy(position);
const visualCard = new Mesh(cardGeometry, cardMaterial);
visualCard.name = card !== null ? card.toString() : "Facedown card";
visualCard.position.copy(position);
if (faceUp) {
card.rotation.x = Math.PI;
console.log("faceUp");
visualCard.rotation.x = Math.PI;
}
return card;
return visualCard;
};
export const createPlayerCards = (
cards: Card[],
positionReference: Vector3
) => {
const playerCards: Mesh<
BoxGeometry,
MeshBasicMaterial[],
Object3DEventMap
>[] = [];
cards.forEach((card, index) => {
const cardPositionX = positionReference.x + (index % 4) * 4 - 6;
const cardPositionY = positionReference.y;
const cardPositionZ =
positionReference.z + (Math.ceil((index + 1) / 4) - 1) * 4 - 8;
export const createPlayerCards = (deck: Deck, positionReference: Vector3) => {
const playerDeck: Object3D[][] = [];
const cardPosition = new Vector3(
cardPositionX,
cardPositionY,
cardPositionZ
);
const playerCard = createCard(card, cardPosition);
playerCards.push(playerCard);
let visualColumn: Object3D[] = [];
deck.forEach((column, columnIndex) => {
column.forEach((card, cardIndex) => {
const cardPositionX = positionReference.x + columnIndex * 4 - 6;
const cardPositionY = positionReference.y;
const cardPositionZ = positionReference.z + cardIndex * 4 - 8;
const cardPosition = new Vector3(
cardPositionX,
cardPositionY,
cardPositionZ
);
const playerCard = createCard(card, cardPosition);
visualColumn.push(playerCard);
if (cardIndex === 2) {
playerDeck.push(visualColumn as VisualColumn);
visualColumn = [];
}
});
});
return playerCards;
return playerDeck as VisualDeck;
};
export const createCardStaple = (

View file

@ -4,8 +4,8 @@ export type Player = {
id: number;
socketId: string;
name: string;
cards: Card[];
knownCardPositions: boolean[];
deck: Deck;
knownCardPositions: KnownCardsColumn[];
playersTurn: boolean;
cardCache: Card | null;
tookDispiledCard: boolean;
@ -15,12 +15,21 @@ export type Player = {
place: number;
};
export type PlayerWithVisualCards = {
export type PlayerVisualDeck = {
player: Player;
cards: Object3D[];
visualDeck: VisualDeck;
};
export type CardValue =
export type VisualColumn = [Object3D, Object3D, Object3D];
export type VisualDeck = VisualColumn[];
export type Column = [Card, Card, Card];
export type KnownCardsColumn = [boolean, boolean, boolean];
export type Deck = Column[];
export type Card =
| -2
| -1
| 0
@ -36,13 +45,7 @@ export type CardValue =
| 10
| 11
| 12
| "X";
export type Card = {
id: number;
name: string;
value: CardValue;
};
| null;
export type CardStack = {
cards: Card[];