diff --git a/src/stateManager/AudioManager.ts b/src/stateManager/AudioManager.ts index b2f69d4..31c014d 100644 --- a/src/stateManager/AudioManager.ts +++ b/src/stateManager/AudioManager.ts @@ -1,3 +1,5 @@ +import { logger } from "@/utils/logger"; + export class AudioManager { private static _instance: AudioManager | null = null; private readonly _audioPools = new Map(); @@ -31,7 +33,10 @@ export class AudioManager { return; } - console.error(`Failed to play sound: ${path}`, error); + logger.error("AudioManager", "Failed to play sound", { + path, + error, + }); }); } diff --git a/src/stateManager/InteractionManager.ts b/src/stateManager/InteractionManager.ts index c09edbf..abc1882 100644 --- a/src/stateManager/InteractionManager.ts +++ b/src/stateManager/InteractionManager.ts @@ -9,6 +9,10 @@ export class InteractionManager { private _focused: InteractableHandle | null = null; private _holding = false; private _holdingHandle: InteractableHandle | null = null; + private _snapshot: InteractionSnapshot = { + focused: null, + holding: false, + }; private readonly _listeners = new Set<() => void>(); static getInstance(): InteractionManager { @@ -22,10 +26,7 @@ export class InteractionManager { private constructor() {} getState(): InteractionSnapshot { - return { - focused: this._focused, - holding: this._holding, - }; + return this._snapshot; } setFocused(handle: InteractableHandle | null): void { @@ -67,11 +68,19 @@ export class InteractionManager { this._focused = null; this._holding = false; this._holdingHandle = null; + this._snapshot = { + focused: null, + holding: false, + }; this._listeners.clear(); InteractionManager._instance = null; } private _emit(): void { + this._snapshot = { + focused: this._focused, + holding: this._holding, + }; this._listeners.forEach((cb) => cb()); } } diff --git a/src/types/logger.ts b/src/types/logger.ts new file mode 100644 index 0000000..a0cbd0c --- /dev/null +++ b/src/types/logger.ts @@ -0,0 +1,15 @@ +export type LogLevel = "debug" | "info" | "warn" | "error"; + +export type LogContext = Record; + +export interface LogEntry { + timestamp: string; + level: LogLevel; + scope: string; + message: string; + context?: LogContext; +} + +export interface LoggerConfig { + minLevel: LogLevel; +} diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 0000000..005d4fd --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,112 @@ +import type { + LogContext, + LogEntry, + LogLevel, + LoggerConfig, +} from "@/types/logger"; + +const LEVEL_PRIORITY: Record = { + debug: 10, + info: 20, + warn: 30, + error: 40, +}; + +const LEVEL_LABELS: Record = { + debug: "DEBUG", + info: "INFO", + warn: "WARN", + error: "ERROR", +}; + +const LEVEL_STYLES: Record = { + debug: "color: #94a3b8; font-weight: 600;", + info: "color: #60a5fa; font-weight: 600;", + warn: "color: #f59e0b; font-weight: 600;", + error: "color: #f87171; font-weight: 600;", +}; + +const SCOPE_STYLE = "color: #e5e7eb; font-weight: 600;"; +const MESSAGE_STYLE = "color: inherit;"; + +class Logger { + private readonly config: LoggerConfig; + + constructor(config: LoggerConfig) { + this.config = config; + } + + debug(scope: string, message: string, context?: LogContext): void { + this.log("debug", scope, message, context); + } + + info(scope: string, message: string, context?: LogContext): void { + this.log("info", scope, message, context); + } + + warn(scope: string, message: string, context?: LogContext): void { + this.log("warn", scope, message, context); + } + + error(scope: string, message: string, context?: LogContext): void { + this.log("error", scope, message, context); + } + + private log( + level: LogLevel, + scope: string, + message: string, + context?: LogContext, + ): void { + if (!this.shouldLog(level)) return; + + const entry: LogEntry = { + timestamp: new Date().toISOString(), + level, + scope, + message, + ...(context ? { context } : {}), + }; + + const formattedMessage = `%c[${LEVEL_LABELS[level]}]%c [${scope}]%c ${message}`; + const args = [ + formattedMessage, + LEVEL_STYLES[level], + SCOPE_STYLE, + MESSAGE_STYLE, + entry, + ] as const; + + switch (level) { + case "debug": + console.debug(...args); + return; + case "info": + console.info(...args); + return; + case "warn": + console.warn(...args); + return; + case "error": + console.error(...args); + } + } + + private shouldLog(level: LogLevel): boolean { + return LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[this.config.minLevel]; + } +} + +function resolveMinLevel(): LogLevel { + if (typeof window === "undefined") { + return "info"; + } + + const debugEnabled = new URLSearchParams(window.location.search).has("debug"); + + return debugEnabled ? "debug" : "info"; +} + +export const logger = new Logger({ + minLevel: resolveMinLevel(), +});