WIP : before-cqrs
This commit is contained in:
Vendored
+16
-4
@@ -5,10 +5,22 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "npm run start:dev -b swc",
|
||||
"name": "Start Nest",
|
||||
"name": "Launch via NPM",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"start:dev",
|
||||
"-b",
|
||||
"swc"
|
||||
],
|
||||
"runtimeExecutable": "npm",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"type": "node",
|
||||
"console": "internalConsole",
|
||||
"outputCapture": "std",
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
}
|
||||
]
|
||||
}
|
||||
Generated
+10
@@ -19,6 +19,7 @@
|
||||
"connect-mongo": "^5.1.0",
|
||||
"express-session": "^1.18.0",
|
||||
"mongoose": "^8.4.1",
|
||||
"mongoose-autopopulate": "^1.1.0",
|
||||
"reflect-metadata": "^0.2.1",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
@@ -7587,6 +7588,15 @@
|
||||
"url": "https://opencollective.com/mongoose"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose-autopopulate": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mongoose-autopopulate/-/mongoose-autopopulate-1.1.0.tgz",
|
||||
"integrity": "sha512-nTlTMlu1fLQ1bmJT7ILKbZmPGt2fHErLO4UJwzMDsHSigjtUYz0l3nvFhg511QkOkZcKBRzOnPn3DmmLIUENzg==",
|
||||
"license": "Apache 2.0",
|
||||
"peerDependencies": {
|
||||
"mongoose": "6.x || 7.x || 8.0.0-rc0 || 8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"connect-mongo": "^5.1.0",
|
||||
"express-session": "^1.18.0",
|
||||
"mongoose": "^8.4.1",
|
||||
"mongoose-autopopulate": "^1.1.0",
|
||||
"reflect-metadata": "^0.2.1",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { Controller, Put } from '@nestjs/common';
|
||||
import { AppService } from './app.service';
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
|
||||
@ApiTags('admin')
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@Get()
|
||||
getHello(): string {
|
||||
return this.appService.getHello();
|
||||
@Put('loadCards')
|
||||
@ApiOperation({ summary: 'Load cards into the database' })
|
||||
loadCards(): void {
|
||||
this.appService.loadCards();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { AppService } from './app.service';
|
||||
import { GamesModule } from './games/games.module';
|
||||
import { UsersModule } from './users/users.module';
|
||||
import { CardsService } from './cards/cards.service';
|
||||
import { CardsModule } from './cards/cards.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -16,7 +17,13 @@ import { CardsService } from './cards/cards.service';
|
||||
username: process.env.MONGO_USER,
|
||||
},
|
||||
dbName: 'heron',
|
||||
connectionFactory: (connection) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
connection.plugin(require('mongoose-autopopulate'));
|
||||
return connection;
|
||||
},
|
||||
}),
|
||||
CardsModule,
|
||||
GamesModule,
|
||||
UsersModule,
|
||||
],
|
||||
|
||||
+4
-2
@@ -1,8 +1,10 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CardsService } from './cards/cards.service';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
getHello(): string {
|
||||
return 'Hello World!';
|
||||
constructor(private readonly cardService: CardsService) {}
|
||||
loadCards(): void {
|
||||
this.cardService.loadCards();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Card, CardSchema } from './schemas/cards.schema';
|
||||
import { CardsService } from './cards.service';
|
||||
// import {
|
||||
// Effect,
|
||||
// EffectCondition,
|
||||
// EffectConditionSchema,
|
||||
// EffectSchema,
|
||||
// } from './schemas/effect.schema';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
MongooseModule.forFeature([{ name: Card.name, schema: CardSchema }]),
|
||||
MongooseModule.forFeature([
|
||||
// { name: Effect.name, schema: EffectSchema },
|
||||
// { name: EffectCondition.name, schema: EffectConditionSchema },
|
||||
{ name: Card.name, schema: CardSchema },
|
||||
]),
|
||||
],
|
||||
exports: [MongooseModule],
|
||||
providers: [CardsService],
|
||||
exports: [MongooseModule, CardsService],
|
||||
})
|
||||
export class CardsModule {
|
||||
schema = CardSchema;
|
||||
|
||||
+118
-3
@@ -1,11 +1,26 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs/promises';
|
||||
import { Effect } from './schemas/effect.schema';
|
||||
import { Card } from './schemas/cards.schema';
|
||||
import { Model } from 'mongoose';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import {
|
||||
CardFaction,
|
||||
CardParser,
|
||||
CardRole,
|
||||
EffectParser,
|
||||
EffectType,
|
||||
EffectTypeVerbose,
|
||||
} from './schemas/cards.types';
|
||||
import { EffectCondition } from './schemas/effect.schema';
|
||||
|
||||
@Injectable()
|
||||
export class CardsService {
|
||||
constructor(@InjectModel(Card.name) private cardModel: Model<Card>) {}
|
||||
|
||||
async loadCards(): Promise<void> {
|
||||
const cardPath = path.join(path.resolve(), 'cards');
|
||||
const cardPath = path.join(path.resolve(), 'src', 'json');
|
||||
const files = await fs.readdir(cardPath);
|
||||
const processed = await Promise.all(
|
||||
files.map(async (f) => {
|
||||
@@ -20,7 +35,107 @@ export class CardsService {
|
||||
}),
|
||||
);
|
||||
|
||||
const packs = Object.fromEntries(processed);
|
||||
console.log(packs);
|
||||
await Promise.all(
|
||||
Object.entries(processed).map(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async ([_, data]: [string, [string, CardParser[]]]) => {
|
||||
await Promise.all(
|
||||
data[1].map((c) => {
|
||||
if (c.role == CardRole.FIRE_GEM || c.role == CardRole.MARKET) {
|
||||
for (let i = 0; i < (c.init_amount ?? 1); i++) {
|
||||
this.createCard(c, data[0]);
|
||||
}
|
||||
} else {
|
||||
for (let j = 0; j < 4; j++) {
|
||||
for (let i = 0; i < (c.init_amount ?? 1); i++) {
|
||||
this.createCard(c, data[0], j);
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private async createCard(
|
||||
json: CardParser,
|
||||
pack: string,
|
||||
playerIndex?: number,
|
||||
) {
|
||||
const createdCard = await this.cardModel.create({
|
||||
cardId: json.id,
|
||||
role: json.role,
|
||||
cardType: json.card_type,
|
||||
faction: json.faction,
|
||||
name: json.name,
|
||||
cost: json.cost,
|
||||
defense: json.defense,
|
||||
guard: json.guard,
|
||||
pack: pack,
|
||||
playerIndex,
|
||||
});
|
||||
if (json.effects) {
|
||||
createdCard.effects = await this.createEffects(json.effects, createdCard);
|
||||
}
|
||||
createdCard.save();
|
||||
}
|
||||
|
||||
private async createEffects(
|
||||
json: EffectParser[],
|
||||
card: Card,
|
||||
): Promise<Effect[]> {
|
||||
return await Promise.all(
|
||||
json.map(async (e) => {
|
||||
const createdEffect = {
|
||||
card: card,
|
||||
effect: e.effect,
|
||||
amount: e.amount,
|
||||
times: e.times ?? 1,
|
||||
per: e.per,
|
||||
faction: e.faction,
|
||||
} as Effect;
|
||||
if (e.sub_effects) {
|
||||
createdEffect.subEffects = await this.createEffects(
|
||||
e.sub_effects,
|
||||
card,
|
||||
);
|
||||
} else {
|
||||
createdEffect.subEffects = [];
|
||||
}
|
||||
|
||||
if (e.effect_type) {
|
||||
createdEffect.condition = await this.createEffectCondition(
|
||||
e.effect_type,
|
||||
card,
|
||||
);
|
||||
}
|
||||
return createdEffect;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private async createEffectCondition(
|
||||
json: EffectType | EffectTypeVerbose,
|
||||
card: Card,
|
||||
): Promise<EffectCondition> {
|
||||
const createdCondition = {} as EffectCondition;
|
||||
if (typeof json === 'string') {
|
||||
createdCondition.effectId = json;
|
||||
if (json == EffectType.FACTION_COMBO) {
|
||||
createdCondition.faction = card.faction;
|
||||
}
|
||||
createdCondition.amount = 1;
|
||||
} else {
|
||||
createdCondition.effectId = json.id;
|
||||
createdCondition.amount = json.amount ?? 1;
|
||||
createdCondition.cardId = json.card_id;
|
||||
if (json.id == EffectType.FACTION_COMBO) {
|
||||
createdCondition.faction = (json.faction ??
|
||||
card.faction) as CardFaction;
|
||||
}
|
||||
}
|
||||
return createdCondition;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,42 @@
|
||||
import { Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||
import { CardFaction, CardRole, CardType, EffectParser } from './cards.types';
|
||||
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||
import { CardFaction, CardRole, CardType } from './cards.types';
|
||||
import { Types } from 'mongoose';
|
||||
import { Effect } from './effect.schema';
|
||||
|
||||
@Schema()
|
||||
export class Card {
|
||||
export class Card extends Types.ObjectId {
|
||||
@Prop({ type: Number })
|
||||
cardId: number;
|
||||
|
||||
@Prop({ type: String, enum: CardRole })
|
||||
role: CardRole;
|
||||
|
||||
@Prop()
|
||||
playerIndex?: number;
|
||||
|
||||
@Prop({ type: String, enum: CardType })
|
||||
cardType: CardType;
|
||||
|
||||
@Prop({ type: String, enum: CardFaction })
|
||||
faction: CardFaction;
|
||||
|
||||
@Prop({ type: String })
|
||||
name: string;
|
||||
initAmount: number;
|
||||
|
||||
@Prop()
|
||||
cost?: number;
|
||||
|
||||
@Prop()
|
||||
defense?: number;
|
||||
|
||||
@Prop()
|
||||
guard?: boolean;
|
||||
effects: EffectParser[];
|
||||
|
||||
@Prop({ type: [Effect], default: [], autopopulate: true })
|
||||
effects: Effect[];
|
||||
|
||||
@Prop()
|
||||
pack: string;
|
||||
}
|
||||
|
||||
export const CardSchema = SchemaFactory.createForClass(Card);
|
||||
|
||||
@@ -5,6 +5,21 @@ export enum CardRole {
|
||||
PERSONAL = 'personal',
|
||||
MARKET = 'market',
|
||||
FIRE_GEM = 'fire_gem',
|
||||
|
||||
HUNTER = 'hunter',
|
||||
TRAVELER = 'traveler',
|
||||
CLERIC = 'cleric',
|
||||
FIGHTER = 'fighter',
|
||||
RANGER = 'ranger',
|
||||
THIEF = 'thief',
|
||||
WIZARD = 'wizard',
|
||||
|
||||
HALF_DEMON = 'half-demon',
|
||||
DWARF = 'dwarf',
|
||||
ELF = 'elf',
|
||||
OGRE = 'ogre',
|
||||
ORC = 'orc',
|
||||
SMALLFOLK = 'smallfolk',
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,6 +48,7 @@ export enum CardFaction {
|
||||
}
|
||||
|
||||
export enum CardEffects {
|
||||
//Base
|
||||
GOLD = 'gold',
|
||||
DAMAGE = 'damage',
|
||||
HEAL = 'heal',
|
||||
@@ -47,14 +63,45 @@ export enum CardEffects {
|
||||
STACK_NEXT_ACTION_BOUGHT = 'stack_next_action_bought',
|
||||
STACK_NEXT_CARD_BOUGHT = 'stack_next_card_bought',
|
||||
PLAY_NEXT_CARD_BOUGHT = 'play_next_card_bought',
|
||||
|
||||
//DLCs
|
||||
BUY_FOR_FREE = 'buy_for_free',
|
||||
|
||||
//Journeys
|
||||
DAMAGE_ALL_CHAMPIONS = 'damage_all_champions',
|
||||
|
||||
//Journeys: Hunters
|
||||
DISCARD_X_AND_DRAW_X = 'discard_x_and_draw_x',
|
||||
PREPARE_ANOTHER_CHAMPION = 'prepare_another_champion',
|
||||
//Journey: Travelers
|
||||
PREPARE_ALL_CHAMPIONS = 'prepare_all_champions',
|
||||
CHEAPER_CHAMPION = 'cheaper_champion',
|
||||
CHEAPER_CHAMPIONS_PASSIVE = 'cheaper_champions_passive',
|
||||
CHEAPER_ACTION = 'cheaper_action',
|
||||
CONTROL_OPPOSING_CHAMPION_PASSIVE = 'control_opposing_champion_passive',
|
||||
|
||||
RESTACK_DISCARDED_ACTION = 'restack_discarded_action',
|
||||
|
||||
//Ancestry
|
||||
KEEP_IN_HAND = 'keep_in_hand',
|
||||
BUY_GEM_FOR_FREE = 'buy_gem_for_free',
|
||||
CHEAPER_SKILLS_PASSIVE = 'cheaper_skills_passive',
|
||||
CHEAPER_CARD_IF_HIGHER_PRICE = 'cheaper_card_if_higher_price',
|
||||
PICK_FACTION = 'pick_faction',
|
||||
}
|
||||
|
||||
export enum Per {
|
||||
//Base
|
||||
CHAMPION = 'champion',
|
||||
OTHER_CHAMPION = 'other_champion',
|
||||
OTHER_GUARD = 'other_guard',
|
||||
CARD_OF_SAME_FACTION = 'card_of_same_faction',
|
||||
OTHER_CARD_OF_SAME_FACTION = 'other_card_of_same_faction',
|
||||
|
||||
//Journey: Travelers
|
||||
STUNNED_CHAMPION = 'stunned_champion',
|
||||
CHAMPION_OF_SAME_FACTION = 'champion_of_same_faction',
|
||||
OTHER_KNIFE_PLAYED = 'other_knife_played',
|
||||
}
|
||||
|
||||
export enum EffectType {
|
||||
@@ -85,6 +132,7 @@ export type EffectParser = {
|
||||
effect_type?: EffectType | EffectTypeVerbose;
|
||||
per?: Per;
|
||||
mutex?: number;
|
||||
faction?: CardFaction;
|
||||
};
|
||||
|
||||
export type CardParser = {
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import mongoose, { Types } from 'mongoose';
|
||||
import {
|
||||
CardEffects,
|
||||
CardFaction,
|
||||
EffectType,
|
||||
EffectTypeSpecial,
|
||||
Per,
|
||||
} from './cards.types';
|
||||
import { Card } from './cards.schema';
|
||||
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||
|
||||
// @Schema()
|
||||
export class EffectCondition {
|
||||
@Prop({ type: String, enum: { ...EffectType, ...EffectTypeSpecial } })
|
||||
effectId: EffectType | EffectTypeSpecial;
|
||||
|
||||
@Prop({ type: Number })
|
||||
amount: number;
|
||||
|
||||
@Prop({ type: Number })
|
||||
cardId?: number;
|
||||
|
||||
@Prop({ type: String, enum: CardFaction })
|
||||
faction?: CardFaction;
|
||||
}
|
||||
|
||||
// export const EffectConditionSchema =
|
||||
// SchemaFactory.createForClass(EffectCondition);
|
||||
|
||||
@Schema()
|
||||
export class Effect extends Types.ObjectId {
|
||||
@Prop({ type: String, enum: CardEffects })
|
||||
effect: CardEffects;
|
||||
|
||||
@Prop({ type: Number, default: 1 })
|
||||
amount!: number;
|
||||
|
||||
@Prop({ type: Number, default: 1 })
|
||||
times!: number;
|
||||
|
||||
// @Prop({ type: [Effect], default: [], autopopulate: true })
|
||||
subEffects: Effect[];
|
||||
|
||||
@Prop({ type: EffectCondition, autopopulate: true })
|
||||
condition?: EffectCondition;
|
||||
|
||||
@Prop({ type: String, enum: Per })
|
||||
per?: Per;
|
||||
|
||||
// @Prop({
|
||||
// type: [
|
||||
// {
|
||||
// type: mongoose.Schema.Types.ObjectId,
|
||||
// ref: 'Effect',
|
||||
// autopopulate: true,
|
||||
// },
|
||||
// ],
|
||||
// })
|
||||
mutex?: Effect[];
|
||||
|
||||
// @Prop({
|
||||
// type: mongoose.Schema.Types.ObjectId,
|
||||
// ref: 'Card',
|
||||
// autopopulate: true,
|
||||
// })
|
||||
card: Card;
|
||||
|
||||
@Prop({ type: String, enum: CardFaction })
|
||||
faction?: CardFaction;
|
||||
}
|
||||
|
||||
export const EffectSchema = SchemaFactory.createForClass(Effect);
|
||||
EffectSchema.add({
|
||||
card: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Card',
|
||||
autopopulate: true,
|
||||
},
|
||||
});
|
||||
|
||||
EffectSchema.add({
|
||||
mutex: {
|
||||
type: [
|
||||
{
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Effect',
|
||||
autopopulate: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { GamesController } from './games.controller';
|
||||
import { GamesService } from './games.service';
|
||||
import { GamesService } from '../services/games.lobby.service';
|
||||
|
||||
describe('GamesController', () => {
|
||||
let controller: GamesController;
|
||||
@@ -0,0 +1,141 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Param,
|
||||
Delete,
|
||||
UseGuards,
|
||||
Session,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Patch,
|
||||
Body,
|
||||
} from '@nestjs/common';
|
||||
import { GamesService } 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';
|
||||
|
||||
@ApiTags('lobby')
|
||||
@Controller('games')
|
||||
export class GamesController {
|
||||
constructor(private readonly gamesService: GamesService) {}
|
||||
|
||||
@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<string, any>) {
|
||||
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<string, any>,
|
||||
@Param('id') id: string,
|
||||
@Body() updateGameDto: UpdateGameDto,
|
||||
) {
|
||||
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<string, any>,
|
||||
@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<string, any>,
|
||||
@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',
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,57 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { DatabaseObjectDto } from './database-object.dto';
|
||||
import { Container, Team } from '../schemas/game.schema';
|
||||
import { User } from 'src/users/schemas/user.entity';
|
||||
import { Game, Player, Team } from '../schemas/game.schema';
|
||||
import { Card } from 'src/cards/schemas/cards.schema';
|
||||
|
||||
export class ReadPlayerDto extends DatabaseObjectDto {
|
||||
constructor(
|
||||
data: Player & ConstructorParameters<typeof DatabaseObjectDto>[0],
|
||||
) {
|
||||
super(data);
|
||||
this.user = new ReadUserDto(data.user);
|
||||
this.board = new ReadContainerDto(data.board);
|
||||
this.discard = new ReadContainerDto(data.discard);
|
||||
this.hand = new ReadContainerDto(data.hand, true);
|
||||
this.stack = new ReadContainerDto(data.stack, true);
|
||||
}
|
||||
user: ReadUserDto;
|
||||
|
||||
board: ReadContainerDto;
|
||||
|
||||
hand: ReadContainerDto;
|
||||
|
||||
stack: ReadContainerDto;
|
||||
|
||||
discard: ReadContainerDto;
|
||||
}
|
||||
|
||||
export class ReadTeamDto extends DatabaseObjectDto {
|
||||
constructor(data: ConstructorParameters<typeof DatabaseObjectDto>[0]) {
|
||||
constructor(data: Team & ConstructorParameters<typeof DatabaseObjectDto>[0]) {
|
||||
super(data);
|
||||
this.health = data.health;
|
||||
this.players = data.players.map((e) => {
|
||||
return new ReadPlayerDto(e);
|
||||
});
|
||||
}
|
||||
health: number;
|
||||
players: ReadPlayerDto[];
|
||||
}
|
||||
|
||||
export class ReadContainerDto extends DatabaseObjectDto {
|
||||
constructor(data: ConstructorParameters<typeof DatabaseObjectDto>[0]) {
|
||||
constructor(
|
||||
data: { cards: Card[] } & ConstructorParameters<
|
||||
typeof DatabaseObjectDto
|
||||
>[0],
|
||||
hidden = false,
|
||||
) {
|
||||
super(data);
|
||||
if (hidden) {
|
||||
this.cards = data.cards.map(() => null);
|
||||
} else {
|
||||
this.cards = data.cards.map((e) => e);
|
||||
}
|
||||
}
|
||||
cards: Card[];
|
||||
}
|
||||
|
||||
export class ReadUserDto extends DatabaseObjectDto {
|
||||
@@ -34,29 +73,20 @@ export class ReadUserDto extends DatabaseObjectDto {
|
||||
}
|
||||
|
||||
export class ReadGameDto extends DatabaseObjectDto {
|
||||
constructor(
|
||||
data: {
|
||||
teams: Team[];
|
||||
market: Container;
|
||||
marketStack: Container;
|
||||
fireGems: Container;
|
||||
currentTurn?: Team;
|
||||
started: boolean;
|
||||
ended: boolean;
|
||||
owner: User;
|
||||
} & ConstructorParameters<typeof DatabaseObjectDto>[0],
|
||||
) {
|
||||
constructor(data: Game & ConstructorParameters<typeof DatabaseObjectDto>[0]) {
|
||||
super(data);
|
||||
this.teams = data.teams.map((e) => new ReadTeamDto(e));
|
||||
this.market = new ReadContainerDto(data.market);
|
||||
this.marketStack = new ReadContainerDto(data.marketStack);
|
||||
this.fireGems = new ReadContainerDto(data.fireGems);
|
||||
this.started = data.started;
|
||||
this.ended = data.ended;
|
||||
this.owner = new ReadUserDto(data.owner);
|
||||
const obj = data;
|
||||
this.teams = obj.teams.map((e) => new ReadTeamDto(e));
|
||||
this.market = new ReadContainerDto(obj.market);
|
||||
this.marketStack = new ReadContainerDto(obj.marketStack, true);
|
||||
this.fireGems = new ReadContainerDto(obj.fireGems);
|
||||
this.started = obj.started;
|
||||
this.ended = obj.ended;
|
||||
this.owner = new ReadUserDto(obj.owner);
|
||||
this.packs = obj.packs;
|
||||
|
||||
if (data.currentTurn) {
|
||||
this.currentTurn = new ReadTeamDto(data.currentTurn);
|
||||
if (obj.currentTurn) {
|
||||
this.currentTurn = new ReadTeamDto(obj.currentTurn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,4 +113,7 @@ export class ReadGameDto extends DatabaseObjectDto {
|
||||
|
||||
@ApiProperty()
|
||||
owner: ReadUserDto;
|
||||
|
||||
@ApiProperty()
|
||||
packs: string[];
|
||||
}
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export class UpdateGameDto {}
|
||||
export class UpdateGameDto {
|
||||
packs: string[];
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Param,
|
||||
Delete,
|
||||
UseGuards,
|
||||
Session,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { GamesService } from './games.service';
|
||||
import { AuthGuard } from './guards/auth.guard';
|
||||
import { ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { DatabaseObjectDto } from './dto/database-object.dto';
|
||||
|
||||
@Controller('games')
|
||||
export class GamesController {
|
||||
constructor(private readonly gamesService: GamesService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create game' })
|
||||
@ApiResponse({
|
||||
status: 201,
|
||||
description: 'Game successfully created.',
|
||||
type: DatabaseObjectDto,
|
||||
})
|
||||
@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<string, any>) {
|
||||
const games = await this.gamesService.findByOwner(session.user._id);
|
||||
if (games.length > 0) {
|
||||
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')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.gamesService.findById(id);
|
||||
}
|
||||
|
||||
// @Patch(':id')
|
||||
// update(@Param('id') id: string, @Body() updateGameDto: UpdateGameDto) {
|
||||
// return this.gamesService.update(+id, updateGameDto);
|
||||
// }
|
||||
|
||||
@Delete(':id')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.gamesService.remove(+id);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { GamesService } from './games.service';
|
||||
import { GamesController } from './games.controller';
|
||||
import { GamesService } from './services/games.lobby.service';
|
||||
import { GamesController } from './controllers/games.controller';
|
||||
import { Game, GameSchema } from './schemas/game.schema';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { UsersModule } from 'src/users/users.module';
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DatabaseObjectDto } from './dto/database-object.dto';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Game } from './schemas/game.schema';
|
||||
import { Model } from 'mongoose';
|
||||
import { ReadGameDto } from './dto/read-games.dto';
|
||||
import { User } from 'src/users/schemas/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class GamesService {
|
||||
constructor(@InjectModel(Game.name) private gameModel: Model<Game>) {}
|
||||
|
||||
async create(owner: User): Promise<DatabaseObjectDto> {
|
||||
const createdGame = new this.gameModel();
|
||||
createdGame.owner = owner;
|
||||
const gamedata = await createdGame.save();
|
||||
return new DatabaseObjectDto(gamedata);
|
||||
}
|
||||
|
||||
async findAll(): Promise<ReadGameDto[]> {
|
||||
const games = await this.gameModel.find().exec();
|
||||
return games.map((e) => new ReadGameDto(e));
|
||||
}
|
||||
|
||||
findById(id: string) {
|
||||
return this.gameModel.findById(id).exec();
|
||||
}
|
||||
|
||||
findByOwner(_id: string) {
|
||||
return this.gameModel.find({ owner: _id }).exec();
|
||||
}
|
||||
|
||||
// update(id: number, updateGameDto: UpdateGameDto) {
|
||||
// return `This action updates a #${id} game`;
|
||||
// }
|
||||
|
||||
remove(id: number) {
|
||||
return `This action removes a #${id} game`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
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<boolean> | Observable<boolean> {
|
||||
const http = context.switchToHttp();
|
||||
const request = http.getRequest<Request>();
|
||||
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<string, any>).user) {
|
||||
throw new HttpException('Please log in', HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
return true;
|
||||
})();
|
||||
}
|
||||
}
|
||||
@@ -5,34 +5,71 @@ import { User } from 'src/users/schemas/user.entity';
|
||||
|
||||
export type GameDocument = HydratedDocument<Game>;
|
||||
|
||||
@Schema()
|
||||
export class Team extends Types.ObjectId {}
|
||||
|
||||
export const TeamSchema = SchemaFactory.createForClass(Team);
|
||||
|
||||
@Schema()
|
||||
export class Container extends Types.ObjectId {
|
||||
@Prop([{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }])
|
||||
@Prop({
|
||||
type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Card' }],
|
||||
autopopulate: true,
|
||||
})
|
||||
cards: Card[];
|
||||
}
|
||||
|
||||
export const ContainerSchema = SchemaFactory.createForClass(Container);
|
||||
|
||||
@Schema()
|
||||
export class Player extends Types.ObjectId {
|
||||
@Prop({
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
autopopulate: true,
|
||||
})
|
||||
user: User;
|
||||
|
||||
@Prop({ type: ContainerSchema, default: {}, autopopulate: true })
|
||||
board!: Container;
|
||||
|
||||
@Prop({ type: ContainerSchema, default: {}, autopopulate: true })
|
||||
hand!: Container;
|
||||
|
||||
@Prop({ type: ContainerSchema, default: {}, autopopulate: true })
|
||||
stack!: Container;
|
||||
|
||||
@Prop({ type: ContainerSchema, default: {}, autopopulate: true })
|
||||
discard!: Container;
|
||||
}
|
||||
|
||||
export const PlayerSchema = SchemaFactory.createForClass(Player);
|
||||
|
||||
@Schema()
|
||||
export class Team extends Types.ObjectId {
|
||||
@Prop({ type: Number, default: 50 })
|
||||
health: number;
|
||||
|
||||
@Prop({ type: [{ type: Player }] })
|
||||
players: Player[];
|
||||
}
|
||||
|
||||
export const TeamSchema = SchemaFactory.createForClass(Team);
|
||||
|
||||
@Schema()
|
||||
export class Game extends Types.ObjectId {
|
||||
@Prop([Team])
|
||||
@Prop({ type: [{ type: Team, autopopulate: true }] })
|
||||
teams: Team[];
|
||||
|
||||
@Prop({ type: ContainerSchema, default: {} })
|
||||
@Prop({ type: ContainerSchema, default: {}, autopopulate: true })
|
||||
market: Container;
|
||||
|
||||
@Prop({ type: ContainerSchema, default: {} })
|
||||
@Prop({ type: ContainerSchema, default: {}, autopopulate: true })
|
||||
marketStack: Container;
|
||||
|
||||
@Prop({ type: ContainerSchema, default: {} })
|
||||
@Prop({ type: ContainerSchema, default: {}, autopopulate: true })
|
||||
fireGems: Container;
|
||||
|
||||
@Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'Game.teams' })
|
||||
@Prop({
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Team',
|
||||
autopopulate: true,
|
||||
})
|
||||
currentTurn?: Team;
|
||||
|
||||
@Prop({ default: false })
|
||||
@@ -41,8 +78,15 @@ export class Game extends Types.ObjectId {
|
||||
@Prop({ default: false })
|
||||
ended: boolean;
|
||||
|
||||
@Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'User' })
|
||||
@Prop({
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
autopopulate: true,
|
||||
})
|
||||
owner: User;
|
||||
|
||||
@Prop({ type: [String], default: [] })
|
||||
packs: string[];
|
||||
}
|
||||
|
||||
export const GameSchema = SchemaFactory.createForClass(Game);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { GamesService } from './games.service';
|
||||
import { GamesService } from './games.lobby.service';
|
||||
|
||||
describe('GamesService', () => {
|
||||
let service: GamesService;
|
||||
@@ -0,0 +1,110 @@
|
||||
import { 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 { 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';
|
||||
|
||||
@Injectable()
|
||||
export class GamesService {
|
||||
constructor(
|
||||
@InjectModel(Game.name) private gameModel: Model<Game>,
|
||||
@InjectModel(Card.name) private cardModel: Model<Card>,
|
||||
) {}
|
||||
|
||||
async create(owner: User): Promise<DatabaseObjectDto> {
|
||||
const createdGame = new this.gameModel();
|
||||
createdGame.owner = owner;
|
||||
createdGame.packs = [
|
||||
'pack1_base_deck',
|
||||
'pack1_base_necros',
|
||||
'pack1_base_wild',
|
||||
'pack1_base_imperial',
|
||||
'pack1_base_guild',
|
||||
];
|
||||
createdGame.teams.push({
|
||||
players: [{ user: owner } as Player],
|
||||
} as Team);
|
||||
|
||||
const gamedata = await createdGame.save();
|
||||
return new ReadGameDto(gamedata);
|
||||
}
|
||||
|
||||
async findAll(): Promise<ReadGameDto[]> {
|
||||
const games = await this.gameModel.find().exec();
|
||||
return await Promise.all(
|
||||
games.map(async (e) => {
|
||||
return await new ReadGameDto(e);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
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<ReadGameDto> {
|
||||
const game = await this.gameModel.findOne({ owner: _id }).exec();
|
||||
if (!game) {
|
||||
return null;
|
||||
}
|
||||
return new ReadGameDto(game);
|
||||
}
|
||||
|
||||
async update(id: string, updateGameDto: UpdateGameDto) {
|
||||
const game = await this.gameModel.findById(id).exec();
|
||||
if (!game) {
|
||||
return null;
|
||||
}
|
||||
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;
|
||||
}
|
||||
game.fireGems.cards = await this.cardModel.find({
|
||||
pack: { $in: game.packs },
|
||||
role: CardRole.FIRE_GEM,
|
||||
});
|
||||
|
||||
game.marketStack.cards = await this.cardModel.find({
|
||||
pack: { $in: game.packs },
|
||||
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;
|
||||
await game.save();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user