From b21e0fe87a00094fc0993291b80338d77fdbda67 Mon Sep 17 00:00:00 2001 From: CapoMK25 Date: Wed, 11 Feb 2026 10:41:03 +0200 Subject: [PATCH 1/5] Implemented daily emotions --- src/common/enum/playerEmotion.enum.ts | 8 +++ src/player/dto/player.dto.ts | 9 +++ src/player/dto/updateEmotion.dto.ts | 9 +++ src/player/player.controller.ts | 97 +++++++++++---------------- src/player/player.service.ts | 39 +++++++++++ src/player/schemas/player.schema.ts | 18 +++++ 6 files changed, 121 insertions(+), 59 deletions(-) create mode 100644 src/common/enum/playerEmotion.enum.ts create mode 100644 src/player/dto/updateEmotion.dto.ts diff --git a/src/common/enum/playerEmotion.enum.ts b/src/common/enum/playerEmotion.enum.ts new file mode 100644 index 000000000..2d5102baa --- /dev/null +++ b/src/common/enum/playerEmotion.enum.ts @@ -0,0 +1,8 @@ +export enum PlayerEmotion { + SORROW = 'Sorrow', + ANGER = 'Anger', + JOY = 'Joy', + PLAYFUL = 'Playful', + LOVE = 'Love', + BLANK = 'Blank', +} \ No newline at end of file diff --git a/src/player/dto/player.dto.ts b/src/player/dto/player.dto.ts index c8c84c832..22cbe888b 100644 --- a/src/player/dto/player.dto.ts +++ b/src/player/dto/player.dto.ts @@ -7,6 +7,7 @@ import { GameStatisticsDto } from './gameStatistics.dto'; import { TaskDto } from './task.dto'; import { AvatarDto } from './avatar.dto'; import { Min } from 'class-validator'; +import { IPlayerEmotion } from '../schemas/player.schema'; @AddType('PlayerDto') export class PlayerDto { @@ -144,4 +145,12 @@ export class PlayerDto { @ExtractField() @Expose() clanRole_id?: string; + + /** + * A historical list of emotions recorded by the player on a daily basis. + * Each entry contains the emotion type and the timestamp of the recording. + * @type {IPlayerEmotion[]} + */ + @Expose() + emotions?: IPlayerEmotion[]; } diff --git a/src/player/dto/updateEmotion.dto.ts b/src/player/dto/updateEmotion.dto.ts new file mode 100644 index 000000000..88be8614b --- /dev/null +++ b/src/player/dto/updateEmotion.dto.ts @@ -0,0 +1,9 @@ +import { IsEnum } from 'class-validator'; +import { PlayerEmotion } from '../../common/enum/playerEmotion.enum'; + +export class UpdateEmotionDto { + @IsEnum(PlayerEmotion, { + message: 'Emotion must be one of these: Sorrow, Anger, Joy, Playful, Love, Blank', + }) + emotion: PlayerEmotion; +} \ No newline at end of file diff --git a/src/player/player.controller.ts b/src/player/player.controller.ts index 2e667d6a6..5fb37ef66 100644 --- a/src/player/player.controller.ts +++ b/src/player/player.controller.ts @@ -7,10 +7,13 @@ import { Param, Post, Put, + Req } from '@nestjs/common'; +import { Request } from 'express'; import { PlayerService } from './player.service'; import { CreatePlayerDto } from './dto/createPlayer.dto'; import { UpdatePlayerDto } from './dto/updatePlayer.dto'; +import { UpdateEmotionDto } from './dto/updateEmotion.dto'; import { PlayerDto } from './dto/player.dto'; import { _idDto } from '../common/dto/_id.dto'; import { BasicDELETE } from '../common/base/decorator/BasicDELETE.decorator'; @@ -41,19 +44,9 @@ export default class PlayerController { /** * Create a player - * - * @remarks Create a new Player. This is not recommended way of creating a new Player and it should be used only in edge cases. - * The recommended way is to create it via /profile POST endpoint. - * - * Player is representing an object, which holds data related to game player. This object can be used inside the game for example while joining a Clan. - * Notice, that the Profile object should not be used inside the game (except for logging-in). */ @ApiResponseDescription({ - success: { - dto: PlayerDto, - modelName: ModelName.PLAYER, - status: 201, - }, + success: { dto: PlayerDto, modelName: ModelName.PLAYER, status: 201 }, errors: [400, 401, 403, 409], hasAuth: false, }) @@ -64,16 +57,42 @@ export default class PlayerController { return this.service.createOne(body); } + /** + * Checks if the authenticated player has already submitted an emotion for the current day. + * This is used by the game client on startup to determine if the emotion selection + * popup should be displayed or not. + * @param req - The request object containing the authenticated user's data. + * @returns An object containing `sentToday` (boolean). + */ + @Get('/emotioncheck') + async checkDailyEmotion(@Req() req) { + const sentToday = await this.service.checkIfEmotionSentToday(req.user.player_id); + return { sentToday }; + } + + /** + * Registers the player's selected emotion for the current day. + * This will append the emotion to the player's history. + * @param req - The request object containing the authenticated user's data. + * @param body - The DTO containing the selected PlayerEmotion enum value. + * @returns The updated player document including the new emotion entry. + * @throws BadRequestException - If the player attempts to submit more than once per day. + */ + @Post('/emotion') + @UniformResponse(ModelName.PLAYER, PlayerDto) + async setDailyEmotion(@Req() req, @Body() body: UpdateEmotionDto) { + + return this.service.addEmotion(req.user.player_id, body.emotion); + } + + /** * Get player by _id * * @remarks Read Player data by its _id field */ @ApiResponseDescription({ - success: { - dto: PlayerDto, - modelName: ModelName.PLAYER, - }, + success: { dto: PlayerDto, modelName: ModelName.PLAYER }, errors: [400, 401, 404], }) @Get('/:_id') @@ -88,14 +107,9 @@ export default class PlayerController { /** * Get all players - * - * @remarks Read all created Players. Remember about the pagination */ @ApiResponseDescription({ - success: { - dto: PlayerDto, - modelName: ModelName.PLAYER, - }, + success: { dto: PlayerDto, modelName: ModelName.PLAYER }, errors: [401, 404], }) @Get() @@ -109,50 +123,23 @@ export default class PlayerController { /** * Update player - * Emit a server event if avatar clothes changed - * - * @remarks Update the Player, which _id is specified in the body. Only Player, which belong to the logged-in Profile can be changed. */ - @ApiResponseDescription({ - success: { - status: 204, - }, - errors: [401, 403, 404, 409], - }) + @ApiResponseDescription({ success: { status: 204 }, errors: [401, 403, 404, 409] }) @Put() @HttpCode(204) @Authorize({ action: Action.update, subject: UpdatePlayerDto }) @BasicPUT(ModelName.PLAYER) public async update(@Body() body: UpdatePlayerDto) { const [player, _] = await this.service.getPlayerById(body._id); - const playerUpdateResults = await this.service.updateOneById(body); - await this.emitEventIfAvatarChange(player, body); - return playerUpdateResults; } /** * Delete player by _id - * - * @remarks Delete Player by its _id field. Notice that only Player, which belongs to loggen-in user Profile can be deleted. - * In case when the Player is the only admin in some Clan and the Clan has some other Players, the Player can not be removed. - * User should be asked to first determine at least one admin for the Clan. - * - * Also, it is not recommended to delete the Player since it can itroduce unexpected behaviour for the user with Profile, - * but without Player. The better way to remove the Player is do it via /profile DELETE. - * - * Player removal basically means removing all data, which is related to the Player: - * CustomCharacters, Clan, except for the Profile data. - * In the case when the Profile does not have a Player, user can only login to the system, but can not play the game. */ - @ApiResponseDescription({ - success: { - status: 204, - }, - errors: [400, 401, 403, 404], - }) + @ApiResponseDescription({ success: { status: 204 }, errors: [400, 401, 403, 404] }) @Delete('/:_id') @Authorize({ action: Action.delete, subject: PlayerDto }) @BasicDELETE(ModelName.PLAYER) @@ -160,15 +147,7 @@ export default class PlayerController { return this.service.deleteOneById(param._id); } - /** - * Check if avatar changed and emit event - * @param player Current player data - * @param body UpdatePlayerDto with new data - */ - private async emitEventIfAvatarChange( - player: PlayerDto, - body: UpdatePlayerDto, - ) { + private async emitEventIfAvatarChange(player: PlayerDto, body: UpdatePlayerDto) { if (player?.avatar?.clothes !== body?.avatar?.clothes) { this.emitterService.EmitNewDailyTaskEvent( body._id, diff --git a/src/player/player.service.ts b/src/player/player.service.ts index 20cd0507f..52b0a6cd0 100644 --- a/src/player/player.service.ts +++ b/src/player/player.service.ts @@ -23,6 +23,7 @@ import { TReadByIdOptions, } from '../common/service/basicService/IService'; import EventEmitterService from '../common/service/EventEmitterService/EventEmitter.service'; +import { PlayerEmotion } from '../common/enum/playerEmotion.enum'; @Injectable() @AddBasicService() @@ -248,4 +249,42 @@ export class PlayerService ); if (updateErrors) throw updateErrors; } + + /** + * Checks if the player has already submitted an emotion today. + * @param playerId - The unique identifier of the player. + * @returns Boolean indicating if an entry for today exists. + */ + async checkIfEmotionSentToday(playerId: string): Promise { + const player = await this.model.findById(playerId).select('emotions').exec(); + if (!player || !player.emotions || player.emotions.length === 0) return false; + + const lastEntry = player.emotions[player.emotions.length - 1]; + + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const entryDate = new Date(lastEntry.date); + entryDate.setHours(0, 0, 0, 0); + + return today.getTime() === entryDate.getTime(); + } + + /** + * Adds a new emotion entry for the player if they haven't sent one today. + * @param playerId - The unique identifier of the player. + * @param emotion - The emotion enum value. + * @returns The updated player document or throws BadRequestException. + */ + async addEmotion(playerId: string, emotion: PlayerEmotion) { + const isSent = await this.checkIfEmotionSentToday(playerId); + + if (isSent) { + throw new BadRequestException('Emotion for today has already been registered.'); + } + + return this.updatePlayerById(playerId, { + $push: { emotions: { emotion, date: new Date() } } + }); + } } diff --git a/src/player/schemas/player.schema.ts b/src/player/schemas/player.schema.ts index 44339fd22..01f09f00b 100644 --- a/src/player/schemas/player.schema.ts +++ b/src/player/schemas/player.schema.ts @@ -5,9 +5,15 @@ import { ExtractField } from '../../common/decorator/response/ExtractField'; import { GameStatistics } from '../gameStatistics.schema'; import { ObjectId } from 'mongodb'; import { Avatar, AvatarSchema } from './avatar.schema'; +import { PlayerEmotion } from '../../common/enum/playerEmotion.enum'; export type PlayerDocument = HydratedDocument; +export interface IPlayerEmotion { + emotion: PlayerEmotion; + date: Date; +} + @Schema({ toJSON: { virtuals: true, getters: true }, toObject: { virtuals: true, getters: true }, @@ -64,6 +70,18 @@ export class Player { @Prop({ type: ObjectId, default: null }) clanRole_id: string | ObjectId | null; + @Prop({ + type: [ + { + emotion: { type: String, enum: Object.values(PlayerEmotion), required: true }, + date: { type: Date, default: Date.now }, + }, + ], + _id: false, + default: [], + }) + emotions?: { emotion: PlayerEmotion; date: Date }[]; + @ExtractField() _id: string; } From a489f4047f888d61faa85db730328991ef2c6d1a Mon Sep 17 00:00:00 2001 From: CapoMK25 Date: Wed, 11 Feb 2026 11:00:19 +0200 Subject: [PATCH 2/5] linter changes --- src/chat/service/baseChat.service.ts | 2 +- src/common/enum/playerEmotion.enum.ts | 2 +- src/player/dto/updateEmotion.dto.ts | 5 +++-- src/player/player.controller.ts | 27 ++++++++++++++++++--------- src/player/player.service.ts | 18 ++++++++++++------ src/player/schemas/player.schema.ts | 20 ++++++++++++-------- 6 files changed, 47 insertions(+), 27 deletions(-) diff --git a/src/chat/service/baseChat.service.ts b/src/chat/service/baseChat.service.ts index 4421ef490..4d92ac57d 100644 --- a/src/chat/service/baseChat.service.ts +++ b/src/chat/service/baseChat.service.ts @@ -126,7 +126,7 @@ export abstract class BaseChatService { client.user.name, reaction.emoji, client.user.playerId, - options + options, ); if (error) { diff --git a/src/common/enum/playerEmotion.enum.ts b/src/common/enum/playerEmotion.enum.ts index 2d5102baa..7de742144 100644 --- a/src/common/enum/playerEmotion.enum.ts +++ b/src/common/enum/playerEmotion.enum.ts @@ -5,4 +5,4 @@ export enum PlayerEmotion { PLAYFUL = 'Playful', LOVE = 'Love', BLANK = 'Blank', -} \ No newline at end of file +} diff --git a/src/player/dto/updateEmotion.dto.ts b/src/player/dto/updateEmotion.dto.ts index 88be8614b..263eb3ba5 100644 --- a/src/player/dto/updateEmotion.dto.ts +++ b/src/player/dto/updateEmotion.dto.ts @@ -3,7 +3,8 @@ import { PlayerEmotion } from '../../common/enum/playerEmotion.enum'; export class UpdateEmotionDto { @IsEnum(PlayerEmotion, { - message: 'Emotion must be one of these: Sorrow, Anger, Joy, Playful, Love, Blank', + message: + 'Emotion must be one of these: Sorrow, Anger, Joy, Playful, Love, Blank', }) emotion: PlayerEmotion; -} \ No newline at end of file +} diff --git a/src/player/player.controller.ts b/src/player/player.controller.ts index 5fb37ef66..16bebd583 100644 --- a/src/player/player.controller.ts +++ b/src/player/player.controller.ts @@ -7,7 +7,7 @@ import { Param, Post, Put, - Req + Req, } from '@nestjs/common'; import { Request } from 'express'; import { PlayerService } from './player.service'; @@ -59,20 +59,22 @@ export default class PlayerController { /** * Checks if the authenticated player has already submitted an emotion for the current day. - * This is used by the game client on startup to determine if the emotion selection + * This is used by the game client on startup to determine if the emotion selection * popup should be displayed or not. * @param req - The request object containing the authenticated user's data. * @returns An object containing `sentToday` (boolean). */ @Get('/emotioncheck') async checkDailyEmotion(@Req() req) { - const sentToday = await this.service.checkIfEmotionSentToday(req.user.player_id); + const sentToday = await this.service.checkIfEmotionSentToday( + req.user.player_id, + ); return { sentToday }; } /** * Registers the player's selected emotion for the current day. - * This will append the emotion to the player's history. + * This will append the emotion to the player's history. * @param req - The request object containing the authenticated user's data. * @param body - The DTO containing the selected PlayerEmotion enum value. * @returns The updated player document including the new emotion entry. @@ -81,11 +83,9 @@ export default class PlayerController { @Post('/emotion') @UniformResponse(ModelName.PLAYER, PlayerDto) async setDailyEmotion(@Req() req, @Body() body: UpdateEmotionDto) { - return this.service.addEmotion(req.user.player_id, body.emotion); } - /** * Get player by _id * @@ -124,7 +124,10 @@ export default class PlayerController { /** * Update player */ - @ApiResponseDescription({ success: { status: 204 }, errors: [401, 403, 404, 409] }) + @ApiResponseDescription({ + success: { status: 204 }, + errors: [401, 403, 404, 409], + }) @Put() @HttpCode(204) @Authorize({ action: Action.update, subject: UpdatePlayerDto }) @@ -139,7 +142,10 @@ export default class PlayerController { /** * Delete player by _id */ - @ApiResponseDescription({ success: { status: 204 }, errors: [400, 401, 403, 404] }) + @ApiResponseDescription({ + success: { status: 204 }, + errors: [400, 401, 403, 404], + }) @Delete('/:_id') @Authorize({ action: Action.delete, subject: PlayerDto }) @BasicDELETE(ModelName.PLAYER) @@ -147,7 +153,10 @@ export default class PlayerController { return this.service.deleteOneById(param._id); } - private async emitEventIfAvatarChange(player: PlayerDto, body: UpdatePlayerDto) { + private async emitEventIfAvatarChange( + player: PlayerDto, + body: UpdatePlayerDto, + ) { if (player?.avatar?.clothes !== body?.avatar?.clothes) { this.emitterService.EmitNewDailyTaskEvent( body._id, diff --git a/src/player/player.service.ts b/src/player/player.service.ts index 52b0a6cd0..6e2b0803e 100644 --- a/src/player/player.service.ts +++ b/src/player/player.service.ts @@ -256,11 +256,15 @@ export class PlayerService * @returns Boolean indicating if an entry for today exists. */ async checkIfEmotionSentToday(playerId: string): Promise { - const player = await this.model.findById(playerId).select('emotions').exec(); - if (!player || !player.emotions || player.emotions.length === 0) return false; + const player = await this.model + .findById(playerId) + .select('emotions') + .exec(); + if (!player || !player.emotions || player.emotions.length === 0) + return false; const lastEntry = player.emotions[player.emotions.length - 1]; - + const today = new Date(); today.setHours(0, 0, 0, 0); @@ -278,13 +282,15 @@ export class PlayerService */ async addEmotion(playerId: string, emotion: PlayerEmotion) { const isSent = await this.checkIfEmotionSentToday(playerId); - + if (isSent) { - throw new BadRequestException('Emotion for today has already been registered.'); + throw new BadRequestException( + 'Emotion for today has already been registered.', + ); } return this.updatePlayerById(playerId, { - $push: { emotions: { emotion, date: new Date() } } + $push: { emotions: { emotion, date: new Date() } }, }); } } diff --git a/src/player/schemas/player.schema.ts b/src/player/schemas/player.schema.ts index 01f09f00b..a3a83ca6f 100644 --- a/src/player/schemas/player.schema.ts +++ b/src/player/schemas/player.schema.ts @@ -71,14 +71,18 @@ export class Player { clanRole_id: string | ObjectId | null; @Prop({ - type: [ - { - emotion: { type: String, enum: Object.values(PlayerEmotion), required: true }, - date: { type: Date, default: Date.now }, - }, - ], - _id: false, - default: [], + type: [ + { + emotion: { + type: String, + enum: Object.values(PlayerEmotion), + required: true, + }, + date: { type: Date, default: Date.now }, + }, + ], + _id: false, + default: [], }) emotions?: { emotion: PlayerEmotion; date: Date }[]; From 033134d743d2e3437563a1fcd7637f2d409a5905 Mon Sep 17 00:00:00 2001 From: CapoMK25 Date: Wed, 11 Feb 2026 11:07:22 +0200 Subject: [PATCH 3/5] Re-added JSDocs that disappeared on the initial commit --- src/player/player.controller.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/player/player.controller.ts b/src/player/player.controller.ts index 16bebd583..a96ee031e 100644 --- a/src/player/player.controller.ts +++ b/src/player/player.controller.ts @@ -44,6 +44,12 @@ export default class PlayerController { /** * Create a player + * + * @remarks Create a new Player. This is not recommended way of creating a new Player and it should be used only in edge cases. + * The recommended way is to create it via /profile POST endpoint. + * + * Player is representing an object, which holds data related to game player. This object can be used inside the game for example while joining a Clan. + * Notice, that the Profile object should not be used inside the game (except for logging-in). */ @ApiResponseDescription({ success: { dto: PlayerDto, modelName: ModelName.PLAYER, status: 201 }, @@ -107,6 +113,8 @@ export default class PlayerController { /** * Get all players + * + * @remarks Read all created Players. Remember about the pagination. */ @ApiResponseDescription({ success: { dto: PlayerDto, modelName: ModelName.PLAYER }, @@ -123,6 +131,9 @@ export default class PlayerController { /** * Update player + * * Emit a server event if avatar clothes changed + * @remarks Update the Player, which _id is specified in the body. + * Only Player, which belong to the logged-in Profile can be changed. */ @ApiResponseDescription({ success: { status: 204 }, @@ -141,6 +152,16 @@ export default class PlayerController { /** * Delete player by _id + * @remarks Delete Player by its _id field. Notice that only Player, which belongs to a logged-in user Profile can be deleted. + * In case when the Player is the only admin in some Clan and the Clan has some other Players, the Player can not be removed. + * User should be asked to first determine at least one admin for the Clan. + * + * Also, it is not recommended to delete the Player since it can introduce unexpected behaviour for the user with Profile, + * but without Player. The better way to remove the Player is do it via /profile DELETE. + * + * Player removal basically means removing all data, which is related to the Player: + * CustomCharacters, Clan, except for the Profile data. + * In the case when the Profile does not have a Player, user can only login to the system, but can not play the game. */ @ApiResponseDescription({ success: { status: 204 }, @@ -153,6 +174,11 @@ export default class PlayerController { return this.service.deleteOneById(param._id); } + /** + * Check if avatar changed and emit event + * @param player Current player data + * @param body UpdatePlayerDto with new data + */ private async emitEventIfAvatarChange( player: PlayerDto, body: UpdatePlayerDto, From ee87674df8f207408cdd3f8ebba4ca7831583e30 Mon Sep 17 00:00:00 2001 From: CapoMK25 Date: Tue, 10 Mar 2026 09:21:19 +0200 Subject: [PATCH 4/5] revamped the emotions according to requirements --- src/player/dto/emotion.dto.ts | 9 +++ src/player/dto/emotionCheck.dto.ts | 11 +++ src/player/dto/player.dto.ts | 7 +- src/player/dto/updateEmotion.dto.ts | 5 +- .../enum/playerEmotion.enum.ts | 0 src/player/player.controller.ts | 54 ++++++++----- src/player/player.service.ts | 79 +++++++++++++------ src/player/schemas/player.schema.ts | 10 +-- 8 files changed, 118 insertions(+), 57 deletions(-) create mode 100644 src/player/dto/emotion.dto.ts create mode 100644 src/player/dto/emotionCheck.dto.ts rename src/{common => player}/enum/playerEmotion.enum.ts (100%) diff --git a/src/player/dto/emotion.dto.ts b/src/player/dto/emotion.dto.ts new file mode 100644 index 000000000..c21d28267 --- /dev/null +++ b/src/player/dto/emotion.dto.ts @@ -0,0 +1,9 @@ +import { Expose } from 'class-transformer'; + +export class EmotionDto { + @Expose() + emotion: string; + + @Expose() + date: Date; +} \ No newline at end of file diff --git a/src/player/dto/emotionCheck.dto.ts b/src/player/dto/emotionCheck.dto.ts new file mode 100644 index 000000000..4d8d5ca96 --- /dev/null +++ b/src/player/dto/emotionCheck.dto.ts @@ -0,0 +1,11 @@ +import { PlayerEmotion } from '../enum/playerEmotion.enum'; + +export class EmotionCheckDto { + constructor(isSent: boolean, currentEmotion: PlayerEmotion) { + this.isSent = isSent; + this.currentEmotion = currentEmotion; + } + + isSent: boolean; + currentEmotion: PlayerEmotion; +} \ No newline at end of file diff --git a/src/player/dto/player.dto.ts b/src/player/dto/player.dto.ts index 22cbe888b..976d22bef 100644 --- a/src/player/dto/player.dto.ts +++ b/src/player/dto/player.dto.ts @@ -7,7 +7,7 @@ import { GameStatisticsDto } from './gameStatistics.dto'; import { TaskDto } from './task.dto'; import { AvatarDto } from './avatar.dto'; import { Min } from 'class-validator'; -import { IPlayerEmotion } from '../schemas/player.schema'; +import { EmotionDto } from './emotion.dto'; @AddType('PlayerDto') export class PlayerDto { @@ -149,8 +149,9 @@ export class PlayerDto { /** * A historical list of emotions recorded by the player on a daily basis. * Each entry contains the emotion type and the timestamp of the recording. - * @type {IPlayerEmotion[]} + * @type {[EmotionDto]} */ + @Type(() => EmotionDto) @Expose() - emotions?: IPlayerEmotion[]; + emotions?: EmotionDto[]; } diff --git a/src/player/dto/updateEmotion.dto.ts b/src/player/dto/updateEmotion.dto.ts index 263eb3ba5..26670ed1b 100644 --- a/src/player/dto/updateEmotion.dto.ts +++ b/src/player/dto/updateEmotion.dto.ts @@ -1,10 +1,11 @@ -import { IsEnum } from 'class-validator'; -import { PlayerEmotion } from '../../common/enum/playerEmotion.enum'; +import { IsEnum, IsNotEmpty } from 'class-validator'; +import { PlayerEmotion } from '../enum/playerEmotion.enum'; export class UpdateEmotionDto { @IsEnum(PlayerEmotion, { message: 'Emotion must be one of these: Sorrow, Anger, Joy, Playful, Love, Blank', }) + @IsNotEmpty() emotion: PlayerEmotion; } diff --git a/src/common/enum/playerEmotion.enum.ts b/src/player/enum/playerEmotion.enum.ts similarity index 100% rename from src/common/enum/playerEmotion.enum.ts rename to src/player/enum/playerEmotion.enum.ts diff --git a/src/player/player.controller.ts b/src/player/player.controller.ts index a96ee031e..9bb6889e1 100644 --- a/src/player/player.controller.ts +++ b/src/player/player.controller.ts @@ -7,9 +7,8 @@ import { Param, Post, Put, - Req, + BadRequestException } from '@nestjs/common'; -import { Request } from 'express'; import { PlayerService } from './player.service'; import { CreatePlayerDto } from './dto/createPlayer.dto'; import { UpdatePlayerDto } from './dto/updatePlayer.dto'; @@ -22,6 +21,8 @@ import { ModelName } from '../common/enum/modelName.enum'; import { NoAuth } from '../auth/decorator/NoAuth.decorator'; import { Authorize } from '../authorization/decorator/Authorize'; import { Action } from '../authorization/enum/action.enum'; +import { LoggedUser } from '../common/decorator/param/LoggedUser.decorator'; +import { User } from '../auth/user'; import { OffsetPaginate } from '../common/interceptor/request/offsetPagination.interceptor'; import { AddSearchQuery } from '../common/interceptor/request/addSearchQuery.interceptor'; import { GetAllQuery } from '../common/decorator/param/GetAllQuery'; @@ -34,6 +35,8 @@ import ApiResponseDescription from '../common/swagger/response/ApiResponseDescri import EventEmitterService from '../common/service/EventEmitterService/EventEmitter.service'; import { ServerTaskName } from '../dailyTasks/enum/serverTaskName.enum'; import { isEqual } from 'lodash'; +import { IServiceReturn } from '../common/service/basicService/IService'; +import { EmotionCheckDto } from './dto/emotionCheck.dto'; @Controller('player') export default class PlayerController { @@ -64,32 +67,43 @@ export default class PlayerController { } /** + * Player emotion check * Checks if the authenticated player has already submitted an emotion for the current day. - * This is used by the game client on startup to determine if the emotion selection - * popup should be displayed or not. - * @param req - The request object containing the authenticated user's data. - * @returns An object containing `sentToday` (boolean). */ + @ApiResponseDescription({ + success: { status: 200 }, + errors: [401, 403, 404], + }) @Get('/emotioncheck') - async checkDailyEmotion(@Req() req) { - const sentToday = await this.service.checkIfEmotionSentToday( - req.user.player_id, - ); - return { sentToday }; + @Authorize({ action: Action.read, subject: PlayerDto }) + public async checkDailyEmotion( + @LoggedUser() user: User, + ): Promise> { + + return await this.service.checkIfEmotionSentToday(user.player_id); } /** - * Registers the player's selected emotion for the current day. - * This will append the emotion to the player's history. - * @param req - The request object containing the authenticated user's data. - * @param body - The DTO containing the selected PlayerEmotion enum value. - * @returns The updated player document including the new emotion entry. - * @throws BadRequestException - If the player attempts to submit more than once per day. - */ + * Registers the player's selected emotion for the current day. + */ + @ApiResponseDescription({ + success: { dto: PlayerDto, modelName: ModelName.PLAYER, status: 201 }, + errors: [400, 401, 403, 409], + }) @Post('/emotion') @UniformResponse(ModelName.PLAYER, PlayerDto) - async setDailyEmotion(@Req() req, @Body() body: UpdateEmotionDto) { - return this.service.addEmotion(req.user.player_id, body.emotion); + @Authorize({ action: Action.create, subject: PlayerDto }) + public async setDailyEmotion( + @LoggedUser() user: User, + @Body() body: UpdateEmotionDto, + ): Promise { + const [player, error] = await this.service.addEmotion(user.player_id, body.emotion); + + if (error) { + throw new BadRequestException(error[0].message); + } + + return player; } /** diff --git a/src/player/player.service.ts b/src/player/player.service.ts index 6e2b0803e..f675b0c6a 100644 --- a/src/player/player.service.ts +++ b/src/player/player.service.ts @@ -17,13 +17,17 @@ import { } from '../common/interface/IHookImplementer'; import { UpdatePlayerDto } from './dto/updatePlayer.dto'; import { PlayerDto } from './dto/player.dto'; +import { EmotionCheckDto } from './dto/emotionCheck.dto'; import BasicService from '../common/service/basicService/BasicService'; +import ServiceError from '../common/service/basicService/ServiceError'; +import { SEReason } from '../common/service/basicService/SEReason'; import { TIServiceReadManyOptions, TReadByIdOptions, + IServiceReturn } from '../common/service/basicService/IService'; import EventEmitterService from '../common/service/EventEmitterService/EventEmitter.service'; -import { PlayerEmotion } from '../common/enum/playerEmotion.enum'; +import { PlayerEmotion } from './enum/playerEmotion.enum'; @Injectable() @AddBasicService() @@ -253,44 +257,69 @@ export class PlayerService /** * Checks if the player has already submitted an emotion today. * @param playerId - The unique identifier of the player. - * @returns Boolean indicating if an entry for today exists. + * @returns - A classic tuple setup [boolean, ServiceError[]] indicating if an entry for today exists. */ - async checkIfEmotionSentToday(playerId: string): Promise { + async checkIfEmotionSentToday(playerId: string): Promise> { const player = await this.model .findById(playerId) .select('emotions') .exec(); - if (!player || !player.emotions || player.emotions.length === 0) - return false; + + if (!player) return [null, [new ServiceError({ reason: SEReason.NOT_FOUND })]]; const lastEntry = player.emotions[player.emotions.length - 1]; + + const today = new Date().setHours(0, 0, 0, 0); + const entryDate = lastEntry ? new Date(lastEntry.date).setHours(0, 0, 0, 0) : null; - const today = new Date(); - today.setHours(0, 0, 0, 0); + if (entryDate !== today) { + return [new EmotionCheckDto(false, PlayerEmotion.BLANK), null]; + } - const entryDate = new Date(lastEntry.date); - entryDate.setHours(0, 0, 0, 0); + const emotionValue = lastEntry.emotion as PlayerEmotion; - return today.getTime() === entryDate.getTime(); + const isSent = emotionValue !== PlayerEmotion.BLANK; + + return [new EmotionCheckDto(isSent, emotionValue), null]; } /** - * Adds a new emotion entry for the player if they haven't sent one today. - * @param playerId - The unique identifier of the player. - * @param emotion - The emotion enum value. - * @returns The updated player document or throws BadRequestException. + * Registers or updates the player's selected emotion for the current day. + * Uses atomic operators via basicService to ensure data integrity and DTO consistency. + * * @param playerId - The unique identifier of the player. + * @param emotion - The selected emotion enum value. + * @returns The updated player data or service errors. */ - async addEmotion(playerId: string, emotion: PlayerEmotion) { - const isSent = await this.checkIfEmotionSentToday(playerId); + async addEmotion(playerId: string, emotion: PlayerEmotion): Promise> { - if (isSent) { - throw new BadRequestException( - 'Emotion for today has already been registered.', - ); - } + const [player, errors] = await this.getPlayerById(playerId); + if (errors) return [null, errors]; - return this.updatePlayerById(playerId, { - $push: { emotions: { emotion, date: new Date() } }, + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const index = (player.emotions || []).findIndex((e) => { + const entryDate = new Date(e.date); + entryDate.setHours(0, 0, 0, 0); + return entryDate.getTime() === today.getTime(); }); - } -} + + let updateQuery: UpdateQuery; + if (index > -1) { + updateQuery = { + $set: { + [`emotions.${index}.emotion`]: emotion, + [`emotions.${index}.date`]: new Date(), + }, + }; + } else { + updateQuery = { + $push: { emotions: { emotion, date: new Date() } }, + }; + } + + const [_, updateErrors] = await this.basicService.updateOneById(playerId, updateQuery); + if (updateErrors) return [null, updateErrors]; + + return this.getPlayerById(playerId); + }} diff --git a/src/player/schemas/player.schema.ts b/src/player/schemas/player.schema.ts index a3a83ca6f..13e70f11f 100644 --- a/src/player/schemas/player.schema.ts +++ b/src/player/schemas/player.schema.ts @@ -5,15 +5,11 @@ import { ExtractField } from '../../common/decorator/response/ExtractField'; import { GameStatistics } from '../gameStatistics.schema'; import { ObjectId } from 'mongodb'; import { Avatar, AvatarSchema } from './avatar.schema'; -import { PlayerEmotion } from '../../common/enum/playerEmotion.enum'; +import { PlayerEmotion } from '../enum/playerEmotion.enum'; +import { EmotionDto } from '../dto/emotion.dto'; export type PlayerDocument = HydratedDocument; -export interface IPlayerEmotion { - emotion: PlayerEmotion; - date: Date; -} - @Schema({ toJSON: { virtuals: true, getters: true }, toObject: { virtuals: true, getters: true }, @@ -84,7 +80,7 @@ export class Player { _id: false, default: [], }) - emotions?: { emotion: PlayerEmotion; date: Date }[]; + emotions?: EmotionDto[]; @ExtractField() _id: string; From 02e48ccc85e5bca8bcd315f7e888bd52e7164c82 Mon Sep 17 00:00:00 2001 From: CapoMK25 Date: Sat, 4 Apr 2026 20:12:26 +0300 Subject: [PATCH 5/5] addressed good suggetions --- src/player/player.controller.ts | 17 +++++++-------- src/player/player.service.ts | 37 +++++++++++++++++---------------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/player/player.controller.ts b/src/player/player.controller.ts index c4e844552..1d695e00d 100644 --- a/src/player/player.controller.ts +++ b/src/player/player.controller.ts @@ -78,17 +78,17 @@ export default class PlayerController { @Authorize({ action: Action.read, subject: PlayerDto }) public async checkDailyEmotion( @LoggedUser() user: User, - ): Promise> { + ): Promise> { return await this.service.checkIfEmotionSentToday(user.player_id); } /** - * Registers the player's selected emotion for the current day. - */ + * Registers the player's selected emotion for the current day. + */ @ApiResponseDescription({ - success: { dto: PlayerDto, modelName: ModelName.PLAYER, status: 201 }, - errors: [400, 401, 403, 409], + success: { dto: null, modelName: ModelName.PLAYER, status: 204 }, + errors: [400, 401, 403, 409], }) @Post('/emotion') @UniformResponse(ModelName.PLAYER, PlayerDto) @@ -96,15 +96,14 @@ export default class PlayerController { public async setDailyEmotion( @LoggedUser() user: User, @Body() body: UpdateEmotionDto, - ): Promise { - const [player, error] = await this.service.addEmotion(user.player_id, body.emotion); + ): Promise { + const [error] = await this.service.addEmotion(user.player_id, body.emotion); if (error) { throw new BadRequestException(error[0].message); } - return player; - } + } /** * Get player by _id diff --git a/src/player/player.service.ts b/src/player/player.service.ts index f675b0c6a..63c7d02f3 100644 --- a/src/player/player.service.ts +++ b/src/player/player.service.ts @@ -255,32 +255,33 @@ export class PlayerService } /** - * Checks if the player has already submitted an emotion today. - * @param playerId - The unique identifier of the player. - * @returns - A classic tuple setup [boolean, ServiceError[]] indicating if an entry for today exists. - */ - async checkIfEmotionSentToday(playerId: string): Promise> { - const player = await this.model - .findById(playerId) - .select('emotions') - .exec(); - - if (!player) return [null, [new ServiceError({ reason: SEReason.NOT_FOUND })]]; - - const lastEntry = player.emotions[player.emotions.length - 1]; - - const today = new Date().setHours(0, 0, 0, 0); - const entryDate = lastEntry ? new Date(lastEntry.date).setHours(0, 0, 0, 0) : null; + * Checks if the player has already submitted an emotion today. + * @param playerId - The unique identifier of the player. + * @returns - A classic tuple setup [boolean, ServiceError[]] indicating if an entry for today exists. + */ + async checkIfEmotionSentToday(playerId: string): Promise> { + const player = await this.model + .findById(playerId) + .select('emotions') + .exec(); + + if (!player) return [null, [new ServiceError({ reason: SEReason.NOT_FOUND })]]; + + const lastEntry = player.emotions[player.emotions.length - 1]; + + const today = new Date().setHours(0, 0, 0, 0); + const entryDate = lastEntry ? new Date(lastEntry.date).setHours(0, 0, 0, 0) : null; if (entryDate !== today) { - return [new EmotionCheckDto(false, PlayerEmotion.BLANK), null]; + + return [false, null]; } const emotionValue = lastEntry.emotion as PlayerEmotion; const isSent = emotionValue !== PlayerEmotion.BLANK; - return [new EmotionCheckDto(isSent, emotionValue), null]; + return [isSent, null]; } /**