Spaces:
Running
Running
| import * as GAME from "vibegame"; | |
| import type { System, Plugin, Component, BuilderOptions } from "vibegame"; | |
| import { gameStore } from "../stores/game"; | |
| import { uiStore } from "../stores/ui"; | |
| import { consoleBuffer } from "../server/console-buffer"; | |
| import { | |
| HTMLDocumentParser, | |
| type ParsedDocument, | |
| } from "./html-document-parser"; | |
| type GameInstance = Awaited<ReturnType<typeof GAME.run>>; | |
| export class GameEngine { | |
| private static instance: GameEngine | null = null; | |
| private gameInstance: GameInstance | null = null; | |
| private constructor() {} | |
| static getInstance(): GameEngine { | |
| if (!GameEngine.instance) { | |
| GameEngine.instance = new GameEngine(); | |
| } | |
| return GameEngine.instance; | |
| } | |
| async startFromDocument(htmlContent: string): Promise<void> { | |
| if (this.gameInstance) { | |
| consoleBuffer.onGameReloadStart(); | |
| this.stop(); | |
| await new Promise((resolve) => setTimeout(resolve, 100)); | |
| } | |
| gameStore.setStarting(true); | |
| console.info("๐ฎ Starting game..."); | |
| uiStore.setError(null); | |
| try { | |
| const parsed = HTMLDocumentParser.parseDocument(htmlContent); | |
| this.renderDocument(parsed); | |
| await this.initializeGame(parsed.scripts); | |
| console.info("โ Game started!"); | |
| consoleBuffer.onGameReloadComplete(); | |
| } catch (error: unknown) { | |
| const errorMsg = error instanceof Error ? error.message : String(error); | |
| uiStore.setError(errorMsg); | |
| console.error(`โ Error: ${errorMsg}`); | |
| gameStore.setInstance(null); | |
| this.gameInstance = null; | |
| } finally { | |
| gameStore.setStarting(false); | |
| } | |
| } | |
| async start(worldContent: string, scripts: string[] = []): Promise<void> { | |
| const parsed: ParsedDocument = { | |
| world: worldContent, | |
| scripts, | |
| }; | |
| await this.startFromParsed(parsed); | |
| } | |
| private async startFromParsed(parsed: ParsedDocument): Promise<void> { | |
| if (this.gameInstance) { | |
| consoleBuffer.onGameReloadStart(); | |
| this.stop(); | |
| await new Promise((resolve) => setTimeout(resolve, 100)); | |
| } | |
| gameStore.setStarting(true); | |
| console.info("๐ฎ Starting game..."); | |
| uiStore.setError(null); | |
| try { | |
| this.renderDocument(parsed); | |
| await this.initializeGame(parsed.scripts); | |
| console.info("โ Game started!"); | |
| consoleBuffer.onGameReloadComplete(); | |
| } catch (error: unknown) { | |
| const errorMsg = error instanceof Error ? error.message : String(error); | |
| uiStore.setError(errorMsg); | |
| console.error(`โ Error: ${errorMsg}`); | |
| gameStore.setInstance(null); | |
| this.gameInstance = null; | |
| } finally { | |
| gameStore.setStarting(false); | |
| } | |
| } | |
| private renderDocument(parsed: ParsedDocument): void { | |
| const container = document.getElementById("world-container"); | |
| if (!container) { | |
| throw new Error("World container not found"); | |
| } | |
| this.clearContainer(container); | |
| container.innerHTML = parsed.world; | |
| } | |
| private clearContainer(container: HTMLElement): void { | |
| while (container.firstChild) { | |
| container.removeChild(container.firstChild); | |
| } | |
| } | |
| private async initializeGame(scripts: string[]): Promise<void> { | |
| GAME.resetBuilder(); | |
| const gameProxy = this.createGameProxy(); | |
| (window as unknown as { GAME: typeof gameProxy }).GAME = gameProxy; | |
| let scriptExecutionFailed = false; | |
| for (const script of scripts) { | |
| try { | |
| const cleanedScript = script.replace(/GAME\.run\(\)/g, ""); | |
| eval(cleanedScript); | |
| } catch (scriptError) { | |
| scriptExecutionFailed = true; | |
| const errorMsg = | |
| scriptError instanceof Error | |
| ? scriptError.message | |
| : String(scriptError); | |
| console.error("Script error:", errorMsg); | |
| } | |
| } | |
| (window as unknown as { GAME: typeof gameProxy | null }).GAME = null; | |
| if (scriptExecutionFailed) { | |
| throw new Error("Script execution failed - game not started"); | |
| } | |
| this.gameInstance = await GAME.run(); | |
| gameStore.setInstance(this.gameInstance); | |
| } | |
| private createGameProxy() { | |
| return { | |
| withSystem: (system: System) => { | |
| GAME.withSystem(system); | |
| return this.createGameProxy(); | |
| }, | |
| withPlugin: (plugin: Plugin) => { | |
| GAME.withPlugin(plugin); | |
| return this.createGameProxy(); | |
| }, | |
| withComponent: (name: string, component: Component) => { | |
| GAME.withComponent(name, component); | |
| return this.createGameProxy(); | |
| }, | |
| configure: (options: BuilderOptions) => { | |
| GAME.configure(options); | |
| return this.createGameProxy(); | |
| }, | |
| withoutDefaultPlugins: () => { | |
| GAME.withoutDefaultPlugins(); | |
| return this.createGameProxy(); | |
| }, | |
| run: () => { | |
| console.warn( | |
| "GAME.run() is not available in user scripts - the framework handles game lifecycle", | |
| ); | |
| return Promise.resolve({ | |
| stop: () => {}, | |
| destroy: () => {}, | |
| step: () => {}, | |
| getState: () => null, | |
| }); | |
| }, | |
| defineComponent: GAME.defineComponent, | |
| defineQuery: GAME.defineQuery, | |
| Types: GAME.Types, | |
| }; | |
| } | |
| stop(): void { | |
| if (this.gameInstance) { | |
| try { | |
| this.gameInstance.destroy(); | |
| console.info("Game instance destroyed"); | |
| } catch (error) { | |
| console.error("Error destroying game:", error); | |
| } | |
| this.gameInstance = null; | |
| gameStore.setInstance(null); | |
| } | |
| const container = document.getElementById("world-container"); | |
| if (container) { | |
| this.clearContainer(container); | |
| } | |
| GAME.resetBuilder(); | |
| } | |
| isRunning(): boolean { | |
| return this.gameInstance !== null; | |
| } | |
| } | |
| export const gameEngine = GameEngine.getInstance(); | |