1
0
forked from noxious/server

#174: Refactor character manager into zoneManager for better DX, major refactor of time and weather system (data is stored in DB now instead of JSON file), npm update, npm format, many other improvements

This commit is contained in:
2024-11-13 13:21:01 +01:00
parent 628b3bf1fa
commit d4e0cbe398
43 changed files with 465 additions and 461 deletions

View File

@ -1,42 +0,0 @@
import { ExtendedCharacter, TSocket } from '../utilities/types'
import { Zone } from '@prisma/client'
import prisma from '../utilities/prisma'
class CharacterManager {
private characters!: ExtendedCharacter[]
public async boot() {
this.characters = []
}
public initCharacter(character: ExtendedCharacter) {
this.characters = [...this.characters, character]
}
public async removeCharacter(character: ExtendedCharacter) {
await prisma.character.update({
where: { id: character.id },
data: {
positionX: character.positionX,
positionY: character.positionY,
rotation: character.rotation,
zoneId: character.zoneId
}
})
this.characters = this.characters.filter((x) => x.id !== character.id)
}
public getCharacterFromSocket(socket: TSocket) {
return this.characters.find((x) => x.id === socket?.characterId)
}
public hasResetMovement(character: ExtendedCharacter) {
return this.characters.find((x) => x.id === character.id)?.resetMovement
}
public getCharactersInZone(zone: Zone) {
return this.characters.filter((x) => x.zoneId === zone.id)
}
}
export default new CharacterManager()

View File

