Refactor asset management and event handling; enhance state management with middleware support and transaction capabilities

master
SanjarBLZK 1 week ago
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,23 +1,171 @@
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();
this.prefetchQueue = [];
this.maxCacheSize = 100 * 1024 * 1024; // 100MB
this.currentCacheSize = 0;
} }
loadAssets() { async initialize(config) {
if (!fs.existsSync(this.assetDir)) return; this.baseDir = path.resolve(config.assetsBasePath);
const files = fs.readdirSync(this.assetDir); this.themeDir = path.resolve(config.assetsBasePath, config.theme);
files.forEach(file => {
const filePath = path.join(this.assetDir, file); try {
this.assets[file] = filePath; 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());
}
getAsset(name) { getCacheStats() {
return this.assets[name]; return {
size: this.currentCacheSize,
maxSize: this.maxCacheSize,
count: this.cache.size,
loading: this.loading.size
};
} }
} }

@ -1,19 +1,67 @@
class EventBus { class EventBus {
constructor() { constructor() {
this.listeners = {}; this.listeners = new Map();
this.history = [];
this.maxHistory = 100;
} }
on(event, callback) { on(event, callback, context = null) {
if (!this.listeners[event]) { if (!this.listeners.has(event)) {
this.listeners[event] = []; this.listeners.set(event, []);
} }
this.listeners[event].push(callback); this.listeners.get(event).push({ callback, context });
return () => this.off(event, callback, context);
} }
emit(event, data) { once(event, callback, context = null) {
if (this.listeners[event]) { const remove = this.on(event, (...args) => {
this.listeners[event].forEach(cb => cb(data)); 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 = [];
} }
} }

@ -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();
engine.events.on('intro:complete', () => { // Start intro sequence
console.log('[module:intro] Complete'); setTimeout(() => {
}); this.state = 'ready';
this.engine.events.emit('intro:ready_for_input');
}, 2000);
}
handleInput(key) {
if (this.state === 'ready' && key === 'Space') {
this.complete();
}
} }
};
complete() {
this.stop();
this.engine.events.emit('intro:complete');
}
}
module.exports = IntroModule;

Loading…
Cancel
Save