Refactor asset management and event handling; enhance state management with middleware support and transaction capabilities
parent
6f602375d1
commit
7ed68c79c8
@ -1,5 +1,21 @@
|
|||||||
{
|
{
|
||||||
"name": "I love X (default)",
|
"theme": "default",
|
||||||
"assetsDir": "../assets/default",
|
"themeName": "I Love X",
|
||||||
"tickRate": 2
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,172 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs').promises;
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
class AssetManager {
|
class AssetManager {
|
||||||
constructor(assetDir) {
|
constructor(eventBus) {
|
||||||
this.assetDir = path.resolve(assetDir);
|
this.eventBus = eventBus;
|
||||||
this.assets = {};
|
this.assetMap = new Map();
|
||||||
}
|
this.cache = new Map();
|
||||||
|
this.loading = new Map();
|
||||||
loadAssets() {
|
this.prefetchQueue = [];
|
||||||
if (!fs.existsSync(this.assetDir)) return;
|
this.maxCacheSize = 100 * 1024 * 1024; // 100MB
|
||||||
const files = fs.readdirSync(this.assetDir);
|
this.currentCacheSize = 0;
|
||||||
files.forEach(file => {
|
}
|
||||||
const filePath = path.join(this.assetDir, file);
|
|
||||||
this.assets[file] = filePath;
|
async initialize(config) {
|
||||||
});
|
this.baseDir = path.resolve(config.assetsBasePath);
|
||||||
}
|
this.themeDir = path.resolve(config.assetsBasePath, config.theme);
|
||||||
|
|
||||||
getAsset(name) {
|
try {
|
||||||
return this.assets[name];
|
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;
|
module.exports = AssetManager;
|
||||||
|
@ -1,20 +1,68 @@
|
|||||||
class EventBus {
|
class EventBus {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.listeners = {};
|
this.listeners = new Map();
|
||||||
}
|
this.history = [];
|
||||||
|
this.maxHistory = 100;
|
||||||
on(event, callback) {
|
}
|
||||||
if (!this.listeners[event]) {
|
|
||||||
this.listeners[event] = [];
|
on(event, callback, context = null) {
|
||||||
}
|
if (!this.listeners.has(event)) {
|
||||||
this.listeners[event].push(callback);
|
this.listeners.set(event, []);
|
||||||
}
|
}
|
||||||
|
this.listeners.get(event).push({ callback, context });
|
||||||
emit(event, data) {
|
return () => this.off(event, callback, context);
|
||||||
if (this.listeners[event]) {
|
}
|
||||||
this.listeners[event].forEach(cb => cb(data));
|
|
||||||
}
|
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;
|
@ -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;
|
@ -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;
|
@ -1,23 +1,37 @@
|
|||||||
class IntroScreen {
|
const GameModule = require('../../core/game-module');
|
||||||
constructor(engine) {
|
|
||||||
this.engine = engine;
|
class IntroModule extends GameModule {
|
||||||
|
constructor(engine, config) {
|
||||||
|
super(engine, config);
|
||||||
this.state = 'welcome';
|
this.state = 'welcome';
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
// Intro screen is now handled by renderer process (HTML/CSS)
|
super.init();
|
||||||
this.engine.events.emit('intro:ready');
|
this.engine.events.emit('intro:ready');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
start() {
|
||||||
init(engine) {
|
super.start();
|
||||||
console.log('[module:intro] Initializing...');
|
console.log('Welkom bij', this.config.themeName);
|
||||||
const intro = new IntroScreen(engine);
|
|
||||||
intro.init();
|
// Start intro sequence
|
||||||
|
setTimeout(() => {
|
||||||
|
this.state = 'ready';
|
||||||
|
this.engine.events.emit('intro:ready_for_input');
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
engine.events.on('intro:complete', () => {
|
handleInput(key) {
|
||||||
console.log('[module:intro] Complete');
|
if (this.state === 'ready' && key === 'Space') {
|
||||||
});
|
this.complete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
complete() {
|
||||||
|
this.stop();
|
||||||
|
this.engine.events.emit('intro:complete');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = IntroModule;
|
||||||
|
Loading…
Reference in new issue