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 { TSocket } from '#application/types'
import { Character } from '#entities/character'
import CharacterRepository from '#repositories/characterRepository'
export abstract class BaseEvent {
protected readonly logger = Logger.type(LoggerType.GAME)
@ -11,6 +13,15 @@ export abstract class BaseEvent {
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 {
this.socket.emit('notification', { title: 'Server message', message })
this.logger.error('character:connect error', `Player ${this.socket.userId}: ${message}`)

View File

@ -1,79 +1,35 @@
import { Server } from 'socket.io'
import type { Prisma } from '@prisma/client'
import { gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma'
import { TSocket } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import { UUID } from '#application/types'
import { Sprite } from '#entities/sprite'
import CharacterRepository from '#repositories/characterRepository'
import SpriteRepository from '#repositories/spriteRepository'
interface CopyPayload {
id: string
id: UUID
}
export default class SpriteCopyEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
export default class SpriteCopyEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:sprite:copy', this.handleEvent.bind(this))
}
private async handleEvent(payload: CopyPayload, callback: (success: boolean) => void): Promise<void> {
try {
if (!(await this.validateGameMasterAccess())) {
return callback(false)
}
if (!(await this.isCharacterGM())) return
const sourceSprite = await prisma.sprite.findUnique({
where: { id: payload.id },
include: {
spriteActions: true
}
})
const sourceSprite = await SpriteRepository.getById(payload.id)
if (!sourceSprite) {
throw new Error('Source sprite not found')
}
const newSprite = await prisma.sprite.create({
data: {
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
}))
}
}
})
const newSprite = new Sprite()
await newSprite.setName(`${sourceSprite.getName()} (Copy)`).setSpriteActions(sourceSprite.getSpriteActions()).save()
callback(true)
} 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 prisma from '#application/prisma'
import { BaseEvent } from '#application/base/baseEvent'
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(
private readonly io: Server,
private readonly socket: TSocket
@ -19,12 +17,7 @@ export default class SpriteCreateEvent {
private async handleEvent(data: undefined, callback: (response: boolean) => void): Promise<void> {
try {
const character = await characterRepository.getById(this.socket.characterId!)
if (!character) return callback(false)
if (character.role !== 'gm') {
return callback(false)
}
if (!(await this.isCharacterGM())) return
const public_folder = Storage.getPublicPath('sprites')

View File

@ -1,45 +1,30 @@
import fs from 'fs'
import { Server } from 'socket.io'
import { gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma'
import { BaseEvent } from '#application/base/baseEvent'
import Storage from '#application/storage'
import { TSocket } from '#application/types'
import CharacterRepository from '#repositories/characterRepository'
import { UUID } from '#application/types'
import SpriteRepository from '#repositories/spriteRepository'
type Payload = {
id: string
id: UUID
}
export default class GMSpriteDeleteEvent {
private readonly public_folder: string
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {
this.public_folder = Storage.getPublicPath('sprites')
}
export default class GMSpriteDeleteEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:sprite:delete', this.handleEvent.bind(this))
}
private async handleEvent(data: Payload, callback: (response: boolean) => void): Promise<void> {
const character = await CharacterRepository.getById(this.socket.characterId!)
if (character?.role !== 'gm') {
return callback(false)
}
if (!(await this.isCharacterGM())) return
try {
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)
} catch (error: any) {
gameMasterLogger.error('gm:sprite:delete error', error.message)
this.logger.error('gm:sprite:delete error', error.message)
callback(false)
}
}
@ -51,12 +36,4 @@ export default class GMSpriteDeleteEvent {
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 { Server } from 'socket.io'
import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository'
import { BaseEvent } from '#application/base/baseEvent'
import SpriteRepository from '#repositories/spriteRepository'
interface IPayload {}
export default class SpriteListEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
export default class SpriteListEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:sprite:list', this.handleEvent.bind(this))
}
private async handleEvent(data: any, callback: (response: Sprite[]) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId!)
if (!character) return callback([])
if (character.role !== 'gm') {
return callback([])
}
private async handleEvent(data: IPayload, callback: (response: Sprite[]) => void): Promise<void> {
if (!(await this.isCharacterGM())) return
// get all sprites
const sprites = await SpriteRepository.getAll()

View File

@ -1,15 +1,11 @@
import { writeFile, mkdir } from 'node:fs/promises'
import sharp from 'sharp'
import { Server } from 'socket.io'
import type { Prisma, SpriteAction } from '@prisma/client'
import { gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma'
import { BaseEvent } from '#application/base/baseEvent'
import Storage from '#application/storage'
import { TSocket } from '#application/types'
import CharacterRepository from '#repositories/characterRepository'
import { SpriteAction } from '#entities/spriteAction'
import SpriteRepository from '#repositories/spriteRepository'
// Constants
const ISOMETRIC_CONFIG = {
@ -41,7 +37,7 @@ interface SpriteActionInput extends Omit<SpriteAction, 'id' | 'spriteId' | 'fram
interface UpdatePayload {
id: string
name: string
spriteActions: Prisma.JsonValue
spriteActions: SpriteAction[]
}
interface ProcessedSpriteAction extends SpriteActionInput {
@ -62,21 +58,14 @@ interface SpriteAnalysis {
contentBounds: ContentBounds
}
export default class SpriteUpdateEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
export default class SpriteUpdateEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:sprite:update', this.handleEvent.bind(this))
}
private async handleEvent(payload: UpdatePayload, callback: (success: boolean) => void): Promise<void> {
try {
if (!(await this.validateGameMasterAccess())) {
return callback(false)
}
if (!(await this.isCharacterGM())) return
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[] {
try {
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) {
@ -392,7 +378,7 @@ export default class SpriteUpdateEvent {
}
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)
}

View File

@ -1,60 +1,36 @@
import fs from 'fs/promises'
import { Server } from 'socket.io'
import { gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma'
import { BaseEvent } from '#application/base/baseEvent'
import Storage from '#application/storage'
import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository'
import { UUID } from '#application/types'
import TileRepository from '#repositories/tileRepository'
type Payload = {
id: string
id: UUID
}
export default class GMTileDeleteEvent {
private readonly public_folder: string
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {
this.public_folder = Storage.getPublicPath('tiles')
}
export default class GMTileDeleteEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:tile:delete', this.handleEvent.bind(this))
}
private async handleEvent(data: Payload, callback: (response: boolean) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) return callback(false)
if (character.role !== 'gm') {
return
}
if (!(await this.isCharacterGM())) return
try {
gameMasterLogger.info(`Deleting tile ${data.id}`)
await this.deleteTileFromDatabase(data.id)
this.logger.info(`Deleting tile ${data.id}`)
await this.deleteTileFile(data.id)
await (await TileRepository.getById(data.id))?.delete()
gameMasterLogger.info(`Tile ${data.id} deleted successfully.`)
callback(true)
} catch (error: any) {
gameMasterLogger.error('gm:tile:delete error', error.message)
callback(false)
this.logger.info(`Tile ${data.id} deleted successfully.`)
return callback(true)
} catch (error: unknown) {
this.logger.error('gm:tile:delete error', error)
return callback(false)
}
}
private async deleteTileFromDatabase(tileId: string): Promise<void> {
await prisma.tile.delete({
where: {
id: tileId
}
})
}
private async deleteTileFile(tileId: string): Promise<void> {
const finalFilePath = Storage.getPublicPath('tiles', `${tileId}.png`)
try {
@ -63,7 +39,7 @@ export default class GMTileDeleteEvent {
if (error.code !== 'ENOENT') {
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 { Tile } from '#entities/tile'
import TileRepository from '#repositories/tileRepository'
interface IPayload {}
@ -11,15 +10,10 @@ export default class TileListEven extends BaseEvent {
}
private async handleEvent(data: IPayload, callback: (response: Tile[]) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId!)
if (!character) return
if (character.role !== 'gm') {
return
}
if (!(await this.isCharacterGM())) return
// get all tiles
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 TileRepository from '#repositories/tileRepository'
import { UUID } from '#application/types'
import TileRepository from '#repositories/tileRepository'
type Payload = {
id: UUID
@ -9,29 +8,22 @@ type Payload = {
tags: string[]
}
export default class TileUpdateEvent extends BaseEvent{
export default class TileUpdateEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:tile:update', this.handleEvent.bind(this))
}
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId!)
if (!character) return callback(false)
if (character.role !== 'gm') {
return
}
if (!(await this.isCharacterGM())) return
try {
const tile = await TileRepository.getById(data.id)
if (!tile) return callback(false)
await tile.setName(data.name).setTags(data.tags).update()
callback(true)
return callback(true)
} catch (error) {
console.error(error)
callback(false)
return callback(false)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,15 +1,16 @@
import { Zone, ZoneEffect, ZoneEventTileType, ZoneObject } from '@prisma/client'
import { Server } from 'socket.io'
import { gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma'
import { TSocket } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import { ZoneEventTileType } from '#application/enums'
import { UUID } from '#application/types'
import { Zone } from '#entities/zone'
import { ZoneEffect } from '#entities/zoneEffect'
import { ZoneEventTile } from '#entities/zoneEventTile'
import { ZoneEventTileTeleport } from '#entities/zoneEventTileTeleport'
import { ZoneObject } from '#entities/zoneObject'
import zoneManager from '#managers/zoneManager'
import CharacterRepository from '#repositories/characterRepository'
import ZoneRepository from '#repositories/zoneRepository'
interface IPayload {
zoneId: number
zoneId: UUID
name: string
width: number
height: number
@ -20,7 +21,7 @@ interface IPayload {
positionX: number
positionY: number
teleport?: {
toZoneId: number
toZoneId: UUID
toPositionX: number
toPositionY: number
toRotation: number
@ -33,44 +34,31 @@ interface IPayload {
zoneObjects: ZoneObject[]
}
export default class ZoneUpdateEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
export default class ZoneUpdateEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:zone_editor:zone:update', this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: Zone | null) => void): Promise<void> {
try {
const character = await CharacterRepository.getById(this.socket.characterId as number)
if (!character) {
gameMasterLogger.error('gm:zone_editor:zone:update error', 'Character not found')
return callback(null)
}
if (!(await this.isCharacterGM())) return
if (character.role !== 'gm') {
gameMasterLogger.info(`User ${character.id} tried to update zone but is not a game master.`)
return callback(null)
}
gameMasterLogger.info(`User ${character.id} has updated zone via zone editor.`)
const character = await this.getCharacter()
this.logger.info(`User ${character!.getId()} has updated zone via zone editor.`)
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)
}
let zone = await ZoneRepository.getById(data.zoneId)
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)
}
// If tiles are larger than the zone, remove the extra tiles
// Validation logic remains the same
if (data.tiles.length > 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)
// 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)
await prisma.zone.update({
where: { id: data.zoneId },
data: {
name: data.name,
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()
}
})
// Clear existing collections
zone.zoneEventTiles.removeAll()
zone.zoneObjects.removeAll()
zone.zoneEffects.removeAll()
// 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)
if (!zone) {
gameMasterLogger.info(`User ${character.id} tried to update zone ${data.zoneId} but it does not exist after update.`)
callback(null)
return
this.logger.info(`User ${character!.getId()} tried to update zone ${data.zoneId} but it does not exist after update.`)
return callback(null)
}
gameMasterLogger.info(`User ${character.id} has updated zone via zone editor.`)
callback(zone)
/**
* @TODO #246: Reload zone for players who are currently in the zone
*/
// Reload zone for players
zoneManager.unloadZone(data.zoneId)
await zoneManager.loadZone(zone)
return callback(zone)
} catch (error: any) {
gameMasterLogger.error(`gm:zone_editor:zone:update error: ${error instanceof Error ? error.message : String(error)}`)
callback(null)
this.logger.error(`gm:zone_editor:zone:update error: ${error instanceof Error ? error.message : String(error)}`)
return callback(null)
}
}
}

View File

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

View File

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

View File

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