1
0
forked from noxious/server

More event progress

This commit is contained in:
Dennis Postma 2025-01-02 02:24:09 +01:00
parent ab89d0cbb0
commit f7dbf09bf5
18 changed files with 194 additions and 464 deletions

View File

@ -2,6 +2,8 @@ import { Server } from 'socket.io'
import Logger, { LoggerType } from '#application/logger' import Logger, { LoggerType } from '#application/logger'
import { TSocket } from '#application/types' import { TSocket } from '#application/types'
import { Character } from '#entities/character'
import CharacterRepository from '#repositories/characterRepository'
export abstract class BaseEvent { export abstract class BaseEvent {
protected readonly logger = Logger.type(LoggerType.GAME) protected readonly logger = Logger.type(LoggerType.GAME)
@ -11,6 +13,15 @@ export abstract class BaseEvent {
readonly socket: TSocket readonly socket: TSocket
) {} ) {}
protected async getCharacter(): Promise<Character | null> {
return CharacterRepository.getById(this.socket.characterId!)
}
protected async isCharacterGM(): Promise<boolean> {
const character = await this.getCharacter()
return character?.getRole() === 'gm'
}
protected emitError(message: string): void { protected emitError(message: string): void {
this.socket.emit('notification', { title: 'Server message', message }) this.socket.emit('notification', { title: 'Server message', message })
this.logger.error('character:connect error', `Player ${this.socket.userId}: ${message}`) this.logger.error('character:connect error', `Player ${this.socket.userId}: ${message}`)

View File

@ -1,79 +1,35 @@
import { Server } from 'socket.io' import { BaseEvent } from '#application/base/baseEvent'
import { UUID } from '#application/types'
import type { Prisma } from '@prisma/client' import { Sprite } from '#entities/sprite'
import { gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma'
import { TSocket } from '#application/types'
import CharacterRepository from '#repositories/characterRepository' import CharacterRepository from '#repositories/characterRepository'
import SpriteRepository from '#repositories/spriteRepository'
interface CopyPayload { interface CopyPayload {
id: string id: UUID
} }
export default class SpriteCopyEvent { export default class SpriteCopyEvent extends BaseEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void { public listen(): void {
this.socket.on('gm:sprite:copy', this.handleEvent.bind(this)) this.socket.on('gm:sprite:copy', this.handleEvent.bind(this))
} }
private async handleEvent(payload: CopyPayload, callback: (success: boolean) => void): Promise<void> { private async handleEvent(payload: CopyPayload, callback: (success: boolean) => void): Promise<void> {
try { try {
if (!(await this.validateGameMasterAccess())) { if (!(await this.isCharacterGM())) return
return callback(false)
}
const sourceSprite = await prisma.sprite.findUnique({ const sourceSprite = await SpriteRepository.getById(payload.id)
where: { id: payload.id },
include: {
spriteActions: true
}
})
if (!sourceSprite) { if (!sourceSprite) {
throw new Error('Source sprite not found') throw new Error('Source sprite not found')
} }
const newSprite = await prisma.sprite.create({ const newSprite = new Sprite()
data: { await newSprite.setName(`${sourceSprite.getName()} (Copy)`).setSpriteActions(sourceSprite.getSpriteActions()).save()
name: `${sourceSprite.name} (Copy)`,
spriteActions: {
create: sourceSprite.spriteActions.map((action) => ({
action: action.action,
sprites: action.sprites as Prisma.InputJsonValue,
originX: action.originX,
originY: action.originY,
isAnimated: action.isAnimated,
isLooping: action.isLooping,
frameWidth: action.frameWidth,
frameHeight: action.frameHeight,
frameRate: action.frameRate
}))
}
}
})
callback(true) callback(true)
} catch (error) { } catch (error) {
this.handleError(error, payload.id, callback) this.logger.error(`Error copying sprite:`, String(error))
callback(false)
} }
} }
private async validateGameMasterAccess(): Promise<boolean> {
const character = await CharacterRepository.getById(this.socket.characterId!)
return character?.role === 'gm'
}
private handleError(error: unknown, spriteId: string, callback: (success: boolean) => void): void {
gameMasterLogger.error(`Error copying sprite ${spriteId}: ${this.getErrorMessage(error)}`)
callback(false)
}
private getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error)
}
} }

View File

@ -2,12 +2,10 @@ import fs from 'fs/promises'
import { Server } from 'socket.io' import { Server } from 'socket.io'
import prisma from '#application/prisma' import { BaseEvent } from '#application/base/baseEvent'
import Storage from '#application/storage' import Storage from '#application/storage'
import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository'
export default class SpriteCreateEvent { export default class SpriteCreateEvent extends BaseEvent {
constructor( constructor(
private readonly io: Server, private readonly io: Server,
private readonly socket: TSocket private readonly socket: TSocket
@ -19,12 +17,7 @@ export default class SpriteCreateEvent {
private async handleEvent(data: undefined, callback: (response: boolean) => void): Promise<void> { private async handleEvent(data: undefined, callback: (response: boolean) => void): Promise<void> {
try { try {
const character = await characterRepository.getById(this.socket.characterId!) if (!(await this.isCharacterGM())) return
if (!character) return callback(false)
if (character.role !== 'gm') {
return callback(false)
}
const public_folder = Storage.getPublicPath('sprites') const public_folder = Storage.getPublicPath('sprites')

View File

@ -1,45 +1,30 @@
import fs from 'fs' import fs from 'fs'
import { Server } from 'socket.io' import { BaseEvent } from '#application/base/baseEvent'
import { gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma'
import Storage from '#application/storage' import Storage from '#application/storage'
import { TSocket } from '#application/types' import { UUID } from '#application/types'
import CharacterRepository from '#repositories/characterRepository' import SpriteRepository from '#repositories/spriteRepository'
type Payload = { type Payload = {
id: string id: UUID
} }
export default class GMSpriteDeleteEvent { export default class GMSpriteDeleteEvent extends BaseEvent {
private readonly public_folder: string
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {
this.public_folder = Storage.getPublicPath('sprites')
}
public listen(): void { public listen(): void {
this.socket.on('gm:sprite:delete', this.handleEvent.bind(this)) this.socket.on('gm:sprite:delete', this.handleEvent.bind(this))
} }
private async handleEvent(data: Payload, callback: (response: boolean) => void): Promise<void> { private async handleEvent(data: Payload, callback: (response: boolean) => void): Promise<void> {
const character = await CharacterRepository.getById(this.socket.characterId!) if (!(await this.isCharacterGM())) return
if (character?.role !== 'gm') {
return callback(false)
}
try { try {
await this.deleteSpriteFolder(data.id) await this.deleteSpriteFolder(data.id)
await this.deleteSpriteFromDatabase(data.id) await (await SpriteRepository.getById(data.id))?.delete()
gameMasterLogger.info(`Sprite ${data.id} deleted.`) this.logger.info(`Sprite ${data.id} deleted.`)
callback(true) callback(true)
} catch (error: any) { } catch (error: any) {
gameMasterLogger.error('gm:sprite:delete error', error.message) this.logger.error('gm:sprite:delete error', error.message)
callback(false) callback(false)
} }
} }
@ -51,12 +36,4 @@ export default class GMSpriteDeleteEvent {
await fs.promises.rmdir(finalFilePath, { recursive: true }) await fs.promises.rmdir(finalFilePath, { recursive: true })
} }
} }
private async deleteSpriteFromDatabase(spriteId: string): Promise<void> {
await prisma.sprite.delete({
where: {
id: spriteId
}
})
}
} }

View File

@ -1,29 +1,17 @@
import { Sprite } from '@prisma/client' import { Sprite } from '@prisma/client'
import { Server } from 'socket.io'
import { TSocket } from '#application/types' import { BaseEvent } from '#application/base/baseEvent'
import characterRepository from '#repositories/characterRepository'
import SpriteRepository from '#repositories/spriteRepository' import SpriteRepository from '#repositories/spriteRepository'
interface IPayload {} interface IPayload {}
export default class SpriteListEvent { export default class SpriteListEvent extends BaseEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void { public listen(): void {
this.socket.on('gm:sprite:list', this.handleEvent.bind(this)) this.socket.on('gm:sprite:list', this.handleEvent.bind(this))
} }
private async handleEvent(data: any, callback: (response: Sprite[]) => void): Promise<void> { private async handleEvent(data: IPayload, callback: (response: Sprite[]) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId!) if (!(await this.isCharacterGM())) return
if (!character) return callback([])
if (character.role !== 'gm') {
return callback([])
}
// get all sprites // get all sprites
const sprites = await SpriteRepository.getAll() const sprites = await SpriteRepository.getAll()

View File

@ -1,15 +1,11 @@
import { writeFile, mkdir } from 'node:fs/promises' import { writeFile, mkdir } from 'node:fs/promises'
import sharp from 'sharp' import sharp from 'sharp'
import { Server } from 'socket.io'
import type { Prisma, SpriteAction } from '@prisma/client' import { BaseEvent } from '#application/base/baseEvent'
import { gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma'
import Storage from '#application/storage' import Storage from '#application/storage'
import { TSocket } from '#application/types' import { SpriteAction } from '#entities/spriteAction'
import CharacterRepository from '#repositories/characterRepository' import SpriteRepository from '#repositories/spriteRepository'
// Constants // Constants
const ISOMETRIC_CONFIG = { const ISOMETRIC_CONFIG = {
@ -41,7 +37,7 @@ interface SpriteActionInput extends Omit<SpriteAction, 'id' | 'spriteId' | 'fram
interface UpdatePayload { interface UpdatePayload {
id: string id: string
name: string name: string
spriteActions: Prisma.JsonValue spriteActions: SpriteAction[]
} }
interface ProcessedSpriteAction extends SpriteActionInput { interface ProcessedSpriteAction extends SpriteActionInput {
@ -62,21 +58,14 @@ interface SpriteAnalysis {
contentBounds: ContentBounds contentBounds: ContentBounds
} }
export default class SpriteUpdateEvent { export default class SpriteUpdateEvent extends BaseEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void { public listen(): void {
this.socket.on('gm:sprite:update', this.handleEvent.bind(this)) this.socket.on('gm:sprite:update', this.handleEvent.bind(this))
} }
private async handleEvent(payload: UpdatePayload, callback: (success: boolean) => void): Promise<void> { private async handleEvent(payload: UpdatePayload, callback: (success: boolean) => void): Promise<void> {
try { try {
if (!(await this.validateGameMasterAccess())) { if (!(await this.isCharacterGM())) return
return callback(false)
}
const parsedActions = this.validateSpriteActions(payload.spriteActions) const parsedActions = this.validateSpriteActions(payload.spriteActions)
@ -111,11 +100,6 @@ export default class SpriteUpdateEvent {
} }
} }
private async validateGameMasterAccess(): Promise<boolean> {
const character = await CharacterRepository.getById(this.socket.characterId!)
return character?.role === 'gm'
}
private validateSpriteActions(actions: Prisma.JsonValue): SpriteActionInput[] { private validateSpriteActions(actions: Prisma.JsonValue): SpriteActionInput[] {
try { try {
const parsed = JSON.parse(JSON.stringify(actions)) as SpriteActionInput[] const parsed = JSON.parse(JSON.stringify(actions)) as SpriteActionInput[]
@ -375,6 +359,8 @@ export default class SpriteUpdateEvent {
} }
} }
}) })
await (await SpriteRepository.getById(id))?.setName(name).setSpriteActions(actions).update()
} }
private mapActionToDatabase(action: ProcessedSpriteAction) { private mapActionToDatabase(action: ProcessedSpriteAction) {
@ -392,7 +378,7 @@ export default class SpriteUpdateEvent {
} }
private handleError(error: unknown, spriteId: string, callback: (success: boolean) => void): void { private handleError(error: unknown, spriteId: string, callback: (success: boolean) => void): void {
gameMasterLogger.error(`Error updating sprite ${spriteId}: ${this.getErrorMessage(error)}`) this.logger.error(`Error updating sprite ${spriteId}: ${this.getErrorMessage(error)}`)
callback(false) callback(false)
} }

View File

@ -1,60 +1,36 @@
import fs from 'fs/promises' import fs from 'fs/promises'
import { Server } from 'socket.io' import { BaseEvent } from '#application/base/baseEvent'
import { gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma'
import Storage from '#application/storage' import Storage from '#application/storage'
import { TSocket } from '#application/types' import { UUID } from '#application/types'
import characterRepository from '#repositories/characterRepository' import TileRepository from '#repositories/tileRepository'
type Payload = { type Payload = {
id: string id: UUID
} }
export default class GMTileDeleteEvent { export default class GMTileDeleteEvent extends BaseEvent {
private readonly public_folder: string
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {
this.public_folder = Storage.getPublicPath('tiles')
}
public listen(): void { public listen(): void {
this.socket.on('gm:tile:delete', this.handleEvent.bind(this)) this.socket.on('gm:tile:delete', this.handleEvent.bind(this))
} }
private async handleEvent(data: Payload, callback: (response: boolean) => void): Promise<void> { private async handleEvent(data: Payload, callback: (response: boolean) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId as number) if (!(await this.isCharacterGM())) return
if (!character) return callback(false)
if (character.role !== 'gm') {
return
}
try { try {
gameMasterLogger.info(`Deleting tile ${data.id}`) this.logger.info(`Deleting tile ${data.id}`)
await this.deleteTileFromDatabase(data.id)
await this.deleteTileFile(data.id) await this.deleteTileFile(data.id)
await (await TileRepository.getById(data.id))?.delete()
gameMasterLogger.info(`Tile ${data.id} deleted successfully.`) this.logger.info(`Tile ${data.id} deleted successfully.`)
callback(true) return callback(true)
} catch (error: any) { } catch (error: unknown) {
gameMasterLogger.error('gm:tile:delete error', error.message) this.logger.error('gm:tile:delete error', error)
callback(false) return callback(false)
} }
} }
private async deleteTileFromDatabase(tileId: string): Promise<void> {
await prisma.tile.delete({
where: {
id: tileId
}
})
}
private async deleteTileFile(tileId: string): Promise<void> { private async deleteTileFile(tileId: string): Promise<void> {
const finalFilePath = Storage.getPublicPath('tiles', `${tileId}.png`) const finalFilePath = Storage.getPublicPath('tiles', `${tileId}.png`)
try { try {
@ -63,7 +39,7 @@ export default class GMTileDeleteEvent {
if (error.code !== 'ENOENT') { if (error.code !== 'ENOENT') {
throw error throw error
} }
gameMasterLogger.warn(`File ${finalFilePath} does not exist.`) this.logger.warn(`File ${finalFilePath} does not exist.`)
} }
} }
} }

View File

@ -1,7 +1,6 @@
import characterRepository from '#repositories/characterRepository'
import TileRepository from '#repositories/tileRepository'
import { BaseEvent } from '#application/base/baseEvent' import { BaseEvent } from '#application/base/baseEvent'
import { Tile } from '#entities/tile' import { Tile } from '#entities/tile'
import TileRepository from '#repositories/tileRepository'
interface IPayload {} interface IPayload {}
@ -11,15 +10,10 @@ export default class TileListEven extends BaseEvent {
} }
private async handleEvent(data: IPayload, callback: (response: Tile[]) => void): Promise<void> { private async handleEvent(data: IPayload, callback: (response: Tile[]) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId!) if (!(await this.isCharacterGM())) return
if (!character) return
if (character.role !== 'gm') {
return
}
// get all tiles // get all tiles
const tiles = await TileRepository.getAll() const tiles = await TileRepository.getAll()
callback(tiles) return callback(tiles)
} }
} }

View File

@ -1,7 +1,6 @@
import characterRepository from '#repositories/characterRepository'
import { BaseEvent } from '#application/base/baseEvent' import { BaseEvent } from '#application/base/baseEvent'
import TileRepository from '#repositories/tileRepository'
import { UUID } from '#application/types' import { UUID } from '#application/types'
import TileRepository from '#repositories/tileRepository'
type Payload = { type Payload = {
id: UUID id: UUID
@ -9,29 +8,22 @@ type Payload = {
tags: string[] tags: string[]
} }
export default class TileUpdateEvent extends BaseEvent{ export default class TileUpdateEvent extends BaseEvent {
public listen(): void { public listen(): void {
this.socket.on('gm:tile:update', this.handleEvent.bind(this)) this.socket.on('gm:tile:update', this.handleEvent.bind(this))
} }
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> { private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId!) if (!(await this.isCharacterGM())) return
if (!character) return callback(false)
if (character.role !== 'gm') {
return
}
try { try {
const tile = await TileRepository.getById(data.id) const tile = await TileRepository.getById(data.id)
if (!tile) return callback(false) if (!tile) return callback(false)
await tile.setName(data.name).setTags(data.tags).update() await tile.setName(data.name).setTags(data.tags).update()
return callback(true)
callback(true)
} catch (error) { } catch (error) {
console.error(error) return callback(false)
callback(false)
} }
} }
} }

View File

@ -1,9 +1,8 @@
import fs from 'fs/promises' import fs from 'fs/promises'
import { writeFile } from 'node:fs/promises' import { writeFile } from 'node:fs/promises'
import Storage from '#application/storage'
import characterRepository from '#repositories/characterRepository'
import { BaseEvent } from '#application/base/baseEvent' import { BaseEvent } from '#application/base/baseEvent'
import Storage from '#application/storage'
import { Tile } from '#entities/tile' import { Tile } from '#entities/tile'
interface ITileData { interface ITileData {
@ -17,12 +16,7 @@ export default class TileUploadEvent extends BaseEvent {
private async handleEvent(data: ITileData, callback: (response: boolean) => void): Promise<void> { private async handleEvent(data: ITileData, callback: (response: boolean) => void): Promise<void> {
try { try {
const character = await characterRepository.getById(this.socket.characterId!) if (!(await this.isCharacterGM())) return
if (!character) return callback(false)
if (character.role !== 'gm') {
return
}
const public_folder = Storage.getPublicPath('tiles') const public_folder = Storage.getPublicPath('tiles')

View File

@ -1,10 +1,5 @@
import { Zone } from '@prisma/client' import { BaseEvent } from '#application/base/baseEvent'
import { Server } from 'socket.io' import { Zone } from '#entities/zone'
import { gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma'
import { TSocket } from '#application/types'
import CharacterRepository from '#repositories/characterRepository'
import ZoneRepository from '#repositories/zoneRepository' import ZoneRepository from '#repositories/zoneRepository'
type Payload = { type Payload = {
@ -13,49 +8,29 @@ type Payload = {
height: number height: number
} }
export default class ZoneCreateEvent { export default class ZoneCreateEvent extends BaseEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void { public listen(): void {
this.socket.on('gm:zone_editor:zone:create', this.handleEvent.bind(this)) this.socket.on('gm:zone_editor:zone:create', this.handleEvent.bind(this))
} }
private async handleEvent(data: Payload, callback: (response: Zone[]) => void): Promise<void> { private async handleEvent(data: Payload, callback: (response: Zone[]) => void): Promise<void> {
try { try {
const character = await CharacterRepository.getById(this.socket.characterId as number) if (!(await this.isCharacterGM())) return
if (!character) {
gameMasterLogger.error('gm:zone_editor:zone:create error', 'Character not found')
callback([])
return
}
if (character.role !== 'gm') { this.logger.info(`User ${(await this.getCharacter())!.getId()} has created a new zone via zone editor.`)
gameMasterLogger.info(`User ${character.id} tried to create zone but is not a game master.`)
callback([])
return
}
gameMasterLogger.info(`User ${character.id} has created a new zone via zone editor.`) const zone = new Zone()
await zone
const zone = await prisma.zone.create({ .setName(data.name)
data: { .setWidth(data.width)
name: data.name, .setHeight(data.height)
width: data.width, .setTiles(Array.from({ length: data.height }, () => Array.from({ length: data.width }, () => 'blank_tile')))
height: data.height, .save()
tiles: Array.from({ length: data.height }, () => Array.from({ length: data.width }, () => 'blank_tile'))
}
})
const zoneList = await ZoneRepository.getAll() const zoneList = await ZoneRepository.getAll()
callback(zoneList) callback(zoneList)
// You might want to emit an event to notify other clients about the new zone
// this.io.emit('gm:zone_created', zone);
} catch (error: any) { } catch (error: any) {
gameMasterLogger.error('gm:zone_editor:zone:create error', error.message) this.logger.error('gm:zone_editor:zone:create error', error.message)
this.socket.emit('notification', { message: 'Failed to create zone.' }) this.socket.emit('notification', { message: 'Failed to create zone.' })
callback([]) callback([])
} }

View File

@ -1,62 +1,29 @@
import { Server } from 'socket.io' import { BaseEvent } from '#application/base/baseEvent'
import { UUID } from '#application/types'
import { gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma'
import { TSocket } from '#application/types'
import CharacterRepository from '#repositories/characterRepository'
import ZoneRepository from '#repositories/zoneRepository' import ZoneRepository from '#repositories/zoneRepository'
type Payload = { type Payload = {
zoneId: number zoneId: UUID
} }
export default class ZoneDeleteEvent { export default class ZoneDeleteEvent extends BaseEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void { public listen(): void {
this.socket.on('gm:zone_editor:zone:delete', this.handleEvent.bind(this)) this.socket.on('gm:zone_editor:zone:delete', this.handleEvent.bind(this))
} }
private async handleEvent(data: Payload, callback: (response: boolean) => void): Promise<void> { private async handleEvent(data: Payload, callback: (response: boolean) => void): Promise<void> {
if (!(await this.isCharacterGM())) return
try { try {
const character = await CharacterRepository.getById(this.socket.characterId as number) this.logger.info(`Deleting zone ${data.zoneId}`)
if (!character) {
gameMasterLogger.error('gm:zone_editor:zone:delete error', 'Character not found')
callback(false)
return
}
if (character.role !== 'gm') { await (await ZoneRepository.getById(data.zoneId))?.delete()
gameMasterLogger.info(`User ${character.id} tried to delete zone but is not a game master.`)
callback(false)
return
}
gameMasterLogger.info(`User ${character.id} has deleted a zone via zone editor.`) this.logger.info(`Zone ${data.zoneId} deleted successfully.`)
return callback(true)
const zone = await ZoneRepository.getById(data.zoneId) } catch (error: unknown) {
if (!zone) { this.logger.error('gm:zone_editor:zone:delete error', error)
gameMasterLogger.error('gm:zone_editor:zone:delete error', 'Zone not found') return callback(false)
callback(false)
return
}
await prisma.zone.delete({
where: {
id: data.zoneId
}
})
callback(true)
// You might want to emit an event to notify other clients about the deleted zone
// this.io.emit('gm:zone_deleted', data.zoneId);
} catch (error: any) {
gameMasterLogger.error('gm:zone_editor:zone:delete error', error.message)
callback(false)
} }
} }
} }

View File

@ -1,44 +1,24 @@
import { Zone } from '@prisma/client' import { BaseEvent } from '#application/base/baseEvent'
import { Server } from 'socket.io' import { Zone } from '#entities/zone'
import { gameMasterLogger } from '#application/logger'
import { TSocket } from '#application/types'
import CharacterRepository from '#repositories/characterRepository'
import ZoneRepository from '#repositories/zoneRepository' import ZoneRepository from '#repositories/zoneRepository'
interface IPayload {} interface IPayload {}
export default class ZoneListEvent { export default class ZoneListEvent extends BaseEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void { public listen(): void {
this.socket.on('gm:zone_editor:zone:list', this.handleEvent.bind(this)) this.socket.on('gm:zone_editor:zone:list', this.handleEvent.bind(this))
} }
private async handleEvent(data: IPayload, callback: (response: Zone[]) => void): Promise<void> { private async handleEvent(data: IPayload, callback: (response: Zone[]) => void): Promise<void> {
try { try {
const character = await CharacterRepository.getById(this.socket.characterId as number) if (!(await this.isCharacterGM())) return
if (!character) {
gameMasterLogger.error('gm:zone_editor:zone:list error', 'Character not found')
callback([])
return
}
if (character.role !== 'gm') { this.logger.info(`User ${(await this.getCharacter())!.getId()} has created a new zone via zone editor.`)
gameMasterLogger.info(`User ${character.id} tried to list zones but is not a game master.`)
callback([])
return
}
gameMasterLogger.info(`User ${character.id} has requested zone list via zone editor.`)
const zones = await ZoneRepository.getAll() const zones = await ZoneRepository.getAll()
callback(zones) callback(zones)
} catch (error: any) { } catch (error: any) {
gameMasterLogger.error('gm:zone_editor:zone:list error', error.message) this.logger.error('gm:zone_editor:zone:list error', error.message)
callback([]) callback([])
} }
} }

View File

@ -1,60 +1,39 @@
import { Zone } from '@prisma/client' import { BaseEvent } from '#application/base/baseEvent'
import { Server } from 'socket.io' import { UUID } from '#application/types'
import { Zone } from '#entities/zone'
import { gameMasterLogger } from '#application/logger'
import { TSocket } from '#application/types'
import CharacterRepository from '#repositories/characterRepository'
import ZoneRepository from '#repositories/zoneRepository' import ZoneRepository from '#repositories/zoneRepository'
interface IPayload { interface IPayload {
zoneId: number zoneId: UUID
} }
export default class ZoneRequestEvent { export default class ZoneRequestEvent extends BaseEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void { public listen(): void {
this.socket.on('gm:zone_editor:zone:request', this.handleEvent.bind(this)) this.socket.on('gm:zone_editor:zone:request', this.handleEvent.bind(this))
} }
private async handleEvent(data: IPayload, callback: (response: Zone | null) => void): Promise<void> { private async handleEvent(data: IPayload, callback: (response: Zone | null) => void): Promise<void> {
try { try {
const character = await CharacterRepository.getById(this.socket.characterId as number) if (!(await this.isCharacterGM())) return
if (!character) {
gameMasterLogger.error('gm:zone_editor:zone:request error', 'Character not found')
callback(null)
return
}
if (character.role !== 'gm') { this.logger.info(`User ${(await this.getCharacter())!.getId()} has requested zone via zone editor.`)
gameMasterLogger.info(`User ${character.id} tried to request zone but is not a game master.`)
callback(null)
return
}
gameMasterLogger.info(`User ${character.id} has requested zone via zone editor.`)
if (!data.zoneId) { if (!data.zoneId) {
gameMasterLogger.info(`User ${character.id} tried to request zone but did not provide a zone id.`) this.logger.info(`User ${(await this.getCharacter())!.getId()} tried to request zone but did not provide a zone id.`)
callback(null) return callback(null)
return
} }
const zone = await ZoneRepository.getById(data.zoneId) const zone = await ZoneRepository.getById(data.zoneId)
if (!zone) { if (!zone) {
gameMasterLogger.info(`User ${character.id} tried to request zone ${data.zoneId} but it does not exist.`) this.logger.info(`User ${(await this.getCharacter())!.getId()} tried to request zone ${data.zoneId} but it does not exist.`)
callback(null) return callback(null)
return
} }
callback(zone) return callback(zone)
} catch (error: any) { } catch (error: any) {
gameMasterLogger.error('gm:zone_editor:zone:request error', error.message) this.logger.error('gm:zone_editor:zone:request error', error.message)
callback(null) return callback(null)
} }
} }
} }

View File

@ -1,15 +1,16 @@
import { Zone, ZoneEffect, ZoneEventTileType, ZoneObject } from '@prisma/client' import { BaseEvent } from '#application/base/baseEvent'
import { Server } from 'socket.io' import { ZoneEventTileType } from '#application/enums'
import { UUID } from '#application/types'
import { gameMasterLogger } from '#application/logger' import { Zone } from '#entities/zone'
import prisma from '#application/prisma' import { ZoneEffect } from '#entities/zoneEffect'
import { TSocket } from '#application/types' import { ZoneEventTile } from '#entities/zoneEventTile'
import { ZoneEventTileTeleport } from '#entities/zoneEventTileTeleport'
import { ZoneObject } from '#entities/zoneObject'
import zoneManager from '#managers/zoneManager' import zoneManager from '#managers/zoneManager'
import CharacterRepository from '#repositories/characterRepository'
import ZoneRepository from '#repositories/zoneRepository' import ZoneRepository from '#repositories/zoneRepository'
interface IPayload { interface IPayload {
zoneId: number zoneId: UUID
name: string name: string
width: number width: number
height: number height: number
@ -20,7 +21,7 @@ interface IPayload {
positionX: number positionX: number
positionY: number positionY: number
teleport?: { teleport?: {
toZoneId: number toZoneId: UUID
toPositionX: number toPositionX: number
toPositionY: number toPositionY: number
toRotation: number toRotation: number
@ -33,44 +34,31 @@ interface IPayload {
zoneObjects: ZoneObject[] zoneObjects: ZoneObject[]
} }
export default class ZoneUpdateEvent { export default class ZoneUpdateEvent extends BaseEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void { public listen(): void {
this.socket.on('gm:zone_editor:zone:update', this.handleEvent.bind(this)) this.socket.on('gm:zone_editor:zone:update', this.handleEvent.bind(this))
} }
private async handleEvent(data: IPayload, callback: (response: Zone | null) => void): Promise<void> { private async handleEvent(data: IPayload, callback: (response: Zone | null) => void): Promise<void> {
try { try {
const character = await CharacterRepository.getById(this.socket.characterId as number) if (!(await this.isCharacterGM())) return
if (!character) {
gameMasterLogger.error('gm:zone_editor:zone:update error', 'Character not found')
return callback(null)
}
if (character.role !== 'gm') { const character = await this.getCharacter()
gameMasterLogger.info(`User ${character.id} tried to update zone but is not a game master.`) this.logger.info(`User ${character!.getId()} has updated zone via zone editor.`)
return callback(null)
}
gameMasterLogger.info(`User ${character.id} has updated zone via zone editor.`)
if (!data.zoneId) { if (!data.zoneId) {
gameMasterLogger.info(`User ${character.id} tried to update zone but did not provide a zone id.`) this.logger.info(`User ${character!.getId()} tried to update zone but did not provide a zone id.`)
return callback(null) return callback(null)
} }
let zone = await ZoneRepository.getById(data.zoneId) let zone = await ZoneRepository.getById(data.zoneId)
if (!zone) { if (!zone) {
gameMasterLogger.info(`User ${character.id} tried to update zone ${data.zoneId} but it does not exist.`) this.logger.info(`User ${character!.getId()} tried to update zone ${data.zoneId} but it does not exist.`)
return callback(null) return callback(null)
} }
// If tiles are larger than the zone, remove the extra tiles // Validation logic remains the same
if (data.tiles.length > data.height) { if (data.tiles.length > data.height) {
data.tiles = data.tiles.slice(0, data.height) data.tiles = data.tiles.slice(0, data.height)
} }
@ -80,81 +68,65 @@ export default class ZoneUpdateEvent {
} }
} }
// If zone event tiles are placed outside the zone's bounds, remove these
data.zoneEventTiles = data.zoneEventTiles.filter((tile) => tile.positionX >= 0 && tile.positionX < data.width && tile.positionY >= 0 && tile.positionY < data.height) data.zoneEventTiles = data.zoneEventTiles.filter((tile) => tile.positionX >= 0 && tile.positionX < data.width && tile.positionY >= 0 && tile.positionY < data.height)
// If zone objects are placed outside the zone's bounds, remove these
data.zoneObjects = data.zoneObjects.filter((obj) => obj.positionX >= 0 && obj.positionX < data.width && obj.positionY >= 0 && obj.positionY < data.height) data.zoneObjects = data.zoneObjects.filter((obj) => obj.positionX >= 0 && obj.positionX < data.width && obj.positionY >= 0 && obj.positionY < data.height)
await prisma.zone.update({ // Clear existing collections
where: { id: data.zoneId }, zone.zoneEventTiles.removeAll()
data: { zone.zoneObjects.removeAll()
name: data.name, zone.zoneEffects.removeAll()
width: data.width,
height: data.height,
tiles: data.tiles,
pvp: data.pvp,
zoneEventTiles: {
deleteMany: { zoneId: data.zoneId },
create: data.zoneEventTiles.map((zoneEventTile) => ({
type: zoneEventTile.type,
positionX: zoneEventTile.positionX,
positionY: zoneEventTile.positionY,
...(zoneEventTile.type === 'TELEPORT' && zoneEventTile.teleport
? {
teleport: {
create: {
toZoneId: zoneEventTile.teleport.toZoneId,
toPositionX: zoneEventTile.teleport.toPositionX,
toPositionY: zoneEventTile.teleport.toPositionY,
toRotation: zoneEventTile.teleport.toRotation
}
}
}
: {})
}))
},
zoneObjects: {
deleteMany: { zoneId: data.zoneId },
create: data.zoneObjects.map((zoneObject) => ({
objectId: zoneObject.objectId,
depth: zoneObject.depth,
isRotated: zoneObject.isRotated,
positionX: zoneObject.positionX,
positionY: zoneObject.positionY
}))
},
zoneEffects: {
deleteMany: { zoneId: data.zoneId },
create: data.zoneEffects.map((zoneEffect) => ({
effect: zoneEffect.effect,
strength: zoneEffect.strength
}))
},
updatedAt: new Date()
}
})
// Create and add new zone event tiles
for (const tile of data.zoneEventTiles) {
const zoneEventTile = new ZoneEventTile().setType(tile.type).setPositionX(tile.positionX).setPositionY(tile.positionY).setZone(zone)
if (tile.teleport) {
const teleport = new ZoneEventTileTeleport()
.setToZone((await ZoneRepository.getById(tile.teleport.toZoneId))!)
.setToPositionX(tile.teleport.toPositionX)
.setToPositionY(tile.teleport.toPositionY)
.setToRotation(tile.teleport.toRotation)
zoneEventTile.setTeleport(teleport)
}
zone.zoneEventTiles.add(zoneEventTile)
}
// Create and add new zone objects
for (const object of data.zoneObjects) {
const zoneObject = new ZoneObject().setMapObject(object.mapObject).setDepth(object.depth).setIsRotated(object.isRotated).setPositionX(object.positionX).setPositionY(object.positionY).setZone(zone)
zone.zoneObjects.add(zoneObject)
}
// Create and add new zone effects
for (const effect of data.zoneEffects) {
const zoneEffect = new ZoneEffect().setEffect(effect.effect).setStrength(effect.strength).setZone(zone)
zone.zoneEffects.add(zoneEffect)
}
// Update zone properties
await zone.setName(data.name).setWidth(data.width).setHeight(data.height).setTiles(data.tiles).setPvp(data.pvp).setUpdatedAt(new Date()).update()
// Reload zone from database to get fresh data
zone = await ZoneRepository.getById(data.zoneId) zone = await ZoneRepository.getById(data.zoneId)
if (!zone) { if (!zone) {
gameMasterLogger.info(`User ${character.id} tried to update zone ${data.zoneId} but it does not exist after update.`) this.logger.info(`User ${character!.getId()} tried to update zone ${data.zoneId} but it does not exist after update.`)
callback(null) return callback(null)
return
} }
gameMasterLogger.info(`User ${character.id} has updated zone via zone editor.`) // Reload zone for players
callback(zone)
/**
* @TODO #246: Reload zone for players who are currently in the zone
*/
zoneManager.unloadZone(data.zoneId) zoneManager.unloadZone(data.zoneId)
await zoneManager.loadZone(zone) await zoneManager.loadZone(zone)
return callback(zone)
} catch (error: any) { } catch (error: any) {
gameMasterLogger.error(`gm:zone_editor:zone:update error: ${error instanceof Error ? error.message : String(error)}`) this.logger.error(`gm:zone_editor:zone:update error: ${error instanceof Error ? error.message : String(error)}`)
callback(null) return callback(null)
} }
} }
} }

View File

@ -1,13 +1,14 @@
import { Server } from 'socket.io' import { Server } from 'socket.io'
import Logger, { LoggerType } from '#application/logger' import Logger, { LoggerType } from '#application/logger'
import SocketManager from '#managers/socketManager'
import worldRepository from '#repositories/worldRepository' import worldRepository from '#repositories/worldRepository'
import worldService from '#services/worldService' import worldService from '#services/worldService'
import SocketManager from '#managers/socketManager'
class DateManager { class DateManager {
private static readonly CONFIG = { private static readonly CONFIG = {
GAME_SPEED: 8, // 24 game hours / 3 real hours GAME_SPEED: 8, // 24 game hours / 3 real hours
UPDATE_INTERVAL: 1000, // 1 second UPDATE_INTERVAL: 1000 // 1 second
} as const } as const
private io: Server | null = null private io: Server | null = null
@ -98,4 +99,4 @@ class DateManager {
} }
} }
export default new DateManager() export default new DateManager()

View File

@ -1,8 +1,9 @@
import { Server } from 'socket.io' import { Server } from 'socket.io'
import Logger, { LoggerType } from '#application/logger' import Logger, { LoggerType } from '#application/logger'
import SocketManager from '#managers/socketManager'
import worldRepository from '#repositories/worldRepository' import worldRepository from '#repositories/worldRepository'
import worldService from '#services/worldService' import worldService from '#services/worldService'
import SocketManager from '#managers/socketManager'
type WeatherState = { type WeatherState = {
isRainEnabled: boolean isRainEnabled: boolean
@ -91,22 +92,12 @@ class WeatherManager {
private updateWeatherProperty(type: 'rain' | 'fog'): void { private updateWeatherProperty(type: 'rain' | 'fog'): void {
if (type === 'rain') { if (type === 'rain') {
this.weatherState.isRainEnabled = !this.weatherState.isRainEnabled this.weatherState.isRainEnabled = !this.weatherState.isRainEnabled
this.weatherState.rainPercentage = this.weatherState.isRainEnabled this.weatherState.rainPercentage = this.weatherState.isRainEnabled ? this.getRandomNumber(WeatherManager.CONFIG.RAIN_PERCENTAGE_RANGE.min, WeatherManager.CONFIG.RAIN_PERCENTAGE_RANGE.max) : 0
? this.getRandomNumber(
WeatherManager.CONFIG.RAIN_PERCENTAGE_RANGE.min,
WeatherManager.CONFIG.RAIN_PERCENTAGE_RANGE.max
)
: 0
} }
if (type === 'fog') { if (type === 'fog') {
this.weatherState.isFogEnabled = !this.weatherState.isFogEnabled this.weatherState.isFogEnabled = !this.weatherState.isFogEnabled
this.weatherState.fogDensity = this.weatherState.isFogEnabled this.weatherState.fogDensity = this.weatherState.isFogEnabled ? this.getRandomNumber(WeatherManager.CONFIG.FOG_DENSITY_RANGE.min, WeatherManager.CONFIG.FOG_DENSITY_RANGE.max) : 0
? this.getRandomNumber(
WeatherManager.CONFIG.FOG_DENSITY_RANGE.min,
WeatherManager.CONFIG.FOG_DENSITY_RANGE.max
)
: 0
} }
} }
@ -132,10 +123,8 @@ class WeatherManager {
} }
private logError(operation: string, error: unknown): void { private logError(operation: string, error: unknown): void {
this.logger.error( this.logger.error(`Failed to ${operation} weather: ${error instanceof Error ? error.message : String(error)}`)
`Failed to ${operation} weather: ${error instanceof Error ? error.message : String(error)}`
)
} }
} }
export default new WeatherManager() export default new WeatherManager()

View File

@ -43,8 +43,8 @@ export class Server {
SocketManager.boot(this.app, this.http), SocketManager.boot(this.app, this.http),
QueueManager.boot(), QueueManager.boot(),
UserManager.boot(), UserManager.boot(),
DateManager.boot(), // DateManager.boot(),
WeatherManager.boot(), // WeatherManager.boot(),
ZoneManager.boot(), ZoneManager.boot(),
ConsoleManager.boot() ConsoleManager.boot()
]) ])