From 8b5c38a28cfc054e9104d307795b6198df4dc971 Mon Sep 17 00:00:00 2001 From: legonzaur Date: Sun, 7 Jul 2024 10:45:44 +0200 Subject: [PATCH] Feat : add tokens, handle mutex effects --- .vscode/launch.json | 49 ++++++++++----------- src/cards/cards.module.ts | 3 +- src/cards/cards.service.ts | 10 ++++- src/cards/schemas/cards.schema.ts | 8 +++- src/cards/schemas/effect.schema.ts | 9 +++- src/games/controllers/turn.controller.ts | 37 ++++++++++++++++ src/games/dto/read-games.dto.ts | 23 ++++++---- src/games/pipe/effect.pipe.ts | 22 ++++++++++ src/games/schemas/game.schema.ts | 16 ++++++- src/games/services/effect.functions.ts | 54 ++++++++++++++++++++++++ src/games/services/games.service.ts | 3 +- src/games/services/turn.service.ts | 21 ++++++--- 12 files changed, 206 insertions(+), 49 deletions(-) create mode 100644 src/games/pipe/effect.pipe.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index f7ecbf6..81ea487 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,27 +1,24 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Launch via NPM", - "request": "launch", - "runtimeArgs": [ - "run", - "start:dev", - "-b", - "swc" - ], - "runtimeExecutable": "npm", - "skipFiles": [ - "/**" - ], - "type": "node", - "console": "internalConsole", - "outputCapture": "std", - "internalConsoleOptions": "openOnSessionStart", - "envFile": "${workspaceFolder}/.env" - } - ] -} \ No newline at end of file + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch via NPM", + "request": "launch", + "runtimeArgs": ["run", "start:dev", "-b", "swc"], + "runtimeExecutable": "npm", + "skipFiles": [ + "/**", + "${workspaceFolder}/node_modules/**/*.js", + "${workspaceFolder}/lib/**/*.js" + ], + "type": "node", + "console": "internalConsole", + "outputCapture": "std", + "internalConsoleOptions": "openOnSessionStart", + "envFile": "${workspaceFolder}/.env" + } + ] +} diff --git a/src/cards/cards.module.ts b/src/cards/cards.module.ts index b220f6a..cac1c26 100644 --- a/src/cards/cards.module.ts +++ b/src/cards/cards.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { Card, CardSchema } from './schemas/cards.schema'; import { CardsService } from './cards.service'; +import { Effect, EffectSchema } from './schemas/effect.schema'; // import { // Effect, // EffectCondition, @@ -12,7 +13,7 @@ import { CardsService } from './cards.service'; @Module({ imports: [ MongooseModule.forFeature([ - // { name: Effect.name, schema: EffectSchema }, + { name: Effect.name, schema: EffectSchema }, // { name: EffectCondition.name, schema: EffectConditionSchema }, { name: Card.name, schema: CardSchema }, ]), diff --git a/src/cards/cards.service.ts b/src/cards/cards.service.ts index 1abd6d4..ef64638 100644 --- a/src/cards/cards.service.ts +++ b/src/cards/cards.service.ts @@ -17,7 +17,10 @@ import { EffectCondition } from './schemas/effect.schema'; @Injectable() export class CardsService { - constructor(@InjectModel(Card.name) private cardModel: Model) {} + constructor( + @InjectModel(Card.name) private cardModel: Model, + @InjectModel(Effect.name) private effectModel: Model, + ) {} async loadCards(): Promise { const cardPath = path.join(path.resolve(), 'src', 'json'); @@ -88,6 +91,9 @@ export class CardsService { ): Promise { return await Promise.all( json.map(async (e) => { + if (e.mutex) { + console.log(e.mutex); + } const createdEffect = { card: card, effect: e.effect, @@ -112,7 +118,7 @@ export class CardsService { card, ); } - return createdEffect; + return await this.effectModel.create(createdEffect); }), ); } diff --git a/src/cards/schemas/cards.schema.ts b/src/cards/schemas/cards.schema.ts index b296b57..21b26a9 100644 --- a/src/cards/schemas/cards.schema.ts +++ b/src/cards/schemas/cards.schema.ts @@ -1,6 +1,6 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { CardFaction, CardRole, CardType } from './cards.types'; -import { Types } from 'mongoose'; +import mongoose, { Types } from 'mongoose'; import { Effect } from './effect.schema'; @Schema() @@ -32,7 +32,11 @@ export class Card extends Types.ObjectId { @Prop() guard?: boolean; - @Prop({ type: [Effect], default: [], autopopulate: true }) + @Prop({ + type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Effect' }], + autopopulate: true, + default: [], + }) effects: Effect[]; @Prop() diff --git a/src/cards/schemas/effect.schema.ts b/src/cards/schemas/effect.schema.ts index 1a6faa3..eb12288 100644 --- a/src/cards/schemas/effect.schema.ts +++ b/src/cards/schemas/effect.schema.ts @@ -38,7 +38,11 @@ export class Effect extends Types.ObjectId { @Prop({ type: Number, default: 1 }) times!: number; - // @Prop({ type: [Effect], default: [], autopopulate: true }) + @Prop({ + type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Effect' }], + autopopulate: true, + default: [], + }) subEffects: Effect[]; @Prop({ type: EffectCondition, autopopulate: true }) @@ -56,6 +60,7 @@ export class Effect extends Types.ObjectId { // }, // ], // }) + @Prop({ type: String }) mutex?: string; // @Prop({ @@ -74,6 +79,6 @@ EffectSchema.add({ card: { type: mongoose.Schema.Types.ObjectId, ref: 'Card', - autopopulate: true, + autopopulate: false, }, }); diff --git a/src/games/controllers/turn.controller.ts b/src/games/controllers/turn.controller.ts index 681c476..6c5f4e5 100644 --- a/src/games/controllers/turn.controller.ts +++ b/src/games/controllers/turn.controller.ts @@ -18,6 +18,8 @@ import { TurnService } from '../services/turn.service'; import { getPlayer, getCurrentTeam, isInTeam } from '../utils'; import { CardPipe } from '../pipe/card.pipe'; import { Card } from 'src/cards/schemas/cards.schema'; +import { EffectPipe } from '../pipe/effect.pipe'; +import { Effect } from 'src/cards/schemas/effect.schema'; @ApiTags('turn') @Controller('games/:gameId/turn') @@ -87,4 +89,39 @@ export class TurnController { } await this.turnService.buyCard(game, currentPlayer, card); } + + @Post('lockEffect/:effectId') + @ApiOperation({ summary: 'Lock an effect' }) + @UseGuards(AuthGuard) + async lockEffect( + @Param('gameId') _gameId: string, + @Param('effectId') _effectId: string, + @Param('gameId', GameStartedPipe) game: Document & Game, + @Param('effectId', EffectPipe) effect: Document & Effect, + @Session() session: Record, + ) { + const currentPlayer = getPlayer(session, game); + const currentTeam = getCurrentTeam(game); + if (!isInTeam(currentTeam, currentPlayer)) { + throw new HttpException( + 'It is not your turn to play', + HttpStatus.FORBIDDEN, + ); + } + if (game.currentTurn.lockedEffects.some((e) => e._id.equals(effect._id))) { + throw new HttpException('Effect already locked', HttpStatus.BAD_REQUEST); + } + const card = (await effect.populate('card')).card; + if (!currentPlayer.board.cards.some((c) => c._id.equals(card._id))) { + throw new HttpException( + 'Card not found in player board', + HttpStatus.NOT_FOUND, + ); + } + if (!game.currentTurn.cardsLocked.some((c) => c._id.equals(card._id))) { + throw new HttpException('Card not locked', HttpStatus.BAD_REQUEST); + } + + await this.turnService.lockEffect(game, currentPlayer, effect, card); + } } diff --git a/src/games/dto/read-games.dto.ts b/src/games/dto/read-games.dto.ts index af90059..1d19ee3 100644 --- a/src/games/dto/read-games.dto.ts +++ b/src/games/dto/read-games.dto.ts @@ -2,6 +2,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { DatabaseObjectDto } from './database-object.dto'; import { Game, Player, PlayingTurn, Team } from '../schemas/game.schema'; import { Card } from 'src/cards/schemas/cards.schema'; +import { Effect } from 'src/cards/schemas/effect.schema'; export class ReadPlayerDto extends DatabaseObjectDto { constructor( @@ -53,14 +54,14 @@ export class ReadContainerDto extends DatabaseObjectDto { typeof DatabaseObjectDto >[0], // eslint-disable-next-line @typescript-eslint/no-unused-vars - _hidden = false, + hidden = false, ) { super(data); - // if (hidden) { - // this.cards = data.cards.map(() => null); - // } else { - this.cards = data.cards; - // } + if (hidden) { + this.cards = data.cards.map(() => null); + } else { + this.cards = data.cards; + } } cards: Card[]; @@ -95,9 +96,12 @@ class ReadPlayingTeamDto { constructor(currentTurn: PlayingTurn) { this.currentTeam = currentTurn.currentTeam; this.cardsLocked = currentTurn.cardsLocked.map((c) => c._id.toHexString()); - this.effectsUsed = currentTurn.effectsUsed; + this.lockedEffects = currentTurn.lockedEffects.map((e) => + e._id.toHexString(), + ); this.damage = currentTurn.damage; this.gold = currentTurn.gold; + this.tokens = currentTurn.tokens; } @ApiProperty() @@ -107,7 +111,10 @@ class ReadPlayingTeamDto { cardsLocked: string[]; @ApiProperty() - effectsUsed: string[]; + lockedEffects: string[]; + + @ApiProperty() + tokens: Effect[]; @ApiProperty() damage: number; diff --git a/src/games/pipe/effect.pipe.ts b/src/games/pipe/effect.pipe.ts new file mode 100644 index 0000000..4155dc2 --- /dev/null +++ b/src/games/pipe/effect.pipe.ts @@ -0,0 +1,22 @@ +import { + PipeTransform, + Injectable, + HttpException, + HttpStatus, +} from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { Effect } from 'src/cards/schemas/effect.schema'; + +@Injectable() +export class EffectPipe implements PipeTransform { + constructor(@InjectModel(Effect.name) private effectmodel: Model) {} + + async transform(value: any) { + const effect = await this.effectmodel.findById(value); + if (!effect) { + throw new HttpException('Effect not found', HttpStatus.NOT_FOUND); + } + return effect; + } +} diff --git a/src/games/schemas/game.schema.ts b/src/games/schemas/game.schema.ts index 1ec8289..aa389dd 100644 --- a/src/games/schemas/game.schema.ts +++ b/src/games/schemas/game.schema.ts @@ -2,6 +2,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 { Effect } from 'src/cards/schemas/effect.schema'; import { User } from 'src/users/schemas/user.entity'; export type GameDocument = HydratedDocument; @@ -29,8 +30,19 @@ export class PlayingTurn extends Types.ObjectId { }) cardsLocked: Card[]; - @Prop({ type: [String], default: [] }) - effectsUsed: string[]; + @Prop({ + type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Effect' }], + autopopulate: false, + default: [], + }) + lockedEffects: Effect[]; + + @Prop({ + type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Effect' }], + autopopulate: true, + default: [], + }) + tokens: Effect[]; @Prop({ default: 0 }) damage: number; diff --git a/src/games/services/effect.functions.ts b/src/games/services/effect.functions.ts index e48d7a6..74c6dd2 100644 --- a/src/games/services/effect.functions.ts +++ b/src/games/services/effect.functions.ts @@ -18,6 +18,22 @@ export function isAutoActivable(effect: Effect) { return false; } +async function addToken( + game: Document & Game, + effect: Effect, + _player: Player, + _card: Card, + _gameService: GameService, + eventEmitter: EventEmitter2, + _session: mongoose.mongo.ClientSession, +) { + game.currentTurn.tokens.push(effect); + eventEmitter.emit('sse.game.' + game._id.toHexString(), { + type: 'currentTurn.addToken', + token: effect, + }); +} + export const effectMappings = { [CardEffects.GOLD]: async ( game: Document & Game, @@ -29,6 +45,10 @@ export const effectMappings = { _session: mongoose.mongo.ClientSession, ) => { game.currentTurn.gold += effect.amount; + eventEmitter.emit('sse.game.' + game._id.toHexString(), { + type: 'consumeEffect', + effect: effect._id.toHexString(), + }); eventEmitter.emit('sse.game.' + game._id.toHexString(), { type: 'currentTurn.updateGold', gold: game.currentTurn.gold, @@ -46,6 +66,10 @@ export const effectMappings = { _session: mongoose.mongo.ClientSession, ) => { game.currentTurn.damage += effect.amount; + eventEmitter.emit('sse.game.' + game._id.toHexString(), { + type: 'consumeEffect', + effect: effect._id.toHexString(), + }); eventEmitter.emit('sse.game.' + game._id.toHexString(), { type: 'currentTurn.updateDamage', damage: game.currentTurn.damage, @@ -66,6 +90,10 @@ export const effectMappings = { t.players.some((p) => p._id.equals(player._id)), ); team.health += effect.amount; + eventEmitter.emit('sse.game.' + game._id.toHexString(), { + type: 'consumeEffect', + effect: effect._id.toHexString(), + }); eventEmitter.emit('sse.game.' + game._id.toHexString(), { type: 'team.updateHealth', team: team._id, @@ -85,6 +113,32 @@ export const effectMappings = { ) => { await gameService.drawToBoard(effect.amount, player, game, session); }, + [CardEffects.PREPARE]: addToken, + [CardEffects.STUN]: addToken, + [CardEffects.SACRIFICE]: addToken, + [CardEffects.DRAW_AND_DISCARD]: addToken, + [CardEffects.MAKE_DISCARD]: addToken, + [CardEffects.RESTACK_DISCARDED_CHAMPION]: addToken, + [CardEffects.RESTACK_DISCARDED_CARD]: addToken, + [CardEffects.STACK_NEXT_ACTION_BOUGHT]: addToken, + [CardEffects.STACK_NEXT_CARD_BOUGHT]: addToken, + [CardEffects.PLAY_NEXT_CARD_BOUGHT]: addToken, + + // [CardEffects.BUY_FOR_FREE]: addToken, + // [CardEffects.DISCARD_X_AND_DRAW_X]: addToken, + // [CardEffects.PREPARE_ANOTHER_CHAMPION]: addToken, + // [CardEffects.CHEAPER_CHAMPION]: addToken, + // [CardEffects.CHEAPER_CHAMPIONS_PASSIVE]: addToken, + // [CardEffects.CHEAPER_ACTION]: addToken, + // [CardEffects.CONTROL_OPPOSING_CHAMPION_PASSIVE]: addToken, + + // [CardEffects.RESTACK_DISCARDED_ACTION]: addToken, + + // [CardEffects.KEEP_IN_HAND]: addToken, + // [CardEffects.BUY_GEM_FOR_FREE]: addToken, + // [CardEffects.CHEAPER_SKILLS_PASSIVE]: addToken, + // [CardEffects.CHEAPER_CARD_IF_HIGHER_PRICE]: addToken, + // [CardEffects.PICK_FACTION]: addToken, } as Record< CardEffects, ( diff --git a/src/games/services/games.service.ts b/src/games/services/games.service.ts index 7a8653a..694849f 100644 --- a/src/games/services/games.service.ts +++ b/src/games/services/games.service.ts @@ -48,7 +48,8 @@ export class GameService { game.teams[(index + 1) % game.teams.length]._id.toHexString(); game.currentTurn.cardsLocked = []; - game.currentTurn.effectsUsed = []; + game.currentTurn.lockedEffects = []; + game.currentTurn.tokens = []; game.currentTurn.damage = 0; game.currentTurn.gold = 0; diff --git a/src/games/services/turn.service.ts b/src/games/services/turn.service.ts index 55ddb2f..e825ec9 100644 --- a/src/games/services/turn.service.ts +++ b/src/games/services/turn.service.ts @@ -40,14 +40,14 @@ export class TurnService { }); for (const effect of card.effects) { if (isAutoActivable(effect)) { - await this.useEffect(game, player, effect, card, session); + await this.lockEffect(game, player, effect, card, session); } } await game.save({ session }); } @WithTransaction - async useEffect( + async lockEffect( game: Document & Game, player: Player, effect: Effect, @@ -64,7 +64,7 @@ export class TurnService { ); } const effectUUID = effect._id.toHexString(); - if (game.currentTurn.effectsUsed.includes(effectUUID)) { + if (game.currentTurn.lockedEffects.some((e) => e._id.equals(effectUUID))) { console.error(`Effect ${effectUUID} already used this turn`); throw new HttpException( `Effect ${effectUUID} already used this turn`, @@ -72,10 +72,21 @@ export class TurnService { ); } this.eventEmitter.emit('sse.game.' + game._id.toHexString(), { - type: 'useEffect', + type: 'lockEffect', effect: effectUUID, }); - game.currentTurn.effectsUsed.push(effectUUID); + if (effect.mutex) { + for (const e of card.effects) { + if (e.mutex == effect.mutex) { + this.eventEmitter.emit('sse.game.' + game._id.toHexString(), { + type: 'lockEffect', + effect: e._id.toHexString(), + }); + game.currentTurn.lockedEffects.push(e); + } + } + } + game.currentTurn.lockedEffects.push(effect); await effectRunner( game, effect,