Renamed folder
This commit is contained in:
18
src/events/character/charactersScreen/characterHairList.ts
Normal file
18
src/events/character/charactersScreen/characterHairList.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import Database from '#application/database'
|
||||
import { CharacterHair } from '#entities/characterHair'
|
||||
import characterHairRepository from '#repositories/characterHairRepository'
|
||||
|
||||
interface IPayload {}
|
||||
|
||||
export default class characterHairListEvent extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('character:hair:list', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: IPayload, callback: (response: CharacterHair[]) => void): Promise<void> {
|
||||
const items: CharacterHair[] = await characterHairRepository.getAllSelectable()
|
||||
await Database.getEntityManager().populate(items, ['sprite'])
|
||||
callback(items)
|
||||
}
|
||||
}
|
64
src/events/character/connect.ts
Normal file
64
src/events/character/connect.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import ZoneManager from '#managers/zoneManager'
|
||||
import CharacterHairRepository from '#repositories/characterHairRepository'
|
||||
import CharacterRepository from '#repositories/characterRepository'
|
||||
|
||||
interface CharacterConnectPayload {
|
||||
characterId: number
|
||||
characterHairId?: number
|
||||
}
|
||||
|
||||
export default class CharacterConnectEvent extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('character:connect', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent({ characterId, characterHairId }: CharacterConnectPayload): Promise<void> {
|
||||
if (!this.socket.userId) {
|
||||
this.emitError('User not authenticated')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (await this.checkForActiveCharacters()) {
|
||||
this.emitError('You are already connected to another character')
|
||||
return
|
||||
}
|
||||
|
||||
const character = await CharacterRepository.getByUserAndId(this.socket.userId, characterId)
|
||||
|
||||
if (!character) {
|
||||
this.emitError('Character not found or does not belong to this user')
|
||||
return
|
||||
}
|
||||
|
||||
// Set character id
|
||||
this.socket.characterId = character.id
|
||||
|
||||
// Set character hair
|
||||
const characterHair = await CharacterHairRepository.getById(characterHairId ?? 0)
|
||||
await character.setCharacterHair(characterHair).update()
|
||||
|
||||
// Emit character connect event
|
||||
this.socket.emit('character:connect', character)
|
||||
} catch (error) {
|
||||
this.handleError('Failed to connect character', error) // @TODO : Make global error handler
|
||||
}
|
||||
}
|
||||
|
||||
private async checkForActiveCharacters(): Promise<boolean> {
|
||||
const characters = await CharacterRepository.getByUserId(this.socket.userId!)
|
||||
return characters?.some((char) => ZoneManager.getCharacterById(char.id)) ?? false
|
||||
}
|
||||
|
||||
private emitError(message: string): void {
|
||||
this.socket.emit('notification', { title: 'Server message', message })
|
||||
this.logger.error('character:connect error', `Player ${this.socket.userId}: ${message}`)
|
||||
}
|
||||
|
||||
private handleError(context: string, error: unknown): void {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
this.emitError(`${context}: ${errorMessage}`)
|
||||
this.logger.error('character:connect error', errorMessage)
|
||||
}
|
||||
}
|
57
src/events/character/create.ts
Normal file
57
src/events/character/create.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { ZodError } from 'zod'
|
||||
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import { ZCharacterCreate } from '#application/zodTypes'
|
||||
import { Character } from '#entities/character'
|
||||
import CharacterRepository from '#repositories/characterRepository'
|
||||
import UserRepository from '#repositories/userRepository'
|
||||
|
||||
export default class CharacterCreateEvent extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('character:create', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: any): Promise<any> {
|
||||
// zod validate
|
||||
try {
|
||||
data = ZCharacterCreate.parse(data)
|
||||
|
||||
const user = await UserRepository.getById(this.socket.userId!)
|
||||
|
||||
if (!user) {
|
||||
return this.socket.emit('notification', { message: 'User not found' })
|
||||
}
|
||||
|
||||
// Check if character name already exists
|
||||
const characterExists = await CharacterRepository.getByName(data.name)
|
||||
|
||||
if (characterExists) {
|
||||
return this.socket.emit('notification', { message: 'Character name already exists' })
|
||||
}
|
||||
|
||||
let characters: Character[] = await CharacterRepository.getByUserId(user.getId())
|
||||
|
||||
if (characters.length >= 4) {
|
||||
return this.socket.emit('notification', { message: 'You can only have 4 characters' })
|
||||
}
|
||||
|
||||
const newCharacter = new Character()
|
||||
await newCharacter.setName(data.name).setUser(user).save()
|
||||
|
||||
if (!newCharacter) return this.socket.emit('notification', { message: 'Failed to create character. Please try again (later).' })
|
||||
|
||||
characters = [...characters, newCharacter]
|
||||
|
||||
this.socket.emit('character:create:success')
|
||||
this.socket.emit('character:list', characters)
|
||||
|
||||
this.logger.info('character:create success')
|
||||
} catch (error: any) {
|
||||
this.logger.error(`character:create error: ${error.message}`)
|
||||
if (error instanceof ZodError) {
|
||||
return this.socket.emit('notification', { message: error.issues[0].message })
|
||||
}
|
||||
return this.socket.emit('notification', { message: 'Could not create character. Please try again (later).' })
|
||||
}
|
||||
}
|
||||
}
|
34
src/events/character/delete.ts
Normal file
34
src/events/character/delete.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import { Character } from '#entities/character'
|
||||
import { Zone } from '#entities/zone'
|
||||
import CharacterRepository from '#repositories/characterRepository'
|
||||
|
||||
type TypePayload = {
|
||||
characterId: number
|
||||
}
|
||||
|
||||
type TypeResponse = {
|
||||
zone: Zone
|
||||
characters: Character[]
|
||||
}
|
||||
|
||||
export default class CharacterDeleteEvent extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('character:delete', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: TypePayload, callback: (response: TypeResponse) => void): Promise<any> {
|
||||
try {
|
||||
const character = await CharacterRepository.getByUserAndId(this.socket.userId!, data.characterId)
|
||||
if (character) {
|
||||
await character.delete()
|
||||
}
|
||||
|
||||
const characters: Character[] = await CharacterRepository.getByUserId(this.socket.userId!)
|
||||
|
||||
this.socket.emit('character:list', characters)
|
||||
} catch (error: any) {
|
||||
return this.socket.emit('notification', { message: 'Character delete failed. Please try again.' })
|
||||
}
|
||||
}
|
||||
}
|
21
src/events/character/list.ts
Normal file
21
src/events/character/list.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import Database from '#application/database'
|
||||
import { Character } from '#entities/character'
|
||||
import CharacterRepository from '#repositories/characterRepository'
|
||||
|
||||
export default class CharacterListEvent extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('character:list', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: any): Promise<void> {
|
||||
try {
|
||||
const characters: Character[] = await CharacterRepository.getByUserId(this.socket.userId!)
|
||||
await Database.getEntityManager().populate(characters, ['characterType', 'characterHair'])
|
||||
|
||||
this.socket.emit('character:list', characters)
|
||||
} catch (error: any) {
|
||||
this.logger.error('character:list error', error.message)
|
||||
}
|
||||
}
|
||||
}
|
46
src/events/chat/gameMaster/alertCommand.ts
Normal file
46
src/events/chat/gameMaster/alertCommand.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import CharacterRepository from '#repositories/characterRepository'
|
||||
import ChatService from '#services/chatService'
|
||||
|
||||
type TypePayload = {
|
||||
message: string
|
||||
}
|
||||
|
||||
export default class AlertCommandEvent extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('chat:message', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
|
||||
try {
|
||||
if (!ChatService.isCommand(data.message, 'alert')) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if character exists
|
||||
const character = await CharacterRepository.getByUserAndId(this.socket.userId!, this.socket.characterId!)
|
||||
if (!character) {
|
||||
this.logger.error('chat:alert_command error', 'Character not found')
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
// Check if the user is the GM
|
||||
if (character.role !== 'gm') {
|
||||
this.logger.info(`User ${character.id} tried to set time but is not a game master.`)
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
const args = ChatService.getArgs('alert', data.message)
|
||||
|
||||
if (!args) {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
this.io.emit('notification', { title: 'Message from GM', message: args.join(' ') })
|
||||
return callback(true)
|
||||
} catch (error: any) {
|
||||
this.logger.error('chat:alert_command error', error.message)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
53
src/events/chat/gameMaster/setTimeCommand.ts
Normal file
53
src/events/chat/gameMaster/setTimeCommand.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import DateManager from '#managers/dateManager'
|
||||
import CharacterRepository from '#repositories/characterRepository'
|
||||
import ChatService from '#services/chatService'
|
||||
|
||||
type TypePayload = {
|
||||
message: string
|
||||
}
|
||||
|
||||
export default class SetTimeCommand extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('chat:message', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
|
||||
try {
|
||||
if (!ChatService.isCommand(data.message, 'time')) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if character exists
|
||||
const character = await CharacterRepository.getByUserAndId(this.socket.userId!, this.socket.characterId!)
|
||||
if (!character) {
|
||||
this.logger.error('chat:alert_command error', 'Character not found')
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the user is the GM
|
||||
if (character.role !== 'gm') {
|
||||
this.logger.info(`User ${character.id} tried to set time but is not a game master.`)
|
||||
return
|
||||
}
|
||||
|
||||
// Get arguments
|
||||
const args = ChatService.getArgs('time', data.message)
|
||||
|
||||
if (!args) {
|
||||
return
|
||||
}
|
||||
|
||||
const time = args[0] // 24h time, e.g. 17:34
|
||||
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
await DateManager.setTime(time)
|
||||
} catch (error: any) {
|
||||
this.logger.error('command error', error.message)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
88
src/events/chat/gameMaster/teleportCommand.ts
Normal file
88
src/events/chat/gameMaster/teleportCommand.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import ZoneManager from '#managers/zoneManager'
|
||||
import zoneManager from '#managers/zoneManager'
|
||||
import ZoneCharacter from '#models/zoneCharacter'
|
||||
import ZoneRepository from '#repositories/zoneRepository'
|
||||
import ChatService from '#services/chatService'
|
||||
|
||||
type TypePayload = {
|
||||
message: string
|
||||
}
|
||||
|
||||
export default class TeleportCommandEvent extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('chat:message', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
|
||||
try {
|
||||
// Check if character exists
|
||||
const zoneCharacter = ZoneManager.getCharacterById(this.socket.characterId!)
|
||||
if (!zoneCharacter) {
|
||||
this.logger.error('chat:message error', 'Character not found')
|
||||
return
|
||||
}
|
||||
|
||||
const character = zoneCharacter.character
|
||||
|
||||
// Check if the user is the GM
|
||||
if (character.role !== 'gm') {
|
||||
this.logger.info(`User ${character.id} tried to set time but is not a game master.`)
|
||||
return
|
||||
}
|
||||
|
||||
if (!ChatService.isCommand(data.message, 'teleport')) return
|
||||
|
||||
const args = ChatService.getArgs('teleport', data.message)
|
||||
|
||||
if (!args || args.length !== 1) {
|
||||
this.socket.emit('notification', { title: 'Server message', message: 'Usage: /teleport <zoneId>' })
|
||||
return
|
||||
}
|
||||
|
||||
const zoneId = parseInt(args[0], 10)
|
||||
if (isNaN(zoneId)) {
|
||||
this.socket.emit('notification', { title: 'Server message', message: 'Invalid zone ID' })
|
||||
return
|
||||
}
|
||||
|
||||
const zone = await ZoneRepository.getById(zoneId)
|
||||
if (!zone) {
|
||||
this.socket.emit('notification', { title: 'Server message', message: 'Zone not found' })
|
||||
return
|
||||
}
|
||||
|
||||
if (character.zoneId === zone.id) {
|
||||
this.socket.emit('notification', { title: 'Server message', message: 'You are already in that zone' })
|
||||
return
|
||||
}
|
||||
|
||||
// Remove character from current zone
|
||||
zoneManager.removeCharacter(character.id)
|
||||
this.io.to(character.zoneId.toString()).emit('zone:character:leave', character.id)
|
||||
this.socket.leave(character.zoneId.toString())
|
||||
|
||||
// Add character to new zone
|
||||
zoneManager.getZoneById(zone.id)?.addCharacter(character)
|
||||
this.io.to(zone.id.toString()).emit('zone:character:join', character)
|
||||
this.socket.join(zone.id.toString())
|
||||
|
||||
character.zoneId = zone.id
|
||||
character.positionX = 0
|
||||
character.positionY = 0
|
||||
|
||||
zoneCharacter.isMoving = false
|
||||
|
||||
this.socket.emit('zone:character:teleport', {
|
||||
zone,
|
||||
characters: ZoneManager.getZoneById(zone.id)?.getCharactersInZone()
|
||||
})
|
||||
|
||||
this.socket.emit('notification', { title: 'Server message', message: `You have been teleported to ${zone.name}` })
|
||||
this.logger.info('teleport', `Character ${character.id} teleported to zone ${zone.id}`)
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Error in teleport command: ${error.message}`)
|
||||
this.socket.emit('notification', { title: 'Server message', message: 'An error occurred while teleporting' })
|
||||
}
|
||||
}
|
||||
}
|
40
src/events/chat/gameMaster/toggleFogCommand.ts
Normal file
40
src/events/chat/gameMaster/toggleFogCommand.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import WeatherManager from '#managers/weatherManager'
|
||||
import CharacterRepository from '#repositories/characterRepository'
|
||||
import ChatService from '#services/chatService'
|
||||
|
||||
type TypePayload = {
|
||||
message: string
|
||||
}
|
||||
|
||||
export default class ToggleFogCommand extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('chat:message', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
|
||||
try {
|
||||
if (!ChatService.isCommand(data.message, 'fog')) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if character exists
|
||||
const character = await CharacterRepository.getByUserAndId(this.socket.userId!, this.socket.characterId!)
|
||||
if (!character) {
|
||||
this.logger.error('chat:alert_command error', 'Character not found')
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the user is the GM
|
||||
if (character.role !== 'gm') {
|
||||
this.logger.info(`User ${character.id} tried to set time but is not a game master.`)
|
||||
return
|
||||
}
|
||||
|
||||
await WeatherManager.toggleFog()
|
||||
} catch (error: any) {
|
||||
this.logger.error('command error', error.message)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
40
src/events/chat/gameMaster/toggleRainCommand.ts
Normal file
40
src/events/chat/gameMaster/toggleRainCommand.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import WeatherManager from '#managers/weatherManager'
|
||||
import CharacterRepository from '#repositories/characterRepository'
|
||||
import ChatService from '#services/chatService'
|
||||
|
||||
type TypePayload = {
|
||||
message: string
|
||||
}
|
||||
|
||||
export default class ToggleRainCommand extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('chat:message', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
|
||||
try {
|
||||
if (!ChatService.isCommand(data.message, 'rain')) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if character exists
|
||||
const character = await CharacterRepository.getByUserAndId(this.socket.userId!, this.socket.characterId!)
|
||||
if (!character) {
|
||||
this.logger.error('chat:alert_command error', 'Character not found')
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the user is the GM
|
||||
if (character.role !== 'gm') {
|
||||
this.logger.info(`User ${character.id} tried to set time but is not a game master.`)
|
||||
return
|
||||
}
|
||||
|
||||
await WeatherManager.toggleRain()
|
||||
} catch (error: any) {
|
||||
this.logger.error('command error', error.message)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
45
src/events/chat/message.ts
Normal file
45
src/events/chat/message.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import ZoneManager from '#managers/zoneManager'
|
||||
import ZoneRepository from '#repositories/zoneRepository'
|
||||
import ChatService from '#services/chatService'
|
||||
|
||||
type TypePayload = {
|
||||
message: string
|
||||
}
|
||||
|
||||
export default class ChatMessageEvent extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('chat:message', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
|
||||
try {
|
||||
if (!data.message || ChatService.isCommand(data.message)) {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
const zoneCharacter = ZoneManager.getCharacterById(this.socket.characterId!)
|
||||
if (!zoneCharacter) {
|
||||
this.logger.error('chat:message error', 'Character not found')
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
const character = zoneCharacter.character
|
||||
|
||||
const zone = await ZoneRepository.getById(character.zone?.id!)
|
||||
if (!zone) {
|
||||
this.logger.error('chat:message error', 'Zone not found')
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
if (await ChatService.sendZoneMessage(this.io, this.socket, data.message, character.id, zone.id)) {
|
||||
return callback(true)
|
||||
}
|
||||
|
||||
callback(false)
|
||||
} catch (error: any) {
|
||||
this.logger.error('chat:message error', error.message)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
40
src/events/disconnect.ts
Normal file
40
src/events/disconnect.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import ZoneManager from '#managers/zoneManager'
|
||||
|
||||
export default class DisconnectEvent extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('disconnect', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: any): Promise<void> {
|
||||
try {
|
||||
if (!this.socket.userId) {
|
||||
this.logger.info('User disconnected but had no user set')
|
||||
return
|
||||
}
|
||||
|
||||
this.io.emit('user:disconnect', this.socket.userId)
|
||||
|
||||
const zoneCharacter = ZoneManager.getCharacterById(this.socket.characterId!)
|
||||
if (!zoneCharacter) {
|
||||
this.logger.info('User disconnected but had no character set')
|
||||
return
|
||||
}
|
||||
|
||||
const character = zoneCharacter.character
|
||||
|
||||
// Save character position and remove from zone
|
||||
zoneCharacter.isMoving = false
|
||||
await zoneCharacter.savePosition()
|
||||
ZoneManager.removeCharacter(this.socket.characterId!)
|
||||
|
||||
this.logger.info('User disconnected along with their character')
|
||||
|
||||
// Inform other clients that the character has left
|
||||
this.io.in(character.zone!.id.toString()).emit('zone:character:leave', character.id)
|
||||
this.io.emit('character:disconnect', character.id)
|
||||
} catch (error: any) {
|
||||
this.logger.error('disconnect error: ' + error.message)
|
||||
}
|
||||
}
|
||||
}
|
28
src/events/gameMaster/assetManager/characterHair/create.ts
Normal file
28
src/events/gameMaster/assetManager/characterHair/create.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import { CharacterHair } from '#entities/characterHair'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
|
||||
export default class CharacterHairCreateEvent extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('gm:characterHair:create', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: undefined, callback: (response: boolean, characterType?: any) => void): Promise<void> {
|
||||
try {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
const newCharacterHair = new CharacterHair()
|
||||
await newCharacterHair.setName('New hair').save()
|
||||
|
||||
callback(true, newCharacterHair)
|
||||
} catch (error) {
|
||||
console.error('Error creating character hair:', error)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
34
src/events/gameMaster/assetManager/characterHair/delete.ts
Normal file
34
src/events/gameMaster/assetManager/characterHair/delete.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import CharacterHairRepository from '#repositories/characterHairRepository'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
|
||||
interface IPayload {
|
||||
id: number
|
||||
}
|
||||
|
||||
export default class characterHairDeleteEvent extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('gm:characterHair:remove', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: IPayload, 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 callback(false)
|
||||
}
|
||||
|
||||
try {
|
||||
const characterHair = await CharacterHairRepository.getById(data.id)
|
||||
if (characterHair) {
|
||||
await characterHair.delete()
|
||||
}
|
||||
|
||||
callback(true)
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting character type ${data.id}: ${error instanceof Error ? error.message : String(error)}`)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
29
src/events/gameMaster/assetManager/characterHair/list.ts
Normal file
29
src/events/gameMaster/assetManager/characterHair/list.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import { CharacterHair } from '#entities/characterHair'
|
||||
import characterHairRepository from '#repositories/characterHairRepository'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
|
||||
interface IPayload {}
|
||||
|
||||
export default class characterHairListEvent extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('gm:characterHair:list', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: IPayload, callback: (response: CharacterHair[]) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) {
|
||||
this.logger.error('gm:characterHair:list error', 'Character not found')
|
||||
return callback([])
|
||||
}
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
this.logger.info(`User ${character.id} tried to list character hair but is not a game master.`)
|
||||
return callback([])
|
||||
}
|
||||
|
||||
// get all objects
|
||||
const items = await characterHairRepository.getAll()
|
||||
callback(items)
|
||||
}
|
||||
}
|
43
src/events/gameMaster/assetManager/characterHair/update.ts
Normal file
43
src/events/gameMaster/assetManager/characterHair/update.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import { CharacterGender } from '#application/enums'
|
||||
import { UUID } from '#application/types'
|
||||
import CharacterHairRepository from '#repositories/characterHairRepository'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
import SpriteRepository from '#repositories/spriteRepository'
|
||||
|
||||
type Payload = {
|
||||
id: number
|
||||
name: string
|
||||
gender: CharacterGender
|
||||
isSelectable: boolean
|
||||
spriteId: UUID
|
||||
}
|
||||
|
||||
export default class CharacterHairUpdateEvent extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('gm:characterHair:update', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
try {
|
||||
const sprite = await SpriteRepository.getById(data.spriteId)
|
||||
const characterHair = await CharacterHairRepository.getById(data.id)
|
||||
|
||||
if (characterHair) {
|
||||
await characterHair.setName(data.name).setGender(data.gender).setIsSelectable(data.isSelectable).setSprite(sprite!).update()
|
||||
}
|
||||
|
||||
return callback(true)
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating character hair: ${error instanceof Error ? error.message : String(error)}`)
|
||||
return callback(false)
|
||||
}
|
||||
}
|
||||
}
|
41
src/events/gameMaster/assetManager/characterType/create.ts
Normal file
41
src/events/gameMaster/assetManager/characterType/create.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { CharacterGender, CharacterRace } from '@prisma/client'
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import prisma from '#application/prisma'
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
|
||||
export default class CharacterTypeCreateEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:characterType:create', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: undefined, callback: (response: boolean, characterType?: any) => void): Promise<void> {
|
||||
try {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
const newCharacterType = await prisma.characterType.create({
|
||||
data: {
|
||||
name: 'New character type',
|
||||
gender: CharacterGender.MALE,
|
||||
race: CharacterRace.HUMAN
|
||||
}
|
||||
})
|
||||
|
||||
callback(true, newCharacterType)
|
||||
} catch (error) {
|
||||
console.error('Error creating character type:', error)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
41
src/events/gameMaster/assetManager/characterType/delete.ts
Normal file
41
src/events/gameMaster/assetManager/characterType/delete.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import { gameMasterLogger } from '#application/logger'
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
import CharacterTypeRepository from '#repositories/characterTypeRepository'
|
||||
|
||||
interface IPayload {
|
||||
id: number
|
||||
}
|
||||
|
||||
export default class CharacterTypeDeleteEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:characterType:remove', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: IPayload, 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 callback(false)
|
||||
}
|
||||
|
||||
try {
|
||||
const characterType = await CharacterTypeRepository.getById(data.id)
|
||||
if (!characterType) return callback(false)
|
||||
|
||||
await characterType.delete()
|
||||
callback(true)
|
||||
} catch (error) {
|
||||
gameMasterLogger.error(`Error deleting character type ${data.id}: ${error instanceof Error ? error.message : String(error)}`)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
37
src/events/gameMaster/assetManager/characterType/list.ts
Normal file
37
src/events/gameMaster/assetManager/characterType/list.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { CharacterType } from '@prisma/client'
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import { gameMasterLogger } from '#application/logger'
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
import CharacterTypeRepository from '#repositories/characterTypeRepository'
|
||||
|
||||
interface IPayload {}
|
||||
|
||||
export default class CharacterTypeListEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:characterType:list', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: IPayload, callback: (response: CharacterType[]) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) {
|
||||
gameMasterLogger.error('gm:characterType:list error', 'Character not found')
|
||||
return callback([])
|
||||
}
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
gameMasterLogger.info(`User ${character.id} tried to list character types but is not a game master.`)
|
||||
return callback([])
|
||||
}
|
||||
|
||||
// get all objects
|
||||
const items = await CharacterTypeRepository.getAll()
|
||||
callback(items)
|
||||
}
|
||||
}
|
53
src/events/gameMaster/assetManager/characterType/update.ts
Normal file
53
src/events/gameMaster/assetManager/characterType/update.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { CharacterGender, CharacterRace } from '@prisma/client'
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import prisma from '#application/prisma'
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
|
||||
type Payload = {
|
||||
id: number
|
||||
name: string
|
||||
gender: CharacterGender
|
||||
race: CharacterRace
|
||||
isSelectable: boolean
|
||||
spriteId: string
|
||||
}
|
||||
|
||||
export default class CharacterTypeUpdateEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:characterType:update', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.characterType.update({
|
||||
where: { id: data.id },
|
||||
data: {
|
||||
name: data.name,
|
||||
gender: data.gender,
|
||||
race: data.race,
|
||||
isSelectable: data.isSelectable,
|
||||
spriteId: data.spriteId
|
||||
}
|
||||
})
|
||||
|
||||
callback(true)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
42
src/events/gameMaster/assetManager/item/create.ts
Normal file
42
src/events/gameMaster/assetManager/item/create.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import prisma from '#application/prisma'
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
|
||||
export default class ItemCreateEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:item:create', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: undefined, callback: (response: boolean, item?: any) => void): Promise<void> {
|
||||
try {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
const newItem = await prisma.item.create({
|
||||
data: {
|
||||
name: 'New Item',
|
||||
itemType: 'WEAPON',
|
||||
stackable: false,
|
||||
rarity: 'COMMON',
|
||||
spriteId: null
|
||||
}
|
||||
})
|
||||
|
||||
callback(true, newItem)
|
||||
} catch (error) {
|
||||
console.error('Error creating item:', error)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
41
src/events/gameMaster/assetManager/item/delete.ts
Normal file
41
src/events/gameMaster/assetManager/item/delete.ts
Normal file
@ -0,0 +1,41 @@
|
||||
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'
|
||||
|
||||
interface IPayload {
|
||||
id: string
|
||||
}
|
||||
|
||||
export default class ItemDeleteEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:item:remove', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: IPayload, 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 callback(false)
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.item.delete({
|
||||
where: { id: data.id }
|
||||
})
|
||||
|
||||
callback(true)
|
||||
} catch (error) {
|
||||
gameMasterLogger.error(`Error deleting item ${data.id}: ${error instanceof Error ? error.message : String(error)}`)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
37
src/events/gameMaster/assetManager/item/list.ts
Normal file
37
src/events/gameMaster/assetManager/item/list.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { Item } from '@prisma/client'
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import { gameMasterLogger } from '#application/logger'
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
import itemRepository from '#repositories/itemRepository'
|
||||
|
||||
interface IPayload {}
|
||||
|
||||
export default class ItemListEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:item:list', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: IPayload, callback: (response: Item[]) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) {
|
||||
gameMasterLogger.error('gm:item:list error', 'Character not found')
|
||||
return callback([])
|
||||
}
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
gameMasterLogger.info(`User ${character.id} tried to list items but is not a game master.`)
|
||||
return callback([])
|
||||
}
|
||||
|
||||
// get all items
|
||||
const items = await itemRepository.getAll()
|
||||
callback(items)
|
||||
}
|
||||
}
|
56
src/events/gameMaster/assetManager/item/update.ts
Normal file
56
src/events/gameMaster/assetManager/item/update.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { ItemType, ItemRarity } 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'
|
||||
|
||||
type Payload = {
|
||||
id: string
|
||||
name: string
|
||||
description: string | null
|
||||
itemType: ItemType
|
||||
stackable: boolean
|
||||
rarity: ItemRarity
|
||||
spriteId: string | null
|
||||
}
|
||||
|
||||
export default class ItemUpdateEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:item:update', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.item.update({
|
||||
where: { id: data.id },
|
||||
data: {
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
itemType: data.itemType,
|
||||
stackable: data.stackable,
|
||||
rarity: data.rarity,
|
||||
spriteId: data.spriteId
|
||||
}
|
||||
})
|
||||
|
||||
return callback(true)
|
||||
} catch (error) {
|
||||
gameMasterLogger.error(`Error updating item: ${error instanceof Error ? error.message : String(error)}`)
|
||||
return callback(false)
|
||||
}
|
||||
}
|
||||
}
|
32
src/events/gameMaster/assetManager/object/list.ts
Normal file
32
src/events/gameMaster/assetManager/object/list.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { Object } from '@prisma/client'
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
import ObjectRepository from '#repositories/objectRepository'
|
||||
|
||||
interface IPayload {}
|
||||
|
||||
export default class ObjectListEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:object:list', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: IPayload, callback: (response: Object[]) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback([])
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
return callback([])
|
||||
}
|
||||
|
||||
// get all objects
|
||||
const objects = await ObjectRepository.getAll()
|
||||
callback(objects)
|
||||
}
|
||||
}
|
59
src/events/gameMaster/assetManager/object/remove.ts
Normal file
59
src/events/gameMaster/assetManager/object/remove.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import fs from 'fs'
|
||||
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import { gameLogger, gameMasterLogger } from '#application/logger'
|
||||
import prisma from '#application/prisma'
|
||||
import { getPublicPath } from '#application/storage'
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
|
||||
interface IPayload {
|
||||
object: string
|
||||
}
|
||||
|
||||
export default class ObjectRemoveEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:object:remove', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: IPayload, 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 callback(false)
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.object.delete({
|
||||
where: {
|
||||
id: data.object
|
||||
}
|
||||
})
|
||||
|
||||
// get root path
|
||||
const public_folder = getPublicPath('objects')
|
||||
|
||||
// remove the tile from the disk
|
||||
const finalFilePath = getPublicPath('objects', data.object + '.png')
|
||||
fs.unlink(finalFilePath, (err) => {
|
||||
if (err) {
|
||||
gameMasterLogger.error(`Error deleting object ${data.object}: ${err.message}`)
|
||||
callback(false)
|
||||
return
|
||||
}
|
||||
|
||||
callback(true)
|
||||
})
|
||||
} catch (error) {
|
||||
gameLogger.error(`Error deleting object ${data.object}: ${error instanceof Error ? error.message : String(error)}`)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
59
src/events/gameMaster/assetManager/object/update.ts
Normal file
59
src/events/gameMaster/assetManager/object/update.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import prisma from '#application/prisma'
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
|
||||
type Payload = {
|
||||
id: string
|
||||
name: string
|
||||
tags: string[]
|
||||
originX: number
|
||||
originY: number
|
||||
isAnimated: boolean
|
||||
frameRate: number
|
||||
frameWidth: number
|
||||
frameHeight: number
|
||||
}
|
||||
|
||||
export default class ObjectUpdateEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:object:update', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
try {
|
||||
const object = await prisma.object.update({
|
||||
where: {
|
||||
id: data.id
|
||||
},
|
||||
data: {
|
||||
name: data.name,
|
||||
tags: data.tags,
|
||||
originX: data.originX,
|
||||
originY: data.originY,
|
||||
isAnimated: data.isAnimated,
|
||||
frameRate: data.frameRate,
|
||||
frameWidth: data.frameWidth,
|
||||
frameHeight: data.frameHeight
|
||||
}
|
||||
})
|
||||
callback(true)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
73
src/events/gameMaster/assetManager/object/upload.ts
Normal file
73
src/events/gameMaster/assetManager/object/upload.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import fs from 'fs/promises'
|
||||
import { writeFile } from 'node:fs/promises'
|
||||
|
||||
import sharp from 'sharp'
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import { gameMasterLogger } from '#application/logger'
|
||||
import prisma from '#application/prisma'
|
||||
import { getPublicPath } from '#application/storage'
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
|
||||
interface IObjectData {
|
||||
[key: string]: Buffer
|
||||
}
|
||||
|
||||
export default class ObjectUploadEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:object:upload', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: IObjectData, callback: (response: boolean) => void): Promise<void> {
|
||||
try {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
const public_folder = getPublicPath('objects')
|
||||
|
||||
// Ensure the folder exists
|
||||
await fs.mkdir(public_folder, { recursive: true })
|
||||
|
||||
const uploadPromises = Object.entries(data).map(async ([key, objectData]) => {
|
||||
// Get image dimensions
|
||||
const metadata = await sharp(objectData).metadata()
|
||||
const width = metadata.width || 0
|
||||
const height = metadata.height || 0
|
||||
|
||||
const object = await prisma.object.create({
|
||||
data: {
|
||||
name: key,
|
||||
tags: [],
|
||||
originX: 0,
|
||||
originY: 0,
|
||||
frameWidth: width,
|
||||
frameHeight: height
|
||||
}
|
||||
})
|
||||
|
||||
const uuid = object.id
|
||||
const filename = `${uuid}.png`
|
||||
const finalFilePath = getPublicPath('objects', filename)
|
||||
await writeFile(finalFilePath, objectData)
|
||||
|
||||
gameMasterLogger.info('gm:object:upload', `Object ${key} uploaded with id ${uuid}`)
|
||||
})
|
||||
|
||||
await Promise.all(uploadPromises)
|
||||
|
||||
callback(true)
|
||||
} catch (error: any) {
|
||||
gameMasterLogger.error('gm:object:upload error', error.message)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
79
src/events/gameMaster/assetManager/sprite/copy.ts
Normal file
79
src/events/gameMaster/assetManager/sprite/copy.ts
Normal file
@ -0,0 +1,79 @@
|
||||
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 CharacterRepository from '#repositories/characterRepository'
|
||||
|
||||
interface CopyPayload {
|
||||
id: string
|
||||
}
|
||||
|
||||
export default class SpriteCopyEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
const sourceSprite = await prisma.sprite.findUnique({
|
||||
where: { id: payload.id },
|
||||
include: {
|
||||
spriteActions: true
|
||||
}
|
||||
})
|
||||
|
||||
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
|
||||
}))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
callback(true)
|
||||
} catch (error) {
|
||||
this.handleError(error, payload.id, callback)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
51
src/events/gameMaster/assetManager/sprite/create.ts
Normal file
51
src/events/gameMaster/assetManager/sprite/create.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import fs from 'fs/promises'
|
||||
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import prisma from '#application/prisma'
|
||||
import { getPublicPath } from '#application/storage'
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
|
||||
export default class SpriteCreateEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:sprite:create', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
const public_folder = getPublicPath('sprites')
|
||||
|
||||
// Ensure the folder exists
|
||||
await fs.mkdir(public_folder, { recursive: true })
|
||||
|
||||
const sprite = await prisma.sprite.create({
|
||||
data: {
|
||||
name: 'New sprite'
|
||||
}
|
||||
})
|
||||
const uuid = sprite.id
|
||||
|
||||
// Create folder with uuid
|
||||
const sprite_folder = getPublicPath('sprites', uuid)
|
||||
await fs.mkdir(sprite_folder, { recursive: true })
|
||||
|
||||
callback(true)
|
||||
} catch (error) {
|
||||
console.error('Error creating sprite:', error)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
62
src/events/gameMaster/assetManager/sprite/delete.ts
Normal file
62
src/events/gameMaster/assetManager/sprite/delete.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import fs from 'fs'
|
||||
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import { gameMasterLogger } from '#application/logger'
|
||||
import prisma from '#application/prisma'
|
||||
import { getPublicPath } from '#application/storage'
|
||||
import { TSocket } from '#application/types'
|
||||
import CharacterRepository from '#repositories/characterRepository'
|
||||
|
||||
type Payload = {
|
||||
id: string
|
||||
}
|
||||
|
||||
export default class GMSpriteDeleteEvent {
|
||||
private readonly public_folder: string
|
||||
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {
|
||||
this.public_folder = getPublicPath('sprites')
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
try {
|
||||
await this.deleteSpriteFolder(data.id)
|
||||
await this.deleteSpriteFromDatabase(data.id)
|
||||
|
||||
gameMasterLogger.info(`Sprite ${data.id} deleted.`)
|
||||
callback(true)
|
||||
} catch (error: any) {
|
||||
gameMasterLogger.error('gm:sprite:delete error', error.message)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteSpriteFolder(spriteId: string): Promise<void> {
|
||||
const finalFilePath = getPublicPath('sprites', spriteId)
|
||||
|
||||
if (fs.existsSync(finalFilePath)) {
|
||||
await fs.promises.rmdir(finalFilePath, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteSpriteFromDatabase(spriteId: string): Promise<void> {
|
||||
await prisma.sprite.delete({
|
||||
where: {
|
||||
id: spriteId
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
32
src/events/gameMaster/assetManager/sprite/list.ts
Normal file
32
src/events/gameMaster/assetManager/sprite/list.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { Sprite } from '@prisma/client'
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
import SpriteRepository from '#repositories/spriteRepository'
|
||||
|
||||
interface IPayload {}
|
||||
|
||||
export default class SpriteListEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
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([])
|
||||
}
|
||||
|
||||
// get all sprites
|
||||
const sprites = await SpriteRepository.getAll()
|
||||
callback(sprites)
|
||||
}
|
||||
}
|
402
src/events/gameMaster/assetManager/sprite/update.ts
Normal file
402
src/events/gameMaster/assetManager/sprite/update.ts
Normal file
@ -0,0 +1,402 @@
|
||||
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 { getPublicPath } from '#application/storage'
|
||||
import { TSocket } from '#application/types'
|
||||
import CharacterRepository from '#repositories/characterRepository'
|
||||
|
||||
// Constants
|
||||
const ISOMETRIC_CONFIG = {
|
||||
tileWidth: 64,
|
||||
tileHeight: 32,
|
||||
centerOffset: 32,
|
||||
bodyRatios: {
|
||||
topStart: 0.15,
|
||||
topEnd: 0.45,
|
||||
weightUpper: 0.7,
|
||||
weightLower: 0.3
|
||||
}
|
||||
} as const
|
||||
|
||||
// Types
|
||||
interface ContentBounds {
|
||||
left: number
|
||||
right: number
|
||||
top: number
|
||||
bottom: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
interface SpriteActionInput extends Omit<SpriteAction, 'id' | 'spriteId' | 'frameWidth' | 'frameHeight'> {
|
||||
sprites: string[]
|
||||
}
|
||||
|
||||
interface UpdatePayload {
|
||||
id: string
|
||||
name: string
|
||||
spriteActions: Prisma.JsonValue
|
||||
}
|
||||
|
||||
interface ProcessedSpriteAction extends SpriteActionInput {
|
||||
frameWidth: number
|
||||
frameHeight: number
|
||||
buffersWithDimensions: ProcessedFrame[]
|
||||
}
|
||||
|
||||
interface ProcessedFrame {
|
||||
buffer: Buffer
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
interface SpriteAnalysis {
|
||||
massCenter: number
|
||||
spinePosition: number
|
||||
contentBounds: ContentBounds
|
||||
}
|
||||
|
||||
export default class SpriteUpdateEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
const parsedActions = this.validateSpriteActions(payload.spriteActions)
|
||||
|
||||
// Process sprites
|
||||
const processedActions = await Promise.all(
|
||||
parsedActions.map(async (action) => {
|
||||
const spriteBuffers = await this.convertBase64ToBuffers(action.sprites)
|
||||
const frameWidth = ISOMETRIC_CONFIG.tileWidth
|
||||
const frameHeight = await this.calculateOptimalHeight(spriteBuffers)
|
||||
const processedFrames = await this.normalizeFrames(spriteBuffers, frameWidth, frameHeight)
|
||||
|
||||
return {
|
||||
...action,
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
buffersWithDimensions: processedFrames
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
await Promise.all([
|
||||
this.updateDatabase(payload.id, payload.name, processedActions),
|
||||
this.saveSpritesToDisk(
|
||||
payload.id,
|
||||
processedActions.filter((a) => a.buffersWithDimensions.length > 0)
|
||||
)
|
||||
])
|
||||
|
||||
callback(true)
|
||||
} catch (error) {
|
||||
this.handleError(error, payload.id, callback)
|
||||
}
|
||||
}
|
||||
|
||||
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[]
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new Error('Sprite actions must be an array')
|
||||
}
|
||||
return parsed
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid sprite actions format: ${this.getErrorMessage(error)}`)
|
||||
}
|
||||
}
|
||||
|
||||
private async convertBase64ToBuffers(sprites: string[]): Promise<Buffer[]> {
|
||||
return sprites.map((sprite) => Buffer.from(sprite.split(',')[1], 'base64'))
|
||||
}
|
||||
|
||||
private async normalizeFrames(buffers: Buffer[], frameWidth: number, frameHeight: number): Promise<ProcessedFrame[]> {
|
||||
return Promise.all(
|
||||
buffers.map(async (buffer) => {
|
||||
const normalizedBuffer = await this.normalizeIsometricSprite(buffer, frameWidth, frameHeight)
|
||||
return {
|
||||
buffer: normalizedBuffer,
|
||||
width: frameWidth,
|
||||
height: frameHeight
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
private async calculateOptimalHeight(buffers: Buffer[]): Promise<number> {
|
||||
if (!buffers.length) return ISOMETRIC_CONFIG.tileHeight // Return default height if no buffers
|
||||
|
||||
const heights = await Promise.all(
|
||||
buffers.map(async (buffer) => {
|
||||
const bounds = await this.findContentBounds(buffer)
|
||||
return bounds.height
|
||||
})
|
||||
)
|
||||
return Math.ceil(Math.max(...heights) / 2) * 2
|
||||
}
|
||||
|
||||
private async normalizeIsometricSprite(buffer: Buffer, frameWidth: number, frameHeight: number): Promise<Buffer> {
|
||||
const analysis = await this.analyzeIsometricSprite(buffer)
|
||||
const idealCenter = Math.floor(frameWidth / 2)
|
||||
const offset = Math.round(idealCenter - analysis.massCenter)
|
||||
|
||||
// Process the input sprite
|
||||
const processedInput = await sharp(buffer)
|
||||
.ensureAlpha()
|
||||
.resize({
|
||||
width: frameWidth, // Set maximum width
|
||||
height: frameHeight, // Set maximum height
|
||||
fit: 'inside', // Ensure image fits within dimensions
|
||||
kernel: sharp.kernel.nearest,
|
||||
position: 'center',
|
||||
withoutEnlargement: true // Don't enlarge smaller images
|
||||
})
|
||||
.png({
|
||||
compressionLevel: 9,
|
||||
adaptiveFiltering: false,
|
||||
palette: true,
|
||||
quality: 100,
|
||||
colors: 256
|
||||
})
|
||||
.toBuffer()
|
||||
|
||||
// Create the final composition
|
||||
return sharp({
|
||||
create: {
|
||||
width: frameWidth,
|
||||
height: frameHeight,
|
||||
channels: 4,
|
||||
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
||||
}
|
||||
})
|
||||
.composite([
|
||||
{
|
||||
input: processedInput,
|
||||
left: offset,
|
||||
top: 0,
|
||||
blend: 'over'
|
||||
}
|
||||
])
|
||||
.png({
|
||||
compressionLevel: 9,
|
||||
adaptiveFiltering: false,
|
||||
palette: true,
|
||||
quality: 100,
|
||||
colors: 256
|
||||
})
|
||||
.toBuffer()
|
||||
}
|
||||
|
||||
private async analyzeIsometricSprite(buffer: Buffer): Promise<SpriteAnalysis> {
|
||||
const { data, info } = await sharp(buffer).raw().ensureAlpha().toBuffer({ resolveWithObject: true })
|
||||
const { width, height } = info
|
||||
const upperStart = Math.floor(height * ISOMETRIC_CONFIG.bodyRatios.topStart)
|
||||
const upperEnd = Math.floor(height * ISOMETRIC_CONFIG.bodyRatios.topEnd)
|
||||
|
||||
const { columnDensity, upperBodyDensity, bounds } = this.calculatePixelDistribution(data, width, height, upperStart, upperEnd)
|
||||
const spinePosition = this.findSpinePosition(upperBodyDensity)
|
||||
const massCenter = this.calculateWeightedMassCenter(columnDensity, upperBodyDensity)
|
||||
|
||||
return {
|
||||
massCenter,
|
||||
spinePosition,
|
||||
contentBounds: bounds
|
||||
}
|
||||
}
|
||||
|
||||
private calculatePixelDistribution(data: Buffer, width: number, height: number, upperStart: number, upperEnd: number) {
|
||||
const columnDensity = new Array(width).fill(0)
|
||||
const upperBodyDensity = new Array(width).fill(0)
|
||||
const bounds = { left: width, right: 0, top: height, bottom: 0 }
|
||||
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
if (data[(y * width + x) * 4 + 3] > 0) {
|
||||
columnDensity[x]++
|
||||
if (y >= upperStart && y <= upperEnd) {
|
||||
upperBodyDensity[x]++
|
||||
}
|
||||
this.updateBounds(bounds, x, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
columnDensity,
|
||||
upperBodyDensity,
|
||||
bounds: {
|
||||
...bounds,
|
||||
width: bounds.right - bounds.left + 1,
|
||||
height: bounds.bottom - bounds.top + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateBounds(bounds: { left: number; right: number; top: number; bottom: number }, x: number, y: number): void {
|
||||
bounds.left = Math.min(bounds.left, x)
|
||||
bounds.right = Math.max(bounds.right, x)
|
||||
bounds.top = Math.min(bounds.top, y)
|
||||
bounds.bottom = Math.max(bounds.bottom, y)
|
||||
}
|
||||
|
||||
private findSpinePosition(density: number[]): number {
|
||||
return density.reduce((maxIdx, curr, idx, arr) => (curr > arr[maxIdx] ? idx : maxIdx), 0)
|
||||
}
|
||||
|
||||
private calculateWeightedMassCenter(columnDensity: number[], upperBodyDensity: number[]): number {
|
||||
const upperMassCenter = this.calculateMassCenter(upperBodyDensity)
|
||||
const lowerMassCenter = this.calculateMassCenter(columnDensity)
|
||||
|
||||
return Math.round(upperMassCenter * ISOMETRIC_CONFIG.bodyRatios.weightUpper + lowerMassCenter * ISOMETRIC_CONFIG.bodyRatios.weightLower)
|
||||
}
|
||||
|
||||
private calculateMassCenter(density: number[]): number {
|
||||
const totalMass = density.reduce((sum, mass) => sum + mass, 0)
|
||||
if (!totalMass) return 0
|
||||
|
||||
const weightedSum = density.reduce((sum, mass, position) => sum + position * mass, 0)
|
||||
return Math.round(weightedSum / totalMass)
|
||||
}
|
||||
|
||||
private async findContentBounds(buffer: Buffer) {
|
||||
const { data, info } = await sharp(buffer).raw().ensureAlpha().toBuffer({ resolveWithObject: true })
|
||||
|
||||
const width = info.width
|
||||
const height = info.height
|
||||
|
||||
let left = width
|
||||
let right = 0
|
||||
let top = height
|
||||
let bottom = 0
|
||||
|
||||
// Find actual content boundaries by checking alpha channel
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const idx = (y * width + x) * 4
|
||||
if (data[idx + 3] > 0) {
|
||||
// If pixel is not transparent
|
||||
left = Math.min(left, x)
|
||||
right = Math.max(right, x)
|
||||
top = Math.min(top, y)
|
||||
bottom = Math.max(bottom, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
width: right - left + 1,
|
||||
height: bottom - top + 1,
|
||||
leftOffset: left,
|
||||
topOffset: top
|
||||
}
|
||||
}
|
||||
|
||||
private async saveSpritesToDisk(id: string, actions: ProcessedSpriteAction[]): Promise<void> {
|
||||
const publicFolder = getPublicPath('sprites', id)
|
||||
await mkdir(publicFolder, { recursive: true })
|
||||
|
||||
await Promise.all(
|
||||
actions.map(async (action) => {
|
||||
const spritesheet = await this.createSpritesheet(action.buffersWithDimensions)
|
||||
await writeFile(getPublicPath('sprites', id, `${action.action}.png`), spritesheet)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
private async createSpritesheet(frames: ProcessedFrame[]): Promise<Buffer> {
|
||||
const background = await sharp({
|
||||
create: {
|
||||
width: ISOMETRIC_CONFIG.tileWidth * frames.length,
|
||||
height: frames[0].height,
|
||||
channels: 4,
|
||||
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
||||
}
|
||||
})
|
||||
.png({
|
||||
compressionLevel: 9,
|
||||
adaptiveFiltering: false,
|
||||
palette: true,
|
||||
quality: 100,
|
||||
colors: 256,
|
||||
dither: 0
|
||||
})
|
||||
.toBuffer()
|
||||
|
||||
return sharp(background)
|
||||
.composite(
|
||||
frames.map((frame, index) => ({
|
||||
input: frame.buffer,
|
||||
left: index * ISOMETRIC_CONFIG.tileWidth,
|
||||
top: 0,
|
||||
blend: 'over'
|
||||
}))
|
||||
)
|
||||
.png({
|
||||
compressionLevel: 9,
|
||||
adaptiveFiltering: false,
|
||||
palette: true,
|
||||
quality: 100,
|
||||
colors: 256,
|
||||
dither: 0
|
||||
})
|
||||
.toBuffer()
|
||||
}
|
||||
|
||||
private async updateDatabase(id: string, name: string, actions: ProcessedSpriteAction[]): Promise<void> {
|
||||
await prisma.sprite.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name,
|
||||
spriteActions: {
|
||||
deleteMany: { spriteId: id },
|
||||
create: actions.map(this.mapActionToDatabase)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private mapActionToDatabase(action: ProcessedSpriteAction) {
|
||||
return {
|
||||
action: action.action,
|
||||
sprites: action.sprites,
|
||||
originX: action.originX,
|
||||
originY: action.originY,
|
||||
isAnimated: action.isAnimated,
|
||||
isLooping: action.isLooping,
|
||||
frameWidth: action.frameWidth,
|
||||
frameHeight: action.frameHeight,
|
||||
frameRate: action.frameRate
|
||||
}
|
||||
}
|
||||
|
||||
private handleError(error: unknown, spriteId: string, callback: (success: boolean) => void): void {
|
||||
gameMasterLogger.error(`Error updating sprite ${spriteId}: ${this.getErrorMessage(error)}`)
|
||||
callback(false)
|
||||
}
|
||||
|
||||
private getErrorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
}
|
69
src/events/gameMaster/assetManager/tile/delete.ts
Normal file
69
src/events/gameMaster/assetManager/tile/delete.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import fs from 'fs/promises'
|
||||
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import { gameMasterLogger } from '#application/logger'
|
||||
import prisma from '#application/prisma'
|
||||
import { getPublicPath } from '#application/storage'
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
|
||||
type Payload = {
|
||||
id: string
|
||||
}
|
||||
|
||||
export default class GMTileDeleteEvent {
|
||||
private readonly public_folder: string
|
||||
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {
|
||||
this.public_folder = getPublicPath('tiles')
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
try {
|
||||
gameMasterLogger.info(`Deleting tile ${data.id}`)
|
||||
await this.deleteTileFromDatabase(data.id)
|
||||
await this.deleteTileFile(data.id)
|
||||
|
||||
gameMasterLogger.info(`Tile ${data.id} deleted successfully.`)
|
||||
callback(true)
|
||||
} catch (error: any) {
|
||||
gameMasterLogger.error('gm:tile:delete error', error.message)
|
||||
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 = getPublicPath('tiles', `${tileId}.png`)
|
||||
try {
|
||||
await fs.unlink(finalFilePath)
|
||||
} catch (error: any) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
throw error
|
||||
}
|
||||
gameMasterLogger.warn(`File ${finalFilePath} does not exist.`)
|
||||
}
|
||||
}
|
||||
}
|
32
src/events/gameMaster/assetManager/tile/list.ts
Normal file
32
src/events/gameMaster/assetManager/tile/list.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { Tile } from '@prisma/client'
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
import TileRepository from '#repositories/tileRepository'
|
||||
|
||||
interface IPayload {}
|
||||
|
||||
export default class TileListEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:tile:list', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: any, callback: (response: Tile[]) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
return
|
||||
}
|
||||
|
||||
// get all tiles
|
||||
const tiles = await TileRepository.getAll()
|
||||
callback(tiles)
|
||||
}
|
||||
}
|
48
src/events/gameMaster/assetManager/tile/update.ts
Normal file
48
src/events/gameMaster/assetManager/tile/update.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import prisma from '#application/prisma'
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
|
||||
type Payload = {
|
||||
id: string
|
||||
name: string
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
export default class TileUpdateEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
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 as number)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const Tile = await prisma.tile.update({
|
||||
where: {
|
||||
id: data.id
|
||||
},
|
||||
data: {
|
||||
name: data.name,
|
||||
tags: data.tags
|
||||
}
|
||||
})
|
||||
|
||||
callback(true)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
60
src/events/gameMaster/assetManager/tile/upload.ts
Normal file
60
src/events/gameMaster/assetManager/tile/upload.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import fs from 'fs/promises'
|
||||
import { writeFile } from 'node:fs/promises'
|
||||
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import { gameMasterLogger } from '#application/logger'
|
||||
import prisma from '#application/prisma'
|
||||
import { getPublicPath } from '#application/storage'
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
|
||||
interface ITileData {
|
||||
[key: string]: Buffer
|
||||
}
|
||||
|
||||
export default class TileUploadEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:tile:upload', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: ITileData, callback: (response: boolean) => void): Promise<void> {
|
||||
try {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
return
|
||||
}
|
||||
|
||||
const public_folder = getPublicPath('tiles')
|
||||
|
||||
// Ensure the folder exists
|
||||
await fs.mkdir(public_folder, { recursive: true })
|
||||
|
||||
const uploadPromises = Object.entries(data).map(async ([key, tileData]) => {
|
||||
const tile = await prisma.tile.create({
|
||||
data: {
|
||||
name: 'New tile'
|
||||
}
|
||||
})
|
||||
const uuid = tile.id
|
||||
const filename = `${uuid}.png`
|
||||
const finalFilePath = getPublicPath('tiles', filename)
|
||||
await writeFile(finalFilePath, tileData)
|
||||
})
|
||||
|
||||
await Promise.all(uploadPromises)
|
||||
|
||||
callback(true)
|
||||
} catch (error) {
|
||||
gameMasterLogger.error('Error uploading tile:', error)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
63
src/events/gameMaster/zoneEditor/create.ts
Normal file
63
src/events/gameMaster/zoneEditor/create.ts
Normal file
@ -0,0 +1,63 @@
|
||||
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 ZoneRepository from '#repositories/zoneRepository'
|
||||
|
||||
type Payload = {
|
||||
name: string
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export default class ZoneCreateEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
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 (character.role !== 'gm') {
|
||||
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 = 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 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.socket.emit('notification', { message: 'Failed to create zone.' })
|
||||
callback([])
|
||||
}
|
||||
}
|
||||
}
|
62
src/events/gameMaster/zoneEditor/delete.ts
Normal file
62
src/events/gameMaster/zoneEditor/delete.ts
Normal file
@ -0,0 +1,62 @@
|
||||
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 ZoneRepository from '#repositories/zoneRepository'
|
||||
|
||||
type Payload = {
|
||||
zoneId: number
|
||||
}
|
||||
|
||||
export default class ZoneDeleteEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
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> {
|
||||
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
|
||||
}
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
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.`)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
45
src/events/gameMaster/zoneEditor/list.ts
Normal file
45
src/events/gameMaster/zoneEditor/list.ts
Normal file
@ -0,0 +1,45 @@
|
||||
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 ZoneRepository from '#repositories/zoneRepository'
|
||||
|
||||
interface IPayload {}
|
||||
|
||||
export default class ZoneListEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
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 (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.`)
|
||||
|
||||
const zones = await ZoneRepository.getAll()
|
||||
callback(zones)
|
||||
} catch (error: any) {
|
||||
gameMasterLogger.error('gm:zone_editor:zone:list error', error.message)
|
||||
callback([])
|
||||
}
|
||||
}
|
||||
}
|
60
src/events/gameMaster/zoneEditor/request.ts
Normal file
60
src/events/gameMaster/zoneEditor/request.ts
Normal file
@ -0,0 +1,60 @@
|
||||
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 ZoneRepository from '#repositories/zoneRepository'
|
||||
|
||||
interface IPayload {
|
||||
zoneId: number
|
||||
}
|
||||
|
||||
export default class ZoneRequestEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
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 (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.`)
|
||||
|
||||
if (!data.zoneId) {
|
||||
gameMasterLogger.info(`User ${character.id} tried to request zone but did not provide a zone id.`)
|
||||
callback(null)
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
callback(zone)
|
||||
} catch (error: any) {
|
||||
gameMasterLogger.error('gm:zone_editor:zone:request error', error.message)
|
||||
callback(null)
|
||||
}
|
||||
}
|
||||
}
|
160
src/events/gameMaster/zoneEditor/update.ts
Normal file
160
src/events/gameMaster/zoneEditor/update.ts
Normal file
@ -0,0 +1,160 @@
|
||||
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 zoneManager from '#managers/zoneManager'
|
||||
import CharacterRepository from '#repositories/characterRepository'
|
||||
import ZoneRepository from '#repositories/zoneRepository'
|
||||
|
||||
interface IPayload {
|
||||
zoneId: number
|
||||
name: string
|
||||
width: number
|
||||
height: number
|
||||
tiles: string[][]
|
||||
pvp: boolean
|
||||
zoneEventTiles: {
|
||||
type: ZoneEventTileType
|
||||
positionX: number
|
||||
positionY: number
|
||||
teleport?: {
|
||||
toZoneId: number
|
||||
toPositionX: number
|
||||
toPositionY: number
|
||||
toRotation: number
|
||||
}
|
||||
}[]
|
||||
zoneEffects: {
|
||||
effect: string
|
||||
strength: number
|
||||
}[]
|
||||
zoneObjects: ZoneObject[]
|
||||
}
|
||||
|
||||
export default class ZoneUpdateEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
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 (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.`)
|
||||
|
||||
if (!data.zoneId) {
|
||||
gameMasterLogger.info(`User ${character.id} 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.`)
|
||||
return callback(null)
|
||||
}
|
||||
|
||||
// If tiles are larger than the zone, remove the extra tiles
|
||||
if (data.tiles.length > data.height) {
|
||||
data.tiles = data.tiles.slice(0, data.height)
|
||||
}
|
||||
for (let i = 0; i < data.tiles.length; i++) {
|
||||
if (data.tiles[i].length > data.width) {
|
||||
data.tiles[i] = data.tiles[i].slice(0, data.width)
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
zoneManager.unloadZone(data.zoneId)
|
||||
await zoneManager.loadZone(zone)
|
||||
} catch (error: any) {
|
||||
gameMasterLogger.error(`gm:zone_editor:zone:update error: ${error instanceof Error ? error.message : String(error)}`)
|
||||
callback(null)
|
||||
}
|
||||
}
|
||||
}
|
22
src/events/login.ts
Normal file
22
src/events/login.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import UserRepository from '#repositories/userRepository'
|
||||
|
||||
export default class LoginEvent extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('login', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private handleEvent(): void {
|
||||
try {
|
||||
if (!this.socket.userId) {
|
||||
this.logger.warn('Login attempt without user data')
|
||||
return
|
||||
}
|
||||
|
||||
this.socket.emit('logged_in', { user: UserRepository.getById(this.socket.userId) })
|
||||
this.logger.info(`User logged in: ${this.socket.userId}`)
|
||||
} catch (error: any) {
|
||||
this.logger.error('login error: ' + error.message)
|
||||
}
|
||||
}
|
||||
}
|
62
src/events/zone/characterJoin.ts
Normal file
62
src/events/zone/characterJoin.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import { Zone } from '#entities/zone'
|
||||
import ZoneManager from '#managers/zoneManager'
|
||||
import zoneManager from '#managers/zoneManager'
|
||||
import zoneCharacter from '#models/zoneCharacter'
|
||||
import CharacterRepository from '#repositories/characterRepository'
|
||||
|
||||
interface IResponse {
|
||||
zone: Zone
|
||||
characters: zoneCharacter[]
|
||||
}
|
||||
|
||||
export default class CharacterJoinEvent extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('zone:character:join', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(callback: (response: IResponse) => void): Promise<void> {
|
||||
try {
|
||||
if (!this.socket.characterId) {
|
||||
this.logger.error('zone:character:join error: Zone requested but no character id set')
|
||||
return
|
||||
}
|
||||
|
||||
const character = await CharacterRepository.getById(this.socket.characterId)
|
||||
if (!character) {
|
||||
this.logger.error('zone:character:join error: Character not found')
|
||||
return
|
||||
}
|
||||
|
||||
const zone = character.zone
|
||||
|
||||
if (!zone) {
|
||||
// @TODO: If zone is not found, spawn back to the start
|
||||
this.logger.error('zone:character:join error: Zone not found')
|
||||
return
|
||||
}
|
||||
|
||||
const loadedZone = ZoneManager.getZoneById(zone.id)
|
||||
if (!loadedZone) {
|
||||
this.logger.error('zone:character:join error: Loaded zone not found')
|
||||
return
|
||||
}
|
||||
|
||||
loadedZone.addCharacter(character)
|
||||
|
||||
this.socket.join(zone.id.toString())
|
||||
|
||||
// Let other clients know of new character
|
||||
this.io.to(zone.id.toString()).emit('zone:character:join', zoneManager.getCharacterById(character.id))
|
||||
|
||||
// Log
|
||||
this.logger.info(`User ${character.id} joined zone ${zone.id}`)
|
||||
|
||||
// Send over zone and characters to socket
|
||||
callback({ zone, characters: loadedZone.getCharactersInZone() })
|
||||
} catch (error: any) {
|
||||
this.logger.error('zone:character:join error: ' + error.message)
|
||||
this.socket.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
51
src/events/zone/characterLeave.ts
Normal file
51
src/events/zone/characterLeave.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import ZoneManager from '#managers/zoneManager'
|
||||
import CharacterRepository from '#repositories/characterRepository'
|
||||
|
||||
export default class ZoneLeaveEvent extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('zone:character:leave', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(): Promise<void> {
|
||||
try {
|
||||
if (!this.socket.characterId) {
|
||||
this.logger.error('zone:character:leave error: Zone requested but no character id set')
|
||||
return
|
||||
}
|
||||
|
||||
const character = await CharacterRepository.getById(this.socket.characterId)
|
||||
if (!character) {
|
||||
this.logger.error('zone:character:leave error: Character not found')
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* @TODO: If zone is not found, spawn back to the start
|
||||
*/
|
||||
const zone = character.zone
|
||||
if (!zone) {
|
||||
this.logger.error('zone:character:leave error: Zone not found')
|
||||
return
|
||||
}
|
||||
|
||||
const loadedZone = ZoneManager.getZoneById(zone.id)
|
||||
if (!loadedZone) {
|
||||
this.logger.error('zone:character:leave error: Loaded zone not found')
|
||||
return
|
||||
}
|
||||
|
||||
this.socket.leave(zone.id.toString())
|
||||
|
||||
// let other clients know of character leaving
|
||||
this.io.to(zone.id.toString()).emit('zone:character:leave', character.id)
|
||||
|
||||
// remove character from zone manager
|
||||
await loadedZone.removeCharacter(character.id)
|
||||
|
||||
this.logger.info('zone:character:leave ' + `Character ${character.id} left zone ${zone.id}`)
|
||||
} catch (error: any) {
|
||||
this.logger.error('zone:character:leave error: ' + error.message)
|
||||
}
|
||||
}
|
||||
}
|
104
src/events/zone/characterMove.ts
Normal file
104
src/events/zone/characterMove.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import { ZoneEventTileWithTeleport } from '#application/types'
|
||||
import ZoneManager from '#managers/zoneManager'
|
||||
import ZoneCharacter from '#models/zoneCharacter'
|
||||
import zoneEventTileRepository from '#repositories/zoneEventTileRepository'
|
||||
import CharacterService from '#services/characterService'
|
||||
import ZoneEventTileService from '#services/zoneEventTileService'
|
||||
|
||||
export default class CharacterMove extends BaseEvent {
|
||||
private readonly characterService = CharacterService
|
||||
private readonly zoneEventTileService = ZoneEventTileService
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('character:move', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent({ positionX, positionY }: { positionX: number; positionY: number }): Promise<void> {
|
||||
const zoneCharacter = ZoneManager.getCharacterById(this.socket.characterId!)
|
||||
if (!zoneCharacter?.character) {
|
||||
this.logger.error('character:move error: Character not found or not initialized')
|
||||
return
|
||||
}
|
||||
|
||||
// If already moving, cancel current movement and wait for it to fully stop
|
||||
if (zoneCharacter.isMoving) {
|
||||
zoneCharacter.isMoving = false
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
}
|
||||
|
||||
const path = await this.characterService.calculatePath(zoneCharacter.character, positionX, positionY)
|
||||
if (!path) {
|
||||
this.io.in(zoneCharacter.character.zone!.id.toString()).emit('character:moveError', 'No valid path found')
|
||||
return
|
||||
}
|
||||
|
||||
// Start new movement
|
||||
zoneCharacter.isMoving = true
|
||||
zoneCharacter.currentPath = path // Add this property to ZoneCharacter class
|
||||
await this.moveAlongPath(zoneCharacter, path)
|
||||
}
|
||||
|
||||
private async moveAlongPath(zoneCharacter: ZoneCharacter, path: Array<{ x: number; y: number }>): Promise<void> {
|
||||
const { character } = zoneCharacter
|
||||
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
if (!zoneCharacter.isMoving || zoneCharacter.currentPath !== path) {
|
||||
return
|
||||
}
|
||||
|
||||
const [start, end] = [path[i], path[i + 1]]
|
||||
character.rotation = CharacterService.calculateRotation(start.x, start.y, end.x, end.y)
|
||||
|
||||
const zoneEventTile = await zoneEventTileRepository.getEventTileByZoneIdAndPosition(character.zone!.id, Math.floor(end.x), Math.floor(end.y))
|
||||
|
||||
if (zoneEventTile?.type === 'BLOCK') break
|
||||
if (zoneEventTile?.type === 'TELEPORT' && zoneEventTile.teleport) {
|
||||
await this.handleZoneEventTile(zoneEventTile as ZoneEventTileWithTeleport)
|
||||
break
|
||||
}
|
||||
|
||||
// Update position first
|
||||
character.positionX = end.x
|
||||
character.positionY = end.y
|
||||
|
||||
// Then emit with the same properties
|
||||
this.io.in(character.zone!.id.toString()).emit('character:move', {
|
||||
id: character.id,
|
||||
positionX: character.positionX,
|
||||
positionY: character.positionY,
|
||||
rotation: character.rotation,
|
||||
isMoving: true
|
||||
})
|
||||
|
||||
await this.characterService.applyMovementDelay()
|
||||
}
|
||||
|
||||
if (zoneCharacter.isMoving && zoneCharacter.currentPath === path) {
|
||||
this.finalizeMovement(zoneCharacter)
|
||||
}
|
||||
}
|
||||
|
||||
private async handleZoneEventTile(zoneEventTile: ZoneEventTileWithTeleport): Promise<void> {
|
||||
const zoneCharacter = ZoneManager.getCharacterById(this.socket.characterId!)
|
||||
if (!zoneCharacter) {
|
||||
this.logger.error('character:move error: Character not found')
|
||||
return
|
||||
}
|
||||
|
||||
if (zoneEventTile.teleport) {
|
||||
await this.zoneEventTileService.handleTeleport(this.io, this.socket, zoneCharacter.character, zoneEventTile.teleport)
|
||||
}
|
||||
}
|
||||
|
||||
private finalizeMovement(zoneCharacter: ZoneCharacter): void {
|
||||
zoneCharacter.isMoving = false
|
||||
this.io.in(zoneCharacter.character.zone!.id.toString()).emit('character:move', {
|
||||
id: zoneCharacter.character.id,
|
||||
positionX: zoneCharacter.character.positionX,
|
||||
positionY: zoneCharacter.character.positionY,
|
||||
rotation: zoneCharacter.character.rotation,
|
||||
isMoving: false
|
||||
})
|
||||
}
|
||||
}
|
17
src/events/zone/weather.ts
Normal file
17
src/events/zone/weather.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import WeatherManager from '#managers/weatherManager'
|
||||
|
||||
export default class Weather extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('weather', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(): Promise<void> {
|
||||
try {
|
||||
const weather = WeatherManager.getWeatherState()
|
||||
this.socket.emit('weather', weather)
|
||||
} catch (error: any) {
|
||||
this.logger.error('weather error: ' + error.message)
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user