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:
@ -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()
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
Reference in New Issue
Block a user