feat: card lock
This commit is contained in:
@@ -21,5 +21,13 @@ module.exports = {
|
|||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'warn', // or "error"
|
||||||
|
{
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
varsIgnorePattern: '^_',
|
||||||
|
caughtErrorsIgnorePattern: '^_',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,32 +13,14 @@ import { LobbyService } from '../services/games.lobby.service';
|
|||||||
import { AuthGuard } from '../guards/auth.guard';
|
import { AuthGuard } from '../guards/auth.guard';
|
||||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
import { StartingDeckDto } from '../dto/update-game.dto';
|
import { StartingDeckDto } from '../dto/update-game.dto';
|
||||||
import { Game, Player } from '../schemas/game.schema';
|
import { Game } from '../schemas/game.schema';
|
||||||
import { Document } from 'mongoose';
|
import { Document } from 'mongoose';
|
||||||
import { GamePipe } from '../pipe/game.pipe';
|
import { GamePipe, GameStartedPipe } from '../pipe/game.pipe';
|
||||||
|
|
||||||
import { ReadUserDto } from '../dto/read-games.dto';
|
|
||||||
import { Observable, fromEvent, map } from 'rxjs';
|
import { Observable, fromEvent, map } from 'rxjs';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
import { GameService } from '../services/games.service';
|
import { GameService } from '../services/games.service';
|
||||||
|
import { getCurrentTeam, getPlayer, isInTeam } from '../utils';
|
||||||
function getPlayer(session: Record<string, any>, 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> & Player;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new HttpException(
|
|
||||||
'You are not part of this game',
|
|
||||||
HttpStatus.FORBIDDEN,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@ApiTags('game')
|
@ApiTags('game')
|
||||||
@Controller('games/:id')
|
@Controller('games/:id')
|
||||||
@@ -72,18 +54,19 @@ export class GamesController {
|
|||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard)
|
||||||
async endTurn(
|
async endTurn(
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
@Param('id', GamePipe) game: Document<Game> & Game,
|
@Param('id', GameStartedPipe) game: Document<Game> & Game,
|
||||||
@Session() session: Record<string, string>,
|
@Session() session: Record<string, string>,
|
||||||
) {
|
) {
|
||||||
if (!game.started || !game.currentTurn) {
|
const currentTeam = getCurrentTeam(game);
|
||||||
throw new HttpException('Game not started', HttpStatus.BAD_REQUEST);
|
const currentPlayer = getPlayer(session, game);
|
||||||
}
|
|
||||||
const player = getPlayer(session, game);
|
|
||||||
const currentTurn = game.teams.find(
|
|
||||||
(t) => t._id.toHexString() == game.currentTurn,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.gameService.endTurn(game, player._id.toHexString());
|
if (!isInTeam(currentTeam, currentPlayer)) {
|
||||||
|
throw new HttpException(
|
||||||
|
'It is not your turn to play',
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await this.gameService.endTurn(game);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Sse('/subscribe')
|
@Sse('/subscribe')
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
Session,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '../guards/auth.guard';
|
||||||
|
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { Game } from '../schemas/game.schema';
|
||||||
|
import { Document, Model } from 'mongoose';
|
||||||
|
import { GameStartedPipe } from '../pipe/game.pipe';
|
||||||
|
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { TurnService } from '../services/turn.service';
|
||||||
|
import { getPlayer, getCurrentTeam, isInTeam } from '../utils';
|
||||||
|
|
||||||
|
@ApiTags('turn')
|
||||||
|
@Controller('games/:gameId/turn')
|
||||||
|
export class TurnController {
|
||||||
|
constructor(
|
||||||
|
private readonly turnService: TurnService,
|
||||||
|
private eventEmitter: EventEmitter2,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post('lockCard/:cardId')
|
||||||
|
@ApiOperation({ summary: 'Lock a card' })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async lockCard(
|
||||||
|
@Param('gameId') _gameId: string,
|
||||||
|
@Param('cardId') cardId: string,
|
||||||
|
@Param('gameId', GameStartedPipe) game: Document<Game> & Game,
|
||||||
|
@Session() session: Record<string, string>,
|
||||||
|
) {
|
||||||
|
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.cardsLocked.some((c) => c._id.toHexString() == cardId)
|
||||||
|
) {
|
||||||
|
throw new HttpException('Card already locked', HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
this.turnService.lockCard(game, currentPlayer, cardId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { DatabaseObjectDto } from './database-object.dto';
|
import { DatabaseObjectDto } from './database-object.dto';
|
||||||
import { Game, Player, Team } from '../schemas/game.schema';
|
import { Game, Player, PlayingTurn, Team } from '../schemas/game.schema';
|
||||||
import { Card } from 'src/cards/schemas/cards.schema';
|
import { Card } from 'src/cards/schemas/cards.schema';
|
||||||
|
|
||||||
export class ReadPlayerDto extends DatabaseObjectDto {
|
export class ReadPlayerDto extends DatabaseObjectDto {
|
||||||
@@ -54,9 +54,11 @@ export class ReadContainerDto extends DatabaseObjectDto {
|
|||||||
// if (hidden) {
|
// if (hidden) {
|
||||||
// this.cards = data.cards.map(() => null);
|
// this.cards = data.cards.map(() => null);
|
||||||
// } else {
|
// } else {
|
||||||
|
this.id = data._id.toHexString();
|
||||||
this.cards = data.cards;
|
this.cards = data.cards;
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
id: string;
|
||||||
cards: Card[];
|
cards: Card[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +87,31 @@ export class ReadUserDto extends DatabaseObjectDto {
|
|||||||
avatar: string;
|
avatar: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ReadPlayingTeamDto {
|
||||||
|
constructor(currentTurn: PlayingTurn) {
|
||||||
|
this.currentTeam = currentTurn.currentTeam;
|
||||||
|
this.cardsLocked = currentTurn.cardsLocked.map((c) => c._id.toHexString());
|
||||||
|
this.effectsUsed = currentTurn.effectsUsed;
|
||||||
|
this.damage = currentTurn.damage;
|
||||||
|
this.gold = currentTurn.gold;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
currentTeam: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
cardsLocked: string[];
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
effectsUsed: string[];
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
damage: number;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
gold: number;
|
||||||
|
}
|
||||||
|
|
||||||
export class ReadGameDto extends DatabaseObjectDto {
|
export class ReadGameDto extends DatabaseObjectDto {
|
||||||
constructor(data: Game & ConstructorParameters<typeof DatabaseObjectDto>[0]) {
|
constructor(data: Game & ConstructorParameters<typeof DatabaseObjectDto>[0]) {
|
||||||
super(data);
|
super(data);
|
||||||
@@ -97,7 +124,7 @@ export class ReadGameDto extends DatabaseObjectDto {
|
|||||||
this.ended = obj.ended;
|
this.ended = obj.ended;
|
||||||
this.owner = new ReadUserDto(obj.owner);
|
this.owner = new ReadUserDto(obj.owner);
|
||||||
this.packs = obj.packs;
|
this.packs = obj.packs;
|
||||||
this.currentTurn = obj.currentTurn;
|
this.currentTurn = new ReadPlayingTeamDto(obj.currentTurn);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@@ -113,7 +140,7 @@ export class ReadGameDto extends DatabaseObjectDto {
|
|||||||
fireGems: ReadContainerDto;
|
fireGems: ReadContainerDto;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
currentTurn?: string;
|
currentTurn: ReadPlayingTeamDto;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
started: boolean;
|
started: boolean;
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { UsersModule } from 'src/users/users.module';
|
|||||||
import { CardsModule } from 'src/cards/cards.module';
|
import { CardsModule } from 'src/cards/cards.module';
|
||||||
import { GamesController } from './controllers/games.controller';
|
import { GamesController } from './controllers/games.controller';
|
||||||
import { GameService } from './services/games.service';
|
import { GameService } from './services/games.service';
|
||||||
|
import { TurnService } from './services/turn.service';
|
||||||
|
import { TurnController } from './controllers/turn.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -14,7 +16,7 @@ import { GameService } from './services/games.service';
|
|||||||
UsersModule,
|
UsersModule,
|
||||||
MongooseModule.forFeature([{ name: Game.name, schema: GameSchema }]),
|
MongooseModule.forFeature([{ name: Game.name, schema: GameSchema }]),
|
||||||
],
|
],
|
||||||
controllers: [LobbyController, GamesController],
|
controllers: [LobbyController, GamesController, TurnController],
|
||||||
providers: [LobbyService, GameService],
|
providers: [LobbyService, GameService, TurnService],
|
||||||
})
|
})
|
||||||
export class GamesModule {}
|
export class GamesModule {}
|
||||||
|
|||||||
@@ -20,3 +20,22 @@ export class GamePipe implements PipeTransform {
|
|||||||
return game;
|
return game;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GameStartedPipe implements PipeTransform {
|
||||||
|
constructor(@InjectModel(Game.name) private gameModel: Model<Game>) {}
|
||||||
|
|
||||||
|
async transform(value: any) {
|
||||||
|
const game = await this.gameModel.findById(value);
|
||||||
|
if (!game) {
|
||||||
|
throw new HttpException('Game not found', HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
if (!game.started) {
|
||||||
|
throw new HttpException(
|
||||||
|
'The game must be started',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return game;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,28 @@ export class Container extends Types.ObjectId {
|
|||||||
|
|
||||||
export const ContainerSchema = SchemaFactory.createForClass(Container);
|
export const ContainerSchema = SchemaFactory.createForClass(Container);
|
||||||
|
|
||||||
|
@Schema()
|
||||||
|
export class PlayingTurn extends Types.ObjectId {
|
||||||
|
@Prop()
|
||||||
|
currentTeam?: string;
|
||||||
|
|
||||||
|
@Prop({
|
||||||
|
type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Card' }],
|
||||||
|
autopopulate: true,
|
||||||
|
default: [],
|
||||||
|
})
|
||||||
|
cardsLocked: Card[];
|
||||||
|
|
||||||
|
@Prop({ type: [String], default: [] })
|
||||||
|
effectsUsed: string[];
|
||||||
|
|
||||||
|
@Prop({ default: 0 })
|
||||||
|
damage: number;
|
||||||
|
|
||||||
|
@Prop({ default: 0 })
|
||||||
|
gold: number;
|
||||||
|
}
|
||||||
|
|
||||||
@Schema()
|
@Schema()
|
||||||
export class Player extends Types.ObjectId {
|
export class Player extends Types.ObjectId {
|
||||||
@Prop({
|
@Prop({
|
||||||
@@ -72,8 +94,8 @@ export class Game extends Types.ObjectId {
|
|||||||
@Prop({ type: ContainerSchema, default: {}, autopopulate: true })
|
@Prop({ type: ContainerSchema, default: {}, autopopulate: true })
|
||||||
fireGems: Container;
|
fireGems: Container;
|
||||||
|
|
||||||
@Prop()
|
@Prop({ type: PlayingTurn, default: {}, autopopulate: true })
|
||||||
currentTurn?: string;
|
currentTurn: PlayingTurn;
|
||||||
|
|
||||||
@Prop({ default: false })
|
@Prop({ default: false })
|
||||||
started: boolean;
|
started: boolean;
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { Game, Player } from '../schemas/game.schema';
|
||||||
|
import { Document } from 'mongoose';
|
||||||
|
import { Card } from 'src/cards/schemas/cards.schema';
|
||||||
|
import { CardEffects } from 'src/cards/schemas/cards.types';
|
||||||
|
import { Effect } from 'src/cards/schemas/effect.schema';
|
||||||
|
|
||||||
|
export const effectMappings = {
|
||||||
|
[CardEffects.GOLD]: (
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
effect: Effect,
|
||||||
|
_player: Player,
|
||||||
|
_card: Card,
|
||||||
|
eventEmitter: EventEmitter2,
|
||||||
|
) => {
|
||||||
|
game.currentTurn.gold += effect.amount;
|
||||||
|
eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'currentTurn.updateGold',
|
||||||
|
gold: game.currentTurn.gold,
|
||||||
|
operation: effect.amount,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
} as Record<
|
||||||
|
CardEffects,
|
||||||
|
(
|
||||||
|
game: Game,
|
||||||
|
effect: Effect,
|
||||||
|
player: Player,
|
||||||
|
card: Card,
|
||||||
|
eventEmitter: EventEmitter2,
|
||||||
|
) => void
|
||||||
|
>;
|
||||||
@@ -2,7 +2,7 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
|||||||
import { DatabaseObjectDto } from '../dto/database-object.dto';
|
import { DatabaseObjectDto } from '../dto/database-object.dto';
|
||||||
import { InjectModel } from '@nestjs/mongoose';
|
import { InjectModel } from '@nestjs/mongoose';
|
||||||
import { Game, Player, Team } from '../schemas/game.schema';
|
import { Game, Player, Team } from '../schemas/game.schema';
|
||||||
import { Document, Model } from 'mongoose';
|
import mongoose, { Document, Model } from 'mongoose';
|
||||||
import { ReadContainerDto, ReadGameDto } from '../dto/read-games.dto';
|
import { ReadContainerDto, ReadGameDto } from '../dto/read-games.dto';
|
||||||
import { User } from 'src/users/schemas/user.entity';
|
import { User } from 'src/users/schemas/user.entity';
|
||||||
import { Card } from 'src/cards/schemas/cards.schema';
|
import { Card } from 'src/cards/schemas/cards.schema';
|
||||||
@@ -10,6 +10,7 @@ import { CardRole } from 'src/cards/schemas/cards.types';
|
|||||||
import { StartingDeckDto, UpdateGameDto } from '../dto/update-game.dto';
|
import { StartingDeckDto, UpdateGameDto } from '../dto/update-game.dto';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
import { GameService } from './games.service';
|
import { GameService } from './games.service';
|
||||||
|
import { WithTransaction } from '../utils';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LobbyService {
|
export class LobbyService {
|
||||||
@@ -88,10 +89,14 @@ export class LobbyService {
|
|||||||
return await game.deleteOne();
|
return await game.deleteOne();
|
||||||
}
|
}
|
||||||
|
|
||||||
async start(game: Document<Game> & Game) {
|
@WithTransaction
|
||||||
|
async start(
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
session?: mongoose.mongo.ClientSession,
|
||||||
|
) {
|
||||||
const random = Math.floor(Math.random() * game.teams.length);
|
const random = Math.floor(Math.random() * game.teams.length);
|
||||||
game.currentTurn = game.teams[random]._id.toHexString();
|
game.currentTurn.currentTeam = game.teams[random]._id.toHexString();
|
||||||
await game.save();
|
await game.save({ session });
|
||||||
game.started = true;
|
game.started = true;
|
||||||
this.eventEmitter.emit('sse.lobby', {
|
this.eventEmitter.emit('sse.lobby', {
|
||||||
type: 'start',
|
type: 'start',
|
||||||
@@ -116,7 +121,7 @@ export class LobbyService {
|
|||||||
type: 'populateContainer',
|
type: 'populateContainer',
|
||||||
container: new ReadContainerDto(player.stack, true),
|
container: new ReadContainerDto(player.stack, true),
|
||||||
});
|
});
|
||||||
await this.gamesService.shuffleContainer(player.stack, game);
|
await this.gamesService.shuffleContainer(player.stack, game, session);
|
||||||
playerIndex++;
|
playerIndex++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,19 +145,33 @@ export class LobbyService {
|
|||||||
container: new ReadContainerDto(game.marketStack, true),
|
container: new ReadContainerDto(game.marketStack, true),
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.gamesService.shuffleContainer(game.marketStack, game);
|
await this.gamesService.shuffleContainer(game.marketStack, game, session);
|
||||||
await this.gamesService.fillMarket(game);
|
await this.gamesService.fillMarket(game, session);
|
||||||
for (const team of game.teams) {
|
for (const team of game.teams) {
|
||||||
for (const player of team.players) {
|
for (const player of team.players) {
|
||||||
if (team._id.toHexString() == game.currentTurn) {
|
if (team._id.toHexString() == game.currentTurn.currentTeam) {
|
||||||
await this.gamesService.drawToHand(3, player, game);
|
await this.gamesService.drawToHand(3, player, game, session);
|
||||||
} else {
|
} else {
|
||||||
await this.gamesService.drawToHand(5, player, game);
|
await this.gamesService.drawToHand(5, player, game, session);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await game.save();
|
const currentTeam = game.teams.find(
|
||||||
|
(t) => t._id.toHexString() == game.currentTurn.currentTeam,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const player of currentTeam.players) {
|
||||||
|
await this.gamesService.distributeCards(
|
||||||
|
player.hand.cards,
|
||||||
|
player.hand,
|
||||||
|
player.board,
|
||||||
|
game,
|
||||||
|
session,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await game.save({ session });
|
||||||
}
|
}
|
||||||
|
|
||||||
async changeStartingDeck(
|
async changeStartingDeck(
|
||||||
|
|||||||
@@ -4,47 +4,7 @@ import { Container, Game, Player } from '../schemas/game.schema';
|
|||||||
import mongoose, { Document, Model } from 'mongoose';
|
import mongoose, { Document, Model } from 'mongoose';
|
||||||
import { Card } from 'src/cards/schemas/cards.schema';
|
import { Card } from 'src/cards/schemas/cards.schema';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { WithTransaction, getCurrentTeam, shuffle } from '../utils';
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
function WithTransaction(
|
|
||||||
target: any,
|
|
||||||
propertyKey: string,
|
|
||||||
descriptor: PropertyDescriptor,
|
|
||||||
) {
|
|
||||||
const originalFunc = descriptor.value;
|
|
||||||
|
|
||||||
descriptor.value = async function (...args: any[]) {
|
|
||||||
if (args.at(-1) instanceof mongoose.mongo.ClientSession) {
|
|
||||||
return originalFunc.apply(this, args);
|
|
||||||
} else {
|
|
||||||
await this.connection
|
|
||||||
.transaction(async (session) => {
|
|
||||||
return originalFunc.apply(this, [...args, session]);
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
if (e instanceof HttpException) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
throw new HttpException(
|
|
||||||
'Another request is processing',
|
|
||||||
HttpStatus.TOO_MANY_REQUESTS,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return descriptor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GameService {
|
export class GameService {
|
||||||
@@ -59,21 +19,11 @@ export class GameService {
|
|||||||
@WithTransaction
|
@WithTransaction
|
||||||
async endTurn(
|
async endTurn(
|
||||||
game: Document<Game> & Game,
|
game: Document<Game> & Game,
|
||||||
currentPlayerId: string,
|
|
||||||
session?: mongoose.mongo.ClientSession,
|
session?: mongoose.mongo.ClientSession,
|
||||||
) {
|
) {
|
||||||
let currentTurn = game.teams.find(
|
let currentTeam = getCurrentTeam(game);
|
||||||
(t) => t._id.toHexString() == game.currentTurn,
|
|
||||||
);
|
for (const player of currentTeam.players) {
|
||||||
if (
|
|
||||||
!currentTurn.players.some((p) => p._id.toHexString() == currentPlayerId)
|
|
||||||
) {
|
|
||||||
throw new HttpException(
|
|
||||||
'It is not your turn to play',
|
|
||||||
HttpStatus.FORBIDDEN,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for (const player of currentTurn.players) {
|
|
||||||
await this.distributeCards(
|
await this.distributeCards(
|
||||||
player.board.cards.filter((c) => !c.defense),
|
player.board.cards.filter((c) => !c.defense),
|
||||||
player.board,
|
player.board,
|
||||||
@@ -84,20 +34,23 @@ export class GameService {
|
|||||||
await this.drawToHand(5, player, game, session);
|
await this.drawToHand(5, player, game, session);
|
||||||
}
|
}
|
||||||
|
|
||||||
const index = game.teams.indexOf(currentTurn);
|
const index = game.teams.indexOf(currentTeam);
|
||||||
game.currentTurn =
|
game.currentTurn.currentTeam =
|
||||||
game.teams[(index + 1) % game.teams.length]._id.toHexString();
|
game.teams[(index + 1) % game.teams.length]._id.toHexString();
|
||||||
|
|
||||||
|
game.currentTurn.cardsLocked = [];
|
||||||
|
game.currentTurn.effectsUsed = [];
|
||||||
|
game.currentTurn.damage = 0;
|
||||||
|
game.currentTurn.gold = 0;
|
||||||
|
|
||||||
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
type: 'endTurn',
|
type: 'endTurn',
|
||||||
currentTurn: game.currentTurn,
|
currentTurn: game.currentTurn,
|
||||||
});
|
});
|
||||||
|
|
||||||
currentTurn = game.teams.find(
|
currentTeam = getCurrentTeam(game);
|
||||||
(t) => t._id.toHexString() == game.currentTurn,
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const player of currentTurn.players) {
|
for (const player of currentTeam.players) {
|
||||||
await this.distributeCards(
|
await this.distributeCards(
|
||||||
player.hand.cards,
|
player.hand.cards,
|
||||||
player.hand,
|
player.hand,
|
||||||
@@ -108,6 +61,7 @@ export class GameService {
|
|||||||
}
|
}
|
||||||
await game.save({ session });
|
await game.save({ session });
|
||||||
}
|
}
|
||||||
|
|
||||||
@WithTransaction
|
@WithTransaction
|
||||||
async drawToBoard(
|
async drawToBoard(
|
||||||
amount,
|
amount,
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||||
|
import { Game, Player } from '../schemas/game.schema';
|
||||||
|
import mongoose, { Document, Model } from 'mongoose';
|
||||||
|
import { Card } from 'src/cards/schemas/cards.schema';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { Effect } from 'src/cards/schemas/effect.schema';
|
||||||
|
import { CardEffects, EffectType } from 'src/cards/schemas/cards.types';
|
||||||
|
import { WithTransaction } from '../utils';
|
||||||
|
import { effectMappings } from './effect.functions';
|
||||||
|
import { InjectConnection, InjectModel } from '@nestjs/mongoose';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TurnService {
|
||||||
|
constructor(
|
||||||
|
private eventEmitter: EventEmitter2,
|
||||||
|
@InjectModel(Game.name) private gameModel: Model<Game>,
|
||||||
|
@InjectConnection() private readonly connection: mongoose.Connection,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@WithTransaction
|
||||||
|
async lockCard(
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
player: Player,
|
||||||
|
cardId: string,
|
||||||
|
session?: mongoose.mongo.ClientSession,
|
||||||
|
) {
|
||||||
|
const card = player.board.cards.find((c) => c._id.toHexString() == cardId);
|
||||||
|
if (!card) {
|
||||||
|
throw new HttpException(
|
||||||
|
'Card not found in player board',
|
||||||
|
HttpStatus.NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
game.currentTurn.cardsLocked.push(card);
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'lockCard',
|
||||||
|
card: card._id.toHexString(),
|
||||||
|
});
|
||||||
|
for (const effect of card.effects) {
|
||||||
|
if (
|
||||||
|
[, CardEffects.HEAL, CardEffects.GOLD, CardEffects.DAMAGE].includes(
|
||||||
|
effect.effect,
|
||||||
|
) &&
|
||||||
|
!effect.mutex &&
|
||||||
|
!effect.per &&
|
||||||
|
(!effect.condition ||
|
||||||
|
effect.condition.effectId == EffectType.CHAMPION_ACTION)
|
||||||
|
) {
|
||||||
|
await this.useEffect(game, player, effect, card, session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await game.save({ session });
|
||||||
|
}
|
||||||
|
|
||||||
|
@WithTransaction
|
||||||
|
async useEffect(
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
player: Player,
|
||||||
|
effect: Effect,
|
||||||
|
card: Card,
|
||||||
|
session?: mongoose.mongo.ClientSession,
|
||||||
|
) {
|
||||||
|
const effectRunner = effectMappings[effect.effect];
|
||||||
|
if (!effectMappings[effect.effect]) {
|
||||||
|
console.warn(`Effect ${effect.effect} not implemented`);
|
||||||
|
return;
|
||||||
|
throw new HttpException(
|
||||||
|
`Effect ${effect.effect} not implemented`,
|
||||||
|
HttpStatus.NOT_IMPLEMENTED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const effectUUID = effect._id.toHexString();
|
||||||
|
if (game.currentTurn.effectsUsed.includes(effectUUID)) {
|
||||||
|
console.error(`Effect ${effectUUID} already used this turn`);
|
||||||
|
throw new HttpException(
|
||||||
|
`Effect ${effectUUID} already used this turn`,
|
||||||
|
HttpStatus.CONFLICT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'useEffect',
|
||||||
|
effect: effectUUID,
|
||||||
|
});
|
||||||
|
game.currentTurn.effectsUsed.push(effectUUID);
|
||||||
|
await effectRunner(game, effect, player, card, this.eventEmitter);
|
||||||
|
await game.save({ session });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { HttpStatus } from '@nestjs/common/enums';
|
||||||
|
import { HttpException } from '@nestjs/common/exceptions';
|
||||||
|
import mongoose, { Document } from 'mongoose';
|
||||||
|
import { Game, Player, Team } from './schemas/game.schema';
|
||||||
|
import { ReadUserDto } from './dto/read-games.dto';
|
||||||
|
|
||||||
|
export 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCurrentTeam(game: Document<Game> & Game) {
|
||||||
|
return game.teams.find(
|
||||||
|
(t) => t._id.toHexString() == game.currentTurn.currentTeam,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPlayer(session: Record<string, any>, 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> & Player;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new HttpException(
|
||||||
|
'You are not part of this game',
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isInTeam(team: Team, player: Player) {
|
||||||
|
if (
|
||||||
|
team.players.some((p) => p._id.toHexString() == player._id.toHexString())
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WithTransaction(
|
||||||
|
target: any,
|
||||||
|
propertyKey: string,
|
||||||
|
descriptor: PropertyDescriptor,
|
||||||
|
) {
|
||||||
|
const originalFunc = descriptor.value;
|
||||||
|
|
||||||
|
descriptor.value = async function (...args: any[]) {
|
||||||
|
if (args.at(-1) instanceof mongoose.mongo.ClientSession) {
|
||||||
|
return originalFunc.apply(this, args);
|
||||||
|
} else {
|
||||||
|
await this.connection
|
||||||
|
.transaction(async (session) => {
|
||||||
|
return originalFunc.apply(this, [...args, session]);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
if (e instanceof HttpException) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
throw new HttpException(
|
||||||
|
'Another request is processing',
|
||||||
|
HttpStatus.TOO_MANY_REQUESTS,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return descriptor;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user