VibeGame / src /lib /services /game-engine.ts
dylanebert
improved prompting/UX
db9635c
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();