task/134-frontend-check #100
@@ -970,6 +970,8 @@ socket.on('game:ended', {
|
||||
message: string;
|
||||
finalPositions: PlayerPosition[];
|
||||
timestamp: string;
|
||||
reason?: string; // Optional: 'gamemaster_left' if GM disconnected
|
||||
gamemasterName?: string; // Optional: GM name if GM left
|
||||
});
|
||||
|
||||
// Server → All Players: Cleanup complete
|
||||
@@ -980,6 +982,36 @@ socket.on('game:cleanup-complete', {
|
||||
});
|
||||
```
|
||||
|
||||
**Game End Scenarios**:
|
||||
|
||||
1. **Normal Win** (Player reaches position 100):
|
||||
```typescript
|
||||
{
|
||||
winner: "player-uuid",
|
||||
winnerName: "Alice",
|
||||
message: "🎉 Alice won the game! Congratulations!",
|
||||
finalPositions: [...],
|
||||
timestamp: "2025-11-06T..."
|
||||
}
|
||||
```
|
||||
|
||||
2. **Gamemaster Disconnect** (Game cancelled):
|
||||
```typescript
|
||||
{
|
||||
reason: "gamemaster_left",
|
||||
gamemasterName: "Bob",
|
||||
message: "🎭 Gamemaster Bob left. Game has ended.",
|
||||
timestamp: "2025-11-06T..."
|
||||
}
|
||||
```
|
||||
|
||||
**Behavior**:
|
||||
- Game immediately cancelled when gamemaster disconnects
|
||||
- All players notified via `game:ended` event
|
||||
- Database updated: `state = CANCELLED`, `enddate = now`
|
||||
- All Redis data and socket connections cleaned up
|
||||
- No winner recorded (game didn't complete normally)
|
||||
|
||||
---
|
||||
|
||||
#### Error Events
|
||||
|
||||
Binary file not shown.
@@ -68,8 +68,6 @@ export class StartGameCommandHandler {
|
||||
orgid: command.orgid || null,
|
||||
gamedecks,
|
||||
players: [],
|
||||
started: false,
|
||||
finished: false,
|
||||
winner: null,
|
||||
state: GameState.WAITING,
|
||||
startdate: null,
|
||||
|
||||
@@ -65,7 +65,6 @@ export class StartGamePlayCommandHandler {
|
||||
|
||||
// Update game state in database
|
||||
const updatedGame = await this.gameRepository.update(game.id, {
|
||||
started: true,
|
||||
state: GameState.ACTIVE,
|
||||
startdate: new Date()
|
||||
});
|
||||
@@ -111,11 +110,6 @@ export class StartGamePlayCommandHandler {
|
||||
throw new Error('Game is not in waiting state and cannot be started');
|
||||
}
|
||||
|
||||
// Check if game is already started
|
||||
if (game.started) {
|
||||
throw new Error('Game has already been started');
|
||||
}
|
||||
|
||||
// Check if there are enough players (at least 2)
|
||||
if (game.players.length < 2) {
|
||||
throw new Error('Game needs at least 2 players to start');
|
||||
|
||||
@@ -1139,6 +1139,36 @@ export class GameWebSocketService {
|
||||
// If the socket was in a game, handle cleanup
|
||||
if (socket.gameCode && socket.playerName) {
|
||||
try {
|
||||
// Check if this player is the gamemaster
|
||||
const game = await this.gameRepository.findByGameCode(socket.gameCode);
|
||||
const isGamemaster = game && socket.userId && game.createdby === socket.userId;
|
||||
|
||||
// If gamemaster leaves, end the game immediately
|
||||
if (isGamemaster && game) {
|
||||
logOther(`Gamemaster ${socket.playerName} left game ${socket.gameCode}, ending game`);
|
||||
|
||||
const gameRoomName = `game_${socket.gameCode}`;
|
||||
|
||||
// Notify all players
|
||||
this.io.of('/game').to(gameRoomName).emit('game:ended', {
|
||||
reason: 'gamemaster_left',
|
||||
gamemasterName: socket.playerName,
|
||||
message: `🎭 Gamemaster ${socket.playerName} left. Game has ended.`,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Update database
|
||||
await this.gameRepository.update(game.id, {
|
||||
state: GameState.CANCELLED,
|
||||
enddate: new Date()
|
||||
});
|
||||
|
||||
// Clean up all game data
|
||||
await this.cleanupGameData(socket.gameCode, game.id);
|
||||
|
||||
return; // Exit early, no need for further cleanup
|
||||
}
|
||||
|
||||
// Clean up any pending card answer
|
||||
if (socket.userId) {
|
||||
const pendingCard = await this.getPendingCard(socket.gameCode, socket.userId);
|
||||
@@ -1222,14 +1252,13 @@ export class GameWebSocketService {
|
||||
if (!game) return;
|
||||
|
||||
// Only clean up games that haven't finished yet
|
||||
if (!game.finished) {
|
||||
if (game.state !== GameState.FINISHED && game.state !== GameState.CANCELLED) {
|
||||
logOther(`Handling abandoned game ${gameCode}`, { gameId: game.id });
|
||||
|
||||
// Mark game as abandoned in database
|
||||
// Mark game as cancelled in database
|
||||
await this.gameRepository.update(game.id, {
|
||||
finished: true,
|
||||
state: GameState.CANCELLED,
|
||||
enddate: new Date(),
|
||||
// Could add an 'abandoned' flag if the database schema supports it
|
||||
});
|
||||
|
||||
// Clean up all Redis data for this abandoned game
|
||||
@@ -2236,8 +2265,8 @@ export class GameWebSocketService {
|
||||
const game = await this.gameRepository.findByGameCode(gameCode);
|
||||
if (game) {
|
||||
await this.gameRepository.update(game.id, {
|
||||
finished: true,
|
||||
winner: winnerId,
|
||||
state: GameState.FINISHED,
|
||||
winnerId: winnerId,
|
||||
enddate: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { Consequence, CardType } from '../Deck/DeckAggregate';
|
||||
import { UserAggregate } from '../User/UserAggregate';
|
||||
import { OrganizationAggregate } from '../Organization/OrganizationAggregate';
|
||||
|
||||
export enum GameState {
|
||||
WAITING = 0,
|
||||
@@ -65,14 +67,8 @@ export class GameAggregate {
|
||||
@Column({ type: 'uuid', array: true, default: () => "'{}'", name: 'playerids' })
|
||||
players!: string[];
|
||||
|
||||
@Column({ type: 'boolean', default: false })
|
||||
started!: boolean;
|
||||
|
||||
@Column({ type: 'boolean', default: false })
|
||||
finished!: boolean;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true, name: 'winnerid' })
|
||||
winner!: string | null;
|
||||
@Column({ type: 'uuid', nullable: true, name: 'winnerId' })
|
||||
winnerId!: string | null;
|
||||
|
||||
@Column({ type: 'int', default: GameState.WAITING })
|
||||
state!: GameState;
|
||||
@@ -86,8 +82,20 @@ export class GameAggregate {
|
||||
@Column({ type: 'timestamp', nullable: true, name: 'finishDate' })
|
||||
enddate!: Date | null;
|
||||
|
||||
@UpdateDateColumn()
|
||||
@UpdateDateColumn({ name: 'updateDate' })
|
||||
updateDate!: Date;
|
||||
|
||||
@ManyToOne(() => UserAggregate, { eager: false })
|
||||
@JoinColumn({ name: 'createdBy' })
|
||||
user!: UserAggregate | null;
|
||||
|
||||
@ManyToOne(() => UserAggregate, { eager: false })
|
||||
@JoinColumn({ name: 'winnerId' })
|
||||
winner!: UserAggregate | null;
|
||||
|
||||
@ManyToOne(() => OrganizationAggregate, { eager: false })
|
||||
@JoinColumn({ name: 'organizationId' })
|
||||
organization!: OrganizationAggregate | null;
|
||||
}
|
||||
|
||||
// Board Generation Types
|
||||
|
||||
@@ -1,32 +1,14 @@
|
||||
import { GameAggregate } from '../Game/GameAggregate';
|
||||
<<<<<<< HEAD
|
||||
import { GameAggregate, GameState } from '../Game/GameAggregate';
|
||||
import { IPaginatedRepository } from './IBaseRepository';
|
||||
|
||||
export interface IGameRepository extends IPaginatedRepository<GameAggregate, { games: GameAggregate[], totalCount: number }> {
|
||||
// Game-specific methods
|
||||
findByGameCode(gamecode: string): Promise<GameAggregate | null>;
|
||||
=======
|
||||
|
||||
export interface IGameRepository {
|
||||
create(game: Partial<GameAggregate>): Promise<GameAggregate>;
|
||||
findByPage(from: number, to: number): Promise<{ games: GameAggregate[], totalCount: number }>;
|
||||
findByPageIncludingDeleted(from: number, to: number): Promise<{ games: GameAggregate[], totalCount: number }>;
|
||||
findById(id: string): Promise<GameAggregate | null>;
|
||||
findByIdIncludingDeleted(id: string): Promise<GameAggregate | null>;
|
||||
findByGameCode(gamecode: string): Promise<GameAggregate | null>;
|
||||
search(query: string, limit?: number, offset?: number): Promise<{ games: GameAggregate[], totalCount: number }>;
|
||||
searchIncludingDeleted(query: string, limit?: number, offset?: number): Promise<{ games: GameAggregate[], totalCount: number }>;
|
||||
update(id: string, update: Partial<GameAggregate>): Promise<GameAggregate | null>;
|
||||
delete(id: string): Promise<any>;
|
||||
softDelete(id: string): Promise<GameAggregate | null>;
|
||||
|
||||
// Game-specific methods
|
||||
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
|
||||
findActiveGames(): Promise<GameAggregate[]>;
|
||||
findGamesByPlayer(playerId: string): Promise<GameAggregate[]>;
|
||||
findWaitingGames(): Promise<GameAggregate[]>;
|
||||
findFinishedGames(from?: number, to?: number): Promise<{ games: GameAggregate[], totalCount: number }>;
|
||||
addPlayerToGame(gameId: string, playerId: string): Promise<GameAggregate | null>;
|
||||
removePlayerFromGame(gameId: string, playerId: string): Promise<GameAggregate | null>;
|
||||
updateGameState(gameId: string, started: boolean, finished?: boolean, winner?: string): Promise<GameAggregate | null>;
|
||||
updateGameState(gameId: string, state: GameState, winner?: string): Promise<GameAggregate | null>;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class Full1757939815984 implements MigrationInterface {
|
||||
name = 'Full1757939815984'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "Chats" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "type" character varying(50) NOT NULL DEFAULT 'direct', "name" character varying(255), "gameId" uuid, "createdBy" uuid, "users" uuid array NOT NULL, "messages" json NOT NULL DEFAULT '[]', "lastActivity" TIMESTAMP, "createDate" TIMESTAMP NOT NULL DEFAULT now(), "updateDate" TIMESTAMP NOT NULL DEFAULT now(), "state" integer NOT NULL DEFAULT '0', "archiveDate" TIMESTAMP, CONSTRAINT "PK_64c36c2b8d86a0d5de4cf64de8d" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE TABLE "Users" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "orgid" uuid, "username" character varying(100) NOT NULL, "password" character varying(255) NOT NULL, "email" character varying(255) NOT NULL, "fname" character varying(100) NOT NULL, "lname" character varying(100) NOT NULL, "token" character varying(255), "TokenExpires" TIMESTAMP, "phone" character varying(20), "state" integer NOT NULL DEFAULT '0', "regdate" TIMESTAMP NOT NULL DEFAULT now(), "updatedate" TIMESTAMP NOT NULL DEFAULT now(), "Orglogindate" TIMESTAMP, CONSTRAINT "UQ_ffc81a3b97dcbf8e320d5106c0d" UNIQUE ("username"), CONSTRAINT "UQ_3c3ab3f49a87e6ddb607f3c4945" UNIQUE ("email"), CONSTRAINT "PK_16d4f7d636df336db11d87413e3" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE TABLE "Contacts" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(255) NOT NULL, "email" character varying(255) NOT NULL, "userid" uuid, "type" integer NOT NULL, "txt" text NOT NULL, "state" integer NOT NULL DEFAULT '0', "createDate" TIMESTAMP NOT NULL DEFAULT now(), "updateDate" TIMESTAMP NOT NULL DEFAULT now(), "adminResponse" text, "responseDate" TIMESTAMP, "respondedBy" uuid, CONSTRAINT "PK_68782cec65c8eef577c62958273" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE TABLE "ChatArchives" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "chatId" uuid NOT NULL, "archivedMessages" json NOT NULL, "archivedAt" TIMESTAMP NOT NULL, "createDate" TIMESTAMP NOT NULL DEFAULT now(), "chatType" character varying(50) NOT NULL, "chatName" character varying(255), "gameId" uuid, "participants" uuid array NOT NULL, CONSTRAINT "PK_fe62979fc2061d7afe278d3f14e" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE TABLE "Games" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "gamecode" character varying(10) NOT NULL, "maxplayers" integer NOT NULL, "logintype" integer NOT NULL DEFAULT '0', "createdby" character varying(255), "orgid" character varying(255), "gamedecks" json NOT NULL, "players" json NOT NULL DEFAULT '[]', "started" boolean NOT NULL DEFAULT false, "finished" boolean NOT NULL DEFAULT false, "winner" character varying(255), "state" integer NOT NULL DEFAULT '0', "create_date" TIMESTAMP NOT NULL DEFAULT now(), "start_date" TIMESTAMP, "end_date" TIMESTAMP, "update_date" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_9d52c646079cbe6f242a85c5c41" UNIQUE ("gamecode"), CONSTRAINT "PK_1950492f583d31609c5e9fbbe12" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE TABLE "Organizations" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(255) NOT NULL, "contactfname" character varying(100) NOT NULL, "contactlname" character varying(100) NOT NULL, "contactphone" character varying(20) NOT NULL, "contactemail" character varying(255) NOT NULL, "state" integer NOT NULL DEFAULT '0', "regdate" TIMESTAMP NOT NULL DEFAULT now(), "updatedate" TIMESTAMP NOT NULL DEFAULT now(), "url" character varying(500), "userinorg" integer NOT NULL DEFAULT '0', "maxOrganizationalDecks" integer, CONSTRAINT "PK_e0690a31419f6666194423526f2" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE TABLE "Decks" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(255) NOT NULL, "type" integer NOT NULL, "user_id" uuid NOT NULL, "creation_date" TIMESTAMP NOT NULL DEFAULT now(), "cards" json NOT NULL, "played_number" integer NOT NULL DEFAULT '0', "ctype" integer NOT NULL DEFAULT '0', "update_date" TIMESTAMP NOT NULL DEFAULT now(), "state" integer NOT NULL DEFAULT '0', "organization_id" uuid, CONSTRAINT "PK_001f26cb3ec39c1f25269943473" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`ALTER TABLE "Decks" ADD CONSTRAINT "FK_06ee28f90d68543a03b14aebe13" FOREIGN KEY ("organization_id") REFERENCES "Organizations"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "Decks" DROP CONSTRAINT "FK_06ee28f90d68543a03b14aebe13"`);
|
||||
await queryRunner.query(`DROP TABLE "Decks"`);
|
||||
await queryRunner.query(`DROP TABLE "Organizations"`);
|
||||
await queryRunner.query(`DROP TABLE "Games"`);
|
||||
await queryRunner.query(`DROP TABLE "ChatArchives"`);
|
||||
await queryRunner.query(`DROP TABLE "Contacts"`);
|
||||
await queryRunner.query(`DROP TABLE "Users"`);
|
||||
await queryRunner.query(`DROP TABLE "Chats"`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class Full1758463929834 implements MigrationInterface {
|
||||
name = 'Full1758463929834'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "Games" DROP COLUMN "winner"`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" DROP COLUMN "create_date"`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" DROP COLUMN "end_date"`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" DROP COLUMN "update_date"`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" ADD "boardsize" integer NOT NULL DEFAULT '50'`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" ADD "winnerid" uuid`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" ADD "createDate" TIMESTAMP NOT NULL DEFAULT now()`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" ADD "finishDate" TIMESTAMP`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" ADD "updateDate" TIMESTAMP NOT NULL DEFAULT now()`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "Games" DROP COLUMN "updateDate"`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" DROP COLUMN "finishDate"`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" DROP COLUMN "createDate"`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" DROP COLUMN "winnerid"`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" DROP COLUMN "boardsize"`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" ADD "update_date" TIMESTAMP NOT NULL DEFAULT now()`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" ADD "end_date" TIMESTAMP`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" ADD "create_date" TIMESTAMP NOT NULL DEFAULT now()`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" ADD "winner" character varying(255)`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class Full1762370334693 implements MigrationInterface {
|
||||
name = 'Full1762370334693'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "Games" RENAME COLUMN "winnerid" TO "winnerId"`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" ADD CONSTRAINT "FK_330362bff8b25bb573f31fb4023" FOREIGN KEY ("winnerId") REFERENCES "Users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "Games" DROP CONSTRAINT "FK_330362bff8b25bb573f31fb4023"`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" RENAME COLUMN "winnerId" TO "winnerid"`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
export class Full1758463928499 implements MigrationInterface {
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
}
|
||||
|
||||
}
|
||||
+1
-2
@@ -1,7 +1,6 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class Full1758463928499 implements MigrationInterface {
|
||||
|
||||
export class Full1762370333970 implements MigrationInterface {
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
}
|
||||
@@ -385,29 +385,26 @@ export class GameRepository implements IGameRepository {
|
||||
}
|
||||
}
|
||||
|
||||
async updateGameState(gameId: string, started: boolean, finished?: boolean, winner?: string): Promise<GameAggregate | null> {
|
||||
async updateGameState(gameId: string, state: GameState, winner?: string): Promise<GameAggregate | null> {
|
||||
const startTime = performance.now();
|
||||
try {
|
||||
const updateData: Partial<GameAggregate> = { started };
|
||||
const updateData: Partial<GameAggregate> = { state };
|
||||
|
||||
if (started && !finished) {
|
||||
updateData.state = GameState.ACTIVE;
|
||||
if (state === GameState.ACTIVE) {
|
||||
updateData.startdate = new Date();
|
||||
}
|
||||
|
||||
if (finished) {
|
||||
updateData.finished = true;
|
||||
updateData.state = GameState.FINISHED;
|
||||
if (state === GameState.FINISHED) {
|
||||
updateData.enddate = new Date();
|
||||
if (winner) {
|
||||
updateData.winner = winner;
|
||||
updateData.winnerId = winner;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.update(gameId, updateData);
|
||||
|
||||
const endTime = performance.now();
|
||||
logDatabase('Game state updated', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${gameId}, started: ${started}, finished: ${finished}, winner: ${winner}`);
|
||||
logDatabase('Game state updated', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${gameId}, state: ${updateData.state}, winner: ${winner}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const endTime = performance.now();
|
||||
|
||||
@@ -1,202 +1,172 @@
|
||||
-- SerpentRace Database Schema
|
||||
-- Generated from TypeORM Entity Aggregates
|
||||
-- This file creates the complete database schema without initial data
|
||||
-- This script was generated by the ERD tool in pgAdmin 4.
|
||||
-- Please log an issue at https://github.com/pgadmin-org/pgadmin4/issues/new/choose if you find any bugs, including reproduction steps.
|
||||
BEGIN;
|
||||
|
||||
-- Enable UUID extension
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
-- Create Users table
|
||||
CREATE TABLE "Users" (
|
||||
"id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
"orgid" UUID NULL,
|
||||
"username" VARCHAR(100) UNIQUE NOT NULL,
|
||||
"password" VARCHAR(255) NOT NULL,
|
||||
"email" VARCHAR(255) UNIQUE NOT NULL,
|
||||
"fname" VARCHAR(100) NOT NULL,
|
||||
"lname" VARCHAR(100) NOT NULL,
|
||||
"token" VARCHAR(255) NULL,
|
||||
"TokenExpires" TIMESTAMP NULL,
|
||||
"phone" VARCHAR(20) NULL,
|
||||
"state" INTEGER NOT NULL DEFAULT 0,
|
||||
"regdate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updateDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"Orglogindate" TIMESTAMP NULL
|
||||
CREATE TABLE IF NOT EXISTS public."ChatArchives"
|
||||
(
|
||||
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"chatId" uuid NOT NULL,
|
||||
"archivedMessages" json NOT NULL,
|
||||
"archivedAt" timestamp without time zone NOT NULL,
|
||||
"createDate" timestamp without time zone NOT NULL DEFAULT now(),
|
||||
"chatType" character varying(50) COLLATE pg_catalog."default" NOT NULL,
|
||||
"chatName" character varying(255) COLLATE pg_catalog."default",
|
||||
"gameId" uuid,
|
||||
participants uuid[] NOT NULL,
|
||||
CONSTRAINT "PK_fe62979fc2061d7afe278d3f14e" PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
-- Create Organizations table
|
||||
CREATE TABLE "Organizations" (
|
||||
"id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
"name" VARCHAR(255) NOT NULL,
|
||||
"contactfname" VARCHAR(100) NOT NULL,
|
||||
"contactlname" VARCHAR(100) NOT NULL,
|
||||
"contactphone" VARCHAR(20) NOT NULL,
|
||||
"contactemail" VARCHAR(255) NOT NULL,
|
||||
"state" INTEGER NOT NULL DEFAULT 0,
|
||||
"regdate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updateDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"url" VARCHAR(500) NULL,
|
||||
"userinorg" INTEGER NOT NULL DEFAULT 0,
|
||||
"maxOrganizationalDecks" INTEGER NULL
|
||||
CREATE TABLE IF NOT EXISTS public."Chats"
|
||||
(
|
||||
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
type character varying(50) COLLATE pg_catalog."default" NOT NULL DEFAULT 'direct'::character varying,
|
||||
name character varying(255) COLLATE pg_catalog."default",
|
||||
"gameId" uuid,
|
||||
"createdBy" uuid,
|
||||
users uuid[] NOT NULL,
|
||||
messages json NOT NULL DEFAULT '[]'::json,
|
||||
"lastActivity" timestamp without time zone,
|
||||
"createDate" timestamp without time zone NOT NULL DEFAULT now(),
|
||||
"updateDate" timestamp without time zone NOT NULL DEFAULT now(),
|
||||
state integer NOT NULL DEFAULT 0,
|
||||
"archiveDate" timestamp without time zone,
|
||||
CONSTRAINT "PK_64c36c2b8d86a0d5de4cf64de8d" PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
-- Create Decks table
|
||||
CREATE TABLE "Decks" (
|
||||
"id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
"name" VARCHAR(255) NOT NULL,
|
||||
"type" INTEGER NOT NULL,
|
||||
"user_id" UUID NOT NULL,
|
||||
"creation_date" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"cards" JSONB NOT NULL DEFAULT '[]',
|
||||
"played_number" INTEGER NOT NULL DEFAULT 0,
|
||||
"ctype" INTEGER NOT NULL DEFAULT 0,
|
||||
"updateDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"state" INTEGER NOT NULL DEFAULT 0,
|
||||
"organization_id" UUID NULL
|
||||
CREATE TABLE IF NOT EXISTS public."Contacts"
|
||||
(
|
||||
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
name character varying(255) COLLATE pg_catalog."default" NOT NULL,
|
||||
email character varying(255) COLLATE pg_catalog."default" NOT NULL,
|
||||
userid uuid,
|
||||
type integer NOT NULL,
|
||||
txt text COLLATE pg_catalog."default" NOT NULL,
|
||||
state integer NOT NULL DEFAULT 0,
|
||||
"createDate" timestamp without time zone NOT NULL DEFAULT now(),
|
||||
"updateDate" timestamp without time zone NOT NULL DEFAULT now(),
|
||||
"adminResponse" text COLLATE pg_catalog."default",
|
||||
"responseDate" timestamp without time zone,
|
||||
"respondedBy" uuid,
|
||||
CONSTRAINT "PK_68782cec65c8eef577c62958273" PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
-- Create Chats table
|
||||
CREATE TABLE "Chats" (
|
||||
"id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
"type" VARCHAR(50) NOT NULL DEFAULT 'direct',
|
||||
"name" VARCHAR(255) NULL,
|
||||
"gameId" UUID NULL,
|
||||
"createdBy" UUID NULL,
|
||||
"users" UUID[] NOT NULL,
|
||||
"messages" JSONB NOT NULL DEFAULT '[]',
|
||||
"lastActivity" TIMESTAMP NULL,
|
||||
"createDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updateDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"state" INTEGER NOT NULL DEFAULT 0,
|
||||
"archiveDate" TIMESTAMP NULL
|
||||
CREATE TABLE IF NOT EXISTS public."Decks"
|
||||
(
|
||||
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
name character varying(255) COLLATE pg_catalog."default" NOT NULL,
|
||||
type integer NOT NULL,
|
||||
user_id uuid NOT NULL,
|
||||
creation_date timestamp without time zone NOT NULL DEFAULT now(),
|
||||
cards json NOT NULL,
|
||||
played_number integer NOT NULL DEFAULT 0,
|
||||
ctype integer NOT NULL DEFAULT 0,
|
||||
"updateDate" timestamp without time zone NOT NULL DEFAULT now(),
|
||||
state integer NOT NULL DEFAULT 0,
|
||||
organization_id uuid,
|
||||
CONSTRAINT "PK_001f26cb3ec39c1f25269943473" PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
-- Create Contacts table
|
||||
CREATE TABLE "Contacts" (
|
||||
"id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
"name" VARCHAR(255) NOT NULL,
|
||||
"email" VARCHAR(255) NOT NULL,
|
||||
"userid" UUID NULL,
|
||||
"type" INTEGER NOT NULL,
|
||||
"txt" TEXT NOT NULL,
|
||||
"state" INTEGER NOT NULL DEFAULT 0,
|
||||
"createDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updateDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"adminResponse" TEXT NULL,
|
||||
"responseDate" TIMESTAMP NULL,
|
||||
"respondedBy" UUID NULL
|
||||
CREATE TABLE IF NOT EXISTS public."Games"
|
||||
(
|
||||
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
gamecode character varying(10) COLLATE pg_catalog."default" NOT NULL,
|
||||
maxplayers integer NOT NULL,
|
||||
logintype integer NOT NULL DEFAULT 0,
|
||||
boardsize integer NOT NULL DEFAULT 50,
|
||||
"createdBy" uuid NOT NULL,
|
||||
organizationid uuid,
|
||||
decks jsonb NOT NULL DEFAULT '[]'::jsonb,
|
||||
playerids uuid[] NOT NULL DEFAULT '{}'::uuid[],
|
||||
"winnerId" uuid,
|
||||
state integer NOT NULL DEFAULT 0,
|
||||
"createDate" timestamp without time zone NOT NULL DEFAULT now(),
|
||||
start_date timestamp without time zone,
|
||||
"finishDate" timestamp without time zone,
|
||||
"updateDate" timestamp without time zone NOT NULL DEFAULT now(),
|
||||
"organizationId" uuid,
|
||||
CONSTRAINT "PK_1950492f583d31609c5e9fbbe12" PRIMARY KEY (id),
|
||||
CONSTRAINT "UQ_9d52c646079cbe6f242a85c5c41" UNIQUE (gamecode)
|
||||
);
|
||||
|
||||
-- Create Games table
|
||||
CREATE TABLE "Games" (
|
||||
"id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
"gamecode" VARCHAR(10) UNIQUE NOT NULL,
|
||||
"maxplayers" INTEGER NOT NULL,
|
||||
"logintype" INTEGER NOT NULL DEFAULT 0,
|
||||
"state" INTEGER NOT NULL DEFAULT 0,
|
||||
"playerids" UUID[] NOT NULL DEFAULT '{}',
|
||||
"decks" JSONB NOT NULL DEFAULT '[]',
|
||||
"boardsize" INTEGER NOT NULL DEFAULT 50,
|
||||
"createDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updateDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"finishDate" TIMESTAMP NULL,
|
||||
"winnerid" UUID NULL,
|
||||
"createdBy" UUID NOT NULL,
|
||||
"organizationid" UUID NULL
|
||||
CREATE TABLE IF NOT EXISTS public."Organizations"
|
||||
(
|
||||
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
name character varying(255) COLLATE pg_catalog."default" NOT NULL,
|
||||
contactfname character varying(100) COLLATE pg_catalog."default" NOT NULL,
|
||||
contactlname character varying(100) COLLATE pg_catalog."default" NOT NULL,
|
||||
contactphone character varying(20) COLLATE pg_catalog."default" NOT NULL,
|
||||
contactemail character varying(255) COLLATE pg_catalog."default" NOT NULL,
|
||||
state integer NOT NULL DEFAULT 0,
|
||||
regdate timestamp without time zone NOT NULL DEFAULT now(),
|
||||
"updateDate" timestamp without time zone NOT NULL DEFAULT now(),
|
||||
url character varying(500) COLLATE pg_catalog."default",
|
||||
userinorg integer NOT NULL DEFAULT 0,
|
||||
"maxOrganizationalDecks" integer,
|
||||
CONSTRAINT "PK_e0690a31419f6666194423526f2" PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
-- Add Foreign Key Constraints
|
||||
ALTER TABLE "Users"
|
||||
ADD CONSTRAINT "FK_Users_Organizations"
|
||||
FOREIGN KEY ("orgid") REFERENCES "Organizations"("id") ON DELETE SET NULL;
|
||||
CREATE TABLE IF NOT EXISTS public."Users"
|
||||
(
|
||||
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
orgid uuid,
|
||||
username character varying(100) COLLATE pg_catalog."default" NOT NULL,
|
||||
password character varying(255) COLLATE pg_catalog."default" NOT NULL,
|
||||
email character varying(255) COLLATE pg_catalog."default" NOT NULL,
|
||||
fname character varying(100) COLLATE pg_catalog."default" NOT NULL,
|
||||
lname character varying(100) COLLATE pg_catalog."default" NOT NULL,
|
||||
token character varying(255) COLLATE pg_catalog."default",
|
||||
"TokenExpires" timestamp without time zone,
|
||||
phone character varying(20) COLLATE pg_catalog."default",
|
||||
state integer NOT NULL DEFAULT 0,
|
||||
regdate timestamp without time zone NOT NULL DEFAULT now(),
|
||||
"updateDate" timestamp without time zone NOT NULL DEFAULT now(),
|
||||
"Orglogindate" timestamp without time zone,
|
||||
CONSTRAINT "PK_16d4f7d636df336db11d87413e3" PRIMARY KEY (id),
|
||||
CONSTRAINT "UQ_3c3ab3f49a87e6ddb607f3c4945" UNIQUE (email),
|
||||
CONSTRAINT "UQ_ffc81a3b97dcbf8e320d5106c0d" UNIQUE (username)
|
||||
);
|
||||
|
||||
ALTER TABLE "Decks"
|
||||
ADD CONSTRAINT "FK_Decks_Users"
|
||||
FOREIGN KEY ("user_id") REFERENCES "Users"("id") ON DELETE CASCADE;
|
||||
CREATE TABLE IF NOT EXISTS public.migrations
|
||||
(
|
||||
id serial NOT NULL,
|
||||
"timestamp" bigint NOT NULL,
|
||||
name character varying COLLATE pg_catalog."default" NOT NULL,
|
||||
CONSTRAINT "PK_8c82d7f526340ab734260ea46be" PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
ALTER TABLE "Decks"
|
||||
ADD CONSTRAINT "FK_Decks_Organizations"
|
||||
FOREIGN KEY ("organization_id") REFERENCES "Organizations"("id") ON DELETE SET NULL;
|
||||
ALTER TABLE IF EXISTS public."Decks"
|
||||
ADD CONSTRAINT "FK_06ee28f90d68543a03b14aebe13" FOREIGN KEY (organization_id)
|
||||
REFERENCES public."Organizations" (id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE NO ACTION;
|
||||
|
||||
ALTER TABLE "Contacts"
|
||||
ADD CONSTRAINT "FK_Contacts_Users"
|
||||
FOREIGN KEY ("userid") REFERENCES "Users"("id") ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE "Contacts"
|
||||
ADD CONSTRAINT "FK_Contacts_RespondedBy"
|
||||
FOREIGN KEY ("respondedBy") REFERENCES "Users"("id") ON DELETE SET NULL;
|
||||
ALTER TABLE IF EXISTS public."Decks"
|
||||
ADD CONSTRAINT "FK_a39059433e29882e1309d3a5e70" FOREIGN KEY (user_id)
|
||||
REFERENCES public."Users" (id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE NO ACTION;
|
||||
|
||||
ALTER TABLE "Chats"
|
||||
ADD CONSTRAINT "FK_Chats_CreatedBy"
|
||||
FOREIGN KEY ("createdBy") REFERENCES "Users"("id") ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE "Chats"
|
||||
ADD CONSTRAINT "FK_Chats_Games"
|
||||
FOREIGN KEY ("gameId") REFERENCES "Games"("id") ON DELETE SET NULL;
|
||||
ALTER TABLE IF EXISTS public."Games"
|
||||
ADD CONSTRAINT "FK_330362bff8b25bb573f31fb4023" FOREIGN KEY ("winnerId")
|
||||
REFERENCES public."Users" (id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE NO ACTION;
|
||||
|
||||
ALTER TABLE "Games"
|
||||
ADD CONSTRAINT "FK_Games_CreatedBy"
|
||||
FOREIGN KEY ("createdBy") REFERENCES "Users"("id") ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE "Games"
|
||||
ADD CONSTRAINT "FK_Games_Organizations"
|
||||
FOREIGN KEY ("organizationid") REFERENCES "Organizations"("id") ON DELETE SET NULL;
|
||||
ALTER TABLE IF EXISTS public."Games"
|
||||
ADD CONSTRAINT "FK_e3c4e8898fa026a5551aefc4f62" FOREIGN KEY ("organizationId")
|
||||
REFERENCES public."Organizations" (id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE NO ACTION;
|
||||
|
||||
ALTER TABLE "Games"
|
||||
ADD CONSTRAINT "FK_Games_Winner"
|
||||
FOREIGN KEY ("winnerid") REFERENCES "Users"("id") ON DELETE SET NULL;
|
||||
|
||||
-- Create Indexes for Performance
|
||||
CREATE INDEX "IDX_Users_Username" ON "Users" ("username");
|
||||
CREATE INDEX "IDX_Users_Email" ON "Users" ("email");
|
||||
CREATE INDEX "IDX_Users_OrgId" ON "Users" ("orgid");
|
||||
CREATE INDEX "IDX_Users_State" ON "Users" ("state");
|
||||
ALTER TABLE IF EXISTS public."Games"
|
||||
ADD CONSTRAINT "FK_f32db60863a8a393b30aa222cd5" FOREIGN KEY ("createdBy")
|
||||
REFERENCES public."Users" (id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE NO ACTION;
|
||||
|
||||
CREATE INDEX "IDX_Organizations_Name" ON "Organizations" ("name");
|
||||
CREATE INDEX "IDX_Organizations_State" ON "Organizations" ("state");
|
||||
|
||||
CREATE INDEX "IDX_Decks_UserId" ON "Decks" ("user_id");
|
||||
CREATE INDEX "IDX_Decks_Type" ON "Decks" ("type");
|
||||
CREATE INDEX "IDX_Decks_CType" ON "Decks" ("ctype");
|
||||
CREATE INDEX "IDX_Decks_State" ON "Decks" ("state");
|
||||
CREATE INDEX "IDX_Decks_OrganizationId" ON "Decks" ("organization_id");
|
||||
|
||||
CREATE INDEX "IDX_Chats_Type" ON "Chats" ("type");
|
||||
CREATE INDEX "IDX_Chats_State" ON "Chats" ("state");
|
||||
CREATE INDEX "IDX_Chats_GameId" ON "Chats" ("gameId");
|
||||
CREATE INDEX "IDX_Chats_CreatedBy" ON "Chats" ("createdBy");
|
||||
|
||||
CREATE INDEX "IDX_Contacts_Type" ON "Contacts" ("type");
|
||||
CREATE INDEX "IDX_Contacts_State" ON "Contacts" ("state");
|
||||
CREATE INDEX "IDX_Contacts_UserId" ON "Contacts" ("userid");
|
||||
|
||||
CREATE INDEX "IDX_Games_GameCode" ON "Games" ("gamecode");
|
||||
CREATE INDEX "IDX_Games_State" ON "Games" ("state");
|
||||
CREATE INDEX "IDX_Games_CreatedBy" ON "Games" ("createdBy");
|
||||
CREATE INDEX "IDX_Games_OrganizationId" ON "Games" ("organizationid");
|
||||
|
||||
-- Comments for documentation
|
||||
COMMENT ON TABLE "Users" IS 'User accounts with authentication and profile information';
|
||||
COMMENT ON TABLE "Organizations" IS 'Organizations that can have multiple users and premium features';
|
||||
COMMENT ON TABLE "Decks" IS 'Card decks for the game, can be public, private, or organizational';
|
||||
COMMENT ON TABLE "Chats" IS 'Chat system supporting direct messages, groups, and game chats';
|
||||
COMMENT ON TABLE "Contacts" IS 'Contact form submissions and support tickets';
|
||||
COMMENT ON TABLE "Games" IS 'Game sessions with players, decks, and game state';
|
||||
|
||||
-- Enum value comments
|
||||
COMMENT ON COLUMN "Users"."state" IS '0=REGISTERED_NOT_VERIFIED, 1=VERIFIED_REGULAR, 2=VERIFIED_PREMIUM, 3=SOFT_DELETE, 4=DEACTIVATED, 5=ADMIN';
|
||||
COMMENT ON COLUMN "Organizations"."state" IS '0=REGISTERED, 1=ACTIVE, 2=SOFT_DELETE';
|
||||
COMMENT ON COLUMN "Decks"."type" IS '0=LUCK, 1=JOKER, 2=QUESTION';
|
||||
COMMENT ON COLUMN "Decks"."ctype" IS '0=PUBLIC, 1=PRIVATE, 2=ORGANIZATION';
|
||||
COMMENT ON COLUMN "Decks"."state" IS '0=ACTIVE, 1=SOFT_DELETE';
|
||||
COMMENT ON COLUMN "Chats"."type" IS 'direct, group, game';
|
||||
COMMENT ON COLUMN "Chats"."state" IS '0=ACTIVE, 1=ARCHIVE, 2=SOFT_DELETE';
|
||||
COMMENT ON COLUMN "Contacts"."type" IS '0=BUG, 1=PROBLEM, 2=QUESTION, 3=SALES, 4=OTHER';
|
||||
COMMENT ON COLUMN "Contacts"."state" IS '0=ACTIVE, 1=RESOLVED, 2=SOFT_DELETE';
|
||||
COMMENT ON COLUMN "Games"."state" IS '0=WAITING, 1=ACTIVE, 2=FINISHED, 3=CANCELLED';
|
||||
COMMENT ON COLUMN "Games"."logintype" IS '0=PUBLIC, 1=PRIVATE, 2=ORGANIZATION';
|
||||
|
||||
-- Grant permissions for application user
|
||||
-- Note: Replace 'serpentrace_app' with your actual application database user
|
||||
-- GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO serpentrace_app;
|
||||
-- GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO serpentrace_app;
|
||||
-- GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO serpentrace_app;
|
||||
END;
|
||||
Generated
+128
-1
@@ -16,6 +16,7 @@
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router-dom": "^7.6.0",
|
||||
"react-toastify": "^11.0.5",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tailwindcss": "^4.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -1259,6 +1260,11 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.7.tgz",
|
||||
@@ -1957,6 +1963,42 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"version": "6.6.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
|
||||
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.17.1",
|
||||
"xmlhttprequest-ssl": "~2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-client/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.18.1",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
|
||||
@@ -3097,7 +3139,6 @@
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
@@ -3478,6 +3519,64 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-client": {
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
|
||||
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io-client": "~6.6.1",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-client/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -3729,6 +3828,34 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xmlhttprequest-ssl": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router-dom": "^7.6.0",
|
||||
"react-toastify": "^11.0.5",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tailwindcss": "^4.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -14,6 +14,7 @@ import CompanyHub from "./pages/Contacts/Contacts"
|
||||
import About from "./pages/About/About"
|
||||
import ScrollToTop from "./components/ScrollToTop"
|
||||
import GameScreen from "./pages/Game/GameScreen"
|
||||
import GameTest from "./pages/Game/GameTest"
|
||||
import Reports from "./pages/Report/Reports"
|
||||
import Lobby from "./pages/Game/Lobby"
|
||||
import ProfileCard from "./components/Userdetails/Userdetails"
|
||||
@@ -68,6 +69,7 @@ function App() {
|
||||
<Route path="/deck-creator" element={<DeckCreator />} />
|
||||
<Route path="/deck-creator/:deckId" element={<DeckCreator />} />
|
||||
<Route path="/game" element={<GameScreen />} />
|
||||
<Route path="/game-test" element={<GameTest />} />
|
||||
{/* <Route path="/contacts" element={<CompanyHub />} /> */}
|
||||
<Route path="/report" element={<Reports />} />
|
||||
<Route path="/choosedeck" element={<ChooseDeck />} />
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { apiClient } from './userApi';
|
||||
|
||||
/**
|
||||
* Create a new game
|
||||
* @param {Object} gameData - Game creation data
|
||||
* @param {string[]} gameData.deckids - Array of deck UUIDs
|
||||
* @param {number} gameData.maxplayers - Maximum players (2-8)
|
||||
* @param {number} gameData.logintype - 0=PUBLIC, 1=PRIVATE, 2=ORGANIZATION
|
||||
* @returns {Promise<Object>} Game data with gameCode
|
||||
*/
|
||||
export const createGame = async (gameData) => {
|
||||
try {
|
||||
const response = await apiClient.post('/games/start', gameData);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error creating game:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Join an existing game
|
||||
* @param {Object} joinData - Join game data
|
||||
* @param {string} joinData.gameCode - 6-character game code
|
||||
* @param {string} [joinData.playerName] - Player name (required for public games)
|
||||
* @returns {Promise<Object>} Game data with gameToken
|
||||
*/
|
||||
export const joinGame = async (joinData) => {
|
||||
try {
|
||||
const response = await apiClient.post('/games/join', joinData);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error joining game:', error);
|
||||
console.error('Join game error response:', error.response?.data);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Start the game (gamemaster only)
|
||||
* @param {string} gameId - Game UUID
|
||||
* @returns {Promise<Object>} Game data with board
|
||||
*/
|
||||
export const startGame = async (gameId) => {
|
||||
try {
|
||||
const response = await apiClient.post(`/games/${gameId}/start`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error starting game:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get user's games
|
||||
* @returns {Promise<Array>} Array of games
|
||||
*/
|
||||
export const getMyGames = async () => {
|
||||
try {
|
||||
const response = await apiClient.get('/games/my-games');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching games:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get active public games
|
||||
* @returns {Promise<Array>} Array of active games
|
||||
*/
|
||||
export const getActiveGames = async () => {
|
||||
try {
|
||||
const response = await apiClient.get('/games/active');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching active games:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,268 @@
|
||||
import { useEffect, useRef, useState, useCallback, useMemo } from 'react';
|
||||
import { io } from 'socket.io-client';
|
||||
import { API_CONFIG } from '../api/userApi';
|
||||
|
||||
const isDev = import.meta.env.DEV;
|
||||
const log = (...args) => isDev && console.log(...args);
|
||||
const warn = (...args) => isDev && console.warn(...args);
|
||||
const error = (...args) => console.error(...args);
|
||||
|
||||
/**
|
||||
* Optimized WebSocket hook for game connection
|
||||
* @param {string} gameToken - JWT token from game join
|
||||
* @returns {Object} WebSocket state and methods
|
||||
*/
|
||||
export const useGameWebSocket = (gameToken) => {
|
||||
const socketRef = useRef(null);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [gameState, setGameState] = useState(null);
|
||||
const [boardData, setBoardData] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [isGamemaster, setIsGamemaster] = useState(false);
|
||||
const [gameStarted, setGameStarted] = useState(false);
|
||||
const eventListenersRef = useRef(new Map());
|
||||
|
||||
// Memoized derived values - no extra state needed
|
||||
const players = useMemo(() => {
|
||||
// Backend sends different player fields depending on game state
|
||||
// connectedPlayers: array of player names (strings) who are connected via WebSocket
|
||||
// players: full player objects with game data (positions, etc.)
|
||||
const connectedPlayers = gameState?.connectedPlayers || [];
|
||||
const gamePlayers = gameState?.players || [];
|
||||
const currentPlayers = gameState?.currentPlayers || [];
|
||||
|
||||
// If we have full player objects, use those
|
||||
if (currentPlayers.length > 0) return currentPlayers;
|
||||
if (gamePlayers.length > 0) return gamePlayers;
|
||||
|
||||
// Otherwise, map connected player names to basic player objects
|
||||
return connectedPlayers.map((name, index) => ({
|
||||
id: `player-${index}`,
|
||||
name: typeof name === 'string' ? name : name.playerName || `Player ${index + 1}`,
|
||||
isOnline: true,
|
||||
isReady: gameState?.readyPlayers?.includes(name) || false,
|
||||
}));
|
||||
}, [gameState?.connectedPlayers, gameState?.players, gameState?.currentPlayers, gameState?.readyPlayers]);
|
||||
const currentTurn = useMemo(() => gameState?.currentPlayer || null, [gameState?.currentPlayer]);
|
||||
|
||||
// Connect to game WebSocket - only once per token
|
||||
useEffect(() => {
|
||||
if (!gameToken) return;
|
||||
|
||||
log('🔌 Connecting to game WebSocket...');
|
||||
|
||||
// Connect to /game namespace
|
||||
socketRef.current = io(`${API_CONFIG.wsURL}/game`, {
|
||||
transports: ['websocket'],
|
||||
reconnection: true,
|
||||
reconnectionAttempts: 5,
|
||||
reconnectionDelay: 1000,
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
const socket = socketRef.current;
|
||||
|
||||
// Connection handlers
|
||||
const handleConnect = () => {
|
||||
log('✅ Connected to game WebSocket');
|
||||
setIsConnected(true);
|
||||
setError(null);
|
||||
socket.emit('game:join', { gameToken });
|
||||
};
|
||||
|
||||
const handleConnectError = (err) => {
|
||||
error('❌ Connection error:', err);
|
||||
setIsConnected(false);
|
||||
setError(err.message);
|
||||
};
|
||||
|
||||
const handleDisconnect = (reason) => {
|
||||
log('🔌 Disconnected:', reason);
|
||||
setIsConnected(false);
|
||||
};
|
||||
|
||||
// Game state handlers - batch updates
|
||||
const handleGameState = (state) => {
|
||||
log('📊 Game state:', state);
|
||||
setGameState(state);
|
||||
};
|
||||
|
||||
const handleGameJoined = (data) => {
|
||||
log('✅ Joined game:', data);
|
||||
// Store if this user is the gamemaster
|
||||
if (data.isGamemaster !== undefined) {
|
||||
setIsGamemaster(data.isGamemaster);
|
||||
}
|
||||
// Backend will send game:state next
|
||||
};
|
||||
|
||||
const handlePlayerJoined = (data) => {
|
||||
log('👤 Player joined:', data.playerName);
|
||||
// Update game state to add the new player to connectedPlayers
|
||||
setGameState(prev => {
|
||||
if (!prev) return prev;
|
||||
|
||||
const currentConnected = prev.connectedPlayers || [];
|
||||
// Only add if not already in the list
|
||||
if (!currentConnected.includes(data.playerName)) {
|
||||
return {
|
||||
...prev,
|
||||
connectedPlayers: [...currentConnected, data.playerName]
|
||||
};
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
};
|
||||
|
||||
const handleGameStarted = (data) => {
|
||||
log('🎮 Game started:', data);
|
||||
// Batch state updates
|
||||
if (data.boardData) setBoardData(data.boardData);
|
||||
if (data.gameState) setGameState(data.gameState);
|
||||
// Signal that game has started
|
||||
setGameStarted(true);
|
||||
};
|
||||
|
||||
const handlePlayerMoved = (moveData) => {
|
||||
log('🏃 Player moved:', moveData.playerName);
|
||||
// Update only the moved player
|
||||
setGameState(prev => {
|
||||
if (!prev?.currentPlayers) return prev;
|
||||
return {
|
||||
...prev,
|
||||
currentPlayers: prev.currentPlayers.map(p =>
|
||||
p.playerId === moveData.playerId
|
||||
? { ...p, boardPosition: moveData.newPosition }
|
||||
: p
|
||||
),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleTurnChanged = (data) => {
|
||||
log('🔄 Turn changed to:', data.currentPlayerName);
|
||||
setGameState(prev => prev ? { ...prev, currentPlayer: data.currentPlayer } : prev);
|
||||
};
|
||||
|
||||
const handleError = (err) => {
|
||||
error('❌ Game error:', err);
|
||||
setError(err.message);
|
||||
};
|
||||
|
||||
// Register all handlers
|
||||
socket.on('connect', handleConnect);
|
||||
socket.on('connect_error', handleConnectError);
|
||||
socket.on('disconnect', handleDisconnect);
|
||||
socket.on('game:state', handleGameState);
|
||||
socket.on('game:state-update', handleGameState);
|
||||
socket.on('game:joined', handleGameJoined);
|
||||
socket.on('game:player-joined', handlePlayerJoined);
|
||||
socket.on('game:started', handleGameStarted);
|
||||
socket.on('game:player-moved', handlePlayerMoved);
|
||||
socket.on('game:turn-changed', handleTurnChanged);
|
||||
socket.on('game:error', handleError);
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
log('🧹 Cleaning up WebSocket connection');
|
||||
socket.removeAllListeners();
|
||||
socket.disconnect();
|
||||
};
|
||||
}, [gameToken]);
|
||||
|
||||
// Optimized event listener management
|
||||
const addEventListener = useCallback((event, handler) => {
|
||||
const socket = socketRef.current;
|
||||
if (!socket) return;
|
||||
|
||||
socket.on(event, handler);
|
||||
eventListenersRef.current.set(event, handler);
|
||||
}, []);
|
||||
|
||||
const removeEventListener = useCallback((event) => {
|
||||
const socket = socketRef.current;
|
||||
if (!socket) return;
|
||||
|
||||
const handler = eventListenersRef.current.get(event);
|
||||
if (handler) {
|
||||
socket.off(event, handler);
|
||||
eventListenersRef.current.delete(event);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Memoized action methods - stable references
|
||||
const rollDice = useCallback((diceValue) => {
|
||||
const socket = socketRef.current;
|
||||
if (!socket || !isConnected) {
|
||||
warn('⚠️ Cannot roll dice: not connected');
|
||||
return false;
|
||||
}
|
||||
|
||||
log('🎲 Rolling dice:', diceValue);
|
||||
socket.emit('game:dice-roll', {
|
||||
gameCode: gameState?.gameCode,
|
||||
diceValue,
|
||||
});
|
||||
return true;
|
||||
}, [isConnected, gameState?.gameCode]);
|
||||
|
||||
const sendMessage = useCallback((message) => {
|
||||
const socket = socketRef.current;
|
||||
if (!socket || !isConnected) {
|
||||
warn('⚠️ Cannot send message: not connected');
|
||||
return false;
|
||||
}
|
||||
|
||||
socket.emit('game:chat', {
|
||||
gameCode: gameState?.gameCode,
|
||||
message,
|
||||
});
|
||||
return true;
|
||||
}, [isConnected, gameState?.gameCode]);
|
||||
|
||||
const setReady = useCallback((ready = true) => {
|
||||
const socket = socketRef.current;
|
||||
if (!socket || !isConnected) {
|
||||
warn('⚠️ Cannot set ready: not connected');
|
||||
return false;
|
||||
}
|
||||
|
||||
socket.emit('game:ready', {
|
||||
gameCode: gameState?.gameCode,
|
||||
ready,
|
||||
});
|
||||
return true;
|
||||
}, [isConnected, gameState?.gameCode]);
|
||||
|
||||
const leaveGame = useCallback(() => {
|
||||
const socket = socketRef.current;
|
||||
if (!socket || !isConnected) {
|
||||
warn('⚠️ Cannot leave game: not connected');
|
||||
return false;
|
||||
}
|
||||
|
||||
socket.emit('game:leave', {
|
||||
gameCode: gameState?.gameCode,
|
||||
});
|
||||
return true;
|
||||
}, [isConnected, gameState?.gameCode]);
|
||||
|
||||
return {
|
||||
socket: socketRef.current,
|
||||
isConnected,
|
||||
gameState,
|
||||
players,
|
||||
boardData,
|
||||
currentTurn,
|
||||
error,
|
||||
isGamemaster,
|
||||
gameStarted,
|
||||
// Methods
|
||||
rollDice,
|
||||
sendMessage,
|
||||
setReady,
|
||||
leaveGame,
|
||||
addEventListener,
|
||||
removeEventListener,
|
||||
};
|
||||
};
|
||||
@@ -1,88 +1,189 @@
|
||||
import React, { useState } from "react"
|
||||
import React, { useState, useEffect, useMemo, useCallback } from "react"
|
||||
import { getVerticalOffset } from "../../utils/randomUtils"
|
||||
import Dice from "../../utils/dice/Dice"
|
||||
import { useGameWebSocket } from "../../hooks/useGameWebSocket"
|
||||
|
||||
const GameScreen = () => {
|
||||
const boardRows = 5
|
||||
const boardCols = 20
|
||||
const totalCells = boardRows * boardCols
|
||||
const cellSize = 40
|
||||
const cellMargin = 2.5
|
||||
const rowSpacing = 70 // Extra spacing between rows
|
||||
const topOffset = rowSpacing * 0.5 // Increase topOffset for more spacing
|
||||
const bottomOffset = rowSpacing * 0.5 // Increase bottomOffset for more spacing
|
||||
const boardWidthPx = boardCols * (cellSize + cellMargin * 2)
|
||||
const boardHeightPx =
|
||||
boardRows * (cellSize + cellMargin * 2 + rowSpacing) + topOffset + bottomOffset - rowSpacing
|
||||
// Constants - outside component to prevent recreation
|
||||
const PLAYER_STYLES = [
|
||||
{ color: "bg-blue-600", emoji: "🐍" },
|
||||
{ color: "bg-green-600", emoji: "🐢" },
|
||||
{ color: "bg-purple-600", emoji: "🐇" },
|
||||
{ color: "bg-yellow-600", emoji: "🦊" },
|
||||
{ color: "bg-red-600", emoji: "🦁" },
|
||||
{ color: "bg-pink-600", emoji: "🐷" },
|
||||
{ color: "bg-orange-600", emoji: "🐯" },
|
||||
{ color: "bg-indigo-600", emoji: "🐺" },
|
||||
]
|
||||
|
||||
// Generate a snake-like path with vertical spacing and vertical offsets
|
||||
const generateWindingPath = () => {
|
||||
const path = []
|
||||
let currentNum = 1
|
||||
|
||||
for (let row = 0; row < boardRows && currentNum <= totalCells; row++) {
|
||||
// Calculate the y position with extra row spacing
|
||||
const baseYPosition = topOffset + row * (cellSize + cellMargin * 2 + rowSpacing)
|
||||
|
||||
// If row number is even, go right; if odd, go left
|
||||
if (row % 2 === 0) {
|
||||
// Left to right
|
||||
for (let col = 0; col < boardCols && currentNum <= totalCells; col++) {
|
||||
path.push({
|
||||
number: currentNum++,
|
||||
x: col * (cellSize + cellMargin * 2),
|
||||
y: baseYPosition + getVerticalOffset(currentNum - 1),
|
||||
type: getFieldType(currentNum - 1),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Right to left
|
||||
for (let col = boardCols - 1; col >= 0 && currentNum <= totalCells; col--) {
|
||||
path.push({
|
||||
number: currentNum++,
|
||||
x: col * (cellSize + cellMargin * 2),
|
||||
y: baseYPosition + getVerticalOffset(currentNum - 1),
|
||||
type: getFieldType(currentNum - 1),
|
||||
})
|
||||
}
|
||||
}
|
||||
const BOARD_CONFIG = {
|
||||
rows: 5,
|
||||
cols: 20,
|
||||
cellSize: 40,
|
||||
cellMargin: 2.5,
|
||||
rowSpacing: 70,
|
||||
}
|
||||
|
||||
return path
|
||||
// Helper functions outside component
|
||||
const mapFieldType = (backendType) => {
|
||||
switch (backendType) {
|
||||
case 'positive': return 'good'
|
||||
case 'negative': return 'bad'
|
||||
case 'luck': return 'clover'
|
||||
default: return 'regular'
|
||||
}
|
||||
}
|
||||
|
||||
const getFieldType = (count) => {
|
||||
const getDefaultFieldType = (count) => {
|
||||
if (count % 17 === 0) return "clover"
|
||||
if (count % 13 === 0) return "bad"
|
||||
if ((count + 5) % 13 === 0) return "good"
|
||||
return "regular"
|
||||
}
|
||||
|
||||
const [path, setPath] = useState(generateWindingPath())
|
||||
const [players, setPlayers] = useState([
|
||||
{ id: 1, name: "Béla", position: 34, score: 25, color: "bg-blue-600", emoji: "🐍" },
|
||||
{ id: 2, name: "Juci", position: 50, score: 30, color: "bg-green-600", emoji: "🐢" },
|
||||
{ id: 3, name: "Kati", position: 70, score: 15, color: "bg-purple-600", emoji: "🐇" },
|
||||
{ id: 3, name: "Fürtös", position: 68, score: 14, color: "bg-yellow-600", emoji: "😂" },
|
||||
])
|
||||
const GameScreen = () => {
|
||||
// WebSocket connection
|
||||
const gameToken = localStorage.getItem('gameToken')
|
||||
const {
|
||||
isConnected,
|
||||
gameState,
|
||||
players: backendPlayers,
|
||||
boardData,
|
||||
currentTurn,
|
||||
error,
|
||||
rollDice,
|
||||
addEventListener,
|
||||
removeEventListener
|
||||
} = useGameWebSocket(gameToken)
|
||||
|
||||
// New: selected dice value from dropdown (null = none)
|
||||
const [selectedDice, setSelectedDice] = useState(null)
|
||||
const [path, setPath] = useState([])
|
||||
const [players, setPlayers] = useState([])
|
||||
|
||||
// Sort players by position in descending order
|
||||
const sortedPlayers = [...players].sort((a, b) => b.position - a.position)
|
||||
// Memoized board dimensions
|
||||
const { rows, cols, totalCells, cellSize, cellMargin, rowSpacing, topOffset, width, height } = useMemo(() => {
|
||||
const { rows, cols, cellSize, cellMargin, rowSpacing } = BOARD_CONFIG
|
||||
const topOffset = rowSpacing * 0.5
|
||||
const bottomOffset = rowSpacing * 0.5
|
||||
const totalCells = rows * cols
|
||||
|
||||
// Handle dice roll completion
|
||||
const handleDiceRoll = (value) => {
|
||||
console.log("Rolled:", value)
|
||||
// reset dropdown selection after roll
|
||||
setSelectedDice(null)
|
||||
// You can add logic here to move the current player based on the dice value
|
||||
return {
|
||||
rows,
|
||||
cols,
|
||||
totalCells,
|
||||
cellSize,
|
||||
cellMargin,
|
||||
rowSpacing,
|
||||
topOffset,
|
||||
bottomOffset,
|
||||
width: cols * (cellSize + cellMargin * 2),
|
||||
height: rows * (cellSize + cellMargin * 2 + rowSpacing) + topOffset + bottomOffset - rowSpacing,
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Memoized path generator - Snake pattern with proper turn handling
|
||||
const generateWindingPath = useCallback((backendFields = null) => {
|
||||
const path = []
|
||||
const hasBackendData = backendFields && Array.isArray(backendFields)
|
||||
|
||||
let currentNum = 1
|
||||
|
||||
// Generate all 100 positions
|
||||
while (currentNum <= totalCells) {
|
||||
const row = Math.floor((currentNum - 1) / cols)
|
||||
const posInRow = (currentNum - 1) % cols
|
||||
const isLeftToRight = row % 2 === 0
|
||||
|
||||
// Calculate column based on direction
|
||||
const col = isLeftToRight ? posInRow : (cols - 1 - posInRow)
|
||||
|
||||
// Base Y position for this row
|
||||
let baseYPosition = topOffset + row * (cellSize + cellMargin * 2 + rowSpacing)
|
||||
|
||||
// Apply vertical offset for wave effect
|
||||
let yOffset = getVerticalOffset(currentNum - 1)
|
||||
|
||||
// Special handling for turn positions (21, 41, 61, 81)
|
||||
// These should be positioned between rows to show the turn
|
||||
if (currentNum % cols === 1 && currentNum > 1) {
|
||||
// This is the first element of a new row (21, 41, 61, 81)
|
||||
// Position it halfway between the previous row and current row
|
||||
baseYPosition = topOffset + (row - 0.5) * (cellSize + cellMargin * 2 + rowSpacing)
|
||||
yOffset = 0 // Reset wave offset for turn positions
|
||||
}
|
||||
|
||||
console.log("Generated path length:", path.length)
|
||||
const backendField = hasBackendData ? backendFields.find(f => f.position === currentNum) : null
|
||||
|
||||
const getFieldStyle = (type) => {
|
||||
path.push({
|
||||
number: currentNum,
|
||||
x: col * (cellSize + cellMargin * 2),
|
||||
y: baseYPosition + yOffset,
|
||||
type: backendField ? mapFieldType(backendField.type) : getDefaultFieldType(currentNum - 1),
|
||||
stepValue: backendField?.stepValue || 0,
|
||||
})
|
||||
|
||||
currentNum++
|
||||
}
|
||||
|
||||
return path
|
||||
}, [rows, cols, totalCells, cellSize, cellMargin, rowSpacing, topOffset])
|
||||
|
||||
// Update path when boardData changes
|
||||
useEffect(() => {
|
||||
if (boardData?.fields) {
|
||||
setPath(generateWindingPath(boardData.fields))
|
||||
} else if (path.length === 0) {
|
||||
setPath(generateWindingPath())
|
||||
}
|
||||
}, [boardData, generateWindingPath])
|
||||
|
||||
// Update players from backend - memoized mapping
|
||||
useEffect(() => {
|
||||
if (!backendPlayers?.length) return
|
||||
|
||||
const mappedPlayers = backendPlayers.map((player, index) => ({
|
||||
id: player.playerId || player.id || index,
|
||||
name: player.playerName || player.name || `Player ${index + 1}`,
|
||||
position: player.boardPosition || 0,
|
||||
score: player.score || 0,
|
||||
color: PLAYER_STYLES[index % PLAYER_STYLES.length].color,
|
||||
emoji: PLAYER_STYLES[index % PLAYER_STYLES.length].emoji,
|
||||
isOnline: player.isOnline !== undefined ? player.isOnline : true,
|
||||
isReady: player.isReady || false,
|
||||
}))
|
||||
|
||||
setPlayers(mappedPlayers)
|
||||
}, [backendPlayers])
|
||||
|
||||
// Listen to player movement - optimized to update only moved player
|
||||
useEffect(() => {
|
||||
if (!addEventListener) return
|
||||
|
||||
const handlePlayerMoved = (moveData) => {
|
||||
setPlayers(prev =>
|
||||
prev.map(p =>
|
||||
p.id === moveData.playerId
|
||||
? { ...p, position: moveData.newPosition }
|
||||
: p
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
addEventListener('game:player-moved', handlePlayerMoved)
|
||||
return () => removeEventListener('game:player-moved')
|
||||
}, [addEventListener, removeEventListener])
|
||||
|
||||
// Sorted players - memoized
|
||||
const sortedPlayers = useMemo(
|
||||
() => [...players].sort((a, b) => b.position - a.position),
|
||||
[players]
|
||||
)
|
||||
|
||||
// Handle dice roll
|
||||
const handleDiceRoll = useCallback((value) => {
|
||||
rollDice(value)
|
||||
}, [rollDice])
|
||||
|
||||
// Get field style - memoized
|
||||
const getFieldStyle = useCallback((type) => {
|
||||
switch (type) {
|
||||
case "clover":
|
||||
return "bg-teal-700 border-teal-500 shadow-teal-800"
|
||||
@@ -93,15 +194,16 @@ const GameScreen = () => {
|
||||
default:
|
||||
return "bg-gray-800 border-gray-600 shadow-gray-900"
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const getPlayerPosition = (playerPosition) => {
|
||||
// Get player position - memoized
|
||||
const getPlayerPosition = useCallback((playerPosition) => {
|
||||
const field = path.find((p) => p.number === playerPosition)
|
||||
return field ? { top: `${field.y}px`, left: `${field.x}px` } : { top: 0, left: 0 }
|
||||
}
|
||||
}, [path])
|
||||
|
||||
// Function to get medal style based on rank
|
||||
const getMedalStyle = (rank) => {
|
||||
// Get medal style - memoized
|
||||
const getMedalStyle = useCallback((rank) => {
|
||||
switch (rank) {
|
||||
case 1:
|
||||
return "bg-yellow-400 text-yellow-900 border-yellow-500 shadow-yellow-600"
|
||||
@@ -112,20 +214,57 @@ const GameScreen = () => {
|
||||
default:
|
||||
return "bg-gray-700 text-gray-300 border-gray-600 shadow-gray-800"
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-gradient-to-br from-gray-900 via-gray-800 to-teal-900 min-h-screen flex items-center justify-center">
|
||||
<div className="w-full">
|
||||
{/* Connection Status Indicator */}
|
||||
<div className="fixed top-4 right-4 z-50">
|
||||
<div className={`px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 ${
|
||||
isConnected
|
||||
? 'bg-green-600 text-white'
|
||||
: 'bg-red-600 text-white'
|
||||
}`}>
|
||||
<div className={`w-3 h-3 rounded-full ${
|
||||
isConnected ? 'bg-green-300 animate-pulse' : 'bg-red-300'
|
||||
}`}></div>
|
||||
<span className="text-sm font-medium">
|
||||
{isConnected ? '🟢 Csatlakozva' : '🔴 Kapcsolódás...'}
|
||||
</span>
|
||||
</div>
|
||||
{error && (
|
||||
<div className="mt-2 px-4 py-2 rounded-lg shadow-lg bg-red-600 text-white text-xs">
|
||||
⚠️ {error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Game Info Bar */}
|
||||
{gameState && (
|
||||
<div className="fixed top-4 left-4 z-50">
|
||||
<div className="bg-gray-800 border border-teal-700 px-4 py-2 rounded-lg shadow-lg">
|
||||
<div className="text-teal-300 text-sm font-medium">
|
||||
🎮 Játék kód: <span className="font-bold text-white">{gameState.gameCode || 'N/A'}</span>
|
||||
</div>
|
||||
{currentTurn && (
|
||||
<div className="text-gray-400 text-xs mt-1">
|
||||
🎯 Köron: <span className="text-white">{players.find(p => p.id === currentTurn)?.name || 'Betöltés...'}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-6 justify-center">
|
||||
{/* Game Board */}
|
||||
<div className="relative bg-gray-800 p-6 rounded-2xl shadow-xl border border-teal-700 flex flex-col items-center justify-center overflow-hidden">
|
||||
{/* Háttér */}
|
||||
{/* Background decoration */}
|
||||
<div className="absolute w-full h-full opacity-10 pointer-events-none overflow-hidden">
|
||||
{[...Array(35)].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="absolute rounded-full bg-teal-600 animate-pulse8"
|
||||
className="absolute rounded-full bg-teal-600 animate-pulse"
|
||||
style={{
|
||||
width: Math.random() * 120 + 40 + "px",
|
||||
height: Math.random() * 120 + 40 + "px",
|
||||
@@ -136,8 +275,9 @@ const GameScreen = () => {
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
<div className="relative" style={{ height: `${boardHeightPx}px`, width: `${boardWidthPx}px` }}>
|
||||
{/* Mezők */}
|
||||
|
||||
<div className="relative" style={{ height: `${height}px`, width: `${width}px` }}>
|
||||
{/* Fields */}
|
||||
{path.map((field) => (
|
||||
<div
|
||||
key={field.number}
|
||||
@@ -163,44 +303,65 @@ const GameScreen = () => {
|
||||
className={`absolute w-6 h-6 ${player.color} rounded-full border-2 border-white shadow-lg flex items-center justify-center text-white text-xs font-bold z-10 animate-bounce`}
|
||||
style={{
|
||||
...getPlayerPosition(player.position),
|
||||
transform: "translate(18px, 18px)",
|
||||
transform: "translate(17px, 17px)",
|
||||
}}
|
||||
>
|
||||
{player.emoji}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Game information */}
|
||||
{/* <div className="bg-white rounded-xl p-2 shadow-lg border border-indigo-100 max-w-3xl mx-auto mt-4 z-10">
|
||||
<p className="text-gray-600 text-sm text-center">
|
||||
<span className="inline-flex items-center mx-2"><span className="w-3 h-3 bg-white border border-gray-300 rounded-full mr-1"></span> Sima</span>
|
||||
<span className="inline-flex items-center mx-2"><span className="w-3 h-3 bg-green-200 border border-green-500 rounded-full mr-1"></span> Lóhere</span>
|
||||
<span className="inline-flex items-center mx-2"><span className="w-3 h-3 bg-red-200 border border-red-500 rounded-full mr-1"></span> Rossz</span>
|
||||
<span className="inline-flex items-center mx-2"><span className="w-3 h-3 bg-blue-200 border border-blue-500 rounded-full mr-1"></span> Jó</span>
|
||||
</p>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
{/* Right sidebar */}
|
||||
<div className="flex-1 max-w-md">
|
||||
<div className="bg-gray-800 rounded-xl p-4 shadow-lg mb-4 border border-teal-700">
|
||||
<h2 className="text-xl font-semibold mb-3 text-teal-300">Játékosok</h2>
|
||||
|
||||
{/* Empty state */}
|
||||
{players.length === 0 && (
|
||||
<div className="text-center py-8 text-gray-400">
|
||||
<div className="text-4xl mb-2">👥</div>
|
||||
<p className="text-sm">Várakozás játékosokra...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Players list */}
|
||||
{sortedPlayers.map((player, index) => (
|
||||
<div
|
||||
key={player.id}
|
||||
className="flex items-center mb-3 p-2 bg-gray-900 rounded-lg hover:bg-gray-700 transition-colors"
|
||||
className="flex items-center mb-3 p-2 bg-gray-900 rounded-lg hover:bg-gray-700 transition-colors relative"
|
||||
>
|
||||
{/* Online indicator */}
|
||||
{player.isOnline && (
|
||||
<div className="absolute top-1 right-1 w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`w-8 h-8 ${player.color} rounded-full mr-3 flex items-center justify-center text-white text-sm font-bold shadow-md`}
|
||||
>
|
||||
{player.emoji}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm text-gray-300 flex items-center">
|
||||
<div className="font-medium text-sm text-gray-300 flex items-center gap-2 flex-wrap">
|
||||
{player.name}
|
||||
|
||||
{/* Ready indicator */}
|
||||
{player.isReady && (
|
||||
<span className="px-2 py-0.5 bg-green-600 text-white text-xs rounded-full">
|
||||
✓ Kész
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Current turn indicator */}
|
||||
{currentTurn === player.id && (
|
||||
<span className="px-2 py-0.5 bg-yellow-500 text-gray-900 text-xs rounded-full font-bold animate-pulse">
|
||||
▶ Köre
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Rank medal */}
|
||||
<span
|
||||
className={`ml-2 px-2 py-1 rounded-full border text-xs font-bold shadow-md ${getMedalStyle(
|
||||
className={`ml-auto px-2 py-1 rounded-full border text-xs font-bold shadow-md ${getMedalStyle(
|
||||
index + 1
|
||||
)}`}
|
||||
>
|
||||
@@ -225,32 +386,34 @@ const GameScreen = () => {
|
||||
<div className="bg-gray-800 rounded-xl p-4 shadow-lg border border-teal-700 text-center">
|
||||
<h2 className="text-xl font-semibold mb-3 text-teal-300">Dobókocka</h2>
|
||||
<p className="text-gray-300 text-sm mb-4">
|
||||
Kattints a kockára dobáshoz vagy válassz egy számot az alábbiból!
|
||||
Kattints a kockára dobáshoz!
|
||||
</p>
|
||||
|
||||
{/* Dropdown to select number 1-6 (triggers animated roll to that number) */}
|
||||
<div className="mb-3">
|
||||
<select
|
||||
value={selectedDice ?? ""}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value ? Number(e.target.value) : null
|
||||
setSelectedDice(v)
|
||||
}}
|
||||
className="bg-gray-900 text-gray-200 rounded-md p-2 border border-gray-700"
|
||||
>
|
||||
<option value="">Válassz számot...</option>
|
||||
<option value="1">1</option>
|
||||
<option value="2">2</option>
|
||||
<option value="3">3</option>
|
||||
<option value="4">4</option>
|
||||
<option value="5">5</option>
|
||||
<option value="6">6</option>
|
||||
</select>
|
||||
<Dice onRoll={handleDiceRoll} />
|
||||
|
||||
{/* Connection warning */}
|
||||
{!isConnected && (
|
||||
<div className="mt-3 text-xs text-red-400">
|
||||
⚠️ Nincs kapcsolat a szerverrel
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Dice onRoll={handleDiceRoll} selectedValue={selectedDice} />
|
||||
{/* Debug Info Panel (Development only) */}
|
||||
{import.meta.env.DEV && (
|
||||
<div className="bg-gray-900 rounded-xl p-4 shadow-lg border border-gray-700 text-left mt-4">
|
||||
<h3 className="text-sm font-semibold mb-2 text-gray-400">🔧 Debug Info</h3>
|
||||
<div className="text-xs text-gray-500 space-y-1">
|
||||
<div>📡 Connected: {isConnected ? '✅' : '❌'}</div>
|
||||
<div>🎮 Game Code: {gameState?.gameCode || 'N/A'}</div>
|
||||
<div>👥 Players: {backendPlayers?.length || 0}</div>
|
||||
<div>🎲 Board Fields: {boardData?.fields?.length || 0}</div>
|
||||
<div>🏁 Current Turn: {currentTurn || 'N/A'}</div>
|
||||
<div>🔑 Token: {gameToken ? '✅' : '❌'}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { createGame, joinGame } from '../../api/gameApi';
|
||||
|
||||
const GameTest = () => {
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [gameCode, setGameCode] = useState('');
|
||||
const [createdGameCode, setCreatedGameCode] = useState('');
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
|
||||
const handleCreateGame = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setShowSuccess(false);
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
setError('Please login first at /login');
|
||||
return;
|
||||
}
|
||||
|
||||
const gameData = {
|
||||
deckids: ['99333c9a-5928-4788-b852-fa482d34ce56'], // Test deck ID as array
|
||||
maxplayers: 4,
|
||||
logintype: 0, // 0=PUBLIC
|
||||
};
|
||||
|
||||
const response = await createGame(gameData);
|
||||
console.log('Game created:', response);
|
||||
|
||||
// Backend returns game object directly
|
||||
const code = response.gamecode || response.gameCode;
|
||||
if (code) {
|
||||
setCreatedGameCode(code);
|
||||
setShowSuccess(true);
|
||||
}
|
||||
|
||||
// Store game token if provided
|
||||
if (response.gameToken) {
|
||||
localStorage.setItem('gameToken', response.gameToken);
|
||||
}
|
||||
|
||||
// Wait 3 seconds to show code, then navigate
|
||||
setTimeout(() => {
|
||||
navigate('/lobby', { state: { gameCode: code } });
|
||||
}, 3000);
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.message || 'Failed to create game');
|
||||
console.error('Create game error:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleJoinGame = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
setError('Kérlek jelentkezz be először a /login oldalon');
|
||||
return;
|
||||
}
|
||||
|
||||
const joinData = {
|
||||
gameCode: gameCode.toUpperCase(),
|
||||
playerName: localStorage.getItem('username') || 'Test Player',
|
||||
};
|
||||
|
||||
const response = await joinGame(joinData);
|
||||
console.log('Joined game:', response);
|
||||
|
||||
// Store game token
|
||||
if (response.data?.gameToken) {
|
||||
localStorage.setItem('gameToken', response.data.gameToken);
|
||||
navigate('/lobby', { state: { gameCode: gameCode.toUpperCase() } });
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.message || 'Nem sikerült csatlakozni a játékhoz');
|
||||
console.error('Join game error:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center p-8">
|
||||
<div className="bg-gray-800 rounded-lg p-8 max-w-md w-full">
|
||||
<h1 className="text-3xl font-bold mb-6 text-center">Game Test</h1>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-500/20 border border-red-500 rounded p-3 mb-4">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showSuccess && createdGameCode && (
|
||||
<div className="bg-green-500/20 border border-green-500 rounded p-4 mb-4">
|
||||
<p className="font-bold text-lg mb-2">Game Created!</p>
|
||||
<p className="text-2xl font-mono tracking-wider text-green-400 mb-2">
|
||||
{createdGameCode}
|
||||
</p>
|
||||
<p className="text-sm text-gray-300">
|
||||
Share this code with other players so they can join!
|
||||
</p>
|
||||
<p className="text-sm text-gray-400 mt-2">
|
||||
Redirecting to game in 3 seconds...
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<button
|
||||
onClick={handleCreateGame}
|
||||
disabled={loading}
|
||||
className="w-full bg-green-600 hover:bg-green-700 disabled:bg-gray-600 text-white font-bold py-3 px-4 rounded transition"
|
||||
>
|
||||
{loading ? 'Creating...' : 'Create New Game'}
|
||||
</button>
|
||||
|
||||
<div className="text-center text-gray-400">OR</div>
|
||||
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={gameCode}
|
||||
onChange={(e) => setGameCode(e.target.value)}
|
||||
placeholder="Enter Game Code"
|
||||
className="w-full bg-gray-700 text-white px-4 py-2 rounded mb-2"
|
||||
/>
|
||||
<button
|
||||
onClick={handleJoinGame}
|
||||
disabled={loading || !gameCode}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 text-white font-bold py-3 px-4 rounded transition"
|
||||
>
|
||||
{loading ? 'Joining...' : 'Join Game'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 pt-6 border-t border-gray-700">
|
||||
<p className="text-sm text-gray-400 mb-2">Quick Access (Dev Only):</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
localStorage.setItem('gameToken', 'test-token-123');
|
||||
navigate('/game');
|
||||
}}
|
||||
className="w-full bg-purple-600 hover:bg-purple-700 text-white font-bold py-2 px-4 rounded text-sm"
|
||||
>
|
||||
Go to Game (with test token)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GameTest;
|
||||
@@ -3,6 +3,8 @@ import { useNavigate, useLocation } from "react-router-dom"
|
||||
import Navbar from "../../components/Navbar/Navbar.jsx"
|
||||
import Background from "../../assets/backgrounds/Background.jsx"
|
||||
import useRequireAuth from "../../hooks/useRequireAuth.jsx"
|
||||
import { useGameWebSocket } from "../../hooks/useGameWebSocket.js"
|
||||
import { startGame } from "../../api/gameApi.js"
|
||||
|
||||
const Lobby = () => {
|
||||
const [visible, setVisible] = useState(false)
|
||||
@@ -12,6 +14,30 @@ const Lobby = () => {
|
||||
|
||||
const [user, setUser] = useRequireAuth()
|
||||
|
||||
// Get game code from location state or WebSocket
|
||||
const gameCodeFromState = location.state?.gameCode
|
||||
const gameToken = localStorage.getItem('gameToken')
|
||||
|
||||
const {
|
||||
isConnected,
|
||||
gameState,
|
||||
players,
|
||||
isGamemaster,
|
||||
gameStarted,
|
||||
} = useGameWebSocket(gameToken)
|
||||
|
||||
const gameCode = gameCodeFromState || gameState?.gameCode || 'Loading...'
|
||||
|
||||
// Filter out gamemaster from player list - gamemaster is NOT a player
|
||||
const currentPlayers = (players || []).filter(p => {
|
||||
// If we have userId info, filter by that
|
||||
if (p.userId) {
|
||||
return p.userId !== gameState?.createdBy
|
||||
}
|
||||
// Otherwise filter by name (less reliable but works for now)
|
||||
return true
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
@@ -23,12 +49,48 @@ const Lobby = () => {
|
||||
return () => observer.disconnect()
|
||||
}, [])
|
||||
|
||||
// Auto-navigate when game starts
|
||||
useEffect(() => {
|
||||
if (gameStarted) {
|
||||
console.log('🎮 Game started, navigating to /game')
|
||||
navigate("/game")
|
||||
}
|
||||
}, [gameStarted, navigate])
|
||||
|
||||
const handleExit = () => {
|
||||
if (window.confirm("Biztosan ki szeretnél lépni a lobbyból?")) {
|
||||
localStorage.removeItem('gameToken')
|
||||
navigate("/home")
|
||||
}
|
||||
}
|
||||
|
||||
const handleStartGame = async () => {
|
||||
try {
|
||||
// Get gameId from gameState
|
||||
const gameId = gameState?.gameId
|
||||
if (!gameId) {
|
||||
alert('Hiba: Játék azonosító nem található')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('Starting game with ID:', gameId)
|
||||
const response = await startGame(gameId)
|
||||
console.log('Game start response:', response)
|
||||
|
||||
// Backend will broadcast game:started event to all players
|
||||
// Navigate to game page
|
||||
navigate("/game")
|
||||
} catch (error) {
|
||||
console.error('Failed to start game:', error)
|
||||
alert(`Nem sikerült elindítani a játékot: ${error.response?.data?.error || error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const copyGameCode = () => {
|
||||
navigator.clipboard.writeText(gameCode)
|
||||
alert('Játék kód vágólapra másolva: ' + gameCode)
|
||||
}
|
||||
|
||||
const getInitials = (name) => {
|
||||
return name
|
||||
.split(" ")
|
||||
@@ -57,31 +119,121 @@ const Lobby = () => {
|
||||
style={{ background: "rgba(0,0,0,0.25)" }}
|
||||
>
|
||||
<h1 className="text-4xl md:text-5xl font-extrabold text-green-300 mb-4 text-center tracking-wide drop-shadow-lg">
|
||||
{user} Lobby-ja
|
||||
Játék Lobby
|
||||
</h1>
|
||||
|
||||
<p className="text-lg text-zinc-300 mb-8 text-center">
|
||||
Játékosok, akik csatlakoztak ehhez a szobához:
|
||||
{/* Game Code Display */}
|
||||
<div className="bg-gradient-to-r from-green-600/20 to-teal-600/20 rounded-xl p-6 mb-6 border-2 border-green-400/50">
|
||||
<p className="text-lg text-zinc-300 mb-2 text-center font-semibold">
|
||||
Játék Kód:
|
||||
</p>
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<p className="text-5xl font-mono font-extrabold text-green-300 tracking-widest drop-shadow-lg">
|
||||
{gameCode}
|
||||
</p>
|
||||
<button
|
||||
onClick={copyGameCode}
|
||||
className="bg-green-600 hover:bg-green-500 text-white px-4 py-2 rounded-lg font-semibold transition-all duration-200 hover:scale-105"
|
||||
title="Másolás vágólapra"
|
||||
>
|
||||
📋 Másolás
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-zinc-400 mt-3 text-center">
|
||||
Oszd meg ezt a kódot másokkal, hogy csatlakozhassanak a játékhoz!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Connection Status */}
|
||||
<div className="mb-4 text-center">
|
||||
<span className={`inline-block px-4 py-2 rounded-full text-sm font-semibold ${
|
||||
isConnected
|
||||
? 'bg-green-600/20 text-green-300 border border-green-400'
|
||||
: 'bg-red-600/20 text-red-300 border border-red-400'
|
||||
}`}>
|
||||
{isConnected ? '🟢 Kapcsolódva' : '🔴 Kapcsolat megszakadt'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-lg text-zinc-300 mb-6 text-center">
|
||||
Játékosok ({currentPlayers.length}):
|
||||
</p>
|
||||
|
||||
<div className="bg-zinc-800/90 rounded-xl shadow-lg p-6 mb-8">
|
||||
<ul className="flex flex-col gap-4">
|
||||
<li className="bg-zinc-700 py-3 px-4 rounded-xl text-green-400 font-semibold flex items-center gap-4 shadow hover:shadow-green-500/20 transition">
|
||||
{currentPlayers.length === 0 ? (
|
||||
<li className="text-center text-zinc-400 py-4">
|
||||
Várakozás játékosokra...
|
||||
</li>
|
||||
) : (
|
||||
currentPlayers.map((player, index) => (
|
||||
<li
|
||||
key={player.id || index}
|
||||
className="bg-zinc-700 py-3 px-4 rounded-xl text-green-400 font-semibold flex items-center gap-4 shadow hover:shadow-green-500/20 transition"
|
||||
>
|
||||
<div
|
||||
className="w-10 h-10 rounded-full flex items-center justify-center text-sm font-semibold"
|
||||
style={{ background: "rgba(34,197,94,0.12)", color: "var(--color-mint)" }}
|
||||
>
|
||||
{getInitials(user)}
|
||||
{getInitials(player.name || `Player ${index + 1}`)}
|
||||
</div>
|
||||
<span className="text-white text-lg">{user}</span>
|
||||
<span className="text-white text-lg flex-1">
|
||||
{player.name || `Player ${index + 1}`}
|
||||
</span>
|
||||
{player.isReady && (
|
||||
<span className="bg-green-600 text-white text-xs px-2 py-1 rounded-full">
|
||||
Kész
|
||||
</span>
|
||||
)}
|
||||
{player.isOnline && (
|
||||
<span className="text-green-400 text-xs">🟢</span>
|
||||
)}
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center">
|
||||
{/* Role indicator */}
|
||||
<div className="mb-6 text-center">
|
||||
{isGamemaster ? (
|
||||
<div className="bg-yellow-600/20 text-yellow-300 px-4 py-3 rounded-lg border border-yellow-400/50">
|
||||
<p className="font-semibold">👑 Te vagy a Gamemaster!</p>
|
||||
<p className="text-sm mt-1">Te nem játszol, csak indítod és moderálod a játékot.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-blue-600/20 text-blue-300 px-4 py-3 rounded-lg border border-blue-400/50">
|
||||
<p className="font-semibold">🎮 Te vagy egy Játékos!</p>
|
||||
<p className="text-sm mt-1">Várj, amíg a gamemaster elindítja a játékot.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center gap-4">
|
||||
{isGamemaster ? (
|
||||
/* Gamemaster view - can start game */
|
||||
<button
|
||||
onClick={handleStartGame}
|
||||
disabled={currentPlayers.length < 2}
|
||||
className={`px-8 py-3 rounded-xl font-semibold shadow-lg transition-transform transform hover:scale-105 ${
|
||||
currentPlayers.length >= 2
|
||||
? 'bg-gradient-to-r from-green-700 to-green-500 hover:from-green-600 hover:to-green-400 text-white hover:shadow-green-400/30'
|
||||
: 'bg-gray-600 text-gray-400 cursor-not-allowed'
|
||||
}`}
|
||||
title={currentPlayers.length < 2 ? 'Minimum 2 játékos szükséges' : 'Játék indítása'}
|
||||
>
|
||||
Játék Indítása
|
||||
</button>
|
||||
) : (
|
||||
/* Player view - cannot start game, just wait */
|
||||
<div className="text-center text-zinc-400">
|
||||
<p className="text-lg">Várakozás a gamemaster-re...</p>
|
||||
<p className="text-sm mt-2">Csak a gamemaster indíthatja el a játékot</p>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={handleExit}
|
||||
className="bg-gradient-to-r from-green-700 to-green-500 hover:from-green-600 hover:to-green-400 text-white px-8 py-3 rounded-xl font-semibold shadow-lg hover:shadow-green-400/30 transition-transform transform hover:scale-105"
|
||||
className="bg-gradient-to-r from-red-700 to-red-500 hover:from-red-600 hover:to-red-400 text-white px-8 py-3 rounded-xl font-semibold shadow-lg hover:shadow-red-400/30 transition-transform transform hover:scale-105"
|
||||
>
|
||||
Kilépés
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react"
|
||||
import React, { useState, useEffect } from "react"
|
||||
import { useNavigate, useLocation } from "react-router-dom"
|
||||
import Navbar from "../../components/Navbar/Navbar.jsx"
|
||||
import Background from "../../assets/backgrounds/Background.jsx"
|
||||
@@ -6,6 +6,7 @@ import Footer from "../../components/Footer/Footer.jsx"
|
||||
import useRequireAuth from "../../hooks/useRequireAuth.jsx"
|
||||
import ButtonGreen from "../../components/Buttons/ButtonGreen.jsx"
|
||||
import { motion } from "framer-motion"
|
||||
import { createGame, joinGame } from "../../api/gameApi.js"
|
||||
|
||||
const GameLobbySetup = () => {
|
||||
const [username] = useRequireAuth({ key: "username", redirectTo: "/login" })
|
||||
@@ -16,19 +17,82 @@ const GameLobbySetup = () => {
|
||||
|
||||
const [maxPlayers, setMaxPlayers] = useState(4)
|
||||
const [isPublic, setIsPublic] = useState(true)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
const [createdGameCode, setCreatedGameCode] = useState('')
|
||||
const [showSuccess, setShowSuccess] = useState(false)
|
||||
|
||||
const handleCreateLobby = () => {
|
||||
console.log({
|
||||
deckIds,
|
||||
maxPlayers,
|
||||
isPublic,
|
||||
const handleCreateLobby = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const username = localStorage.getItem('username')
|
||||
|
||||
console.log('Creating game - username:', username)
|
||||
|
||||
if (!username) {
|
||||
setError('Kérlek jelentkezz be először!')
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Backend expects deckids (array), maxplayers (number), logintype (0=PUBLIC, 1=PRIVATE)
|
||||
const gameData = {
|
||||
deckids: deckIds, // Array of deck UUIDs
|
||||
maxplayers: maxPlayers, // Number
|
||||
logintype: isPublic ? 0 : 1, // 0=PUBLIC, 1=PRIVATE, 2=ORGANIZATION
|
||||
}
|
||||
|
||||
console.log('Creating game with data:', gameData)
|
||||
const response = await createGame(gameData)
|
||||
console.log('Game created:', response)
|
||||
|
||||
// Verify localStorage still has username
|
||||
console.log('After create - username:', localStorage.getItem('username'))
|
||||
|
||||
// Backend returns game object directly
|
||||
const code = response.gamecode || response.gameCode
|
||||
if (code) {
|
||||
setCreatedGameCode(code)
|
||||
setShowSuccess(true)
|
||||
}
|
||||
|
||||
// Creator needs to join their own game to get a gameToken
|
||||
// This allows the WebSocket to recognize them as the gamemaster
|
||||
try {
|
||||
const username = localStorage.getItem('username')
|
||||
const joinResponse = await joinGame({
|
||||
gameCode: code,
|
||||
playerName: username
|
||||
})
|
||||
// Itt küldd el az API-nak a lobby létrehozását
|
||||
// navigate("/game-lobby", { state: { lobbyId: response.lobbyId } })
|
||||
|
||||
if (joinResponse.gameToken) {
|
||||
localStorage.setItem('gameToken', joinResponse.gameToken)
|
||||
console.log('Creator joined game as gamemaster, token stored')
|
||||
}
|
||||
} catch (joinError) {
|
||||
console.error('Failed to join game as creator:', joinError)
|
||||
// Continue anyway - the creator can still try to join manually
|
||||
}
|
||||
|
||||
// Wait 3 seconds to show code, then navigate to lobby
|
||||
setTimeout(() => {
|
||||
console.log('Navigating to lobby with code:', code)
|
||||
navigate('/lobby', { state: { gameCode: code } })
|
||||
}, 3000)
|
||||
} catch (err) {
|
||||
console.error('Create game error:', err)
|
||||
console.error('Error response:', err.response?.data)
|
||||
console.error('Error status:', err.response?.status)
|
||||
setError(err.response?.data?.message || err.response?.data?.error || 'Nem sikerült létrehozni a játékot')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (deckIds.length === 0) {
|
||||
navigate("/choose-deck")
|
||||
navigate("/choosedeck")
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -67,6 +131,27 @@ const GameLobbySetup = () => {
|
||||
{deckIds.length} pakli kiválasztva. Add meg a játék részleteit.
|
||||
</motion.p>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-500/20 border border-red-500 rounded-lg p-4 mb-6">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{createdGameCode && (
|
||||
<div className="bg-green-500/20 border border-green-500 rounded-lg p-6 mb-6">
|
||||
<p className="font-bold text-xl mb-2">Játék Létrehozva! 🎉</p>
|
||||
<p className="text-3xl font-mono tracking-wider text-green-400 mb-2">
|
||||
{createdGameCode}
|
||||
</p>
|
||||
<p className="text-sm text-gray-300">
|
||||
Oszd meg ezt a kódot más játékosokkal, hogy csatlakozhassanak!
|
||||
</p>
|
||||
<p className="text-sm text-gray-400 mt-2">
|
||||
Átirányítás a lobby-hoz 3 másodperc múlva...
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-[color:var(--color-surface)]/80 backdrop-blur-lg rounded-2xl p-8 shadow-lg space-y-6">
|
||||
{/* Max Players */}
|
||||
<div>
|
||||
@@ -115,11 +200,17 @@ const GameLobbySetup = () => {
|
||||
<div className="flex justify-center gap-4 mt-8">
|
||||
<ButtonGreen
|
||||
text="Vissza"
|
||||
onClick={() => navigate("/choose-deck")}
|
||||
onClick={() => navigate("/choosedeck")}
|
||||
width="w-auto px-8"
|
||||
className="bg-gray-600 hover:bg-gray-700"
|
||||
disabled={loading}
|
||||
/>
|
||||
<ButtonGreen
|
||||
text={loading ? "Létrehozás..." : "Lobby Létrehozása"}
|
||||
onClick={handleCreateLobby}
|
||||
width="w-auto px-8"
|
||||
disabled={loading}
|
||||
/>
|
||||
<ButtonGreen text="Lobby Létrehozása" onClick={handleCreateLobby} width="w-auto px-8" />
|
||||
</div>
|
||||
</motion.section>
|
||||
</main>
|
||||
|
||||
@@ -1,24 +1,75 @@
|
||||
// src/pages/Home/Home.jsx
|
||||
// Régi PlayMenu-s oldal, "Home" néven
|
||||
|
||||
import { useEffect } from "react"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import useRequireAuth from "../../hooks/useRequireAuth"
|
||||
import Navbar from "../../components/Navbar/Navbar"
|
||||
import Footer from "../../components/Footer/Footer.jsx"
|
||||
import Background from "../../assets/backgrounds/Background.jsx"
|
||||
import PlayMenu from "../../components/Landingpage/PlayMenu.jsx"
|
||||
import { joinGame } from "../../api/gameApi.js"
|
||||
|
||||
export default function Home() {
|
||||
const navigate = useNavigate()
|
||||
// a hook inicializálja a user-t a localStorage-ból és visszaadja a state-et + settert
|
||||
const [user, setUser] = useRequireAuth({ redirect: false }) // no redirect on unauthenticated visitors
|
||||
const [isJoining, setIsJoining] = useState(false)
|
||||
|
||||
// Dummy callbackok és user példa
|
||||
const handleJoinGame = (code) => {
|
||||
alert(`Csatlakozás játékhoz: ${code}`)
|
||||
// Join game handler - csatlakozás játékhoz kóddal
|
||||
const handleJoinGame = async (code) => {
|
||||
if (!user) {
|
||||
alert('Kérlek először jelentkezz be!')
|
||||
navigate('/login')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('=== JOIN GAME DEBUG ===')
|
||||
console.log('Current user:', user)
|
||||
console.log('Game code:', code)
|
||||
console.log('LocalStorage username:', localStorage.getItem('username'))
|
||||
console.log('LocalStorage authLevel:', localStorage.getItem('authLevel'))
|
||||
console.log('======================')
|
||||
|
||||
setIsJoining(true)
|
||||
try {
|
||||
const joinData = {
|
||||
gameCode: code.toUpperCase(),
|
||||
playerName: user || 'Player',
|
||||
}
|
||||
|
||||
console.log('Sending join request with:', joinData)
|
||||
const response = await joinGame(joinData)
|
||||
console.log('Joined game:', response)
|
||||
|
||||
// Backend returns game object directly
|
||||
if (response.gameToken) {
|
||||
localStorage.setItem('gameToken', response.gameToken)
|
||||
}
|
||||
|
||||
navigate('/lobby', { state: { gameCode: code.toUpperCase() } })
|
||||
} catch (err) {
|
||||
const errorMsg = err.response?.data?.error || err.response?.data?.message || 'Nem sikerült csatlakozni a játékhoz'
|
||||
alert(errorMsg)
|
||||
console.error('Join game error:', err)
|
||||
console.error('Error details:', err.response?.data)
|
||||
} finally {
|
||||
setIsJoining(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Create game handler - új játék létrehozása
|
||||
const handleCreateGame = () => {
|
||||
alert("Új játék létrehozása")
|
||||
if (!user) {
|
||||
alert('Kérlek először jelentkezz be!')
|
||||
navigate('/login')
|
||||
return
|
||||
}
|
||||
|
||||
// Navigate to choose deck page to start game creation flow
|
||||
navigate('/choosedeck')
|
||||
}
|
||||
|
||||
const userObj = { name: user }
|
||||
|
||||
// ha szükséges a user módosítása máshol: setUser("újnév") automatikusan menti localStorage-be
|
||||
|
||||
@@ -13,6 +13,17 @@ export default defineConfig({
|
||||
},
|
||||
hmr: {
|
||||
clientPort: 5173,
|
||||
},
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://backend:3000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/socket.io': {
|
||||
target: 'http://backend:3000',
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
preview: {
|
||||
|
||||
Reference in New Issue
Block a user