Feat : add tokens, handle mutex effects

This commit is contained in:
2024-07-07 10:45:44 +02:00
parent d8cf8560c0
commit 8b5c38a28c
12 changed files with 206 additions and 49 deletions
+23 -26
View File
@@ -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": [
"<node_internals>/**"
],
"type": "node",
"console": "internalConsole",
"outputCapture": "std",
"internalConsoleOptions": "openOnSessionStart",
"envFile": "${workspaceFolder}/.env"
}
]
}
// 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": [
"<node_internals>/**",
"${workspaceFolder}/node_modules/**/*.js",
"${workspaceFolder}/lib/**/*.js"
],
"type": "node",
"console": "internalConsole",
"outputCapture": "std",
"internalConsoleOptions": "openOnSessionStart",
"envFile": "${workspaceFolder}/.env"
}
]
}
+2 -1
View File
@@ -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 },
]),
+8 -2
View File
@@ -17,7 +17,10 @@ import { EffectCondition } from './schemas/effect.schema';
@Injectable()
export class CardsService {
constructor(@InjectModel(Card.name) private cardModel: Model<Card>) {}
constructor(
@InjectModel(Card.name) private cardModel: Model<Card>,
@InjectModel(Effect.name) private effectModel: Model<Effect>,
) {}
async loadCards(): Promise<void> {
const cardPath = path.join(path.resolve(), 'src', 'json');
@@ -88,6 +91,9 @@ export class CardsService {
): Promise<Effect[]> {
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);
}),
);
}
+6 -2
View File
@@ -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()
+7 -2
View File
@@ -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,
},
});
+37
View File
@@ -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> & Game,
@Param('effectId', EffectPipe) effect: Document<Effect> & Effect,
@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.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);
}
}
+15 -8
View File
@@ -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;
+22
View File
@@ -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<Effect>) {}
async transform(value: any) {
const effect = await this.effectmodel.findById(value);
if (!effect) {
throw new HttpException('Effect not found', HttpStatus.NOT_FOUND);
}
return effect;
}
}
+14 -2
View File
@@ -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<Game>;
@@ -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;
+54
View File
@@ -18,6 +18,22 @@ export function isAutoActivable(effect: Effect) {
return false;
}
async function addToken(
game: Document<Game> & 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> & 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,
(
+2 -1
View File
@@ -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;
+16 -5
View File
@@ -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> & 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,