Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/player/dto/emotion.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Expose } from 'class-transformer';

export class EmotionDto {
@Expose()
emotion: string;

@Expose()
date: Date;
}
11 changes: 11 additions & 0 deletions src/player/dto/emotionCheck.dto.ts
Original file line number Diff line number Diff line change
@@ -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;
}
10 changes: 10 additions & 0 deletions src/player/dto/player.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { EmotionDto } from './emotion.dto';

@AddType('PlayerDto')
export class PlayerDto {
Expand Down Expand Up @@ -144,4 +145,13 @@ 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 {[EmotionDto]}
*/
@Type(() => EmotionDto)
@Expose()
emotions?: EmotionDto[];
}
11 changes: 11 additions & 0 deletions src/player/dto/updateEmotion.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
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;
}
8 changes: 8 additions & 0 deletions src/player/enum/playerEmotion.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export enum PlayerEmotion {
SORROW = 'Sorrow',
ANGER = 'Anger',
JOY = 'Joy',
PLAYFUL = 'Playful',
LOVE = 'Love',
BLANK = 'Blank',
}
89 changes: 59 additions & 30 deletions src/player/player.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,21 @@ import {
Param,
Post,
Put,
BadRequestException
} from '@nestjs/common';
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';
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';
Expand All @@ -30,6 +34,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';
import { MongooseError } from 'mongoose';

@Controller('player')
Expand All @@ -41,19 +47,15 @@ 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,
})
Expand All @@ -64,16 +66,52 @@ export default class PlayerController {
return this.service.createOne(body);
}

/**
* Player emotion check
* Checks if the authenticated player has already submitted an emotion for the current day.
*/
@ApiResponseDescription({
success: { status: 200 },
errors: [401, 403, 404],
})
@Get('/emotioncheck')
@Authorize({ action: Action.read, subject: PlayerDto })
public async checkDailyEmotion(
@LoggedUser() user: User,
): Promise<IServiceReturn<boolean>> {

return await this.service.checkIfEmotionSentToday(user.player_id);
}

/**
* Registers the player's selected emotion for the current day.
*/
@ApiResponseDescription({
success: { dto: null, modelName: ModelName.PLAYER, status: 204 },
errors: [400, 401, 403, 409],
})
@Post('/emotion')
@UniformResponse(ModelName.PLAYER, PlayerDto)
@Authorize({ action: Action.create, subject: PlayerDto })
public async setDailyEmotion(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This controller function should return nothing, since you return 201 status_code (means No Content).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in my latest commit.

@LoggedUser() user: User,
@Body() body: UpdateEmotionDto,
): Promise<void> {
const [error] = await this.service.addEmotion(user.player_id, body.emotion);

if (error) {
throw new BadRequestException(error[0].message);
}

}

/**
* 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')
Expand All @@ -88,14 +126,11 @@ export default class PlayerController {

/**
* Get all players
*
* @remarks Read all created Players. Remember about the pagination
*
* @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()
Expand All @@ -109,14 +144,12 @@ 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.
* * 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,
},
success: { status: 204 },
errors: [401, 403, 404, 409],
})
@Put()
Expand All @@ -125,33 +158,29 @@ export default class PlayerController {
@UniformResponse()
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;

if (playerUpdateResults instanceof MongooseError)
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.
* @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 itroduce unexpected behaviour for the user with Profile,
* 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,
},
success: { status: 204 },
errors: [400, 401, 403, 404],
})
@Delete('/:_id')
Expand Down
77 changes: 76 additions & 1 deletion src/player/player.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +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 './enum/playerEmotion.enum';

@Injectable()
@AddBasicService()
Expand Down Expand Up @@ -248,4 +253,74 @@ 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 - A classic tuple setup [boolean, ServiceError[]] indicating if an entry for today exists.
*/
async checkIfEmotionSentToday(playerId: string): Promise<IServiceReturn<boolean>> {
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 [false, null];
}

const emotionValue = lastEntry.emotion as PlayerEmotion;

const isSent = emotionValue !== PlayerEmotion.BLANK;

return [isSent, null];
}

/**
* 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): Promise<IServiceReturn<PlayerDto>> {

const [player, errors] = await this.getPlayerById(playerId);
if (errors) return [null, errors];

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<Player>;
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);
}}
18 changes: 18 additions & 0 deletions src/player/schemas/player.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ 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 '../enum/playerEmotion.enum';
import { EmotionDto } from '../dto/emotion.dto';

export type PlayerDocument = HydratedDocument<Player>;

Expand Down Expand Up @@ -64,6 +66,22 @@ 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?: EmotionDto[];

@ExtractField()
_id: string;
}
Expand Down