feat: add poc

This commit is contained in:
Juan Sebastián Montoya 2026-01-06 14:02:09 -05:00
parent 43d27b04d9
commit 4a4fa05ce4
53 changed files with 6191 additions and 0 deletions

14
src/core/Component.js Normal file
View file

@ -0,0 +1,14 @@
/**
* Base Component class for ECS architecture
* Components are pure data containers
*/
export class Component {
constructor(type) {
this.type = type;
}
static getType() {
return this.name;
}
}

139
src/core/Engine.js Normal file
View file

@ -0,0 +1,139 @@
import { System } from './System.js';
import { Entity } from './Entity.js';
import { EventBus } from './EventBus.js';
/**
* Main game engine - manages ECS, game loop, and systems
*/
export class Engine {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.entities = [];
this.systems = [];
this.events = new EventBus();
this.running = false;
this.lastTime = 0;
// Set canvas size
this.canvas.width = 1024;
this.canvas.height = 768;
// Game state
this.deltaTime = 0;
}
/**
* Add a system to the engine
*/
addSystem(system) {
if (system instanceof System) {
system.init(this);
this.systems.push(system);
// Sort by priority (lower priority runs first)
this.systems.sort((a, b) => a.priority - b.priority);
}
return this;
}
/**
* Emit an event locally
*/
emit(event, data) {
this.events.emit(event, data);
}
/**
* Subscribe to an event
*/
on(event, callback) {
return this.events.on(event, callback);
}
/**
* Create and add an entity
*/
createEntity() {
const entity = new Entity();
this.entities.push(entity);
return entity;
}
/**
* Remove an entity
*/
removeEntity(entity) {
const index = this.entities.indexOf(entity);
if (index > -1) {
this.entities.splice(index, 1);
}
}
/**
* Get all entities
*/
getEntities() {
return this.entities.filter(e => e.active);
}
/**
* Main game loop
*/
start() {
if (this.running) return;
this.running = true;
this.lastTime = performance.now();
this.gameLoop();
}
/**
* Stop the game loop
*/
stop() {
this.running = false;
}
/**
* Game loop using requestAnimationFrame
*/
gameLoop = (currentTime = 0) => {
if (!this.running) return;
// Calculate delta time
this.deltaTime = (currentTime - this.lastTime) / 1000; // Convert to seconds
this.lastTime = currentTime;
// Clamp delta time to prevent large jumps
this.deltaTime = Math.min(this.deltaTime, 0.1);
// Update all systems
const menuSystem = this.systems.find(s => s.name === 'MenuSystem');
const gameState = menuSystem ? menuSystem.getGameState() : 'playing';
const isPaused = gameState === 'paused' || gameState === 'start';
this.systems.forEach(system => {
// Skip game systems if paused/start menu (but allow MenuSystem, UISystem, and RenderSystem)
if (isPaused && system.name !== 'MenuSystem' && system.name !== 'UISystem' && system.name !== 'RenderSystem') {
return;
}
system.update(this.deltaTime, this.entities);
});
// Update input system's previous states at end of frame
const inputSystem = this.systems.find(s => s.name === 'InputSystem');
if (inputSystem && inputSystem.updatePreviousStates) {
inputSystem.updatePreviousStates();
}
// Continue loop
requestAnimationFrame(this.gameLoop);
}
/**
* Clear the canvas
*/
clear() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
}

58
src/core/Entity.js Normal file
View file

@ -0,0 +1,58 @@
/**
* Entity class - represents a game object with a unique ID
* Entities are just containers for components
*/
export class Entity {
static nextId = 0;
constructor() {
this.id = Entity.nextId++;
this.components = new Map();
this.active = true;
}
/**
* Add a component to this entity
*/
addComponent(component) {
this.components.set(component.type, component);
return this;
}
/**
* Get a component by type
*/
getComponent(type) {
return this.components.get(type);
}
/**
* Check if entity has a component
*/
hasComponent(type) {
return this.components.has(type);
}
/**
* Check if entity has all specified components
*/
hasComponents(...types) {
return types.every(type => this.components.has(type));
}
/**
* Remove a component
*/
removeComponent(type) {
this.components.delete(type);
return this;
}
/**
* Get all components
*/
getAllComponents() {
return Array.from(this.components.values());
}
}

57
src/core/EventBus.js Normal file
View file

@ -0,0 +1,57 @@
/**
* Lightweight EventBus for pub/sub communication between systems
*/
export const Events = {
// Combat Events
DAMAGE_DEALT: 'combat:damage_dealt',
ENTITY_DIED: 'combat:entity_died',
// Evolution Events
EVOLVED: 'evolution:evolved',
MUTATION_GAINED: 'evolution:mutation_gained',
// Leveling Events
EXP_GAINED: 'stats:exp_gained',
LEVEL_UP: 'stats:level_up',
// Skill Events
SKILL_LEARNED: 'skills:learned',
SKILL_COOLDOWN_STARTED: 'skills:cooldown_started'
};
export class EventBus {
constructor() {
this.listeners = new Map();
}
/**
* Subscribe to an event
*/
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(callback);
return () => this.off(event, callback);
}
/**
* Unsubscribe from an event
*/
off(event, callback) {
if (!this.listeners.has(event)) return;
const callbacks = this.listeners.get(event);
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
/**
* Emit an event
*/
emit(event, data) {
if (!this.listeners.has(event)) return;
this.listeners.get(event).forEach(callback => callback(data));
}
}

45
src/core/System.js Normal file
View file

@ -0,0 +1,45 @@
/**
* Base System class for ECS architecture
* Systems contain logic that operates on entities with specific components
*/
export class System {
constructor(name) {
this.name = name;
this.requiredComponents = [];
this.priority = 0; // Lower priority runs first
}
/**
* Check if an entity matches this system's requirements
*/
matches(entity) {
if (!entity.active) return false;
return this.requiredComponents.every(componentType =>
entity.hasComponent(componentType)
);
}
/**
* Update method - override in subclasses
*/
update(deltaTime, entities) {
// Filter entities that match this system's requirements
const matchingEntities = entities.filter(entity => this.matches(entity));
this.process(deltaTime, matchingEntities);
}
/**
* Process matching entities - override in subclasses
*/
process(_deltaTime, _entities) {
// Override in subclasses
}
/**
* Called when system is added to engine
*/
init(engine) {
this.engine = engine;
}
}