Feature/VFX and animations #5

Merged
jusemon merged 4 commits from Feature/VFX-and-animations into main 2026-01-06 22:00:16 -05:00
20 changed files with 385 additions and 313 deletions
Showing only changes of commit 3db2bb9160 - Show all commits

62
src/core/Constants.js Normal file
View file

@ -0,0 +1,62 @@
/**
* Centralized Constants and Enums
*/
export const GameState = {
START: 'start',
PLAYING: 'playing',
PAUSED: 'paused',
GAME_OVER: 'gameOver'
};
export const ComponentType = {
POSITION: 'Position',
VELOCITY: 'Velocity',
SPRITE: 'Sprite',
HEALTH: 'Health',
COMBAT: 'Combat',
AI: 'AI',
EVOLUTION: 'Evolution',
STATS: 'Stats',
SKILLS: 'Skills',
SKILL_PROGRESS: 'SkillProgress',
ABSORBABLE: 'Absorbable',
STEALTH: 'Stealth'
};
export const EntityType = {
SLIME: 'slime',
HUMANOID: 'humanoid',
BEAST: 'beast',
ELEMENTAL: 'elemental',
PROJECTILE: 'projectile'
};
export const AnimationState = {
IDLE: 'idle',
WALK: 'walk'
};
export const VFXType = {
IMPACT: 'impact',
ABSORPTION: 'absorption'
};
export const SystemName = {
MENU: 'MenuSystem',
UI: 'UISystem',
PLAYER_CONTROLLER: 'PlayerControllerSystem',
ABSORPTION: 'AbsorptionSystem',
COMBAT: 'CombatSystem',
PROJECTILE: 'ProjectileSystem',
VFX: 'VFXSystem',
MOVEMENT: 'MovementSystem',
AI: 'AISystem',
DEATH: 'DeathSystem',
RENDER: 'RenderSystem',
INPUT: 'InputSystem',
SKILL_EFFECT: 'SkillEffectSystem',
SKILL: 'SkillSystem',
STEALTH: 'StealthSystem',
HEALTH_REGEN: 'HealthRegenerationSystem'
};

View file

