diff --git a/src/app.module.ts b/src/app.module.ts index 51c8a7e..2be7056 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -16,12 +16,13 @@ import {Transport} from '@nestjs/microservices'; import {GameModule} from './game/game.module'; import {MemberModule} from './member/member.module'; import {EmpireModule} from './empire/empire.module'; -import { PresetsModule } from './presets/presets.module'; +import {PresetsModule} from './presets/presets.module'; import {SystemModule} from "./system/system.module"; -import { GameLogicModule } from './game-logic/game-logic.module'; +import {GameLogicModule} from './game-logic/game-logic.module'; import {IncomingMessage} from 'http'; import {AuthService} from './auth/auth.service'; -import {FriendsModule} from "./friend/friend.module"; +import {FriendModule} from "./friend/friend.module"; +import {JobModule} from "./job/job.module"; @Module({ imports: [ @@ -53,11 +54,12 @@ import {FriendsModule} from "./friend/friend.module"; }), AuthModule, UserModule, - FriendsModule, + FriendModule, GameModule, MemberModule, SystemModule, EmpireModule, + JobModule, AchievementSummaryModule, AchievementModule, PresetsModule, diff --git a/src/friend/friend.controller.ts b/src/friend/friend.controller.ts index 4f6b7ea..81b71c1 100644 --- a/src/friend/friend.controller.ts +++ b/src/friend/friend.controller.ts @@ -22,20 +22,20 @@ import {NotFound, ObjectIdPipe} from '@mean-stream/nestx'; import {Types} from 'mongoose'; import {Validated} from '../util/validated.decorator'; import {Throttled} from '../util/throttled.decorator'; -import {FriendsService} from './friend.service'; import {Auth, AuthUser} from '../auth/auth.decorator'; import {Friend} from './friend.schema'; import {FriendStatus, UpdateFriendDto} from './friend.dto'; import {User} from '../user/user.schema'; import {UniqueConflict} from '../util/unique-conflict.decorator'; +import {FriendService} from "./friend.service"; @Controller('users/:from/friends') @ApiTags('Friends') @Validated() @Throttled() -export class FriendsController { +export class FriendController { constructor( - private readonly friendsService: FriendsService, + private readonly friendService: FriendService, ) { } @@ -60,7 +60,7 @@ export class FriendsController { if (!from.equals(user._id)) { throw new ForbiddenException('You can only access your own friends list.'); } - return this.friendsService.getFriends(from, status as FriendStatus); + return this.friendService.getFriends(from, status as FriendStatus); } @Put(':to') @@ -81,11 +81,11 @@ export class FriendsController { if (from.equals(to)) { throw new ConflictException('You cannot send a friend request to yourself.'); } - const existingRequest = await this.friendsService.findOne({$or: [{from, to}, {from: to, to: from}]}); + const existingRequest = await this.friendService.findOne({$or: [{from, to}, {from: to, to: from}]}); if (existingRequest) { throw new ConflictException('Friend request already exists.'); } - return this.friendsService.create({from, to, status: 'requested'}); + return this.friendService.create({from, to, status: 'requested'}); } @Patch(':to') @@ -106,7 +106,7 @@ export class FriendsController { if (!to.equals(user._id)) { throw new ForbiddenException('You can only accept friend requests to your own account.'); } - return this.friendsService.acceptFriendRequest(to, from, dto); + return this.friendService.acceptFriendRequest(to, from, dto); } @Delete(':to') @@ -125,8 +125,8 @@ export class FriendsController { if (!from.equals(user._id) && !to.equals(user._id)) { throw new ForbiddenException('You can only delete friends from or to your own account.'); } - const deleted = await this.friendsService.deleteOne({from, to}); - const inverse = await this.friendsService.deleteOne({from: to, to: from}); + const deleted = await this.friendService.deleteOne({from, to}); + const inverse = await this.friendService.deleteOne({from: to, to: from}); return deleted || inverse; } } diff --git a/src/friend/friend.handler.ts b/src/friend/friend.handler.ts index 8bf7f2c..76f8f31 100644 --- a/src/friend/friend.handler.ts +++ b/src/friend/friend.handler.ts @@ -1,17 +1,17 @@ import {Injectable} from "@nestjs/common"; -import {FriendsService} from "./friend.service"; import {OnEvent} from "@nestjs/event-emitter"; import {User} from "../user/user.schema"; +import {FriendService} from "./friend.service"; @Injectable() -export class FriendsHandler { +export class FriendHandler { constructor( - private friendsService: FriendsService, + private friendService: FriendService, ) { } @OnEvent('users.*.deleted') async onUserDeleted(user: User): Promise { - await this.friendsService.deleteMany({$or: [{from: user._id}, {to: user._id}]}); + await this.friendService.deleteMany({$or: [{from: user._id}, {to: user._id}]}); } } diff --git a/src/friend/friend.module.ts b/src/friend/friend.module.ts index 80bc849..68ee74a 100644 --- a/src/friend/friend.module.ts +++ b/src/friend/friend.module.ts @@ -1,17 +1,17 @@ import {Module} from '@nestjs/common'; import {MongooseModule} from '@nestjs/mongoose'; import {Friend, FriendSchema} from "./friend.schema"; -import {FriendsService} from "./friend.service"; -import {FriendsController} from "./friend.controller"; -import {FriendsHandler} from "./friend.handler"; +import {FriendService} from "./friend.service"; +import {FriendHandler} from "./friend.handler"; +import {FriendController} from "./friend.controller"; @Module({ imports: [ MongooseModule.forFeature([{name: Friend.name, schema: FriendSchema}]), ], - providers: [FriendsService, FriendsHandler], - controllers: [FriendsController], - exports: [FriendsService], + providers: [FriendService, FriendHandler], + controllers: [FriendController], + exports: [FriendService], }) -export class FriendsModule { +export class FriendModule { } diff --git a/src/friend/friend.service.ts b/src/friend/friend.service.ts index 27b389e..82133a5 100644 --- a/src/friend/friend.service.ts +++ b/src/friend/friend.service.ts @@ -7,7 +7,7 @@ import {FriendStatus, UpdateFriendDto} from './friend.dto'; @Injectable() @EventRepository() -export class FriendsService extends MongooseRepository { +export class FriendService extends MongooseRepository { constructor( @InjectModel(Friend.name) private friendModel: Model, private eventEmitter: EventService, diff --git a/src/job/job-type.enum.ts b/src/job/job-type.enum.ts new file mode 100644 index 0000000..54c7ed8 --- /dev/null +++ b/src/job/job-type.enum.ts @@ -0,0 +1,6 @@ +export enum JobType { + BUILDING = 'building', + DISTRICT = 'district', + UPGRADE = 'upgrade', + TECHNOLOGY = 'technology', +} diff --git a/src/job/job.controller.ts b/src/job/job.controller.ts new file mode 100644 index 0000000..8d49077 --- /dev/null +++ b/src/job/job.controller.ts @@ -0,0 +1,114 @@ +import { + Body, + Controller, + Delete, ForbiddenException, + Get, + Param, + Post, + Query, +} from '@nestjs/common'; +import { + ApiCreatedResponse, + ApiForbiddenResponse, + ApiOkResponse, + ApiOperation, + ApiQuery, + ApiTags, +} from '@nestjs/swagger'; +import {Types} from 'mongoose'; +import {NotFound, ObjectIdPipe} from '@mean-stream/nestx'; +import {Validated} from '../util/validated.decorator'; +import {Throttled} from '../util/throttled.decorator'; +import {Auth, AuthUser} from '../auth/auth.decorator'; +import {Job} from './job.schema'; +import {User} from '../user/user.schema'; +import {CreateJobDto} from './job.dto'; +import {JobService} from './job.service'; +import {EmpireService} from "../empire/empire.service"; +import {JobType} from "./job-type.enum"; + +@Controller('games/:game/empires/:empire/jobs') +@ApiTags('Jobs') +@Validated() +@Throttled() +export class JobController { + constructor( + private readonly jobService: JobService, + private readonly empireService: EmpireService, + ) { + } + + @Get() + @Auth() + @ApiOperation({description: 'Get the job list with optional filters for system and type.'}) + @ApiOkResponse({type: [Job]}) + @ApiForbiddenResponse({description: 'You can only access jobs for your own empire.'}) + @NotFound() + @ApiQuery({ + name: 'system', + description: 'Filter jobs by system', + required: false, + type: String, + example: '60d6f7eb8b4b8a001d6f7eb1', + }) + @ApiQuery({ + name: 'type', + description: 'Filter jobs by type (`building`, `district`, `upgrade`, `technology`).', + required: false, + enum: JobType, + }) + async getJobs( + @Param('game', ObjectIdPipe) game: Types.ObjectId, + @Param('empire', ObjectIdPipe) empire: Types.ObjectId, + @AuthUser() user: User, + @Query('system', ObjectIdPipe) system?: Types.ObjectId, + @Query('type') type?: string, + ): Promise { + const userEmpire = await this.empireService.findOne({game, user: user._id}); + if (!userEmpire || !empire.equals(userEmpire._id)) { + throw new ForbiddenException('You can only access jobs for your own empire.'); + } + // TODO: Return jobs with given filters + return Array.of(new Job()); + } + + @Post() + @Auth() + @ApiOperation({description: 'Create a new job for your empire.'}) + @ApiCreatedResponse({type: Job}) + @ApiForbiddenResponse({description: 'You can only create jobs for your own empire.'}) + @NotFound() + async createJob( + @Param('game', ObjectIdPipe) game: Types.ObjectId, + @Param('empire', ObjectIdPipe) empire: Types.ObjectId, + @AuthUser() user: User, + @Body() createJobDto: CreateJobDto, + ): Promise { + const userEmpire = await this.empireService.findOne({game, user: user._id}); + if (!userEmpire || !empire.equals(userEmpire._id)) { + throw new ForbiddenException('You can only create jobs for your own empire.'); + } + // TODO: Create job + return new Job(); + } + + @Delete(':id') + @Auth() + @ApiOperation({description: 'Delete a job from your empire.'}) + @ApiOkResponse({type: Job}) + @NotFound('Job not found.') + @ApiForbiddenResponse({description: 'You can only delete jobs from your own empire.'}) + async deleteJob( + @Param('game', ObjectIdPipe) game: Types.ObjectId, + @Param('empire', ObjectIdPipe) empire: Types.ObjectId, + @Param('id', ObjectIdPipe) id: Types.ObjectId, + @AuthUser() user: User, + ): Promise { + const userEmpire = await this.empireService.findOne({game, user: user._id}); + if (!userEmpire || !empire.equals(userEmpire._id)) { + throw new ForbiddenException('You can only delete jobs for your own empire.'); + } + // TODO: Delete job + return null; + } +} diff --git a/src/job/job.dto.ts b/src/job/job.dto.ts new file mode 100644 index 0000000..7b13ae2 --- /dev/null +++ b/src/job/job.dto.ts @@ -0,0 +1,5 @@ +import {PickType} from '@nestjs/swagger'; +import {Job} from "./job.schema"; + +export class CreateJobDto extends PickType(Job, ['system', 'type', 'building', 'district', 'technology'] as const) { +} diff --git a/src/job/job.module.ts b/src/job/job.module.ts new file mode 100644 index 0000000..a0e08d0 --- /dev/null +++ b/src/job/job.module.ts @@ -0,0 +1,18 @@ +import {Module} from '@nestjs/common'; +import {MongooseModule} from '@nestjs/mongoose'; +import {Job, JobSchema} from './job.schema'; +import {JobController} from "./job.controller"; +import {JobService} from "./job.service"; +import {EmpireModule} from "../empire/empire.module"; + +@Module({ + imports: [ + MongooseModule.forFeature([{name: Job.name, schema: JobSchema}]), + EmpireModule, + ], + controllers: [JobController], + providers: [JobService], + exports: [JobService], +}) +export class JobModule { +} diff --git a/src/job/job.schema.ts b/src/job/job.schema.ts new file mode 100644 index 0000000..eaf37c9 --- /dev/null +++ b/src/job/job.schema.ts @@ -0,0 +1,68 @@ +import {Prop, Schema, SchemaFactory} from '@nestjs/mongoose'; +import {Document, Types} from 'mongoose'; +import {BUILDING_NAMES, BuildingName} from "../game-logic/buildings"; +import {DISTRICT_NAMES, DistrictName} from "../game-logic/districts"; +import {RESOURCES_SCHEMA_PROPERTIES, TECHNOLOGY_TAGS, TechnologyTag} from "../game-logic/types"; +import {ResourceName} from "../game-logic/resources"; +import {GLOBAL_SCHEMA_OPTIONS, GlobalSchema} from '../util/schema'; +import {IsEnum, IsIn, IsNumber, IsOptional} from 'class-validator'; +import {ApiProperty, ApiPropertyOptional} from '@nestjs/swagger'; +import {OptionalRef, Ref} from "@mean-stream/nestx"; +import {JobType} from "./job-type.enum"; + +export type JobDocument = Job & Document; + +@Schema(GLOBAL_SCHEMA_OPTIONS) +export class Job extends GlobalSchema { + @Prop({required: true}) + @ApiProperty({description: 'Current progress of the job'}) + @IsNumber() + progress: number; + + @Prop({required: true}) + @ApiProperty({description: 'Total progress steps required for the job'}) + @IsNumber() + total: number; + + @Ref('Game') + game: Types.ObjectId; + + @Ref('Empire') + empire: Types.ObjectId; + + @OptionalRef('System') + system?: Types.ObjectId; + + @Prop({required: true, type: String, enum: JobType}) + @ApiProperty({enum: JobType, description: 'Type of the job'}) + @IsEnum(JobType) + type: JobType; + + @Prop({type: String}) + @IsOptional() + @ApiPropertyOptional({required: false, description: 'Building name for the job'}) + @IsIn(BUILDING_NAMES) + building?: BuildingName; + + @Prop({type: String}) + @IsOptional() + @ApiPropertyOptional({required: false, description: 'District name for the job'}) + @IsIn(DISTRICT_NAMES) + district?: DistrictName; + + @Prop({type: String}) + @IsOptional() + @ApiPropertyOptional({required: false, description: 'Technology name for the job'}) + @IsIn(TECHNOLOGY_TAGS) + technology?: TechnologyTag; + + @Prop({type: Map, of: Number, default: {}}) + @IsOptional() + @ApiPropertyOptional({ + description: 'Initial cost of resources for the job', + ...RESOURCES_SCHEMA_PROPERTIES, + }) + cost?: Record; +} + +export const JobSchema = SchemaFactory.createForClass(Job); diff --git a/src/job/job.service.ts b/src/job/job.service.ts new file mode 100644 index 0000000..ac3341a --- /dev/null +++ b/src/job/job.service.ts @@ -0,0 +1,16 @@ +import {Injectable} from "@nestjs/common"; +import {InjectModel} from "@nestjs/mongoose"; +import {Job} from "./job.schema"; +import {Model} from "mongoose"; +import {EventRepository, EventService, MongooseRepository} from "@mean-stream/nestx"; + +@Injectable() +@EventRepository() +export class JobService extends MongooseRepository { + constructor( + @InjectModel(Job.name) private jobModel: Model, + private eventEmitter: EventService, + ) { + super(jobModel); + } +}