UNPKG

yukinovel

Version:

Yukinovel is a simple web visual novel engine.

350 lines (349 loc) 14.3 kB
import { AudioManager } from './AudioManager.js'; import { SaveManager } from './SaveManager.js'; import { UIRenderer } from './UIRenderer.js'; import { LanguageManager } from './LanguageManager.js'; import { PluginManager } from './PluginManager.js'; export class Game { constructor(script) { this.eventHandlers = {}; this.currentSceneDialogueHistory = []; this.globalDialogueHistory = []; this.isGameStarted = false; this.script = script; this.state = { currentScene: script.scenes[0]?.id || '', currentDialogue: 0, variables: {}, history: [] }; this.audioManager = new AudioManager(); this.saveManager = new SaveManager(); this.uiRenderer = new UIRenderer(this); this.languageManager = new LanguageManager(); this.pluginManager = new PluginManager(this); } // Mount game to DOM element mount(selector) { const element = document.querySelector(selector); if (!element) { throw new Error(`Element with selector "${selector}" not found`); } this.container = element; this.initialize(); } async initialize() { this.languageManager.initialize(this.script); // Setup container this.container.innerHTML = ''; this.container.style.position = 'relative'; this.container.style.width = typeof this.script.settings?.width === 'string' ? this.script.settings.width : `${this.script.settings?.width || 800}px`; this.container.style.height = typeof this.script.settings?.height === 'string' ? this.script.settings.height : `${this.script.settings?.height || 600}px`; this.container.style.overflow = 'hidden'; this.container.style.backgroundColor = '#000'; this.container.style.fontFamily = 'Arial, sans-serif'; // Initialize plugins await this.pluginManager.initialize(); // Execute onStart hook if (!this.isGameStarted) { await this.pluginManager.executeHooks('onStart', this.pluginManager.createHookContext()); this.isGameStarted = true; } // Render initial UI this.uiRenderer.render(this.container); if (this.script.settings?.mainMenu) { this.showMainMenu(); } else { this.startScene(this.state.currentScene); } } // Scene management async startScene(sceneId, fadeOptions) { const scene = this.script.scenes.find(s => s.id === sceneId); if (!scene) { console.error(`Scene "${sceneId}" not found`); return; } // Execute onSceneWillStart hook const context = this.pluginManager.createHookContext({ scene }); await this.pluginManager.executeHooks('onSceneWillStart', context); this.state.currentScene = sceneId; this.state.currentDialogue = 0; this.currentSceneDialogueHistory = []; if (scene.music) { // Execute music hooks const musicContext = this.pluginManager.createHookContext({ scene, music: scene.music }); await this.pluginManager.executeHooks('onMusicWillPlay', musicContext); this.audioManager.playMusic(scene.music); await this.pluginManager.executeHooks('onMusicPlayed', musicContext); } if (fadeOptions?.backgroundFade) { this.uiRenderer.updateSceneWithFade(scene, true, fadeOptions.backgroundAnimation); } else { this.uiRenderer.updateScene(scene); } await this.showDialogue(); this.emit('scene', { scene }); // Execute onSceneStarted hook await this.pluginManager.executeHooks('onSceneStarted', context); } // Dialogue management async showDialogue() { const currentScene = this.getCurrentScene(); if (!currentScene) return; const dialogue = currentScene.dialogue[this.state.currentDialogue]; if (!dialogue) { await this.pluginManager.executeHooks('onEnd', this.pluginManager.createHookContext()); this.emit('end', {}); return; } if (!this.currentSceneDialogueHistory.includes(dialogue)) { this.currentSceneDialogueHistory.push(dialogue); } const existingGlobalEntry = this.globalDialogueHistory.find(entry => entry.dialogue === dialogue && entry.sceneId === this.state.currentScene); if (!existingGlobalEntry) { this.globalDialogueHistory.push({ dialogue: dialogue, sceneId: this.state.currentScene, timestamp: new Date() }); } // Execute onDialogueWillDisplay hook const context = this.pluginManager.createHookContext({ scene: currentScene, dialogue }); await this.pluginManager.executeHooks('onDialogueWillDisplay', context); this.uiRenderer.updateDialogue(dialogue); this.emit('dialogue', { dialogue }); // Execute onDialogueDisplayed hook await this.pluginManager.executeHooks('onDialogueDisplayed', context); } async next() { const currentScene = this.getCurrentScene(); if (!currentScene) return; const dialogue = currentScene.dialogue[this.state.currentDialogue]; if (!dialogue) return; if (dialogue.action) { await this.handleAction(dialogue.action, dialogue.target); return; } if (dialogue.choices && dialogue.choices.length > 0) { // Execute onChoicesWillDisplay hook const context = this.pluginManager.createHookContext({ scene: currentScene, dialogue, choices: dialogue.choices }); await this.pluginManager.executeHooks('onChoicesWillDisplay', context); this.uiRenderer.showChoices(dialogue.choices); // Execute onChoicesDisplayed hook await this.pluginManager.executeHooks('onChoicesDisplayed', context); return; } // Execute onDialogueWillHide hook const hideContext = this.pluginManager.createHookContext({ scene: currentScene, dialogue }); await this.pluginManager.executeHooks('onDialogueWillHide', hideContext); this.state.currentDialogue++; if (this.state.currentDialogue >= currentScene.dialogue.length) { await this.pluginManager.executeHooks('onEnd', this.pluginManager.createHookContext()); this.emit('end', {}); return; } // Execute onDialogueHidden hook await this.pluginManager.executeHooks('onDialogueHidden', hideContext); await this.showDialogue(); } async makeChoice(choice) { this.state.history.push(`Choice: ${choice.text}`); // Execute onChoiceSelected hook const context = this.pluginManager.createHookContext({ choice }); await this.pluginManager.executeHooks('onChoiceSelected', context); await this.handleAction(choice.action, choice.target); } async handleAction(action, target) { const currentScene = this.getCurrentScene(); const currentDialogue = currentScene?.dialogue[this.state.currentDialogue]; switch (action) { case 'jump': if (target) { const fadeAnimation = currentDialogue?.fadeAnimation; const fadeOptions = { backgroundFade: fadeAnimation?.enabled === true && fadeAnimation?.backgroundFade !== false, backgroundAnimation: typeof fadeAnimation?.backgroundFade === 'object' ? fadeAnimation.backgroundFade : undefined }; // Execute onSceneWillEnd hook for current scene if (currentScene) { await this.pluginManager.executeHooks('onSceneWillEnd', this.pluginManager.createHookContext({ scene: currentScene })); } await this.startScene(target, fadeOptions); // Execute onSceneEnded hook for previous scene if (currentScene) { await this.pluginManager.executeHooks('onSceneEnded', this.pluginManager.createHookContext({ scene: currentScene })); } } break; case 'end': await this.pluginManager.executeHooks('onEnd', this.pluginManager.createHookContext()); this.emit('end', {}); break; case 'save': // this.uiRenderer.showSavePanel(); break; case 'load': // this.uiRenderer.showLoadPanel(); break; } } // Save/Load system async saveGame(slot = 0) { // Execute onWillSave hook const context = this.pluginManager.createHookContext({ slot }); await this.pluginManager.executeHooks('onWillSave', context); this.state.savedAt = new Date(); this.saveManager.save(slot, this.state); this.emit('save', { slot }); // Execute onSaved hook await this.pluginManager.executeHooks('onSaved', context); } async loadGame(slot = 0) { // Execute onWillLoad hook const context = this.pluginManager.createHookContext({ slot }); await this.pluginManager.executeHooks('onWillLoad', context); const savedState = this.saveManager.load(slot); if (savedState) { this.state = savedState; await this.startScene(this.state.currentScene, { backgroundFade: true }); this.emit('load', { slot }); // Execute onLoaded hook await this.pluginManager.executeHooks('onLoaded', context); } } async showMainMenu() { // Execute onMenuWillShow hook const context = this.pluginManager.createHookContext({ menuType: 'main' }); await this.pluginManager.executeHooks('onMenuWillShow', context); this.uiRenderer.showMainMenu(); // Execute onMenuShown hook await this.pluginManager.executeHooks('onMenuShown', context); } async startNewGame() { this.globalDialogueHistory = []; this.state.currentDialogue = 0; this.state.currentScene = this.script.scenes[0]?.id || ''; // Execute onMenuWillHide hook const hideContext = this.pluginManager.createHookContext({ menuType: 'main' }); await this.pluginManager.executeHooks('onMenuWillHide', hideContext); this.uiRenderer.hideMainMenu(); // Execute onMenuHidden hook await this.pluginManager.executeHooks('onMenuHidden', hideContext); await this.startScene(this.state.currentScene); } async continueGame() { // Execute onMenuWillHide hook const hideContext = this.pluginManager.createHookContext({ menuType: 'main' }); await this.pluginManager.executeHooks('onMenuWillHide', hideContext); this.uiRenderer.hideMainMenu(); // Execute onMenuHidden hook await this.pluginManager.executeHooks('onMenuHidden', hideContext); await this.startScene(this.state.currentScene, { backgroundFade: true }); } // Event system on(event, handler) { if (!this.eventHandlers[event]) { this.eventHandlers[event] = []; } this.eventHandlers[event].push(handler); } emit(event, data) { if (this.eventHandlers[event]) { this.eventHandlers[event].forEach(handler => handler({ type: event, data })); } } // Getters getCurrentScene() { return this.script.scenes.find(s => s.id === this.state.currentScene); } getCurrentSceneDialogueHistory() { return [...this.currentSceneDialogueHistory]; } getGlobalDialogueHistory() { return [...this.globalDialogueHistory]; } getSceneById(sceneId) { return this.script.scenes.find(s => s.id === sceneId); } getState() { return { ...this.state }; } getScript() { return this.script; } getLanguageManager() { return this.languageManager; } getCurrentLanguage() { return this.languageManager.getCurrentLanguage(); } async setLanguage(languageCode) { // Execute onLanguageWillChange hook const context = this.pluginManager.createHookContext({ language: languageCode, previousLanguage: this.languageManager.getCurrentLanguage() }); await this.pluginManager.executeHooks('onLanguageWillChange', context); this.languageManager.setLanguage(languageCode); // Execute onLanguageChanged hook await this.pluginManager.executeHooks('onLanguageChanged', context); } getAvailableLanguages() { return this.languageManager.getAvailableLanguages(); } getText(key, fallback) { return this.languageManager.getText(key, fallback); } getAudioManager() { return this.audioManager; } getUIRenderer() { return this.uiRenderer; } getSaveManager() { return this.saveManager; } getConfirmModal() { return this.uiRenderer.getConfirmModal(); } // Plugin system methods getPluginManager() { return this.pluginManager; } async registerPlugin(plugin) { return this.pluginManager.register(plugin); } async unregisterPlugin(pluginName) { return this.pluginManager.unregister(pluginName); } getPlugin(name) { return this.pluginManager.getPlugin(name); } getPluginAPI(name) { return this.pluginManager.getPluginAPI(name); } hasPlugin(name) { return this.pluginManager.hasPlugin(name); } async emitCustomEvent(eventName, data = {}) { const context = this.pluginManager.createHookContext(data); await this.pluginManager.executeCustomHooks(eventName, context); } }