@ -2,6 +2,7 @@ import { System } from './System.js';
import { Entity } from './Entity.js'; import { Entity } from './Entity.js';
import { EventBus } from './EventBus.js'; import { EventBus } from './EventBus.js';
import { LevelLoader } from './LevelLoader.js'; import { LevelLoader } from './LevelLoader.js';
import { GameState, SystemName } from './Constants.js';
/** /**
* Main game engine - manages ECS, game loop, and systems * Main game engine - manages ECS, game loop, and systems
@ -118,20 +119,21 @@ export class Engine {
this.deltaTime = Math.min(this.deltaTime, 0.1); this.deltaTime = Math.min(this.deltaTime, 0.1);
// Update all systems // Update all systems
const menuSystem = this.systems.find(s => s.name === 'MenuSystem'); const menuSystem = this.systems.find(s => s.name === SystemName.MENU);
const gameState = menuSystem ? menuSystem.getGameState() : 'playing'; const gameState = menuSystem ? menuSystem.getGameState() : GameState.PLAYING;
const isPaused = gameState === 'paused' || gameState === 'start' || gameState === 'gameOver'; const isPaused = [GameState.PAUSED, GameState.START, GameState.GAME_OVER].includes(gameState);
const unskippedSystems = [SystemName.MENU, SystemName.UI, SystemName.RENDER];
this.systems.forEach(system => { this.systems.forEach(system => {
// Skip game systems if paused/start menu (but allow MenuSystem, UISystem, and RenderSystem) // Skip game systems if paused/start menu (but allow MenuSystem, UISystem, and RenderSystem)
if (isPaused && system.name !== 'MenuSystem' && system.name !== 'UISystem' && system.name !== 'RenderSystem') { if (isPaused && !unskippedSystems.includes(system.name)) {
return; return;
} }
system.update(this.deltaTime, this.entities); system.update(this.deltaTime, this.entities);
}); });
// Update input system's previous states at end of frame // Update input system's previous states at end of frame
const inputSystem = this.systems.find(s => s.name === 'InputSystem'); const inputSystem = this.systems.find(s => s.name === SystemName.INPUT);
if (inputSystem && inputSystem.updatePreviousStates) { if (inputSystem && inputSystem.updatePreviousStates) {
inputSystem.updatePreviousStates(); inputSystem.updatePreviousStates();
} }

View file

@ -1,3 +1,5 @@
import { EntityType, AnimationState } from './Constants.js';
/** /**
* Sprite Library defining pixel art grids as 2D arrays. * Sprite Library defining pixel art grids as 2D arrays.
* 0: Transparent * 0: Transparent
@ -7,8 +9,8 @@
*/ */
export const SpriteLibrary = { export const SpriteLibrary = {
// 8x8 Slime - Bottom-heavy blob // 8x8 Slime - Bottom-heavy blob
slime: { [EntityType.SLIME]: {
idle: [ [AnimationState.IDLE]: [
[ [
[0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 0, 0], // Top [0, 0, 1, 1, 1, 1, 0, 0], // Top
@ -30,7 +32,7 @@ export const SpriteLibrary = {
[1, 1, 1, 1, 1, 1, 1, 1] // Squashed base [1, 1, 1, 1, 1, 1, 1, 1] // Squashed base
] ]
], ],
walk: [ [AnimationState.WALK]: [
[ [
[0, 0, 1, 1, 1, 1, 0, 0], [0, 0, 1, 1, 1, 1, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 0], [0, 1, 1, 1, 1, 1, 1, 0],
@ -55,8 +57,8 @@ export const SpriteLibrary = {
}, },
// 8x8 Humanoid - Simple Walk Cycle // 8x8 Humanoid - Simple Walk Cycle
humanoid: { [EntityType.HUMANOID]: {
idle: [ [AnimationState.IDLE]: [
[ [
[0, 0, 0, 1, 1, 0, 0, 0], [0, 0, 0, 1, 1, 0, 0, 0],
[0, 0, 2, 1, 1, 2, 0, 0], [0, 0, 2, 1, 1, 2, 0, 0],
@ -68,7 +70,7 @@ export const SpriteLibrary = {
[0, 0, 1, 0, 0, 1, 0, 0] [0, 0, 1, 0, 0, 1, 0, 0]
] ]
], ],
walk: [ [AnimationState.WALK]: [
[ [
[0, 0, 0, 1, 1, 0, 0, 0], [0, 0, 0, 1, 1, 0, 0, 0],
[0, 0, 2, 1, 1, 2, 0, 0], [0, 0, 2, 1, 1, 2, 0, 0],
@ -93,8 +95,8 @@ export const SpriteLibrary = {
}, },
// 8x8 Beast - Bounding Cycle // 8x8 Beast - Bounding Cycle
beast: { [EntityType.BEAST]: {
idle: [ [AnimationState.IDLE]: [
[ [
[0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 1],
@ -106,7 +108,7 @@ export const SpriteLibrary = {
[0, 1, 0, 0, 0, 0, 1, 0] [0, 1, 0, 0, 0, 0, 1, 0]
] ]
], ],
walk: [ [AnimationState.WALK]: [
[ [
[1, 0, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 1],
[0, 1, 1, 1, 1, 1, 1, 0], [0, 1, 1, 1, 1, 1, 1, 0],
@ -131,8 +133,8 @@ export const SpriteLibrary = {
}, },
// 8x8 Elemental - Floating Pulse // 8x8 Elemental - Floating Pulse
elemental: { [EntityType.ELEMENTAL]: {
idle: [ [AnimationState.IDLE]: [
[ [
[0, 0, 2, 1, 1, 2, 0, 0], [0, 0, 2, 1, 1, 2, 0, 0],
[0, 1, 1, 2, 2, 1, 1, 0], [0, 1, 1, 2, 2, 1, 1, 0],
@ -156,8 +158,8 @@ export const SpriteLibrary = {
] ]
}, },
projectile: { [EntityType.PROJECTILE]: {
idle: [ [AnimationState.IDLE]: [
[ [
[1, 1], [1, 1],
[1, 1] [1, 1]

View file

@ -31,6 +31,9 @@ import { AI } from './components/AI.js';
import { Absorbable } from './components/Absorbable.js'; import { Absorbable } from './components/Absorbable.js';
import { SkillProgress } from './components/SkillProgress.js'; import { SkillProgress } from './components/SkillProgress.js';
// Constants
import { EntityType, ComponentType } from './core/Constants.js';
// Initialize game // Initialize game
const canvas = document.getElementById('game-canvas'); const canvas = document.getElementById('game-canvas');
if (!canvas) { if (!canvas) {
@ -60,7 +63,7 @@ if (!canvas) {
const player = engine.createEntity(); const player = engine.createEntity();
player.addComponent(new Position(160, 120)); // Center of 320x240 player.addComponent(new Position(160, 120)); // Center of 320x240
player.addComponent(new Velocity(0, 0, 100)); // Slower speed for small resolution player.addComponent(new Velocity(0, 0, 100)); // Slower speed for small resolution
player.addComponent(new Sprite('#00ff96', 14, 14, 'slime')); // 14x14 pixel sprite player.addComponent(new Sprite('#00ff96', 14, 14, EntityType.SLIME)); // 14x14 pixel sprite
player.addComponent(new Health(100)); player.addComponent(new Health(100));
player.addComponent(new Stats()); player.addComponent(new Stats());
player.addComponent(new Evolution()); player.addComponent(new Evolution());
@ -84,17 +87,17 @@ if (!canvas) {
let color, evolutionData, skills; let color, evolutionData, skills;
switch (type) { switch (type) {
case 'humanoid': case EntityType.HUMANOID:
color = '#ff5555'; // Humanoid red color = '#ff5555'; // Humanoid red
evolutionData = { human: 10, beast: 0, slime: -2 }; evolutionData = { human: 10, beast: 0, slime: -2 };
skills = ['fire_breath']; skills = ['fire_breath'];
break; break;
case 'beast': case EntityType.BEAST:
color = '#ffaa00'; // Beast orange color = '#ffaa00'; // Beast orange
evolutionData = { human: 0, beast: 10, slime: -2 }; evolutionData = { human: 0, beast: 10, slime: -2 };
skills = ['pounce']; skills = ['pounce'];
break; break;
case 'elemental': case EntityType.ELEMENTAL:
color = '#00bfff'; color = '#00bfff';
evolutionData = { human: 3, beast: 3, slime: 8 }; evolutionData = { human: 3, beast: 3, slime: 8 };
skills = ['fire_breath']; skills = ['fire_breath'];
@ -123,7 +126,7 @@ if (!canvas) {
for (let i = 0; i < 8; i++) { for (let i = 0; i < 8; i++) {
const x = 20 + Math.random() * 280; // Fit in 320 width const x = 20 + Math.random() * 280; // Fit in 320 width
const y = 20 + Math.random() * 200; // Fit in 240 height const y = 20 + Math.random() * 200; // Fit in 240 height
const types = ['humanoid', 'beast', 'elemental']; const types = [EntityType.HUMANOID, EntityType.BEAST, EntityType.ELEMENTAL];
const type = types[Math.floor(Math.random() * types.length)]; const type = types[Math.floor(Math.random() * types.length)];
createCreature(engine, x, y, type); createCreature(engine, x, y, type);
} }
@ -131,13 +134,13 @@ if (!canvas) {
// Spawn new creatures periodically // Spawn new creatures periodically
setInterval(() => { setInterval(() => {
const existingCreatures = engine.getEntities().filter(e => const existingCreatures = engine.getEntities().filter(e =>
e.hasComponent('AI') && e !== player e.hasComponent(ComponentType.AI) && e !== player
); );
if (existingCreatures.length < 10) { if (existingCreatures.length < 10) {
const x = 20 + Math.random() * 280; const x = 20 + Math.random() * 280;
const y = 20 + Math.random() * 200; const y = 20 + Math.random() * 200;
const types = ['humanoid', 'beast', 'elemental']; const types = [EntityType.HUMANOID, EntityType.BEAST, EntityType.ELEMENTAL];
const type = types[Math.floor(Math.random() * types.length)]; const type = types[Math.floor(Math.random() * types.length)];
createCreature(engine, x, y, type); createCreature(engine, x, y, type);
} }

View file

@ -1,24 +1,25 @@
import { System } from '../core/System.js'; import { System } from '../core/System.js';
import { GameConfig } from '../GameConfig.js'; import { GameConfig } from '../GameConfig.js';
import { SystemName, ComponentType } from '../core/Constants.js';
export class AISystem extends System { export class AISystem extends System {
constructor() { constructor() {
super('AISystem'); super(SystemName.AI);
this.requiredComponents = ['Position', 'Velocity', 'AI']; this.requiredComponents = [ComponentType.POSITION, ComponentType.VELOCITY, ComponentType.AI];
this.priority = 15; this.priority = 15;
} }
process(deltaTime, entities) { process(deltaTime, entities) {
const playerController = this.engine.systems.find(s => s.name === 'PlayerControllerSystem'); const playerController = this.engine.systems.find(s => s.name === SystemName.PLAYER_CONTROLLER);
const player = playerController ? playerController.getPlayerEntity() : null; const player = playerController ? playerController.getPlayerEntity() : null;
const playerPos = player?.getComponent('Position'); const playerPos = player?.getComponent(ComponentType.POSITION);
const config = GameConfig.AI; const config = GameConfig.AI;
entities.forEach(entity => { entities.forEach(entity => {
const health = entity.getComponent('Health'); const health = entity.getComponent(ComponentType.HEALTH);
const ai = entity.getComponent('AI'); const ai = entity.getComponent(ComponentType.AI);
const position = entity.getComponent('Position'); const position = entity.getComponent(ComponentType.POSITION);
const velocity = entity.getComponent('Velocity'); const velocity = entity.getComponent(ComponentType.VELOCITY);
if (!ai || !position || !velocity) return; if (!ai || !position || !velocity) return;
@ -39,7 +40,7 @@ export class AISystem extends System {
const distance = Math.sqrt(dx * dx + dy * dy); const distance = Math.sqrt(dx * dx + dy * dy);
// Update awareness based on distance and player stealth // Update awareness based on distance and player stealth
const playerStealth = player?.getComponent('Stealth'); const playerStealth = player?.getComponent(ComponentType.STEALTH);
const playerVisibility = playerStealth ? playerStealth.visibility : 1.0; const playerVisibility = playerStealth ? playerStealth.visibility : 1.0;
if (distance < ai.alertRadius) { if (distance < ai.alertRadius) {
@ -50,10 +51,10 @@ export class AISystem extends System {
} }
// Biological Reputation Logic // Biological Reputation Logic
const playerEvolution = player?.getComponent('Evolution'); const playerEvolution = player?.getComponent(ComponentType.EVOLUTION);
const playerForm = playerEvolution ? playerEvolution.getDominantForm() : 'slime'; const playerForm = playerEvolution ? playerEvolution.getDominantForm() : 'slime';
const entityType = entity.getComponent('Sprite')?.color === '#ffaa00' ? 'beast' : const entityType = entity.getComponent(ComponentType.SPRITE)?.color === '#ffaa00' ? 'beast' :
entity.getComponent('Sprite')?.color === '#ff5555' ? 'humanoid' : 'other'; entity.getComponent(ComponentType.SPRITE)?.color === '#ff5555' ? 'humanoid' : 'other';
// Check if player is "one of us" or "too scary" // Check if player is "one of us" or "too scary"
let isPassive = false; let isPassive = false;
@ -64,8 +65,8 @@ export class AISystem extends System {
if (ai.awareness < config.passiveAwarenessThreshold) isPassive = true; if (ai.awareness < config.passiveAwarenessThreshold) isPassive = true;
} else if (entityType === 'beast' && playerForm === 'beast') { } else if (entityType === 'beast' && playerForm === 'beast') {
// Beasts might flee from a dominant beast player // Beasts might flee from a dominant beast player
const playerStats = player?.getComponent('Stats'); const playerStats = player?.getComponent(ComponentType.STATS);
const entityStats = entity.getComponent('Stats'); const entityStats = entity.getComponent(ComponentType.STATS);
if (playerStats && entityStats && playerStats.level > entityStats.level) { if (playerStats && entityStats && playerStats.level > entityStats.level) {
shouldFlee = true; shouldFlee = true;
} }
@ -85,7 +86,7 @@ export class AISystem extends System {
} else if (ai.awareness > config.detectionAwarenessThreshold && distance < ai.chaseRadius) { } else if (ai.awareness > config.detectionAwarenessThreshold && distance < ai.chaseRadius) {
if (ai.behaviorType !== 'flee') { if (ai.behaviorType !== 'flee') {
// Check if in attack range - if so, use combat behavior // Check if in attack range - if so, use combat behavior
const combat = entity.getComponent('Combat'); const combat = entity.getComponent(ComponentType.COMBAT);
if (combat && distance <= combat.attackRange) { if (combat && distance <= combat.attackRange) {
ai.setBehavior('combat'); ai.setBehavior('combat');
ai.state = 'combat'; ai.state = 'combat';
@ -103,7 +104,7 @@ export class AISystem extends System {
} }
} else if (ai.behaviorType === 'chase') { } else if (ai.behaviorType === 'chase') {
// Update from chase to combat if in range // Update from chase to combat if in range
const combat = entity.getComponent('Combat'); const combat = entity.getComponent(ComponentType.COMBAT);
if (combat && distance <= combat.attackRange) { if (combat && distance <= combat.attackRange) {
ai.setBehavior('combat'); ai.setBehavior('combat');
ai.state = 'combat'; ai.state = 'combat';
@ -151,7 +152,7 @@ export class AISystem extends System {
const distance = Math.sqrt(dx * dx + dy * dy); const distance = Math.sqrt(dx * dx + dy * dy);
// Check if we should switch to combat // Check if we should switch to combat
const combat = entity.getComponent('Combat'); const combat = entity.getComponent(ComponentType.COMBAT);
if (combat && distance <= combat.attackRange) { if (combat && distance <= combat.attackRange) {
ai.setBehavior('combat'); ai.setBehavior('combat');
ai.state = 'combat'; ai.state = 'combat';
@ -193,7 +194,7 @@ export class AISystem extends System {
const dy = targetPos.y - position.y; const dy = targetPos.y - position.y;
const distance = Math.sqrt(dx * dx + dy * dy); const distance = Math.sqrt(dx * dx + dy * dy);
const combat = entity.getComponent('Combat'); const combat = entity.getComponent(ComponentType.COMBAT);
if (combat && distance > combat.attackRange) { if (combat && distance > combat.attackRange) {
// Move closer if out of range // Move closer if out of range
const speed = ai.wanderSpeed; const speed = ai.wanderSpeed;

View file

@ -1,25 +1,26 @@
import { System } from '../core/System.js'; import { System } from '../core/System.js';
import { GameConfig } from '../GameConfig.js'; import { GameConfig } from '../GameConfig.js';
import { Events } from '../core/EventBus.js'; import { Events } from '../core/EventBus.js';
import { SystemName, ComponentType } from '../core/Constants.js';
export class AbsorptionSystem extends System { export class AbsorptionSystem extends System {
constructor() { constructor() {
super('AbsorptionSystem'); super(SystemName.ABSORPTION);
this.requiredComponents = ['Position', 'Absorbable']; this.requiredComponents = [ComponentType.POSITION, ComponentType.ABSORBABLE];
this.priority = 25; this.priority = 25;
} }
process(_deltaTime, _entities) { process(_deltaTime, _entities) {
const playerController = this.engine.systems.find(s => s.name === 'PlayerControllerSystem'); const playerController = this.engine.systems.find(s => s.name === SystemName.PLAYER_CONTROLLER);
const player = playerController ? playerController.getPlayerEntity() : null; const player = playerController ? playerController.getPlayerEntity() : null;
if (!player) return; if (!player) return;
const playerPos = player.getComponent('Position'); const playerPos = player.getComponent(ComponentType.POSITION);
const playerEvolution = player.getComponent('Evolution'); const playerEvolution = player.getComponent(ComponentType.EVOLUTION);
const playerSkills = player.getComponent('Skills'); const playerSkills = player.getComponent(ComponentType.SKILLS);
const playerStats = player.getComponent('Stats'); const playerStats = player.getComponent(ComponentType.STATS);
const skillProgress = player.getComponent('SkillProgress'); const skillProgress = player.getComponent(ComponentType.SKILL_PROGRESS);
if (!playerPos || !playerEvolution) return; if (!playerPos || !playerEvolution) return;
@ -33,19 +34,19 @@ export class AbsorptionSystem extends System {
if (entity === player) return; if (entity === player) return;
// Allow inactive entities if they're dead and absorbable // Allow inactive entities if they're dead and absorbable
if (!entity.active) { if (!entity.active) {
const health = entity.getComponent('Health'); const health = entity.getComponent(ComponentType.HEALTH);
const absorbable = entity.getComponent('Absorbable'); const absorbable = entity.getComponent(ComponentType.ABSORBABLE);
// Only process inactive entities if they're dead and not yet absorbed // Only process inactive entities if they're dead and not yet absorbed
if (!health || !health.isDead() || !absorbable || absorbable.absorbed) { if (!health || !health.isDead() || !absorbable || absorbable.absorbed) {
return; return;
} }
} }
if (!entity.hasComponent('Absorbable')) return; if (!entity.hasComponent(ComponentType.ABSORBABLE)) return;
if (!entity.hasComponent('Health')) return; if (!entity.hasComponent(ComponentType.HEALTH)) return;
const absorbable = entity.getComponent('Absorbable'); const absorbable = entity.getComponent(ComponentType.ABSORBABLE);
const health = entity.getComponent('Health'); const health = entity.getComponent(ComponentType.HEALTH);
const entityPos = entity.getComponent('Position'); const entityPos = entity.getComponent(ComponentType.POSITION);
if (!entityPos) return; if (!entityPos) return;
@ -68,8 +69,8 @@ export class AbsorptionSystem extends System {
if (absorbable.absorbed) return; if (absorbable.absorbed) return;
absorbable.absorbed = true; absorbable.absorbed = true;
const entityPos = entity.getComponent('Position'); const entityPos = entity.getComponent(ComponentType.POSITION);
const health = player.getComponent('Health'); const health = player.getComponent(ComponentType.HEALTH);
const config = GameConfig.Absorption; const config = GameConfig.Absorption;
// Add evolution points // Add evolution points
@ -111,7 +112,7 @@ export class AbsorptionSystem extends System {
// Visual effect // Visual effect
if (entityPos) { if (entityPos) {
const vfxSystem = this.engine.systems.find(s => s.name === 'VFXSystem'); const vfxSystem = this.engine.systems.find(s => s.name === SystemName.VFX);
if (vfxSystem) { if (vfxSystem) {
vfxSystem.createAbsorption(entityPos.x, entityPos.y); vfxSystem.createAbsorption(entityPos.x, entityPos.y);
} }

View file

@ -1,35 +1,36 @@
import { System } from '../core/System.js'; import { System } from '../core/System.js';
import { GameConfig } from '../GameConfig.js'; import { GameConfig } from '../GameConfig.js';
import { Events } from '../core/EventBus.js'; import { Events } from '../core/EventBus.js';
import { SystemName, ComponentType } from '../core/Constants.js';
export class CombatSystem extends System { export class CombatSystem extends System {
constructor() { constructor() {
super('CombatSystem'); super(SystemName.COMBAT);
this.requiredComponents = ['Position', 'Combat', 'Health']; this.requiredComponents = [ComponentType.POSITION, ComponentType.COMBAT, ComponentType.HEALTH];
this.priority = 20; this.priority = 20;
} }
process(deltaTime, entities) { process(deltaTime, entities) {
// Update combat cooldowns // Update combat cooldowns
entities.forEach(entity => { entities.forEach(entity => {
const combat = entity.getComponent('Combat'); const combat = entity.getComponent(ComponentType.COMBAT);
if (combat) { if (combat) {
combat.update(deltaTime); combat.update(deltaTime);
} }
}); });
// Handle player attacks // Handle player attacks
const playerController = this.engine.systems.find(s => s.name === 'PlayerControllerSystem'); const playerController = this.engine.systems.find(s => s.name === SystemName.PLAYER_CONTROLLER);
const player = playerController ? playerController.getPlayerEntity() : null; const player = playerController ? playerController.getPlayerEntity() : null;
if (player && player.hasComponent('Combat')) { if (player && player.hasComponent(ComponentType.COMBAT)) {
this.handlePlayerCombat(player, deltaTime); this.handlePlayerCombat(player, deltaTime);
} }
// Handle creature attacks // Handle creature attacks
const creatures = entities.filter(e => const creatures = entities.filter(e =>
e.hasComponent('AI') && e.hasComponent(ComponentType.AI) &&
e.hasComponent('Combat') && e.hasComponent(ComponentType.COMBAT) &&
e !== player e !== player
); );
@ -42,9 +43,9 @@ export class CombatSystem extends System {
} }
handlePlayerCombat(player, _deltaTime) { handlePlayerCombat(player, _deltaTime) {
const inputSystem = this.engine.systems.find(s => s.name === 'InputSystem'); const inputSystem = this.engine.systems.find(s => s.name === SystemName.INPUT);
const combat = player.getComponent('Combat'); const combat = player.getComponent(ComponentType.COMBAT);
const position = player.getComponent('Position'); const position = player.getComponent(ComponentType.POSITION);
if (!inputSystem || !combat || !position) return; if (!inputSystem || !combat || !position) return;
@ -72,10 +73,10 @@ export class CombatSystem extends System {
} }
handleCreatureCombat(creature, player, _deltaTime) { handleCreatureCombat(creature, player, _deltaTime) {
const ai = creature.getComponent('AI'); const ai = creature.getComponent(ComponentType.AI);
const combat = creature.getComponent('Combat'); const combat = creature.getComponent(ComponentType.COMBAT);
const position = creature.getComponent('Position'); const position = creature.getComponent(ComponentType.POSITION);
const playerPos = player?.getComponent('Position'); const playerPos = player?.getComponent(ComponentType.POSITION);
if (!ai || !combat || !position) return; if (!ai || !combat || !position) return;
@ -98,16 +99,12 @@ export class CombatSystem extends System {
performAttack(attacker, combat, attackerPos) { performAttack(attacker, combat, attackerPos) {
const entities = this.engine.getEntities(); const entities = this.engine.getEntities();
const stats = attacker.getComponent('Stats');
const _baseDamage = stats ?
(combat.attackDamage + stats.strength * 0.5) :
combat.attackDamage;
entities.forEach(target => { entities.forEach(target => {
if (target === attacker) return; if (target === attacker) return;
if (!target.hasComponent('Health')) return; if (!target.hasComponent(ComponentType.HEALTH)) return;
const targetPos = target.getComponent('Position'); const targetPos = target.getComponent(ComponentType.POSITION);
if (!targetPos) return; if (!targetPos) return;
// Check if in attack range and angle // Check if in attack range and angle
@ -124,14 +121,14 @@ export class CombatSystem extends System {
// Attack arc // Attack arc
const attackArc = GameConfig.Combat.defaultAttackArc; const attackArc = GameConfig.Combat.defaultAttackArc;
if (minDiff < attackArc) { if (minDiff < attackArc) {
const health = target.getComponent('Health'); const health = target.getComponent(ComponentType.HEALTH);
const config = GameConfig.Combat; const config = GameConfig.Combat;
const stats = attacker.getComponent('Stats'); const stats = attacker.getComponent(ComponentType.STATS);
const baseDamage = stats ? (combat.attackDamage + stats.strength * 0.5) : combat.attackDamage; const baseDamage = stats ? (combat.attackDamage + stats.strength * 0.5) : combat.attackDamage;
// Defense bonus from Hardened Shell // Defense bonus from Hardened Shell
let finalDamage = baseDamage; let finalDamage = baseDamage;
const targetEvolution = target.getComponent('Evolution'); const targetEvolution = target.getComponent(ComponentType.EVOLUTION);
if (targetEvolution && targetEvolution.mutationEffects.hardenedShell) { if (targetEvolution && targetEvolution.mutationEffects.hardenedShell) {
finalDamage *= config.hardenedShellReduction; finalDamage *= config.hardenedShellReduction;
@ -149,7 +146,7 @@ export class CombatSystem extends System {
// Damage reflection from Electric Skin // Damage reflection from Electric Skin
if (targetEvolution && targetEvolution.mutationEffects.electricSkin) { if (targetEvolution && targetEvolution.mutationEffects.electricSkin) {
const attackerHealth = attacker.getComponent('Health'); const attackerHealth = attacker.getComponent(ComponentType.HEALTH);
if (attackerHealth) { if (attackerHealth) {
const reflectedDamage = actualDamage * config.damageReflectionPercent; const reflectedDamage = actualDamage * config.damageReflectionPercent;
attackerHealth.takeDamage(reflectedDamage); attackerHealth.takeDamage(reflectedDamage);
@ -169,7 +166,7 @@ export class CombatSystem extends System {
} }
// Apply knockback // Apply knockback
const velocity = target.getComponent('Velocity'); const velocity = target.getComponent(ComponentType.VELOCITY);
if (velocity) { if (velocity) {
const knockbackPower = config.knockbackPower; const knockbackPower = config.knockbackPower;
const kx = Math.cos(angle) * knockbackPower; const kx = Math.cos(angle) * knockbackPower;

View file

@ -1,12 +1,13 @@
import { System } from '../core/System.js'; import { System } from '../core/System.js';
import { SystemName, ComponentType } from '../core/Constants.js';
/** /**
* System to handle entity death - removes dead entities immediately * System to handle entity death - removes dead entities immediately
*/ */
export class DeathSystem extends System { export class DeathSystem extends System {
constructor() { constructor() {
super('DeathSystem'); super(SystemName.DEATH);
this.requiredComponents = ['Health']; this.requiredComponents = [ComponentType.HEALTH];
this.priority = 50; // Run after absorption (absorption is priority 25) this.priority = 50; // Run after absorption (absorption is priority 25)
} }
@ -19,15 +20,15 @@ export class DeathSystem extends System {
process(deltaTime, allEntities) { process(deltaTime, allEntities) {
allEntities.forEach(entity => { allEntities.forEach(entity => {
const health = entity.getComponent('Health'); const health = entity.getComponent(ComponentType.HEALTH);
if (!health) return; if (!health) return;
// Check if entity is dead // Check if entity is dead
if (health.isDead()) { if (health.isDead()) {
// Check if player died // Check if player died
const evolution = entity.getComponent('Evolution'); const evolution = entity.getComponent(ComponentType.EVOLUTION);
if (evolution) { if (evolution) {
const menuSystem = this.engine.systems.find(s => s.name === 'MenuSystem'); const menuSystem = this.engine.systems.find(s => s.name === SystemName.MENU);
if (menuSystem) { if (menuSystem) {
menuSystem.showGameOver(); menuSystem.showGameOver();
} }
@ -45,7 +46,7 @@ export class DeathSystem extends System {
} }
// Check if it's absorbable - if so, give a short window for absorption // Check if it's absorbable - if so, give a short window for absorption
const absorbable = entity.getComponent('Absorbable'); const absorbable = entity.getComponent(ComponentType.ABSORBABLE);
if (absorbable && !absorbable.absorbed) { if (absorbable && !absorbable.absorbed) {
// Give 3 seconds for player to absorb, then remove // Give 3 seconds for player to absorb, then remove
const timeSinceDeath = (Date.now() - entity.deathTime) / 1000; const timeSinceDeath = (Date.now() - entity.deathTime) / 1000;

View file

@ -1,18 +1,19 @@
import { System } from '../core/System.js'; import { System } from '../core/System.js';
import { SystemName, ComponentType } from '../core/Constants.js';
/** /**
* System to handle health regeneration * System to handle health regeneration
*/ */
export class HealthRegenerationSystem extends System { export class HealthRegenerationSystem extends System {
constructor() { constructor() {
super('HealthRegenerationSystem'); super(SystemName.HEALTH_REGEN);
this.requiredComponents = ['Health']; this.requiredComponents = [ComponentType.HEALTH];
this.priority = 35; this.priority = 35;
} }
process(deltaTime, entities) { process(deltaTime, entities) {
entities.forEach(entity => { entities.forEach(entity => {
const health = entity.getComponent('Health'); const health = entity.getComponent(ComponentType.HEALTH);
if (!health || health.regeneration <= 0) return; if (!health || health.regeneration <= 0) return;
// Regenerate health over time // Regenerate health over time

View file

@ -1,8 +1,9 @@
import { System } from '../core/System.js'; import { System } from '../core/System.js';
import { SystemName } from '../core/Constants.js';
export class InputSystem extends System { export class InputSystem extends System {
constructor() { constructor() {
super('InputSystem'); super(SystemName.INPUT);
this.requiredComponents = []; // No required components - handles input globally this.requiredComponents = []; // No required components - handles input globally
this.priority = 0; // Run first this.priority = 0; // Run first

View file

@ -1,18 +1,19 @@
import { System } from '../core/System.js'; import { System } from '../core/System.js';
import { PixelFont } from '../core/PixelFont.js'; import { PixelFont } from '../core/PixelFont.js';
import { Palette } from '../core/Palette.js'; import { Palette } from '../core/Palette.js';
import { GameState, ComponentType, SystemName } from '../core/Constants.js';
/** /**
* System to handle game menus (start, pause) * System to handle game menus (start, pause)
*/ */
export class MenuSystem extends System { export class MenuSystem extends System {
constructor(engine) { constructor(engine) {
super('MenuSystem'); super(SystemName.MENU);
this.requiredComponents = []; // No required components this.requiredComponents = []; // No required components
this.priority = 1; // Run early this.priority = 1; // Run early
this.engine = engine; this.engine = engine;
this.ctx = engine.ctx; this.ctx = engine.ctx;
this.gameState = 'start'; // 'start', 'playing', 'paused' this.gameState = GameState.START;
this.paused = false; this.paused = false;
} }
@ -24,16 +25,16 @@ export class MenuSystem extends System {
setupInput() { setupInput() {
window.addEventListener('keydown', (e) => { window.addEventListener('keydown', (e) => {
if (e.key === 'Escape' || e.key === 'p' || e.key === 'P') { if (e.key === 'Escape' || e.key === 'p' || e.key === 'P') {
if (this.gameState === 'playing') { if (this.gameState === GameState.PLAYING) {
this.togglePause(); this.togglePause();
} }
} }
if (e.key === 'Enter' || e.key === ' ') { if (e.key === 'Enter' || e.key === ' ') {
if (this.gameState === 'start') { if (this.gameState === GameState.START) {
this.startGame(); this.startGame();
} else if (this.gameState === 'paused') { } else if (this.gameState === GameState.PAUSED) {
this.resumeGame(); this.resumeGame();
} else if (this.gameState === 'gameOver') { } else if (this.gameState === GameState.GAME_OVER) {
this.restartGame(); this.restartGame();
} }
} }
@ -41,7 +42,7 @@ export class MenuSystem extends System {
} }
showGameOver() { showGameOver() {
this.gameState = 'gameOver'; this.gameState = GameState.GAME_OVER;
this.paused = true; this.paused = true;
} }
@ -50,7 +51,7 @@ export class MenuSystem extends System {
} }
startGame() { startGame() {
this.gameState = 'playing'; this.gameState = GameState.PLAYING;
this.paused = false; this.paused = false;
if (!this.engine.running) { if (!this.engine.running) {
this.engine.start(); this.engine.start();
@ -58,28 +59,23 @@ export class MenuSystem extends System {
} }
togglePause() { togglePause() {
if (this.gameState === 'playing') { if (this.gameState === GameState.PLAYING) {
this.gameState = 'paused'; this.gameState = GameState.PAUSED;
this.paused = true; this.paused = true;
} else if (this.gameState === 'paused') { } else if (this.gameState === GameState.PAUSED) {
this.resumeGame(); this.resumeGame();
} }
} }
resumeGame() { resumeGame() {
this.gameState = 'playing'; this.gameState = GameState.PLAYING;
this.paused = false; this.paused = false;
} }
process(_deltaTime, _entities) { process(_deltaTime, _entities) {
// Don't update game systems if paused or at start menu // Don't update game systems if paused or at start menu
if (this.gameState === 'paused' || this.gameState === 'start') { if (this.gameState === GameState.PAUSED || this.gameState === GameState.START) {
// Pause all other systems // Logic for system handling is moved to the update loop in Engine
this.engine.systems.forEach(system => {
if (system !== this && system.name !== 'MenuSystem' && system.name !== 'UISystem') {
// Systems will check game state themselves
}
});
} }
} }
@ -92,7 +88,7 @@ export class MenuSystem extends System {
ctx.fillStyle = 'rgba(32, 21, 51, 0.8)'; // Semi-transparent VOID ctx.fillStyle = 'rgba(32, 21, 51, 0.8)'; // Semi-transparent VOID
ctx.fillRect(0, 0, width, height); ctx.fillRect(0, 0, width, height);
if (this.gameState === 'start') { if (this.gameState === GameState.START) {
const title = 'SLIME GENESIS'; const title = 'SLIME GENESIS';
const titleW = PixelFont.getTextWidth(title, 2); const titleW = PixelFont.getTextWidth(title, 2);
PixelFont.drawText(ctx, title, (width - titleW) / 2, height / 2 - 40, Palette.CYAN, 2); PixelFont.drawText(ctx, title, (width - titleW) / 2, height / 2 - 40, Palette.CYAN, 2);
@ -112,7 +108,7 @@ export class MenuSystem extends System {
PixelFont.drawText(ctx, line, (width - lineW) / 2, height / 2 + 25 + i * 10, Palette.ROYAL_BLUE, 1); PixelFont.drawText(ctx, line, (width - lineW) / 2, height / 2 + 25 + i * 10, Palette.ROYAL_BLUE, 1);
}); });
} else if (this.gameState === 'paused') { } else if (this.gameState === GameState.PAUSED) {
const paused = 'PAUSED'; const paused = 'PAUSED';
const pausedW = PixelFont.getTextWidth(paused, 2); const pausedW = PixelFont.getTextWidth(paused, 2);
PixelFont.drawText(ctx, paused, (width - pausedW) / 2, 20, Palette.SKY_BLUE, 2); PixelFont.drawText(ctx, paused, (width - pausedW) / 2, 20, Palette.SKY_BLUE, 2);
@ -122,8 +118,8 @@ export class MenuSystem extends System {
PixelFont.drawText(ctx, resume, (width - resumeW) / 2, 45, Palette.WHITE, 1); PixelFont.drawText(ctx, resume, (width - resumeW) / 2, 45, Palette.WHITE, 1);
// Draw Stats and Knowledge (Moved from HUD) // Draw Stats and Knowledge (Moved from HUD)
const player = this.engine.getEntities().find(e => e.hasComponent('Evolution')); const player = this.engine.getEntities().find(e => e.hasComponent(ComponentType.EVOLUTION));
const uiSystem = this.engine.systems.find(s => s.name === 'UISystem'); const uiSystem = this.engine.systems.find(s => s.name === SystemName.UI);
if (player && uiSystem) { if (player && uiSystem) {
// Draw Stats on the left // Draw Stats on the left
@ -132,7 +128,7 @@ export class MenuSystem extends System {
// Draw Learning Progress on the right // Draw Learning Progress on the right
uiSystem.drawSkillProgress(player, width - 110, 80); uiSystem.drawSkillProgress(player, width - 110, 80);
} }
} else if (this.gameState === 'gameOver') { } else if (this.gameState === GameState.GAME_OVER) {
const dead = 'YOU PERISHED'; const dead = 'YOU PERISHED';
const deadW = PixelFont.getTextWidth(dead, 2); const deadW = PixelFont.getTextWidth(dead, 2);
PixelFont.drawText(ctx, dead, (width - deadW) / 2, height / 2 - 30, Palette.WHITE, 2); PixelFont.drawText(ctx, dead, (width - deadW) / 2, height / 2 - 30, Palette.WHITE, 2);
@ -152,7 +148,7 @@ export class MenuSystem extends System {
} }
isPaused() { isPaused() {
return this.paused || this.gameState === 'start'; return this.paused || this.gameState === GameState.START;
} }
} }

View file

@ -1,17 +1,18 @@
import { System } from '../core/System.js'; import { System } from '../core/System.js';
import { SystemName, ComponentType } from '../core/Constants.js';
export class MovementSystem extends System { export class MovementSystem extends System {
constructor() { constructor() {
super('MovementSystem'); super(SystemName.MOVEMENT);
this.requiredComponents = ['Position', 'Velocity']; this.requiredComponents = [ComponentType.POSITION, ComponentType.VELOCITY];
this.priority = 10; this.priority = 10;
} }
process(deltaTime, entities) { process(deltaTime, entities) {
entities.forEach(entity => { entities.forEach(entity => {
const position = entity.getComponent('Position'); const position = entity.getComponent(ComponentType.POSITION);
const velocity = entity.getComponent('Velocity'); const velocity = entity.getComponent(ComponentType.VELOCITY);
const health = entity.getComponent('Health'); const health = entity.getComponent(ComponentType.HEALTH);
if (!position || !velocity) return; if (!position || !velocity) return;

View file

@ -1,26 +1,27 @@
import { System } from '../core/System.js'; import { System } from '../core/System.js';
import { SystemName, ComponentType } from '../core/Constants.js';
export class PlayerControllerSystem extends System { export class PlayerControllerSystem extends System {
constructor() { constructor() {
super('PlayerControllerSystem'); super(SystemName.PLAYER_CONTROLLER);
this.requiredComponents = ['Position', 'Velocity']; this.requiredComponents = [ComponentType.POSITION, ComponentType.VELOCITY];
this.priority = 5; this.priority = 5;
this.playerEntity = null; this.playerEntity = null;
} }
process(deltaTime, entities) { process(deltaTime, entities) {
// Find player entity (first entity with player tag or specific component) // Find player entity (entity with Evolution component)
if (!this.playerEntity) { if (!this.playerEntity) {
this.playerEntity = entities.find(e => e.hasComponent('Evolution')); this.playerEntity = entities.find(e => e.hasComponent(ComponentType.EVOLUTION));
} }
if (!this.playerEntity) return; if (!this.playerEntity) return;
const inputSystem = this.engine.systems.find(s => s.name === 'InputSystem'); const inputSystem = this.engine.systems.find(s => s.name === SystemName.INPUT);
if (!inputSystem) return; if (!inputSystem) return;
const velocity = this.playerEntity.getComponent('Velocity'); const velocity = this.playerEntity.getComponent(ComponentType.VELOCITY);
const position = this.playerEntity.getComponent('Position'); const position = this.playerEntity.getComponent(ComponentType.POSITION);
if (!velocity || !position) return; if (!velocity || !position) return;
// Movement input // Movement input

View file

@ -1,23 +1,24 @@
import { System } from '../core/System.js'; import { System } from '../core/System.js';
import { Events } from '../core/EventBus.js'; import { Events } from '../core/EventBus.js';
import { Palette } from '../core/Palette.js'; import { Palette } from '../core/Palette.js';
import { SystemName, ComponentType } from '../core/Constants.js';
export class ProjectileSystem extends System { export class ProjectileSystem extends System {
constructor() { constructor() {
super('ProjectileSystem'); super(SystemName.PROJECTILE);
this.requiredComponents = ['Position', 'Velocity']; this.requiredComponents = [ComponentType.POSITION, ComponentType.VELOCITY];
this.priority = 18; this.priority = 18;
} }
process(deltaTime, entities) { process(deltaTime, entities) {
const playerController = this.engine.systems.find(s => s.name === 'PlayerControllerSystem'); const playerController = this.engine.systems.find(s => s.name === SystemName.PLAYER_CONTROLLER);
const _player = playerController ? playerController.getPlayerEntity() : null; const _player = playerController ? playerController.getPlayerEntity() : null;
entities.forEach(entity => { entities.forEach(entity => {
const health = entity.getComponent('Health'); const health = entity.getComponent(ComponentType.HEALTH);
if (!health || !health.isProjectile) return; if (!health || !health.isProjectile) return;
const position = entity.getComponent('Position'); const position = entity.getComponent(ComponentType.POSITION);
if (!position) return; if (!position) return;
// Check range - remove if traveled beyond max range // Check range - remove if traveled beyond max range
@ -46,10 +47,10 @@ export class ProjectileSystem extends System {
allEntities.forEach(target => { allEntities.forEach(target => {
if (target.id === entity.owner) return; if (target.id === entity.owner) return;
if (target.id === entity.id) return; if (target.id === entity.id) return;
if (!target.hasComponent('Health')) return; if (!target.hasComponent(ComponentType.HEALTH)) return;
if (target.getComponent('Health').isProjectile) return; if (target.getComponent(ComponentType.HEALTH).isProjectile) return;
const targetPos = target.getComponent('Position'); const targetPos = target.getComponent(ComponentType.POSITION);
if (!targetPos) return; if (!targetPos) return;
const dx = targetPos.x - position.x; const dx = targetPos.x - position.x;
@ -58,13 +59,13 @@ export class ProjectileSystem extends System {
if (distance < 8) { if (distance < 8) {
// Hit! // Hit!
const targetHealth = target.getComponent('Health'); const targetHealth = target.getComponent(ComponentType.HEALTH);
const damage = entity.damage || 10; const damage = entity.damage || 10;
targetHealth.takeDamage(damage); targetHealth.takeDamage(damage);
// Impact animation // Impact animation
const vfxSystem = this.engine.systems.find(s => s.name === 'VFXSystem'); const vfxSystem = this.engine.systems.find(s => s.name === SystemName.VFX);
const velocity = entity.getComponent('Velocity'); const velocity = entity.getComponent(ComponentType.VELOCITY);
if (vfxSystem) { if (vfxSystem) {
const angle = velocity ? Math.atan2(velocity.vy, velocity.vx) : null; const angle = velocity ? Math.atan2(velocity.vy, velocity.vx) : null;
vfxSystem.createImpact(position.x, position.y, Palette.CYAN, angle); vfxSystem.createImpact(position.x, position.y, Palette.CYAN, angle);

View file

@ -1,11 +1,12 @@
import { System } from '../core/System.js'; import { System } from '../core/System.js';
import { Palette } from '../core/Palette.js'; import { Palette } from '../core/Palette.js';
import { SpriteLibrary } from '../core/SpriteLibrary.js'; import { SpriteLibrary } from '../core/SpriteLibrary.js';
import { ComponentType, SystemName, AnimationState, VFXType, EntityType } from '../core/Constants.js';
export class RenderSystem extends System { export class RenderSystem extends System {
constructor(engine) { constructor(engine) {
super('RenderSystem'); super(SystemName.RENDER);
this.requiredComponents = ['Position', 'Sprite']; this.requiredComponents = [ComponentType.POSITION, ComponentType.SPRITE];
this.priority = 100; // Render last this.priority = 100; // Render last
this.engine = engine; this.engine = engine;
this.ctx = engine.ctx; this.ctx = engine.ctx;
@ -25,12 +26,12 @@ export class RenderSystem extends System {
// Get all entities including inactive ones for rendering dead absorbable entities // Get all entities including inactive ones for rendering dead absorbable entities
const allEntities = this.engine.entities; const allEntities = this.engine.entities;
allEntities.forEach(entity => { allEntities.forEach(entity => {
const health = entity.getComponent('Health'); const health = entity.getComponent(ComponentType.HEALTH);
const evolution = entity.getComponent('Evolution'); const evolution = entity.getComponent(ComponentType.EVOLUTION);
// Skip inactive entities UNLESS they're dead and absorbable (for absorption window) // Skip inactive entities UNLESS they're dead and absorbable (for absorption window)
if (!entity.active) { if (!entity.active) {
const absorbable = entity.getComponent('Absorbable'); const absorbable = entity.getComponent(ComponentType.ABSORBABLE);
if (health && health.isDead() && absorbable && !absorbable.absorbed) { if (health && health.isDead() && absorbable && !absorbable.absorbed) {
// Render dead absorbable entities even if inactive (fade them out) // Render dead absorbable entities even if inactive (fade them out)
this.drawEntity(entity, deltaTime, true); // Pass fade flag this.drawEntity(entity, deltaTime, true); // Pass fade flag
@ -41,7 +42,7 @@ export class RenderSystem extends System {
// Don't render dead non-player entities (unless they're absorbable, handled above) // Don't render dead non-player entities (unless they're absorbable, handled above)
if (health && health.isDead() && !evolution) { if (health && health.isDead() && !evolution) {
const absorbable = entity.getComponent('Absorbable'); const absorbable = entity.getComponent(ComponentType.ABSORBABLE);
if (!absorbable || absorbable.absorbed) { if (!absorbable || absorbable.absorbed) {
return; // Skip dead non-absorbable entities return; // Skip dead non-absorbable entities
} }
@ -105,9 +106,9 @@ export class RenderSystem extends System {
} }
drawEntity(entity, deltaTime, isDeadFade = false) { drawEntity(entity, deltaTime, isDeadFade = false) {
const position = entity.getComponent('Position'); const position = entity.getComponent(ComponentType.POSITION);
const sprite = entity.getComponent('Sprite'); const sprite = entity.getComponent(ComponentType.SPRITE);
const health = entity.getComponent('Health'); const health = entity.getComponent(ComponentType.HEALTH);
if (!position || !sprite) return; if (!position || !sprite) return;
@ -120,7 +121,7 @@ export class RenderSystem extends System {
// Fade out dead entities // Fade out dead entities
let alpha = sprite.alpha; let alpha = sprite.alpha;
if (isDeadFade && health && health.isDead()) { if (isDeadFade && health && health.isDead()) {
const absorbable = entity.getComponent('Absorbable'); const absorbable = entity.getComponent(ComponentType.ABSORBABLE);
if (absorbable && !absorbable.absorbed) { if (absorbable && !absorbable.absorbed) {
// Calculate fade based on time since death // Calculate fade based on time since death
const deathTime = entity.deathTime || Date.now(); const deathTime = entity.deathTime || Date.now();
@ -135,24 +136,22 @@ export class RenderSystem extends System {
this.ctx.scale(sprite.scale, sprite.scale); this.ctx.scale(sprite.scale, sprite.scale);
// Update animation time for slime morphing // Update animation time for slime morphing
if (sprite.shape === 'slime') { if (sprite.shape === EntityType.SLIME) {
sprite.animationTime += deltaTime; sprite.animationTime += deltaTime;
sprite.morphAmount = Math.sin(sprite.animationTime * 3) * 0.2 + 0.8; sprite.morphAmount = Math.sin(sprite.animationTime * 3) * 0.2 + 0.8;
} }
// Map legacy colors to new Palette if necessary // Map legacy colors to new Palette if necessary
let drawColor = sprite.color; let drawColor = sprite.color;
if (sprite.shape === 'slime') drawColor = Palette.CYAN; if (sprite.shape === EntityType.SLIME) drawColor = Palette.CYAN;
// Map other colors? For now keep them if they match, but we should enforce palette eventually.
// The previous code had specific hardcoded colors.
this.ctx.fillStyle = drawColor; this.ctx.fillStyle = drawColor;
// Select appropriate animation state based on velocity // Select appropriate animation state based on velocity
const velocity = entity.getComponent('Velocity'); const velocity = entity.getComponent(ComponentType.VELOCITY);
if (velocity) { if (velocity) {
const isMoving = Math.abs(velocity.vx) > 1 || Math.abs(velocity.vy) > 1; const isMoving = Math.abs(velocity.vx) > 1 || Math.abs(velocity.vy) > 1;
sprite.animationState = isMoving ? 'walk' : 'idle'; sprite.animationState = isMoving ? AnimationState.WALK : AnimationState.IDLE;
} }
// Lookup animation data // Lookup animation data
@ -162,7 +161,7 @@ export class RenderSystem extends System {
} }
// Get animation frames for the current state // Get animation frames for the current state
let frames = spriteData[sprite.animationState] || spriteData['idle']; let frames = spriteData[sprite.animationState] || spriteData[AnimationState.IDLE];
// If frames is still not an array (fallback for simple grids or missing states) // If frames is still not an array (fallback for simple grids or missing states)
if (!frames || !Array.isArray(frames)) { if (!frames || !Array.isArray(frames)) {
@ -238,7 +237,7 @@ export class RenderSystem extends System {
} }
// Draw combat indicator if attacking (This DOES rotate) // Draw combat indicator if attacking (This DOES rotate)
const combat = entity.getComponent('Combat'); const combat = entity.getComponent(ComponentType.COMBAT);
if (combat && combat.isAttacking) { if (combat && combat.isAttacking) {
this.ctx.save(); this.ctx.save();
this.ctx.rotate(position.rotation); this.ctx.rotate(position.rotation);
@ -247,13 +246,13 @@ export class RenderSystem extends System {
} }
// Draw stealth indicator // Draw stealth indicator
const stealth = entity.getComponent('Stealth'); const stealth = entity.getComponent(ComponentType.STEALTH);
if (stealth && stealth.isStealthed) { if (stealth && stealth.isStealthed) {
this.drawStealthIndicator(stealth, sprite); this.drawStealthIndicator(stealth, sprite);
} }
// Mutation Visual Effects - Simplified for pixel art // Mutation Visual Effects - Simplified for pixel art
const evolution = entity.getComponent('Evolution'); const evolution = entity.getComponent(ComponentType.EVOLUTION);
if (evolution) { if (evolution) {
if (evolution.mutationEffects.glowingBody) { if (evolution.mutationEffects.glowingBody) {
// Simple outline (square) // Simple outline (square)
@ -275,10 +274,8 @@ export class RenderSystem extends System {
this.ctx.restore(); this.ctx.restore();
} }
drawVFX() { drawVFX() {
const vfxSystem = this.engine.systems.find(s => s.name === 'VFXSystem'); const vfxSystem = this.engine.systems.find(s => s.name === SystemName.VFX);
if (!vfxSystem) return; if (!vfxSystem) return;
const ctx = this.ctx; const ctx = this.ctx;
@ -287,7 +284,7 @@ export class RenderSystem extends System {
particles.forEach(p => { particles.forEach(p => {
ctx.fillStyle = p.color; ctx.fillStyle = p.color;
// Fade based on lifetime for impact, or keep solid/flicker for absorption // Fade based on lifetime for impact, or keep solid/flicker for absorption
ctx.globalAlpha = p.type === 'impact' ? Math.min(1, p.lifetime / 0.3) : 0.8; ctx.globalAlpha = p.type === VFXType.IMPACT ? Math.min(1, p.lifetime / 0.3) : 0.8;
// Snap to integers for pixel crispness // Snap to integers for pixel crispness
const x = Math.floor(p.x); const x = Math.floor(p.x);
@ -333,7 +330,6 @@ export class RenderSystem extends System {
ctx.fillRect(startX, startY, fillWidth, barHeight); ctx.fillRect(startX, startY, fillWidth, barHeight);
} }
drawAttackIndicator(combat, _position) { drawAttackIndicator(combat, _position) {
const ctx = this.ctx; const ctx = this.ctx;
const length = 25; // Scaled down const length = 25; // Scaled down
@ -395,7 +391,8 @@ export class RenderSystem extends System {
} }
drawSkillEffects() { drawSkillEffects() {
const skillEffectSystem = this.engine.systems.find(s => s.name === 'SkillEffectSystem'); const skillEffectSystem = this.engine.systems.find(s => s.name === SystemName.SKILL_EFFECT);
// NOTE: SKILL_EFFECT was missing in my constants. I should add it.
if (!skillEffectSystem) return; if (!skillEffectSystem) return;
const effects = skillEffectSystem.getEffects(); const effects = skillEffectSystem.getEffects();

View file

@ -1,11 +1,12 @@
import { System } from '../core/System.js'; import { System } from '../core/System.js';
import { SystemName } from '../core/Constants.js';
/** /**
* System to track and render skill effects (Fire Breath, Pounce, etc.) * System to track and render skill effects (Fire Breath, Pounce, etc.)
*/ */
export class SkillEffectSystem extends System { export class SkillEffectSystem extends System {
constructor() { constructor() {
super('SkillEffectSystem'); super(SystemName.SKILL_EFFECT);
this.requiredComponents = []; // No required components this.requiredComponents = []; // No required components
this.priority = 50; // Run after skills but before rendering this.priority = 50; // Run after skills but before rendering
this.activeEffects = []; this.activeEffects = [];

View file

@ -1,19 +1,20 @@
import { System } from '../core/System.js'; import { System } from '../core/System.js';
import { SkillRegistry } from '../skills/SkillRegistry.js'; import { SkillRegistry } from '../skills/SkillRegistry.js';
import { SystemName, ComponentType } from '../core/Constants.js';
export class SkillSystem extends System { export class SkillSystem extends System {
constructor() { constructor() {
super('SkillSystem'); super(SystemName.SKILL);
this.requiredComponents = ['Skills']; this.requiredComponents = [ComponentType.SKILLS];
this.priority = 30; this.priority = 30;
} }
process(deltaTime, entities) { process(deltaTime, entities) {
const inputSystem = this.engine.systems.find(s => s.name === 'InputSystem'); const inputSystem = this.engine.systems.find(s => s.name === SystemName.INPUT);
if (!inputSystem) return; if (!inputSystem) return;
entities.forEach(entity => { entities.forEach(entity => {
const skills = entity.getComponent('Skills'); const skills = entity.getComponent(ComponentType.SKILLS);
if (!skills) return; if (!skills) return;
// Update cooldowns // Update cooldowns
@ -43,7 +44,7 @@ export class SkillSystem extends System {
} }
if (skill.activate(entity, this.engine)) { if (skill.activate(entity, this.engine)) {
const skills = entity.getComponent('Skills'); const skills = entity.getComponent(ComponentType.SKILLS);
if (skills) { if (skills) {
skills.setCooldown(skillId, skill.cooldown); skills.setCooldown(skillId, skill.cooldown);
} }

View file

@ -1,22 +1,23 @@
import { System } from '../core/System.js'; import { System } from '../core/System.js';
import { SystemName, ComponentType } from '../core/Constants.js';
export class StealthSystem extends System { export class StealthSystem extends System {
constructor() { constructor() {
super('StealthSystem'); super(SystemName.STEALTH);
this.requiredComponents = ['Stealth']; this.requiredComponents = [ComponentType.STEALTH];
this.priority = 12; this.priority = 12;
} }
process(deltaTime, entities) { process(deltaTime, entities) {
const inputSystem = this.engine.systems.find(s => s.name === 'InputSystem'); const inputSystem = this.engine.systems.find(s => s.name === SystemName.INPUT);
const playerController = this.engine.systems.find(s => s.name === 'PlayerControllerSystem'); const playerController = this.engine.systems.find(s => s.name === SystemName.PLAYER_CONTROLLER);
const player = playerController ? playerController.getPlayerEntity() : null; const player = playerController ? playerController.getPlayerEntity() : null;
entities.forEach(entity => { entities.forEach(entity => {
const stealth = entity.getComponent('Stealth'); const stealth = entity.getComponent(ComponentType.STEALTH);
const velocity = entity.getComponent('Velocity'); const velocity = entity.getComponent(ComponentType.VELOCITY);
const combat = entity.getComponent('Combat'); const combat = entity.getComponent(ComponentType.COMBAT);
const evolution = entity.getComponent('Evolution'); const evolution = entity.getComponent(ComponentType.EVOLUTION);
if (!stealth) return; if (!stealth) return;

View file

@ -3,10 +3,11 @@ import { SkillRegistry } from '../skills/SkillRegistry.js';
import { Events } from '../core/EventBus.js'; import { Events } from '../core/EventBus.js';
import { PixelFont } from '../core/PixelFont.js'; import { PixelFont } from '../core/PixelFont.js';
import { Palette } from '../core/Palette.js'; import { Palette } from '../core/Palette.js';
import { GameState, ComponentType, SystemName } from '../core/Constants.js';
export class UISystem extends System { export class UISystem extends System {
constructor(engine) { constructor(engine) {
super('UISystem'); super(SystemName.UI);
this.requiredComponents = []; // No required components - renders UI this.requiredComponents = []; // No required components - renders UI
this.priority = 200; // Render after everything else this.priority = 200; // Render after everything else
this.engine = engine; this.engine = engine;
@ -44,11 +45,11 @@ export class UISystem extends System {
this.updateDamageNumbers(deltaTime); this.updateDamageNumbers(deltaTime);
this.updateNotifications(deltaTime); this.updateNotifications(deltaTime);
const menuSystem = this.engine.systems.find(s => s.name === 'MenuSystem'); const menuSystem = this.engine.systems.find(s => s.name === SystemName.MENU);
const gameState = menuSystem ? menuSystem.getGameState() : 'playing'; const gameState = menuSystem ? menuSystem.getGameState() : GameState.PLAYING;
// Only draw menu overlay if in start, paused, or gameOver state // Only draw menu overlay if in start, paused, or gameOver state
if (gameState === 'start' || gameState === 'paused' || gameState === 'gameOver') { if (gameState === GameState.START || gameState === GameState.PAUSED || gameState === GameState.GAME_OVER) {
if (menuSystem) { if (menuSystem) {
menuSystem.drawMenu(); menuSystem.drawMenu();
} }
@ -56,7 +57,7 @@ export class UISystem extends System {
return; return;
} }
const playerController = this.engine.systems.find(s => s.name === 'PlayerControllerSystem'); const playerController = this.engine.systems.find(s => s.name === SystemName.PLAYER_CONTROLLER);
const player = playerController ? playerController.getPlayerEntity() : null; const player = playerController ? playerController.getPlayerEntity() : null;
if (!player) return; if (!player) return;
@ -70,9 +71,9 @@ export class UISystem extends System {
} }
drawHUD(player) { drawHUD(player) {
const health = player.getComponent('Health'); const health = player.getComponent(ComponentType.HEALTH);
const stats = player.getComponent('Stats'); const stats = player.getComponent(ComponentType.STATS);
const evolution = player.getComponent('Evolution'); const evolution = player.getComponent(ComponentType.EVOLUTION);
if (!health || !stats || !evolution) return; if (!health || !stats || !evolution) return;
@ -106,7 +107,7 @@ export class UISystem extends System {
} }
drawSkills(player) { drawSkills(player) {
const skills = player.getComponent('Skills'); const skills = player.getComponent(ComponentType.SKILLS);
if (!skills) return; if (!skills) return;
const ctx = this.ctx; const ctx = this.ctx;
@ -132,8 +133,8 @@ export class UISystem extends System {
} }
drawStats(player, x, y) { drawStats(player, x, y) {
const stats = player.getComponent('Stats'); const stats = player.getComponent(ComponentType.STATS);
const evolution = player.getComponent('Evolution'); const evolution = player.getComponent(ComponentType.EVOLUTION);
if (!stats || !evolution) return; if (!stats || !evolution) return;
const ctx = this.ctx; const ctx = this.ctx;
@ -150,7 +151,7 @@ export class UISystem extends System {
} }
drawSkillProgress(player, x, y) { drawSkillProgress(player, x, y) {
const skillProgress = player.getComponent('SkillProgress'); const skillProgress = player.getComponent(ComponentType.SKILL_PROGRESS);
if (!skillProgress) return; if (!skillProgress) return;
const ctx = this.ctx; const ctx = this.ctx;

View file

@ -1,18 +1,19 @@
import { System } from '../core/System.js'; import { System } from '../core/System.js';
import { Palette } from '../core/Palette.js'; import { Palette } from '../core/Palette.js';
import { SystemName, ComponentType, VFXType } from '../core/Constants.js';
export class VFXSystem extends System { export class VFXSystem extends System {
constructor() { constructor() {
super('VFXSystem'); super(SystemName.VFX);
this.requiredComponents = []; this.requiredComponents = [];
this.priority = 40; this.priority = 40;
this.particles = []; this.particles = [];
} }
process(deltaTime, _entities) { process(deltaTime, _entities) {
const playerController = this.engine.systems.find(s => s.name === 'PlayerControllerSystem'); const playerController = this.engine.systems.find(s => s.name === SystemName.PLAYER_CONTROLLER);
const player = playerController ? playerController.getPlayerEntity() : null; const player = playerController ? playerController.getPlayerEntity() : null;
const playerPos = player ? player.getComponent('Position') : null; const playerPos = player ? player.getComponent(ComponentType.POSITION) : null;
for (let i = this.particles.length - 1; i >= 0; i--) { for (let i = this.particles.length - 1; i >= 0; i--) {
const p = this.particles[i]; const p = this.particles[i];
@ -25,7 +26,7 @@ export class VFXSystem extends System {
} }
// Behavior logic // Behavior logic
if (p.type === 'absorption' && playerPos) { if (p.type === VFXType.ABSORPTION && playerPos) {
// Attract to player // Attract to player
const dx = playerPos.x - p.x; const dx = playerPos.x - p.x;
const dy = playerPos.y - p.y; const dy = playerPos.y - p.y;
@ -77,7 +78,7 @@ export class VFXSystem extends System {
lifetime: 0.2 + Math.random() * 0.3, lifetime: 0.2 + Math.random() * 0.3,
size: 1 + Math.random() * 2, size: 1 + Math.random() * 2,
color: color, color: color,
type: 'impact' type: VFXType.IMPACT
}); });
} }
} }
@ -93,7 +94,7 @@ export class VFXSystem extends System {
lifetime: 1.5, lifetime: 1.5,
size: 2, size: 2,
color: color, color: color,
type: 'absorption' type: VFXType.ABSORPTION
}); });
} }
} }