mirror of
https://github.com/HexCardGames/HexDeck.git
synced 2025-09-03 18:48:38 +02:00
feat(frontend): implement basic card rendering, drawing and playing
This commit is contained in:
109
frontend/src/components/Cards/ClassicCard.svelte
Normal file
109
frontend/src/components/Cards/ClassicCard.svelte
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Card } from "../../stores/sessionStore";
|
||||||
|
|
||||||
|
interface ClassicCard {
|
||||||
|
Symbol: string;
|
||||||
|
Color: string;
|
||||||
|
}
|
||||||
|
export let data: ClassicCard = { Color: "", Symbol: "" };
|
||||||
|
export let canUpdateCard: boolean = false;
|
||||||
|
export let updateCard: (newCard: Card) => void = () => {};
|
||||||
|
export let width: number;
|
||||||
|
export let height: number;
|
||||||
|
|
||||||
|
const COLOR_MAP: { [key: string]: string } = {
|
||||||
|
red: "0#990000",
|
||||||
|
green: "#009900",
|
||||||
|
blue: "#000099",
|
||||||
|
yellow: "#999900",
|
||||||
|
black: "#000000",
|
||||||
|
"": "#808080",
|
||||||
|
};
|
||||||
|
|
||||||
|
function selectColor(color: string) {
|
||||||
|
let newCard: ClassicCard = {
|
||||||
|
Color: color,
|
||||||
|
Symbol: data.Symbol,
|
||||||
|
};
|
||||||
|
updateCard(newCard);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="card" style:background={COLOR_MAP[data.Color]} style:--width={`${width}px`} style:--height={`${height}px`}>
|
||||||
|
{#if data.Symbol.length <= 2}
|
||||||
|
<span class="symbol">{data.Symbol}</span>
|
||||||
|
{:else}
|
||||||
|
<span class="symbol large">{data.Symbol.split(":")[1].replace("_", " ")}</span>
|
||||||
|
{/if}
|
||||||
|
{#if data.Color == "black" && canUpdateCard}
|
||||||
|
<div class="select mt-5">
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class="bg-red-400"
|
||||||
|
aria-label="Red"
|
||||||
|
on:click={() => {
|
||||||
|
selectColor("red");
|
||||||
|
}}
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
class="bg-green-400"
|
||||||
|
aria-label="Green"
|
||||||
|
on:click={() => {
|
||||||
|
selectColor("green");
|
||||||
|
}}
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class="bg-blue-400"
|
||||||
|
aria-label="Blue"
|
||||||
|
on:click={() => {
|
||||||
|
selectColor("blue");
|
||||||
|
}}
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
class="bg-yellow-400"
|
||||||
|
aria-label="Yellow"
|
||||||
|
on:click={() => {
|
||||||
|
selectColor("yellow");
|
||||||
|
}}
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: var(--width);
|
||||||
|
height: var(--height);
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select button {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card span {
|
||||||
|
text-align: left;
|
||||||
|
word-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .symbol {
|
||||||
|
font-size: 25px;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .symbol.large {
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
91
frontend/src/components/Game/CardDisplay.svelte
Normal file
91
frontend/src/components/Game/CardDisplay.svelte
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Card, CardInfoObj, CardPlayedObj } from "../../stores/sessionStore";
|
||||||
|
|
||||||
|
export let cardWidth: number;
|
||||||
|
export let cardHeight: number;
|
||||||
|
export let cards: CardInfoObj[] | CardPlayedObj[];
|
||||||
|
export let cardComponent;
|
||||||
|
export let click: (index: number) => void = () => {};
|
||||||
|
export let updateCard: (index: number, newCard: Card) => void = () => {};
|
||||||
|
export let canPlayCards: boolean = true;
|
||||||
|
export let canUpdateCards: boolean = false;
|
||||||
|
export let centerDistancePx: number;
|
||||||
|
export let maxRotationDeg: number;
|
||||||
|
export let fullwidth: boolean = false;
|
||||||
|
export let hoverOffset: number = canPlayCards ? 40 : 0;
|
||||||
|
|
||||||
|
let rotationDeg = 0;
|
||||||
|
$: rotationDeg = Math.min(maxRotationDeg, 1.5 * cards.length);
|
||||||
|
let maxOffset = [0, 0];
|
||||||
|
let offset = [0, 0];
|
||||||
|
$: if (centerDistancePx && rotationDeg) maxOffset = getCardOffset(getCardRotation(0, cards.length, maxRotationDeg));
|
||||||
|
$: if (centerDistancePx && rotationDeg) offset = getCardOffset(getCardRotation(0, cards.length, rotationDeg));
|
||||||
|
|
||||||
|
function getCardRotation(i: number, totalCards: number, maxRotationDeg: number): number {
|
||||||
|
if (totalCards == 1) return 0;
|
||||||
|
return -maxRotationDeg + (i / (totalCards - 1)) * 2 * maxRotationDeg;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCardOffset(angle: number): [number, number] {
|
||||||
|
let slope = Math.tan(angle * (Math.PI / 180));
|
||||||
|
let y = Math.sqrt(slope ** 2 + 1) * (centerDistancePx / (slope ** 2 + 1));
|
||||||
|
let x = slope * y;
|
||||||
|
return [x, centerDistancePx - y];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="cards relative flex justify-center box-content"
|
||||||
|
class:fullwidth
|
||||||
|
style:--height={`${maxOffset[1] + cardHeight}px`}
|
||||||
|
style:--width={`${-offset[0] * 2 + cardWidth}px`}
|
||||||
|
style:--hover-offset={`${hoverOffset}px`}
|
||||||
|
>
|
||||||
|
{#each cards as cardInfo, i}
|
||||||
|
{@const rotation = getCardRotation(i, cards.length, rotationDeg)}
|
||||||
|
{@const position = getCardOffset(rotation)}
|
||||||
|
<button
|
||||||
|
class="absolute card drop-shadow-lg"
|
||||||
|
disabled={!(cardInfo as CardInfoObj).CanPlay}
|
||||||
|
on:click={() => click(i)}
|
||||||
|
class:canPlayCards
|
||||||
|
style:--rotation={`${rotation}deg`}
|
||||||
|
style:--left={`${position[0]}px`}
|
||||||
|
style:--top={`${position[1]}px`}
|
||||||
|
>
|
||||||
|
<svelte:component
|
||||||
|
this={cardComponent}
|
||||||
|
width={cardWidth}
|
||||||
|
height={cardHeight}
|
||||||
|
data={cardInfo.Card}
|
||||||
|
canUpdateCard={canUpdateCards}
|
||||||
|
updateCard={(newCard: Card) => {
|
||||||
|
updateCard(i, newCard);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.cards {
|
||||||
|
height: var(--height);
|
||||||
|
width: var(--width);
|
||||||
|
padding-top: var(--hover-offset);
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
transform: translate(var(--left), var(--top)) rotate(var(--rotation));
|
||||||
|
transition: 0.2s transform;
|
||||||
|
cursor: unset;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.card.canPlayCards:enabled {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.card.canPlayCards:disabled {
|
||||||
|
filter: brightness(0.4);
|
||||||
|
}
|
||||||
|
.card.canPlayCards:hover {
|
||||||
|
transform: translate(var(--left), var(--top)) rotate(var(--rotation)) translate(0px, calc(0px - var(--hover-offset)));
|
||||||
|
}
|
||||||
|
</style>
|
37
frontend/src/components/Game/OpponentDisplay.svelte
Normal file
37
frontend/src/components/Game/OpponentDisplay.svelte
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { PlayerObj, PlayerStateObj } from "../../stores/sessionStore";
|
||||||
|
import CardDisplay from "./CardDisplay.svelte";
|
||||||
|
|
||||||
|
export let player: PlayerObj;
|
||||||
|
export let state: PlayerStateObj | undefined;
|
||||||
|
export let cardComponent;
|
||||||
|
export let cardWidth: number;
|
||||||
|
export let cardHeight: number;
|
||||||
|
|
||||||
|
export let centerDistancePx;
|
||||||
|
export let maxRotationDeg;
|
||||||
|
export let rotationDeg;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if state}
|
||||||
|
<div class="opponentDisplay flex flex-col justify-center items-center" style:--rotation={`${rotationDeg}deg`}>
|
||||||
|
<p class:font-bold={state?.Active}>{player.Username}</p>
|
||||||
|
<CardDisplay
|
||||||
|
{cardComponent}
|
||||||
|
{cardWidth}
|
||||||
|
{cardHeight}
|
||||||
|
cards={Array(state?.NumCards).fill({ Card: undefined })}
|
||||||
|
canPlayCards={false}
|
||||||
|
canUpdateCards={false}
|
||||||
|
hoverOffset={0}
|
||||||
|
{centerDistancePx}
|
||||||
|
{maxRotationDeg}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.opponentDisplay {
|
||||||
|
transform: rotate(var(--rotation));
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import EndScreen from "./../components/Game/EndScreen.svelte";
|
import EndScreen from "./../views/Game/EndScreen.svelte";
|
||||||
import Main from "./../components/Game/Main.svelte";
|
import Main from "./../views/Game/Main.svelte";
|
||||||
import Lobby from "../components/Game/Lobby.svelte";
|
import Lobby from "../views/Game/Lobby.svelte";
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { GameState, sessionStore } from "../stores/sessionStore";
|
import { GameState, sessionStore } from "../stores/sessionStore";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { writable, get } from "svelte/store";
|
import { writable, get, derived } from "svelte/store";
|
||||||
import { io, Socket } from "socket.io-client";
|
import { io, Socket } from "socket.io-client";
|
||||||
|
|
||||||
export enum GameState {
|
export enum GameState {
|
||||||
@ -14,7 +14,7 @@ interface PlayerPermissionObj {
|
|||||||
|
|
||||||
interface GameOptions {}
|
interface GameOptions {}
|
||||||
|
|
||||||
interface PlayerObj {
|
export interface PlayerObj {
|
||||||
PlayerId: string;
|
PlayerId: string;
|
||||||
Username: string;
|
Username: string;
|
||||||
Permissions: number;
|
Permissions: number;
|
||||||
@ -26,19 +26,21 @@ interface SessionData {
|
|||||||
joinCode: string | null;
|
joinCode: string | null;
|
||||||
gameOptions: GameOptions;
|
gameOptions: GameOptions;
|
||||||
players: Array<PlayerObj>;
|
players: Array<PlayerObj>;
|
||||||
cardDeckId: string | null;
|
cardDeckId: number | null;
|
||||||
gameState: GameState;
|
gameState: GameState;
|
||||||
socket: Socket | null;
|
socket: Socket | null;
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
userId: string | null;
|
userId: string | null;
|
||||||
messages: string[];
|
messages: any[];
|
||||||
sessionToken: string | null;
|
sessionToken: string | null;
|
||||||
|
playedCards: CardPlayedObj[];
|
||||||
|
ownCards: CardInfoObj[];
|
||||||
|
playerStates: { [key: string]: PlayerStateObj };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RoomInfoObj {
|
interface RoomInfoObj {
|
||||||
RoomId: string;
|
RoomId: string;
|
||||||
JoinCode: string;
|
JoinCode: string;
|
||||||
TopCard: any;
|
TopCard: Card;
|
||||||
GameState: GameState;
|
GameState: GameState;
|
||||||
CardDeckId: number;
|
CardDeckId: number;
|
||||||
Winner?: string;
|
Winner?: string;
|
||||||
@ -51,8 +53,45 @@ interface StatusInfoObj {
|
|||||||
Message: string;
|
Message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CardInfoObj {
|
||||||
|
CanPlay: boolean;
|
||||||
|
Card: Card;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OwnCardsObj {
|
||||||
|
Cards: CardInfoObj[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerStateObj {
|
||||||
|
PlayerId: string;
|
||||||
|
NumCards: number;
|
||||||
|
Active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardPlayedObj {
|
||||||
|
Card: Card;
|
||||||
|
CardIndex: number;
|
||||||
|
PlayedBy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PlayedCardUpdateObj {
|
||||||
|
UpdatedBy: string;
|
||||||
|
Card: Card;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PlayCardReq {
|
||||||
|
CardIndex?: number;
|
||||||
|
CardData: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdatePlayedCardReq {
|
||||||
|
CardData: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Card = any;
|
||||||
|
|
||||||
class SessionManager {
|
class SessionManager {
|
||||||
private store = writable<SessionData>({
|
store = writable<SessionData>({
|
||||||
roomId: null,
|
roomId: null,
|
||||||
joinCode: null,
|
joinCode: null,
|
||||||
gameState: -1,
|
gameState: -1,
|
||||||
@ -64,6 +103,9 @@ class SessionManager {
|
|||||||
userId: null,
|
userId: null,
|
||||||
messages: [],
|
messages: [],
|
||||||
sessionToken: null,
|
sessionToken: null,
|
||||||
|
playedCards: [],
|
||||||
|
ownCards: [],
|
||||||
|
playerStates: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
private socket: Socket | null = null;
|
private socket: Socket | null = null;
|
||||||
@ -184,6 +226,10 @@ class SessionManager {
|
|||||||
if (!userId) userId = storedSessionIds?.userId;
|
if (!userId) userId = storedSessionIds?.userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!sessionToken || !userId) {
|
||||||
|
console.warn("Socket connection requested without sessionToken or userId");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
console.warn(`Socket already connected! Rejecting new connection to ${sessionToken}`);
|
console.warn(`Socket already connected! Rejecting new connection to ${sessionToken}`);
|
||||||
return;
|
return;
|
||||||
@ -202,6 +248,10 @@ class SessionManager {
|
|||||||
this.socket?.on("disconnect", this.handleDisconnect.bind(this));
|
this.socket?.on("disconnect", this.handleDisconnect.bind(this));
|
||||||
this.socket?.on("Status", this.handleStatus.bind(this));
|
this.socket?.on("Status", this.handleStatus.bind(this));
|
||||||
this.socket?.on("RoomInfo", this.handleRoomInfo.bind(this));
|
this.socket?.on("RoomInfo", this.handleRoomInfo.bind(this));
|
||||||
|
this.socket?.on("OwnCards", this.handleOwnCardsUpdate.bind(this));
|
||||||
|
this.socket?.on("PlayerState", this.handlePlayerStateUpdate.bind(this));
|
||||||
|
this.socket?.on("CardPlayed", this.handleCardPlayed.bind(this));
|
||||||
|
this.socket?.on("PlayedCardUpdate", this.handlePlayedCardUpdate.bind(this));
|
||||||
this.socket?.on("error", this.handleError.bind(this));
|
this.socket?.on("error", this.handleError.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,6 +298,12 @@ class SessionManager {
|
|||||||
cardDeckId: message.CardDeckId,
|
cardDeckId: message.CardDeckId,
|
||||||
players: message.Players,
|
players: message.Players,
|
||||||
}));
|
}));
|
||||||
|
if (message.TopCard && get(this.store).playedCards.length == 0) {
|
||||||
|
this.store.update((state) => ({
|
||||||
|
...state,
|
||||||
|
playedCards: [{ Card: message.TopCard, CardIndex: -1, PlayedBy: "" }],
|
||||||
|
}));
|
||||||
|
}
|
||||||
this.saveJoinCode();
|
this.saveJoinCode();
|
||||||
this.store.update((state) => ({
|
this.store.update((state) => ({
|
||||||
...state,
|
...state,
|
||||||
@ -255,16 +311,78 @@ class SessionManager {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleOwnCardsUpdate(message: OwnCardsObj) {
|
||||||
|
this.store.update((state) => ({
|
||||||
|
...state,
|
||||||
|
ownCards: message.Cards,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private handlePlayerStateUpdate(message: PlayerStateObj) {
|
||||||
|
get(this.store).playerStates[message.PlayerId] = message;
|
||||||
|
this.store.update((state) => state);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleCardPlayed(message: CardPlayedObj) {
|
||||||
|
this.store.update((state) => ({
|
||||||
|
...state,
|
||||||
|
playedCards: [...state.playedCards, message],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private handlePlayedCardUpdate(message: PlayedCardUpdateObj) {
|
||||||
|
if (get(this.store).playedCards.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
get(this.store).playedCards[get(this.store).playedCards.length - 1].Card = message.Card;
|
||||||
|
this.store.update((state) => state);
|
||||||
|
}
|
||||||
|
|
||||||
private handleError(error: string) {
|
private handleError(error: string) {
|
||||||
console.error("Socket error:", error);
|
console.error("Socket error:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessage(message: string) {
|
get players(): PlayerObj[] {
|
||||||
if (this.socket && message.trim()) {
|
return get(this.store).players;
|
||||||
this.socket.emit("event", message);
|
}
|
||||||
|
|
||||||
|
get ownCards(): Card[] {
|
||||||
|
return get(this.store).ownCards;
|
||||||
|
}
|
||||||
|
|
||||||
|
get playedCards(): CardPlayedObj[] {
|
||||||
|
return get(this.store).playedCards;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlayerState(playerId: string): PlayerStateObj | undefined {
|
||||||
|
return get(this.store).playerStates[playerId];
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage(event: string, message: string) {
|
||||||
|
if (this.socket) {
|
||||||
|
this.socket.emit(event, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
drawCard() {
|
||||||
|
this.sendMessage("DrawCard", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
playCard(cardIndex: number, data?: any) {
|
||||||
|
let request: PlayCardReq = {
|
||||||
|
CardIndex: cardIndex,
|
||||||
|
CardData: data,
|
||||||
|
};
|
||||||
|
this.sendMessage("PlayCard", JSON.stringify(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePlayedCard(data?: any) {
|
||||||
|
let request: UpdatePlayedCardReq = {
|
||||||
|
CardData: data,
|
||||||
|
};
|
||||||
|
this.sendMessage("UpdatePlayedCard", JSON.stringify(request));
|
||||||
|
}
|
||||||
|
|
||||||
leaveRoom() {
|
leaveRoom() {
|
||||||
console.log("leave room");
|
console.log("leave room");
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
@ -295,6 +413,9 @@ class SessionManager {
|
|||||||
userId: null,
|
userId: null,
|
||||||
messages: [],
|
messages: [],
|
||||||
sessionToken: null,
|
sessionToken: null,
|
||||||
|
playedCards: [],
|
||||||
|
ownCards: [],
|
||||||
|
playerStates: {},
|
||||||
});
|
});
|
||||||
window.history.replaceState({}, "", "/");
|
window.history.replaceState({}, "", "/");
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import RenamePlayer from "./../RenamePlayer.svelte";
|
import RenamePlayer from "../../components/RenamePlayer.svelte";
|
||||||
import { Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell, TableSearch, Badge, Button, Modal, Popover, Tooltip } from "flowbite-svelte";
|
import { Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell, TableSearch, Badge, Button, Modal, Popover, Tooltip } from "flowbite-svelte";
|
||||||
import { CircleArrowOutUpLeft, Copy, AlertCircle, UserX, Play, TextCursorInput, Gamepad2 } from "lucide-svelte";
|
import { CircleArrowOutUpLeft, Copy, AlertCircle, UserX, Play, TextCursorInput, Gamepad2 } from "lucide-svelte";
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
75
frontend/src/views/Game/Main.svelte
Normal file
75
frontend/src/views/Game/Main.svelte
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { PlayerObj } from "../../stores/sessionStore";
|
||||||
|
import { derived, get } from "svelte/store";
|
||||||
|
import ClassicCard from "../../components/Cards/ClassicCard.svelte";
|
||||||
|
import CardDisplay from "../../components/Game/CardDisplay.svelte";
|
||||||
|
import { sessionStore } from "../../stores/sessionStore";
|
||||||
|
import OpponentDisplay from "../../components/Game/OpponentDisplay.svelte";
|
||||||
|
|
||||||
|
let maxRotationDeg = 20;
|
||||||
|
let centerDistancePx = 200;
|
||||||
|
let cardWidth = 100;
|
||||||
|
let cardHeight = 150;
|
||||||
|
let cardComponent = ClassicCard;
|
||||||
|
|
||||||
|
let opponents = derived(sessionStore.store, ($store) => $store.players.filter((e) => e.PlayerId != sessionStore.getUserId()));
|
||||||
|
let playerActive = derived(sessionStore.store, ($store) => ($store.playerStates[$store.userId ?? ""] ?? "").Active ?? false);
|
||||||
|
let perSide = 0;
|
||||||
|
$: perSide = Math.ceil($opponents.length / 3);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#snippet OpponentCards(players: PlayerObj[], rotationDeg: number)}
|
||||||
|
{#each players as player}
|
||||||
|
<OpponentDisplay {cardComponent} {cardHeight} {cardWidth} {rotationDeg} {player} state={sessionStore.getPlayerState(player.PlayerId)} {centerDistancePx} {maxRotationDeg} />
|
||||||
|
{/each}
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<div class="game relative grid h-full grid-rows-[1fr_2fr_1fr]">
|
||||||
|
<div class="top">{@render OpponentCards($opponents.slice(0, perSide), 0)}</div>
|
||||||
|
<div class="middle grid grid-cols-[1fr_300px_1fr]">
|
||||||
|
<div class="left flex justify-center items-center">
|
||||||
|
{@render OpponentCards($opponents.slice(perSide, perSide * 2), -90)}
|
||||||
|
</div>
|
||||||
|
<div class="center grid grid-cols-2">
|
||||||
|
<div class="drawCard flex justify-center items-center">
|
||||||
|
<button class="draw" on:click={() => sessionStore.drawCard()}>
|
||||||
|
<svelte:component this={cardComponent} width={cardWidth} height={cardHeight} data={{ Color: "black", Symbol: "special:draw" }} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="cardStack flex justify-center items-center">
|
||||||
|
<CardDisplay
|
||||||
|
{cardComponent}
|
||||||
|
{cardHeight}
|
||||||
|
{cardWidth}
|
||||||
|
canPlayCards={false}
|
||||||
|
canUpdateCards={true}
|
||||||
|
updateCard={(_, data) => {
|
||||||
|
sessionStore.updatePlayedCard(data);
|
||||||
|
}}
|
||||||
|
cards={[{ Card: undefined, PlayedBy: "", CardIndex: -1 }, ...$sessionStore.playedCards]}
|
||||||
|
centerDistancePx={0}
|
||||||
|
maxRotationDeg={10}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right flex justify-center items-center">
|
||||||
|
{@render OpponentCards($opponents.slice(perSide * 2, perSide * 3), 90)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="cardContainer w-full overflow-y-clip overflow-x-auto">
|
||||||
|
<div class="ownCards w-full min-w-min box-content flex items-end justify-center" class:opacity-60={!$playerActive}>
|
||||||
|
<CardDisplay
|
||||||
|
{cardHeight}
|
||||||
|
{cardWidth}
|
||||||
|
{cardComponent}
|
||||||
|
fullwidth
|
||||||
|
click={(i) => {
|
||||||
|
if (get(playerActive)) sessionStore.playCard(i);
|
||||||
|
}}
|
||||||
|
centerDistancePx={centerDistancePx * 3}
|
||||||
|
{maxRotationDeg}
|
||||||
|
cards={$sessionStore.ownCards}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
Reference in New Issue
Block a user