Full example: Mini RPG
This demo brings together all the engine's concepts in a single game loop. It is the best way to see how the subsystems collaborate while staying decoupled.
The loop, concept by concept
Every action in the demo touches a different subsystem:
- Attack → reads the
attackstat (StatManager). The damage is the final value, modifiers included. - Defeat an enemy →
gainExperience()(ExperienceManager) accumulates XP and may trigger a level up. - Level up → the
LEVEL_UPevent (Event Bus) updates the UI and the facade grants 2 skill points per level. - Upgrade Brute Force →
upgradeSkill()(SkillManager) spends the points and injects a passive modifier into theattackstat. - The next hit is stronger → back to step 1, but with a higher
attack. The circle closes. - Save / Load → snapshot persistence: save the minimal state and restore it, with the passives re-injected.
What it demonstrates about the architecture
The important thing is not what the demo does, but how it is built:
- The UI (the bars, the log, the floating numbers) does not know the game logic. It only reacts to the
XP_GAINEDandLEVEL_UPevents. - The
SkillManagermodifies theattackstat without theStatManagerknowing that skills exist: it communicates by injecting a per-source modifier. - The entire game state reduces to a small JSON. No derived value is saved.
This is CatalystEngine's strength: each piece does one thing only, the pieces talk to each other only through explicit contracts (events and modifiers), and the result is a system you can extend or reconnect to any frontend.
The equivalent code
The skeleton of what the demo does, in a few lines:
typescript
import { CatalystEngine, ModifierType } from 'catalyst-engine';
const engine = new CatalystEngine(
{ attack: 8 },
{ type: 'EXPONENTIAL', baseXP: 60, multiplier: 1.4 }
);
engine.skills.loadSkillTree([
{
id: 'brute_force', name: 'Brute Force', description: '+5 ATK/lvl',
maxLevel: 5, currentLevel: 0, requiredPlayerLevel: 1,
prerequisiteSkillIds: [], pointCostPerLevel: 1, type: 'PASSIVE',
passiveModifiers: [{ id: 'fb', source: 'attack', type: ModifierType.FLAT, value: 5 }],
},
]);
// The UI listens, it does not query
engine.events.on('LEVEL_UP', ({ currentLevel }) => hud.setLevel(currentLevel));
engine.events.on('XP_GAINED', ({ percentage }) => hud.setXpBar(percentage));
// Combat loop
function onEnemyDefeated(reward: number) {
engine.exp.gainExperience(reward); // → may emit LEVEL_UP
}
function onUpgrade() {
engine.skills.upgradeSkill('brute_force', engine.exp.currentLevel); // → upgrades attack
}
function attackDamage() {
return engine.stats.getStatValue('attack'); // final value, passives included
}