diff --git a/configs/default.json b/configs/default.json index bd2af15..df24bf7 100644 --- a/configs/default.json +++ b/configs/default.json @@ -1,5 +1,21 @@ { - "name": "I love X (default)", - "assetsDir": "../assets/default", - "tickRate": 2 + "theme": "default", + "themeName": "I Love X", + "assetsBasePath": "./assets/default/", + "modules": [ + {"name": "intro", "configFile": "intro/config.json"}, + {"name": "finale", "configFile": "finale/config.json"} + ], + "ui": { + "colors": { + "primary": "#FF0000", + "secondary": "#00FF00", + "background": "#000000", + "text": "#FFFFFF" + }, + "fonts": { + "heading": "Arial", + "body": "Arial" + } + } } diff --git a/src/core/asset-manager.js b/src/core/asset-manager.js index 559692d..779ae54 100644 --- a/src/core/asset-manager.js +++ b/src/core/asset-manager.js @@ -1,24 +1,172 @@ -const fs = require('fs'); +const fs = require('fs').promises; const path = require('path'); class AssetManager { - constructor(assetDir) { - this.assetDir = path.resolve(assetDir); - this.assets = {}; - } - - loadAssets() { - if (!fs.existsSync(this.assetDir)) return; - const files = fs.readdirSync(this.assetDir); - files.forEach(file => { - const filePath = path.join(this.assetDir, file); - this.assets[file] = filePath; - }); - } - - getAsset(name) { - return this.assets[name]; - } + constructor(eventBus) { + this.eventBus = eventBus; + this.assetMap = new Map(); + this.cache = new Map(); + this.loading = new Map(); + this.prefetchQueue = []; + this.maxCacheSize = 100 * 1024 * 1024; // 100MB + this.currentCacheSize = 0; + } + + async initialize(config) { + this.baseDir = path.resolve(config.assetsBasePath); + this.themeDir = path.resolve(config.assetsBasePath, config.theme); + + try { + await this.scanAssets(this.baseDir, 'base'); + await this.scanAssets(this.themeDir, 'theme'); + this.eventBus.emit('assets:initialized'); + } catch (error) { + this.eventBus.emit('assets:error', error); + throw error; + } + } + + async scanAssets(dir, namespace) { + try { + const entries = await fs.readdir(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + await this.scanAssets(fullPath, `${namespace}/${entry.name}`); + } else { + const key = `${namespace}/${entry.name}`; + const stats = await fs.stat(fullPath); + this.assetMap.set(key, { + path: fullPath, + size: stats.size, + type: this.getAssetType(entry.name), + loaded: false + }); + } + } + } catch (error) { + this.eventBus.emit('assets:scan:error', { dir, error }); + throw error; + } + } + + getAssetType(filename) { + const ext = path.extname(filename).toLowerCase(); + const types = { + '.png': 'image', + '.jpg': 'image', + '.jpeg': 'image', + '.gif': 'image', + '.wav': 'audio', + '.mp3': 'audio', + '.ogg': 'audio', + '.json': 'data', + '.txt': 'data' + }; + return types[ext] || 'unknown'; + } + + async loadAsset(key) { + if (this.cache.has(key)) { + return this.cache.get(key); + } + + if (this.loading.has(key)) { + return this.loading.get(key); + } + + const asset = this.assetMap.get(key); + if (!asset) { + throw new Error(`Asset not found: ${key}`); + } + + const loadPromise = this._loadAssetData(asset); + this.loading.set(key, loadPromise); + + try { + const data = await loadPromise; + this.cache.set(key, data); + this.loading.delete(key); + this.currentCacheSize += asset.size; + this.maintainCache(); + return data; + } catch (error) { + this.loading.delete(key); + throw error; + } + } + + async _loadAssetData(asset) { + switch (asset.type) { + case 'image': + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = reject; + img.src = asset.path; + }); + case 'audio': + return new Promise((resolve, reject) => { + const audio = new Audio(); + audio.oncanplaythrough = () => resolve(audio); + audio.onerror = reject; + audio.src = asset.path; + }); + case 'data': + const data = await fs.readFile(asset.path, 'utf8'); + return asset.path.endsWith('.json') ? JSON.parse(data) : data; + default: + return fs.readFile(asset.path); + } + } + + maintainCache() { + while (this.currentCacheSize > this.maxCacheSize) { + const [oldestKey] = this.cache.keys(); + const asset = this.assetMap.get(oldestKey); + this.currentCacheSize -= asset.size; + this.cache.delete(oldestKey); + } + } + + async prefetch(keys) { + this.prefetchQueue.push(...keys); + this.processPrefetchQueue(); + } + + async processPrefetchQueue() { + while (this.prefetchQueue.length > 0) { + const key = this.prefetchQueue.shift(); + if (!this.cache.has(key)) { + try { + await this.loadAsset(key); + } catch (error) { + this.eventBus.emit('assets:prefetch:error', { key, error }); + } + } + } + } + + unloadAsset(key) { + if (this.cache.has(key)) { + const asset = this.assetMap.get(key); + this.currentCacheSize -= asset.size; + this.cache.delete(key); + } + } + + getLoadedAssets() { + return Array.from(this.cache.keys()); + } + + getCacheStats() { + return { + size: this.currentCacheSize, + maxSize: this.maxCacheSize, + count: this.cache.size, + loading: this.loading.size + }; + } } module.exports = AssetManager; diff --git a/src/core/event-bus.js b/src/core/event-bus.js index 30184de..d169807 100644 --- a/src/core/event-bus.js +++ b/src/core/event-bus.js @@ -1,20 +1,68 @@ class EventBus { - constructor() { - this.listeners = {}; - } - - on(event, callback) { - if (!this.listeners[event]) { - this.listeners[event] = []; - } - this.listeners[event].push(callback); - } - - emit(event, data) { - if (this.listeners[event]) { - this.listeners[event].forEach(cb => cb(data)); - } - } + constructor() { + this.listeners = new Map(); + this.history = []; + this.maxHistory = 100; + } + + on(event, callback, context = null) { + if (!this.listeners.has(event)) { + this.listeners.set(event, []); + } + this.listeners.get(event).push({ callback, context }); + return () => this.off(event, callback, context); + } + + once(event, callback, context = null) { + const remove = this.on(event, (...args) => { + remove(); + callback.apply(context, args); + }, context); + return remove; + } + + off(event, callback, context = null) { + if (!this.listeners.has(event)) return; + + const listeners = this.listeners.get(event); + this.listeners.set(event, listeners.filter(listener => + listener.callback !== callback || listener.context !== context + )); + } + + emit(event, ...args) { + this.logEvent(event, args); + + if (this.listeners.has(event)) { + this.listeners.get(event).forEach(listener => { + try { + listener.callback.apply(listener.context, args); + } catch (error) { + console.error(`Error in event listener for ${event}:`, error); + } + }); + } + } + + logEvent(event, args) { + this.history.push({ + timestamp: Date.now(), + event, + args + }); + + if (this.history.length > this.maxHistory) { + this.history.shift(); + } + } + + getEventHistory() { + return [...this.history]; + } + + clearHistory() { + this.history = []; + } } -module.exports = EventBus; +module.exports = EventBus; \ No newline at end of file diff --git a/src/core/game-module.js b/src/core/game-module.js new file mode 100644 index 0000000..24ed700 --- /dev/null +++ b/src/core/game-module.js @@ -0,0 +1,43 @@ +class GameModule { + constructor(engine, config) { + this.engine = engine; + this.config = config; + this.isActive = false; + } + + init() { + console.log(`[${this.constructor.name}] Initializing...`); + } + + start() { + console.log(`[${this.constructor.name}] Starting...`); + this.isActive = true; + } + + pause() { + console.log(`[${this.constructor.name}] Pausing...`); + this.isActive = false; + } + + resume() { + console.log(`[${this.constructor.name}] Resuming...`); + this.isActive = true; + } + + stop() { + console.log(`[${this.constructor.name}] Stopping...`); + this.isActive = false; + } + + getScore() { + return 0; + } + + update() { + if (this.isActive) { + // Module-specific update logic + } + } +} + +module.exports = GameModule; diff --git a/src/core/state-manager.js b/src/core/state-manager.js new file mode 100644 index 0000000..99b9b59 --- /dev/null +++ b/src/core/state-manager.js @@ -0,0 +1,96 @@ +class StateManager { + constructor(eventBus) { + this.eventBus = eventBus; + this.state = new Map(); + this.history = []; + this.maxHistory = 100; + this.middleware = []; + } + + // State getters en setters met middleware support + get(key) { + return this.state.get(key); + } + + set(key, value) { + const oldValue = this.state.get(key); + + // Run middleware + const middlewareChain = Promise.resolve({ key, value, oldValue }) + .then(context => + this.middleware.reduce( + (promise, middleware) => promise.then(middleware), + Promise.resolve(context) + ) + ); + + middlewareChain + .then(context => { + this.state.set(context.key, context.value); + this.logStateChange(context.key, context.oldValue, context.value); + this.eventBus.emit('state:changed', { + key: context.key, + oldValue: context.oldValue, + newValue: context.value + }); + }) + .catch(error => { + console.error('State update failed:', error); + this.eventBus.emit('state:error', { key, error }); + }); + } + + // Middleware toevoegen voor state manipulatie + addMiddleware(middleware) { + this.middleware.push(middleware); + } + + // State geschiedenis + logStateChange(key, oldValue, newValue) { + this.history.push({ + timestamp: Date.now(), + key, + oldValue, + newValue + }); + + if (this.history.length > this.maxHistory) { + this.history.shift(); + } + } + + getHistory() { + return [...this.history]; + } + + // Snapshot en herstel functionaliteit + createSnapshot() { + return { + timestamp: Date.now(), + state: new Map(this.state) + }; + } + + restoreSnapshot(snapshot) { + this.state = new Map(snapshot.state); + this.eventBus.emit('state:restored', snapshot); + } + + // Bulk updates met transactie support + transaction(updates) { + const snapshot = this.createSnapshot(); + + try { + updates.forEach(({ key, value }) => { + this.set(key, value); + }); + this.eventBus.emit('transaction:complete', updates); + } catch (error) { + this.restoreSnapshot(snapshot); + this.eventBus.emit('transaction:failed', { error, updates }); + throw error; + } + } +} + +module.exports = StateManager; diff --git a/src/modules/intro/index.js b/src/modules/intro/index.js index 139ce7c..a655e5e 100644 --- a/src/modules/intro/index.js +++ b/src/modules/intro/index.js @@ -1,23 +1,37 @@ -class IntroScreen { - constructor(engine) { - this.engine = engine; +const GameModule = require('../../core/game-module'); + +class IntroModule extends GameModule { + constructor(engine, config) { + super(engine, config); this.state = 'welcome'; } init() { - // Intro screen is now handled by renderer process (HTML/CSS) + super.init(); this.engine.events.emit('intro:ready'); } -} -module.exports = { - init(engine) { - console.log('[module:intro] Initializing...'); - const intro = new IntroScreen(engine); - intro.init(); + start() { + super.start(); + console.log('Welkom bij', this.config.themeName); + + // Start intro sequence + setTimeout(() => { + this.state = 'ready'; + this.engine.events.emit('intro:ready_for_input'); + }, 2000); + } - engine.events.on('intro:complete', () => { - console.log('[module:intro] Complete'); - }); + handleInput(key) { + if (this.state === 'ready' && key === 'Space') { + this.complete(); + } } -}; + + complete() { + this.stop(); + this.engine.events.emit('intro:complete'); + } +} + +module.exports = IntroModule;