diff --git a/package-lock.json b/package-lock.json index d10355c..08352b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@nestjs/common": "^10.3.2", "@nestjs/config": "^3.2.2", "@nestjs/core": "^10.3.2", + "@nestjs/event-emitter": "^2.0.4", "@nestjs/mapped-types": "^2.0.5", "@nestjs/mongoose": "^10.0.6", "@nestjs/platform-express": "^10.3.2", @@ -1828,6 +1829,19 @@ } } }, + "node_modules/@nestjs/event-emitter": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-2.0.4.tgz", + "integrity": "sha512-quMiw8yOwoSul0pp3mOonGz8EyXWHSBTqBy8B0TbYYgpnG1Ix2wGUnuTksLWaaBiiOTDhciaZ41Y5fJZsSJE1Q==", + "license": "MIT", + "dependencies": { + "eventemitter2": "6.4.9" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, "node_modules/@nestjs/mapped-types": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz", @@ -5018,6 +5032,12 @@ "node": ">= 0.6" } }, + "node_modules/eventemitter2": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", + "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==", + "license": "MIT" + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", diff --git a/package.json b/package.json index f1cbb9e..cfcdcc9 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@nestjs/common": "^10.3.2", "@nestjs/config": "^3.2.2", "@nestjs/core": "^10.3.2", + "@nestjs/event-emitter": "^2.0.4", "@nestjs/mapped-types": "^2.0.5", "@nestjs/mongoose": "^10.0.6", "@nestjs/platform-express": "^10.3.2", diff --git a/src/app.module.ts b/src/app.module.ts index cfdfb5d..ab84e42 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -7,9 +7,11 @@ import { GamesModule } from './games/games.module'; import { UsersModule } from './users/users.module'; import { CardsService } from './cards/cards.service'; import { CardsModule } from './cards/cards.module'; +import { EventEmitterModule } from '@nestjs/event-emitter'; @Module({ imports: [ + EventEmitterModule.forRoot(), ConfigModule.forRoot(), MongooseModule.forRoot(process.env.MONGO_ENDPOINT, { auth: { diff --git a/src/games/controllers/games.controller.ts b/src/games/controllers/games.controller.ts index a66a305..63a2510 100644 --- a/src/games/controllers/games.controller.ts +++ b/src/games/controllers/games.controller.ts @@ -1,141 +1,83 @@ import { + Body, Controller, - Get, - Post, - Param, - Delete, - UseGuards, - Session, HttpException, HttpStatus, - Patch, - Body, + Param, + Post, + Session, + Sse, + UseGuards, } from '@nestjs/common'; -import { GamesService } from '../services/games.lobby.service'; +import { LobbyService } from '../services/games.lobby.service'; import { AuthGuard } from '../guards/auth.guard'; -import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { ReadGameDto } from '../dto/read-games.dto'; -import { GameGuard } from '../guards/game.guard'; -import { UpdateGameDto } from '../dto/update-game.dto'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { StartingDeckDto } from '../dto/update-game.dto'; +import { Game, Player } from '../schemas/game.schema'; +import { Document } from 'mongoose'; +import { GamePipe } from '../pipe/game.pipe'; -@ApiTags('lobby') -@Controller('games') +import { ReadUserDto } from '../dto/read-games.dto'; +import { Observable, fromEvent, map } from 'rxjs'; +import { EventEmitter2 } from '@nestjs/event-emitter'; + +function getPlayer(session: Record, game: Game) { + const user = session.user as ReadUserDto; + if (!user) { + throw new HttpException('Please log in', HttpStatus.UNAUTHORIZED); + } + for (const team of game.teams) { + for (const player of team.players) { + if (player.user._id.toHexString() == user._id) { + return player as Document & Player; + } + } + } + throw new HttpException( + 'You are not part of this game', + HttpStatus.FORBIDDEN, + ); +} + +@ApiTags('game') +@Controller('games/:id') export class GamesController { - constructor(private readonly gamesService: GamesService) {} + constructor( + private readonly lobbyService: LobbyService, + private eventEmitter: EventEmitter2, + ) {} - @Post() - @ApiOperation({ summary: 'Create game' }) - @ApiResponse({ - status: 201, - description: 'Game successfully created.', - type: ReadGameDto, - }) - @ApiResponse({ - status: 400, - description: 'You can only create one game at a time', - }) - @ApiResponse({ status: 403, description: 'Forbidden.' }) + @Post('changeStartingDeck') + @ApiOperation({ summary: 'Pick a different starting deck' }) @UseGuards(AuthGuard) - async create(@Session() session: Record) { - const games = await this.gamesService.findByOwner(session.user._id); - if (games) { - throw new HttpException( - 'You can only create one game at a time', - HttpStatus.BAD_REQUEST, - ); - } - return this.gamesService.create(session.user); - } - - @Get() - @ApiOperation({ summary: 'List all games' }) - findAll() { - const games = this.gamesService.findAll(); - return games; - } - - @Get(':id') - @ApiOperation({ summary: 'Get game information' }) - @UseGuards(GameGuard) - async findOne(@Param('id') id: string) { - const game = await this.gamesService.findById(id); - return game; - } - - @Patch(':id') - @ApiOperation({ summary: 'Edit game settings' }) - @UseGuards(AuthGuard, GameGuard) - async update( - @Session() session: Record, + async changeStartingDeck( @Param('id') id: string, - @Body() updateGameDto: UpdateGameDto, + @Param('id', GamePipe) game: Document & Game, + @Body() startingDeckDto: StartingDeckDto, + @Session() session: Record, ) { - const game = await this.gamesService.findById(id); - if (game.owner._id.toString() != session.user._id) { - throw new HttpException( - 'You can only edit games you own', - HttpStatus.FORBIDDEN, - ); - } - return this.gamesService.update(id, updateGameDto); - } - - @Delete(':id') - @ApiOperation({ summary: 'Delete game' }) - @UseGuards(AuthGuard, GameGuard) - async remove( - @Session() session: Record, - @Param('id') id: string, - ) { - const game = await this.gamesService.findById(id); - if (game.owner._id.toString() != session.user._id) { - throw new HttpException( - 'You can only delete games you own', - HttpStatus.FORBIDDEN, - ); - } - const deleted = await this.gamesService.remove(id); - if (deleted) { - return; - } - } - - @Post(':id/start') - @ApiOperation({ summary: 'Start game' }) - @ApiResponse({ - status: 201, - description: 'Game successfully started.', - }) - @ApiResponse({ - status: 400, - description: 'Bad Request', - }) - @ApiResponse({ - status: 403, - description: 'You can only start games you own', - }) - @UseGuards(AuthGuard, GameGuard) - async start( - @Session() session: Record, - @Param('id') id: string, - ) { - const game = await this.gamesService.findById(id); if (game.started) { - throw new HttpException('Game already started', HttpStatus.BAD_REQUEST); - } - if (game.teams.length < 2 && !process.env.DEV_MODE) { throw new HttpException( - 'Not enough teams to start the game', + 'You cannot do that after the game has started', HttpStatus.BAD_REQUEST, ); } - if (game.owner._id.toString() != session.user._id) { - throw new HttpException( - 'You can only start games you own', - HttpStatus.FORBIDDEN, - ); - } - this.gamesService.start(id); - return; + const player = getPlayer(session, game); + await this.lobbyService.changeStartingDeck(startingDeckDto, player, game); + } + + @Sse('/subscribe') + subscribe( + @Param('id') id: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + @Param('id', GamePipe) _game: Document & Game, + ): Observable<{ data: string }> { + return fromEvent(this.eventEmitter, 'sse.game.' + id).pipe( + map((payload) => { + return { + data: JSON.stringify(payload), + }; + }), + ); } } diff --git a/src/games/controllers/games.controller.spec.ts b/src/games/controllers/games.lobby.controller.spec.ts similarity index 50% rename from src/games/controllers/games.controller.spec.ts rename to src/games/controllers/games.lobby.controller.spec.ts index 17b4de4..fa04ddd 100644 --- a/src/games/controllers/games.controller.spec.ts +++ b/src/games/controllers/games.lobby.controller.spec.ts @@ -1,17 +1,17 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { GamesController } from './games.controller'; -import { GamesService } from '../services/games.lobby.service'; +import { LobbyController } from './games.lobby.controller'; +import { LobbyService } from '../services/games.lobby.service'; describe('GamesController', () => { - let controller: GamesController; + let controller: LobbyController; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - controllers: [GamesController], - providers: [GamesService], + controllers: [LobbyController], + providers: [LobbyService], }).compile(); - controller = module.get(GamesController); + controller = module.get(LobbyController); }); it('should be defined', () => { diff --git a/src/games/controllers/games.lobby.controller.ts b/src/games/controllers/games.lobby.controller.ts new file mode 100644 index 0000000..4b53358 --- /dev/null +++ b/src/games/controllers/games.lobby.controller.ts @@ -0,0 +1,159 @@ +import { + Controller, + Get, + Post, + Param, + Delete, + UseGuards, + Session, + HttpException, + HttpStatus, + Patch, + Body, + Sse, +} from '@nestjs/common'; +import { LobbyService } from '../services/games.lobby.service'; +import { AuthGuard } from '../guards/auth.guard'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { ReadGameDto } from '../dto/read-games.dto'; +import { UpdateGameDto } from '../dto/update-game.dto'; +import { GamePipe } from '../pipe/game.pipe'; +import { Game } from '../schemas/game.schema'; +import { Document } from 'mongoose'; +import { Observable, fromEvent, map } from 'rxjs'; +import { EventEmitter2 } from '@nestjs/event-emitter'; + +@ApiTags('lobby') +@Controller('games') +export class LobbyController { + constructor( + private readonly gamesService: LobbyService, + private eventEmitter: EventEmitter2, + ) {} + + @Post() + @ApiOperation({ summary: 'Create game' }) + @ApiResponse({ + status: 201, + description: 'Game successfully created.', + type: ReadGameDto, + }) + @ApiResponse({ + status: 400, + description: 'You can only create one game at a time', + }) + @ApiResponse({ status: 403, description: 'Forbidden.' }) + @UseGuards(AuthGuard) + async create(@Session() session: Record) { + const games = await this.gamesService.findByOwner(session.user._id); + if (games) { + throw new HttpException( + 'You can only create one game at a time', + HttpStatus.BAD_REQUEST, + ); + } + return this.gamesService.create(session.user); + } + + @Get() + @ApiOperation({ summary: 'List all games' }) + findAll() { + const games = this.gamesService.findAll(); + return games; + } + + @Sse('/subscribe') + subscribe(): Observable<{ data: string }> { + console.log('subscris'); + return fromEvent(this.eventEmitter, 'sse.lobby').pipe( + map((payload) => { + return { + data: JSON.stringify(payload), + }; + }), + ); + } + + @Get(':id') + @ApiOperation({ summary: 'Get game information' }) + async findOne(@Param('id') id: string, @Param('id', GamePipe) game: Game) { + return new ReadGameDto(game); + } + + @Patch(':id') + @ApiOperation({ summary: 'Edit game settings' }) + @UseGuards(AuthGuard) + async update( + @Param('id') id: string, + @Session() session: Record, + @Body() updateGameDto: UpdateGameDto, + @Param('id', GamePipe) game: Document & Game, + ) { + if (game.owner._id.toString() != session.user._id) { + throw new HttpException( + 'You can only edit games you own', + HttpStatus.FORBIDDEN, + ); + } + return this.gamesService.update(game, updateGameDto); + } + + @Delete(':id') + @ApiOperation({ summary: 'Delete game' }) + @UseGuards(AuthGuard) + async remove( + @Param('id') id: string, + @Session() session: Record, + @Param('id', GamePipe) game: Document & Game, + ) { + if (game.owner._id.toString() != session.user._id) { + throw new HttpException( + 'You can only delete games you own', + HttpStatus.FORBIDDEN, + ); + } + const deleted = await this.gamesService.remove(game); + if (deleted) { + return; + } + } + + @Post(':id/start') + @ApiOperation({ summary: 'Start game' }) + @ApiResponse({ + status: 201, + description: 'Game successfully started.', + }) + @ApiResponse({ + status: 400, + description: 'Bad Request', + }) + @ApiResponse({ + status: 403, + description: 'You can only start games you own', + }) + @UseGuards(AuthGuard) + async start( + @Param('id') id: string, + @Session() session: Record, + @Param('id', GamePipe) game: Document & Game, + ) { + if (game.started) { + throw new HttpException('Game already started', HttpStatus.BAD_REQUEST); + } + if (game.teams.length < 2 && !process.env.DEV_MODE) { + throw new HttpException( + 'Not enough teams to start the game', + HttpStatus.BAD_REQUEST, + ); + } + if (game.owner._id.toString() != session.user._id) { + throw new HttpException( + 'You can only start games you own', + HttpStatus.FORBIDDEN, + ); + } + this.gamesService.start(game); + return; + } +} diff --git a/src/games/dto/read-games.dto.ts b/src/games/dto/read-games.dto.ts index c25dc24..55c3927 100644 --- a/src/games/dto/read-games.dto.ts +++ b/src/games/dto/read-games.dto.ts @@ -13,6 +13,8 @@ export class ReadPlayerDto extends DatabaseObjectDto { this.discard = new ReadContainerDto(data.discard); this.hand = new ReadContainerDto(data.hand, true); this.stack = new ReadContainerDto(data.stack, true); + this.race = data.race; + this.class = data.class; } user: ReadUserDto; @@ -23,6 +25,9 @@ export class ReadPlayerDto extends DatabaseObjectDto { stack: ReadContainerDto; discard: ReadContainerDto; + + class: string; + race: string; } export class ReadTeamDto extends DatabaseObjectDto { diff --git a/src/games/dto/update-game.dto.ts b/src/games/dto/update-game.dto.ts index bb84caa..2f84a8c 100644 --- a/src/games/dto/update-game.dto.ts +++ b/src/games/dto/update-game.dto.ts @@ -1,3 +1,8 @@ export class UpdateGameDto { packs: string[]; } + +export class StartingDeckDto { + class?: string; + race?: string; +} diff --git a/src/games/games.module.ts b/src/games/games.module.ts index df126c7..4278ab5 100644 --- a/src/games/games.module.ts +++ b/src/games/games.module.ts @@ -1,10 +1,12 @@ import { Module } from '@nestjs/common'; -import { GamesService } from './services/games.lobby.service'; -import { GamesController } from './controllers/games.controller'; +import { LobbyService } from './services/games.lobby.service'; +import { LobbyController } from './controllers/games.lobby.controller'; import { Game, GameSchema } from './schemas/game.schema'; import { MongooseModule } from '@nestjs/mongoose'; import { UsersModule } from 'src/users/users.module'; import { CardsModule } from 'src/cards/cards.module'; +import { GamesController } from './controllers/games.controller'; +import { GameService } from './services/games.service'; @Module({ imports: [ @@ -12,7 +14,7 @@ import { CardsModule } from 'src/cards/cards.module'; UsersModule, MongooseModule.forFeature([{ name: Game.name, schema: GameSchema }]), ], - controllers: [GamesController], - providers: [GamesService], + controllers: [LobbyController, GamesController], + providers: [LobbyService, GameService], }) export class GamesModule {} diff --git a/src/games/guards/game.guard.ts b/src/games/guards/game.guard.ts deleted file mode 100644 index 517ccb9..0000000 --- a/src/games/guards/game.guard.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { - Injectable, - CanActivate, - ExecutionContext, - HttpException, - HttpStatus, -} from '@nestjs/common'; -import { Request } from 'express'; -import { Observable } from 'rxjs'; -import { GamesService } from '../services/games.lobby.service'; - -@Injectable() -export class GameGuard implements CanActivate { - constructor(private readonly gamesService: GamesService) {} - - canActivate( - context: ExecutionContext, - ): boolean | Promise | Observable { - const http = context.switchToHttp(); - const request = http.getRequest(); - return (async () => { - const game = await this.gamesService.findById(request.params.id); - if (!game) { - throw new HttpException('Game not found', HttpStatus.NOT_FOUND); - } - if (!(request.session as Record).user) { - throw new HttpException('Please log in', HttpStatus.UNAUTHORIZED); - } - return true; - })(); - } -} diff --git a/src/games/pipe/game.pipe.ts b/src/games/pipe/game.pipe.ts new file mode 100644 index 0000000..e136827 --- /dev/null +++ b/src/games/pipe/game.pipe.ts @@ -0,0 +1,22 @@ +import { + PipeTransform, + Injectable, + HttpException, + HttpStatus, +} from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Game } from '../schemas/game.schema'; +import { Model } from 'mongoose'; + +@Injectable() +export class GamePipe implements PipeTransform { + constructor(@InjectModel(Game.name) private gameModel: Model) {} + + async transform(value: any) { + const game = await this.gameModel.findById(value); + if (!game) { + throw new HttpException('Game not found', HttpStatus.NOT_FOUND); + } + return game; + } +} diff --git a/src/games/schemas/game.schema.ts b/src/games/schemas/game.schema.ts index 3bbc9d8..cef7737 100644 --- a/src/games/schemas/game.schema.ts +++ b/src/games/schemas/game.schema.ts @@ -1,6 +1,7 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import mongoose, { HydratedDocument, Types } from 'mongoose'; import { Card } from 'src/cards/schemas/cards.schema'; +import { CardRole } from 'src/cards/schemas/cards.types'; import { User } from 'src/users/schemas/user.entity'; export type GameDocument = HydratedDocument; @@ -25,6 +26,12 @@ export class Player extends Types.ObjectId { }) user: User; + @Prop({ type: String, default: CardRole.PERSONAL }) + class?: string; + + @Prop() + race?: string; + @Prop({ type: ContainerSchema, default: {}, autopopulate: true }) board!: Container; @@ -53,7 +60,7 @@ export const TeamSchema = SchemaFactory.createForClass(Team); @Schema() export class Game extends Types.ObjectId { - @Prop({ type: [{ type: Team, autopopulate: true }] }) + @Prop({ type: [{ type: TeamSchema, autopopulate: true }] }) teams: Team[]; @Prop({ type: ContainerSchema, default: {}, autopopulate: true }) @@ -65,11 +72,7 @@ export class Game extends Types.ObjectId { @Prop({ type: ContainerSchema, default: {}, autopopulate: true }) fireGems: Container; - @Prop({ - type: mongoose.Schema.Types.ObjectId, - ref: 'Team', - autopopulate: true, - }) + @Prop({ type: Team, autopopulate: true }) currentTurn?: Team; @Prop({ default: false }) diff --git a/src/games/services/games.lobby.service.spec.ts b/src/games/services/games.lobby.service.spec.ts index b2b405f..6eaae5f 100644 --- a/src/games/services/games.lobby.service.spec.ts +++ b/src/games/services/games.lobby.service.spec.ts @@ -1,15 +1,15 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { GamesService } from './games.lobby.service'; +import { LobbyService } from './games.lobby.service'; describe('GamesService', () => { - let service: GamesService; + let service: LobbyService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [GamesService], + providers: [LobbyService], }).compile(); - service = module.get(GamesService); + service = module.get(LobbyService); }); it('should be defined', () => { diff --git a/src/games/services/games.lobby.service.ts b/src/games/services/games.lobby.service.ts index 0ed6ad2..1c64737 100644 --- a/src/games/services/games.lobby.service.ts +++ b/src/games/services/games.lobby.service.ts @@ -1,19 +1,23 @@ -import { Injectable } from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { DatabaseObjectDto } from '../dto/database-object.dto'; import { InjectModel } from '@nestjs/mongoose'; import { Game, Player, Team } from '../schemas/game.schema'; -import { Model } from 'mongoose'; -import { ReadGameDto } from '../dto/read-games.dto'; +import { Document, Model } from 'mongoose'; +import { ReadContainerDto, ReadGameDto } from '../dto/read-games.dto'; import { User } from 'src/users/schemas/user.entity'; import { Card } from 'src/cards/schemas/cards.schema'; import { CardRole } from 'src/cards/schemas/cards.types'; -import { UpdateGameDto } from '../dto/update-game.dto'; +import { StartingDeckDto, UpdateGameDto } from '../dto/update-game.dto'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { GameService } from './games.service'; @Injectable() -export class GamesService { +export class LobbyService { constructor( @InjectModel(Game.name) private gameModel: Model, @InjectModel(Card.name) private cardModel: Model, + private readonly gamesService: GameService, + private eventEmitter: EventEmitter2, ) {} async create(owner: User): Promise { @@ -31,7 +35,10 @@ export class GamesService { } as Team); const gamedata = await createdGame.save(); - return new ReadGameDto(gamedata); + const dto = new ReadGameDto(gamedata); + this.eventEmitter.emit('sse.lobby', { type: 'create', ...dto }); + + return dto; } async findAll(): Promise { @@ -43,14 +50,6 @@ export class GamesService { ); } - async findById(id: string) { - const game = await this.gameModel.findById(id).exec(); - if (!game) { - return null; - } - return new ReadGameDto(game); - } - async findByOwner(_id: string): Promise { const game = await this.gameModel.findOne({ owner: _id }).exec(); if (!game) { @@ -59,24 +58,71 @@ export class GamesService { return new ReadGameDto(game); } - async update(id: string, updateGameDto: UpdateGameDto) { - const game = await this.gameModel.findById(id).exec(); - if (!game) { - return null; - } + async update(game: Document & Game, updateGameDto: UpdateGameDto) { game.packs = updateGameDto.packs; - return; - } - - async remove(id: string) { - return await this.gameModel.findByIdAndDelete(id); - } - - async start(id: string) { - const game = await this.gameModel.findById(id).exec(); - if (!game) { - return null; + const packs = await this.cardModel.distinct('pack'); + if (updateGameDto.packs.every((e) => packs.includes(e))) { + await game.save(); + this.eventEmitter.emit('sse.lobby', { + type: 'update', + _id: game._id, + ...updateGameDto, + }); + this.eventEmitter.emit('sse.game.' + game._id.toHexString(), { + type: 'update', + ...updateGameDto, + }); + return; } + throw new HttpException('Pack not found', HttpStatus.NOT_FOUND); + } + + async remove(game: Document & Game) { + this.eventEmitter.emit('sse.lobby', { + _id: game._id, + }); + this.eventEmitter.emit('sse.game.' + game._id.toHexString(), { + type: 'delete', + }); + return await game.deleteOne(); + } + + async start(game: Document & Game) { + const random = Math.floor(Math.random() * game.teams.length); + game.currentTurn = game.teams[random]; + await game.save(); + game.started = true; + this.eventEmitter.emit('sse.lobby', { + type: 'start', + _id: game._id, + }); + this.eventEmitter.emit('sse.game.' + game._id.toHexString(), { + type: 'start', + game: new ReadGameDto(game), + }); + let playerIndex = 0; + await Promise.all( + game.teams.map((team) => + Promise.all( + team.players.map(async (player) => { + player.stack.cards = await this.cardModel + .find({ + pack: { $in: game.packs }, + role: { $in: [player.class, player.race] }, + playerIndex, + }) + .exec(); + this.eventEmitter.emit('sse.game.' + game._id.toHexString(), { + type: 'populateContainer', + container: new ReadContainerDto(player.stack, true), + }); + this.gamesService.shuffleContainer(player.stack, game); + playerIndex++; + }), + ), + ), + ); + game.fireGems.cards = await this.cardModel.find({ pack: { $in: game.packs }, role: CardRole.FIRE_GEM, @@ -87,24 +133,51 @@ export class GamesService { role: CardRole.MARKET, }); - let playerIndex = 0; - await Promise.all( - game.teams.map((team) => - Promise.all( - team.players.map(async (player) => { - player.stack.cards = await this.cardModel - .find({ - pack: { $in: game.packs }, - role: CardRole.PERSONAL, - playerIndex, - }) - .exec(); - playerIndex++; - }), - ), - ), - ); - game.started = true; + this.eventEmitter.emit('sse.game.' + game._id.toHexString(), { + type: 'populateContainer', + container: new ReadContainerDto(game.fireGems), + }); + this.eventEmitter.emit('sse.game.' + game._id.toHexString(), { + type: 'populateContainer', + container: new ReadContainerDto(game.marketStack, true), + }); + + await this.gamesService.shuffleContainer(game.marketStack, game); await game.save(); } + + async changeStartingDeck( + data: StartingDeckDto, + player: Document & Player, + game: Document & Game, + ) { + const packs = await this.cardModel + .find({ pack: { $in: game.packs } }) + .distinct('role'); + + if ( + !Object.values(CardRole).includes(data.class) || + !Object.values(CardRole).includes(data.race) || + data.race == CardRole.FIRE_GEM || + data.race == CardRole.MARKET || + data.class == CardRole.FIRE_GEM || + data.class == CardRole.MARKET + ) { + throw new HttpException('Invalid race or class', HttpStatus.NOT_FOUND); + } + if ( + packs.includes(data.class as CardRole) && + packs.includes(data.race as CardRole) + ) { + player.class = data.class; + player.race = data.race; + await game.save(); + this.eventEmitter.emit('sse.game.changeStartingDeck', { + _id: player._id, + ...data, + }); + return; + } + throw new HttpException('Pack not found', HttpStatus.NOT_FOUND); + } } diff --git a/src/games/services/games.service.ts b/src/games/services/games.service.ts new file mode 100644 index 0000000..2de949d --- /dev/null +++ b/src/games/services/games.service.ts @@ -0,0 +1,61 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Container, Game } from '../schemas/game.schema'; +import { Document, Model } from 'mongoose'; +import { Card } from 'src/cards/schemas/cards.schema'; +import { EventEmitter2 } from '@nestjs/event-emitter'; + +function shuffle(a) { + let j, x, i; + for (i = a.length - 1; i > 0; i--) { + j = Math.floor(Math.random() * (i + 1)); + x = a[i]; + a[i] = a[j]; + a[j] = x; + } + return a; +} + +@Injectable() +export class GameService { + constructor( + @InjectModel(Game.name) private gameModel: Model, + @InjectModel(Card.name) private cardModel: Model, + private eventEmitter: EventEmitter2, + ) {} + + async shuffleContainer(container: Container, game: Document & Game) { + container.cards = shuffle(container.cards); + this.eventEmitter.emit('sse.game.' + game._id.toHexString(), { + type: 'shuffleContainer', + container: container._id.toHexString(), + }); + await game.save(); + } + + async distributeCards( + cards: Card[], + from: Container, + to: Container, + game: Document & Game, + ) { + if (cards.some((c) => !from.cards.includes(c))) { + throw new HttpException( + 'Card not found in container', + HttpStatus.NOT_FOUND, + ); + } + for (const c of cards) { + const index = from.cards.indexOf(c); + from.cards.splice(index, 1); + to.cards.push(c); + } + this.eventEmitter.emit('sse.game.' + game._id.toHexString(), { + type: 'distributeCards', + from: from._id.toHexString(), + to: to._id.toHexString(), + cards: cards, + }); + await game.save(); + } +} diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 79403e0..0a4f7f0 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -110,7 +110,6 @@ export class UsersService { } } const data2 = await req2.json(); - console.log(data2); const dto = new LoginDto(data2.id, data2.global_name, data2.avatar); const user = await this.getOrCreateUser(dto.userId, dto); session.user = user;