Browse Source
Add AppRuntime as the central runtime orchestrator and migrate init wiring to runtime-managed game/scene/event instances. Introduce RuntimeEvents plus plugin hooks so scene-change and render lifecycle signals are extensible without breaking existing init exports. Made-with: Cursormaster
6 changed files with 242 additions and 6 deletions
@ -0,0 +1,60 @@ |
|||
import Game from "@/core/Game"; |
|||
import SceneManager from "@/scene/SceneManager"; |
|||
import { RuntimeEvents } from "./RuntimeEvents"; |
|||
import type { RuntimePlugin, RuntimePluginContext } from "./RuntimePlugin"; |
|||
|
|||
export class AppRuntime { |
|||
private static instance: AppRuntime; |
|||
|
|||
public readonly game: Game; |
|||
public readonly sceneManager: SceneManager; |
|||
public readonly events: RuntimeEvents; |
|||
|
|||
private readonly installedPlugins = new Set<string>(); |
|||
private startedAt = 0; |
|||
|
|||
constructor( |
|||
game: Game = Game.getInstance(), |
|||
sceneManager: SceneManager = SceneManager.getInstance(), |
|||
events: RuntimeEvents = new RuntimeEvents() |
|||
) { |
|||
this.game = game; |
|||
this.sceneManager = sceneManager; |
|||
this.events = events; |
|||
} |
|||
|
|||
static getInstance(): AppRuntime { |
|||
if (!AppRuntime.instance) { |
|||
AppRuntime.instance = new AppRuntime(); |
|||
} |
|||
return AppRuntime.instance; |
|||
} |
|||
|
|||
get pluginContext(): RuntimePluginContext { |
|||
return { |
|||
game: this.game, |
|||
sceneManager: this.sceneManager, |
|||
events: this.events, |
|||
}; |
|||
} |
|||
|
|||
use(plugin: RuntimePlugin): this { |
|||
if (this.installedPlugins.has(plugin.name)) { |
|||
return this; |
|||
} |
|||
|
|||
plugin.setup(this.pluginContext); |
|||
this.installedPlugins.add(plugin.name); |
|||
return this; |
|||
} |
|||
|
|||
markReady(): void { |
|||
if (this.startedAt > 0) { |
|||
return; |
|||
} |
|||
this.startedAt = Date.now(); |
|||
this.events.emit("runtime:ready", { startedAt: this.startedAt }); |
|||
} |
|||
} |
|||
|
|||
export const appRuntime = AppRuntime.getInstance(); |
|||
@ -0,0 +1,59 @@ |
|||
type EventCallback<T = unknown> = (payload: T) => void; |
|||
|
|||
export interface RuntimeEventMap { |
|||
"runtime:ready": { startedAt: number }; |
|||
"scene:changed": { current: string; previous?: string }; |
|||
"game:rendered": undefined; |
|||
} |
|||
|
|||
export class RuntimeEvents { |
|||
private events = new Map<string, Set<EventCallback>>(); |
|||
|
|||
on<T = unknown>(event: string, callback: EventCallback<T>): () => void { |
|||
if (!this.events.has(event)) { |
|||
this.events.set(event, new Set()); |
|||
} |
|||
|
|||
this.events.get(event)!.add(callback as EventCallback); |
|||
return () => this.off(event, callback); |
|||
} |
|||
|
|||
off<T = unknown>(event: string, callback: EventCallback<T>): void { |
|||
const callbacks = this.events.get(event); |
|||
if (!callbacks) { |
|||
return; |
|||
} |
|||
|
|||
callbacks.delete(callback as EventCallback); |
|||
if (callbacks.size === 0) { |
|||
this.events.delete(event); |
|||
} |
|||
} |
|||
|
|||
once<T = unknown>(event: string, callback: EventCallback<T>): () => void { |
|||
const wrapped: EventCallback<T> = (payload) => { |
|||
callback(payload); |
|||
this.off(event, wrapped); |
|||
}; |
|||
return this.on(event, wrapped); |
|||
} |
|||
|
|||
emit<T = unknown>(event: string, payload: T): void { |
|||
const callbacks = this.events.get(event); |
|||
if (!callbacks) { |
|||
return; |
|||
} |
|||
|
|||
callbacks.forEach((cb) => { |
|||
cb(payload); |
|||
}); |
|||
} |
|||
|
|||
clear(): void { |
|||
this.events.clear(); |
|||
} |
|||
|
|||
getEventCount(): number { |
|||
return this.events.size; |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
import type Game from "@/core/Game"; |
|||
import type SceneManager from "@/scene/SceneManager"; |
|||
import type { RuntimeEvents } from "./RuntimeEvents"; |
|||
|
|||
export interface RuntimePluginContext { |
|||
game: Game; |
|||
sceneManager: SceneManager; |
|||
events: RuntimeEvents; |
|||
} |
|||
|
|||
export interface RuntimePlugin { |
|||
name: string; |
|||
setup: (context: RuntimePluginContext) => void | (() => void); |
|||
} |
|||
@ -0,0 +1,80 @@ |
|||
import { describe, expect, it, vi } from "vitest"; |
|||
import { RuntimeEvents } from "@/kernel/RuntimeEvents"; |
|||
import { AppRuntime } from "@/kernel/AppRuntime"; |
|||
import type { RuntimePlugin } from "@/kernel/RuntimePlugin"; |
|||
|
|||
vi.mock("@/core/Game", () => { |
|||
class MockGame { |
|||
private static instance: MockGame; |
|||
|
|||
static getInstance(): MockGame { |
|||
if (!MockGame.instance) { |
|||
MockGame.instance = new MockGame(); |
|||
} |
|||
return MockGame.instance; |
|||
} |
|||
} |
|||
|
|||
return { default: MockGame }; |
|||
}); |
|||
|
|||
vi.mock("@/scene/SceneManager", () => { |
|||
class MockSceneManager { |
|||
private static instance: MockSceneManager; |
|||
|
|||
static getInstance(): MockSceneManager { |
|||
if (!MockSceneManager.instance) { |
|||
MockSceneManager.instance = new MockSceneManager(); |
|||
} |
|||
return MockSceneManager.instance; |
|||
} |
|||
} |
|||
|
|||
return { default: MockSceneManager }; |
|||
}); |
|||
|
|||
describe("RuntimeEvents", () => { |
|||
it("supports on/off/emit", () => { |
|||
const events = new RuntimeEvents(); |
|||
const handler = vi.fn(); |
|||
|
|||
events.on("runtime:ready", handler); |
|||
events.emit("runtime:ready", { startedAt: 1 }); |
|||
events.off("runtime:ready", handler); |
|||
events.emit("runtime:ready", { startedAt: 2 }); |
|||
|
|||
expect(handler).toHaveBeenCalledTimes(1); |
|||
expect(handler).toHaveBeenCalledWith({ startedAt: 1 }); |
|||
}); |
|||
|
|||
it("supports once subscriptions", () => { |
|||
const events = new RuntimeEvents(); |
|||
const handler = vi.fn(); |
|||
|
|||
events.once("scene:changed", handler); |
|||
events.emit("scene:changed", { current: "a", previous: undefined }); |
|||
events.emit("scene:changed", { current: "b", previous: "a" }); |
|||
|
|||
expect(handler).toHaveBeenCalledTimes(1); |
|||
expect(handler).toHaveBeenCalledWith({ current: "a", previous: undefined }); |
|||
}); |
|||
}); |
|||
|
|||
describe("AppRuntime plugin lifecycle", () => { |
|||
it("installs plugin and keeps compatibility exports", () => { |
|||
const runtime = new AppRuntime(); |
|||
const setup = vi.fn(); |
|||
const plugin: RuntimePlugin = { |
|||
name: "test-plugin", |
|||
setup, |
|||
}; |
|||
|
|||
runtime.use(plugin); |
|||
runtime.use(plugin); |
|||
|
|||
expect(setup).toHaveBeenCalledTimes(1); |
|||
expect(runtime.game).toBeDefined(); |
|||
expect(runtime.sceneManager).toBeDefined(); |
|||
expect(runtime.events).toBeDefined(); |
|||
}); |
|||
}); |
|||
Loading…
Reference in new issue