@ -1,7 +1,7 @@
import { Server } from 'socket.io'
import { appLogger } from '../utilities/logger'
import { getRootPath } from '../utilities/storage'
import { readJsonValue, setJsonValue } from '../utilities/json'
import prisma from '../utilities/prisma'
import worldService from '../services/worldService'
class DateManager {
private static readonly GAME_SPEED = 8 // 24 game hours / 3 real hours
@ -18,7 +18,6 @@ class DateManager {
appLogger.info('Date manager loaded')
}
// When a GM sets the time, update the current date and update the world file
public async setTime(time: string): Promise<void> {
try {
let newDate: Date
@ -45,8 +44,13 @@ class DateManager {
private async loadDate(): Promise<void> {
try {
const dateString = await readJsonValue<string>(this.getWorldFilePath(), 'date')
this.currentDate = new Date(dateString)
const world = await prisma.world.findFirst({
orderBy: { date: 'desc' }
})
if (world) {
this.currentDate = world.date
}
} catch (error) {
appLogger.error(`Failed to load date: ${error instanceof Error ? error.message : String(error)}`)
this.currentDate = new Date() // Use current date as fallback
@ -57,7 +61,7 @@ class DateManager {
this.intervalId = setInterval(() => {
this.advanceGameTime()
this.emitDate()
this.saveDate()
void this.saveDate()
}, DateManager.UPDATE_INTERVAL)
}
@ -72,14 +76,22 @@ class DateManager {
private async saveDate(): Promise<void> {
try {
await setJsonValue(this.getWorldFilePath(), 'date', this.currentDate)
await worldService.update({
date: this.currentDate
})
} catch (error) {
appLogger.error(`Failed to save date: ${error instanceof Error ? error.message : String(error)}`)
}
}
private getWorldFilePath(): string {
return getRootPath('data', 'world.json')
public cleanup(): void {
if (this.intervalId) {
clearInterval(this.intervalId)
}
}
public getCurrentDate(): Date {
return this.currentDate
}
}

View File

@ -1,7 +1,7 @@
import { Server } from 'socket.io'
import { appLogger } from '../utilities/logger'
import { getRootPath } from '../utilities/storage'
import { readJsonValue, setJsonValue } from '../utilities/json'
import prisma from '../utilities/prisma'
import worldService from '../services/worldService'
interface WeatherState {
isRainEnabled: boolean
@ -37,46 +37,48 @@ class WeatherManager {
? Math.floor(Math.random() * 50) + 50 // 50-100%
: 0
// Save weather
await this.saveWeather()
// Emit weather
this.emitWeather()
}
public async toggleFog(): Promise<void> {
this.weatherState.isFogEnabled = !this.weatherState.isFogEnabled
this.weatherState.fogDensity = this.weatherState.isFogEnabled
? Math.random() * 0.7 + 0.3 // 0.3-1.0
? Math.floor((Math.random() * 0.7 + 0.3) * 100) // Convert 0.3-1.0 to 30-100
: 0
// Save weather
await this.saveWeather()
// Emit weather
this.emitWeather()
}
private async loadWeather(): Promise<void> {
try {
this.weatherState.isRainEnabled = await readJsonValue<boolean>(this.getWorldFilePath(), 'isRainEnabled')
this.weatherState.rainPercentage = await readJsonValue<number>(this.getWorldFilePath(), 'rainPercentage')
this.weatherState.isFogEnabled = await readJsonValue<boolean>(this.getWorldFilePath(), 'isFogEnabled')
this.weatherState.fogDensity = await readJsonValue<number>(this.getWorldFilePath(), 'fogDensity')
const world = await prisma.world.findFirst({
orderBy: { date: 'desc' }
})
if (world) {
this.weatherState = {
isRainEnabled: world.isRainEnabled,
rainPercentage: world.rainPercentage,
isFogEnabled: world.isFogEnabled,
fogDensity: world.fogDensity
}
}
} catch (error) {
appLogger.error(`Failed to load weather: ${error instanceof Error ? error.message : String(error)}`)
}
}
public async getWeatherState(): Promise<WeatherState> {
public getWeatherState(): WeatherState {
return this.weatherState
}
private startWeatherLoop(): void {
this.intervalId = setInterval(() => {
this.intervalId = setInterval(async () => {
this.updateWeather()
this.emitWeather()
this.saveWeather().catch((error) => {
await this.saveWeather().catch((error) => {
appLogger.error(`Failed to save weather: ${error instanceof Error ? error.message : String(error)}`)
})
}, WeatherManager.UPDATE_INTERVAL)
@ -95,7 +97,7 @@ class WeatherManager {
if (Math.random() < WeatherManager.FOG_CHANCE) {
this.weatherState.isFogEnabled = !this.weatherState.isFogEnabled
this.weatherState.fogDensity = this.weatherState.isFogEnabled
? Math.random() * 0.7 + 0.3 // 0.3-1.0
? Math.floor((Math.random() * 0.7 + 0.3) * 100) // Convert 0.3-1.0 to 30-100
: 0
}
}
@ -106,20 +108,21 @@ class WeatherManager {
private async saveWeather(): Promise<void> {
try {
const promises = [
await setJsonValue(this.getWorldFilePath(), 'isRainEnabled', this.weatherState.isRainEnabled),
await setJsonValue(this.getWorldFilePath(), 'rainPercentage', this.weatherState.rainPercentage),
await setJsonValue(this.getWorldFilePath(), 'isFogEnabled', this.weatherState.isFogEnabled),
await setJsonValue(this.getWorldFilePath(), 'fogDensity', this.weatherState.fogDensity)
]
await Promise.all(promises)
await worldService.update({
isRainEnabled: this.weatherState.isRainEnabled,
rainPercentage: this.weatherState.rainPercentage,
isFogEnabled: this.weatherState.isFogEnabled,
fogDensity: this.weatherState.fogDensity
})
} catch (error) {
appLogger.error(`Failed to save weather: ${error instanceof Error ? error.message : String(error)}`)
}
}
private getWorldFilePath(): string {
return getRootPath('data', 'world.json')
public cleanup(): void {
if (this.intervalId) {
clearInterval(this.intervalId)
}
}
}

View File

@ -3,53 +3,53 @@ import ZoneRepository from '../repositories/zoneRepository'
import ZoneService from '../services/zoneService'
import LoadedZone from '../models/loadedZone'
import { gameLogger } from '../utilities/logger'
import ZoneCharacter from '../models/zoneCharacter'
class ZoneManager {
private loadedZones: LoadedZone[] = []
private readonly zones = new Map<number, LoadedZone>()
// Method to initialize zoneEditor manager
public async boot() {
public async boot(): Promise<void> {
// Create first zone if it doesn't exist
if (!(await ZoneRepository.getById(1))) {
const zoneService = new ZoneService()
await zoneService.createDemoZone()
await new ZoneService().createDemoZone()
}
const zones = await ZoneRepository.getAll()
await Promise.all(zones.map((zone) => this.loadZone(zone)))
for (const zone of zones) {
await this.loadZone(zone)
}
gameLogger.info('Zone manager loaded')
gameLogger.info(`Zone manager loaded with ${this.zones.size} zones`)
}
// Method to handle individual zoneEditor loading
public async loadZone(zone: Zone) {
public async loadZone(zone: Zone): Promise<void> {
const loadedZone = new LoadedZone(zone)
this.loadedZones.push(loadedZone)
this.zones.set(zone.id, loadedZone)
gameLogger.info(`Zone ID ${zone.id} loaded`)
}
// Method to handle individual zoneEditor unloading
public unloadZone(zoneId: number) {
this.loadedZones = this.loadedZones.filter((loadedZone) => loadedZone.getZone().id !== zoneId)
public unloadZone(zoneId: number): void {
this.zones.delete(zoneId)
gameLogger.info(`Zone ID ${zoneId} unloaded`)
}
// Getter for loaded zones
public getLoadedZones(): LoadedZone[] {
return this.loadedZones
return Array.from(this.zones.values())
}
// Getter for zone by id
public getZoneById(zoneId: number): LoadedZone | undefined {
return this.loadedZones.find((loadedZone) => loadedZone.getZone().id === zoneId)
return this.zones.get(zoneId)
}
}
export interface ZoneAssets {
tiles: string[]
objects: string[]
public getCharacter(characterId: number): ZoneCharacter | undefined {
for (const zone of this.zones.values()) {
const character = zone.getCharactersInZone().find((char) => char.character.id === characterId)
if (character) return character
}
return undefined
}
public removeCharacter(characterId: number): void {
this.zones.forEach((zone) => zone.removeCharacter(characterId))
}
}
export default new ZoneManager()