diff --git a/src/main.js b/src/main.js index 8d2dd8c..db69006 100644 --- a/src/main.js +++ b/src/main.js @@ -14,6 +14,7 @@ import { DeathSystem } from './systems/DeathSystem.js'; import { MenuSystem } from './systems/MenuSystem.js'; import { RenderSystem } from './systems/RenderSystem.js'; import { UISystem } from './systems/UISystem.js'; +import { VFXSystem } from './systems/VFXSystem.js'; // Components import { Position } from './components/Position.js'; @@ -51,6 +52,7 @@ if (!canvas) { engine.addSystem(new SkillEffectSystem()); engine.addSystem(new HealthRegenerationSystem()); engine.addSystem(new DeathSystem()); + engine.addSystem(new VFXSystem()); engine.addSystem(new RenderSystem(engine)); engine.addSystem(new UISystem(engine)); @@ -82,25 +84,25 @@ if (!canvas) { let color, evolutionData, skills; switch (type) { - case 'humanoid': - color = '#ff5555'; // Humanoid red - evolutionData = { human: 10, beast: 0, slime: -2 }; - skills = ['fire_breath']; - break; - case 'beast': - color = '#ffaa00'; // Beast orange - evolutionData = { human: 0, beast: 10, slime: -2 }; - skills = ['pounce']; - break; - case 'elemental': - color = '#00bfff'; - evolutionData = { human: 3, beast: 3, slime: 8 }; - skills = ['fire_breath']; - break; - default: - color = '#888888'; - evolutionData = { human: 2, beast: 2, slime: 2 }; - skills = []; + case 'humanoid': + color = '#ff5555'; // Humanoid red + evolutionData = { human: 10, beast: 0, slime: -2 }; + skills = ['fire_breath']; + break; + case 'beast': + color = '#ffaa00'; // Beast orange + evolutionData = { human: 0, beast: 10, slime: -2 }; + skills = ['pounce']; + break; + case 'elemental': + color = '#00bfff'; + evolutionData = { human: 3, beast: 3, slime: 8 }; + skills = ['fire_breath']; + break; + default: + color = '#888888'; + evolutionData = { human: 2, beast: 2, slime: 2 }; + skills = []; } creature.addComponent(new Sprite(color, 10, 10, type)); diff --git a/src/systems/AbsorptionSystem.js b/src/systems/AbsorptionSystem.js index 0bbcd97..750fd42 100644 --- a/src/systems/AbsorptionSystem.js +++ b/src/systems/AbsorptionSystem.js @@ -7,10 +7,9 @@ export class AbsorptionSystem extends System { super('AbsorptionSystem'); this.requiredComponents = ['Position', 'Absorbable']; this.priority = 25; - this.absorptionEffects = []; // Visual effects } - process(deltaTime, _entities) { + process(_deltaTime, _entities) { const playerController = this.engine.systems.find(s => s.name === 'PlayerControllerSystem'); const player = playerController ? playerController.getPlayerEntity() : null; @@ -62,8 +61,7 @@ export class AbsorptionSystem extends System { } }); - // Update visual effects - this.updateEffects(deltaTime); + // NO LOCAL Update visual effects } absorbEntity(player, entity, absorbable, evolution, skills, stats, skillProgress) { @@ -113,7 +111,10 @@ export class AbsorptionSystem extends System { // Visual effect if (entityPos) { - this.addAbsorptionEffect(entityPos.x, entityPos.y); + const vfxSystem = this.engine.systems.find(s => s.name === 'VFXSystem'); + if (vfxSystem) { + vfxSystem.createAbsorption(entityPos.x, entityPos.y); + } } // Mark as absorbed - DeathSystem will handle removal after absorption window @@ -140,37 +141,5 @@ export class AbsorptionSystem extends System { } } - addAbsorptionEffect(x, y) { - for (let i = 0; i < 20; i++) { - this.absorptionEffects.push({ - x, - y, - vx: (Math.random() - 0.5) * 200, - vy: (Math.random() - 0.5) * 200, - lifetime: 0.5 + Math.random() * 0.5, - size: 3 + Math.random() * 5, - color: `hsl(${120 + Math.random() * 60}, 100%, 50%)` - }); - } - } - - updateEffects(deltaTime) { - for (let i = this.absorptionEffects.length - 1; i >= 0; i--) { - const effect = this.absorptionEffects[i]; - effect.x += effect.vx * deltaTime; - effect.y += effect.vy * deltaTime; - effect.lifetime -= deltaTime; - effect.vx *= 0.95; - effect.vy *= 0.95; - - if (effect.lifetime <= 0) { - this.absorptionEffects.splice(i, 1); - } - } - } - - getEffects() { - return this.absorptionEffects; - } } diff --git a/src/systems/CombatSystem.js b/src/systems/CombatSystem.js index 2334a89..54b3c3c 100644 --- a/src/systems/CombatSystem.js +++ b/src/systems/CombatSystem.js @@ -29,8 +29,8 @@ export class CombatSystem extends System { // Handle creature attacks const creatures = entities.filter(e => e.hasComponent('AI') && - e.hasComponent('Combat') && - e !== player + e.hasComponent('Combat') && + e !== player ); creatures.forEach(creature => { @@ -163,10 +163,9 @@ export class CombatSystem extends System { } } - // If target is dead, emit event + // If target is dead, let DeathSystem handle removal/deactivation if (health.isDead()) { this.engine.emit(Events.ENTITY_DIED, { entity: target }); - target.active = false; } // Apply knockback diff --git a/src/systems/DeathSystem.js b/src/systems/DeathSystem.js index 1bffa95..9a90330 100644 --- a/src/systems/DeathSystem.js +++ b/src/systems/DeathSystem.js @@ -35,9 +35,13 @@ export class DeathSystem extends System { } // Mark as inactive immediately so it stops being processed by other systems - if (entity.active) { - entity.active = false; - entity.deathTime = Date.now(); // Set death time when first marked dead + if (entity.active || !entity.deathTime) { + if (entity.active) { + entity.active = false; + } + if (!entity.deathTime) { + entity.deathTime = Date.now(); // Set death time when first marked dead + } } // Check if it's absorbable - if so, give a short window for absorption diff --git a/src/systems/ProjectileSystem.js b/src/systems/ProjectileSystem.js index 6e4e9ee..9c7e38d 100644 --- a/src/systems/ProjectileSystem.js +++ b/src/systems/ProjectileSystem.js @@ -1,4 +1,6 @@ import { System } from '../core/System.js'; +import { Events } from '../core/EventBus.js'; +import { Palette } from '../core/Palette.js'; export class ProjectileSystem extends System { constructor() { @@ -54,16 +56,23 @@ export class ProjectileSystem extends System { const dy = targetPos.y - position.y; const distance = Math.sqrt(dx * dx + dy * dy); - if (distance < 20) { + if (distance < 8) { // Hit! const targetHealth = target.getComponent('Health'); const damage = entity.damage || 10; targetHealth.takeDamage(damage); - // If target is dead, mark it for immediate removal + // Impact animation + const vfxSystem = this.engine.systems.find(s => s.name === 'VFXSystem'); + const velocity = entity.getComponent('Velocity'); + if (vfxSystem) { + const angle = velocity ? Math.atan2(velocity.vy, velocity.vx) : null; + vfxSystem.createImpact(position.x, position.y, Palette.CYAN, angle); + } + + // If target is dead, let DeathSystem handle removal/deactivation if (targetHealth.isDead()) { - target.active = false; - // DeathSystem will handle removal + this.engine.emit(Events.ENTITY_DIED, { entity: target }); } // Remove projectile @@ -74,7 +83,7 @@ export class ProjectileSystem extends System { // Boundary check const canvas = this.engine.canvas; if (position.x < 0 || position.x > canvas.width || - position.y < 0 || position.y > canvas.height) { + position.y < 0 || position.y > canvas.height) { this.engine.removeEntity(entity); } }); diff --git a/src/systems/RenderSystem.js b/src/systems/RenderSystem.js index 1cd8362..24cd310 100644 --- a/src/systems/RenderSystem.js +++ b/src/systems/RenderSystem.js @@ -52,6 +52,7 @@ export class RenderSystem extends System { // Draw skill effects this.drawSkillEffects(); + this.drawVFX(); } drawBackground() { @@ -276,6 +277,29 @@ export class RenderSystem extends System { + drawVFX() { + const vfxSystem = this.engine.systems.find(s => s.name === 'VFXSystem'); + if (!vfxSystem) return; + + const ctx = this.ctx; + const particles = vfxSystem.getParticles(); + + particles.forEach(p => { + ctx.fillStyle = p.color; + // 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; + + // Snap to integers for pixel crispness + const x = Math.floor(p.x); + const y = Math.floor(p.y); + const size = Math.floor(p.size); + + ctx.fillRect(x - size / 2, y - size / 2, size, size); + }); + + ctx.globalAlpha = 1.0; + } + drawHealthBar(health, sprite) { // Pixel art health bar const ctx = this.ctx; @@ -381,15 +405,15 @@ export class RenderSystem extends System { ctx.save(); switch (effect.type) { - case 'fire_breath': - this.drawFireBreath(ctx, effect); - break; - case 'pounce': - this.drawPounce(ctx, effect); - break; - case 'pounce_impact': - this.drawPounceImpact(ctx, effect); - break; + case 'fire_breath': + this.drawFireBreath(ctx, effect); + break; + case 'pounce': + this.drawPounce(ctx, effect); + break; + case 'pounce_impact': + this.drawPounceImpact(ctx, effect); + break; } ctx.restore(); diff --git a/src/systems/UISystem.js b/src/systems/UISystem.js index 134e334..21189a1 100644 --- a/src/systems/UISystem.js +++ b/src/systems/UISystem.js @@ -67,7 +67,6 @@ export class UISystem extends System { // REMOVED drawStats and drawSkillProgress from active gameplay this.drawDamageNumbers(); this.drawNotifications(); - this.drawAbsorptionEffects(); } drawHUD(player) { @@ -225,34 +224,5 @@ export class UISystem extends System { }); } - drawAbsorptionEffects() { - const absorptionSystem = this.engine.systems.find(s => s.name === 'AbsorptionSystem'); - if (!absorptionSystem || !absorptionSystem.getEffects) return; - - const effects = absorptionSystem.getEffects(); - const ctx = this.ctx; - - effects.forEach(effect => { - const alpha = Math.min(1, effect.lifetime * 2); - - // Glow effect - ctx.shadowBlur = 10; - ctx.shadowColor = effect.color; - ctx.fillStyle = effect.color; - ctx.globalAlpha = alpha; - ctx.beginPath(); - ctx.arc(effect.x, effect.y, effect.size, 0, Math.PI * 2); - ctx.fill(); - - // Inner bright core - ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; - ctx.beginPath(); - ctx.arc(effect.x, effect.y, effect.size * 0.5, 0, Math.PI * 2); - ctx.fill(); - - ctx.globalAlpha = 1.0; - ctx.shadowBlur = 0; - }); - } } diff --git a/src/systems/VFXSystem.js b/src/systems/VFXSystem.js new file mode 100644 index 0000000..62f24d3 --- /dev/null +++ b/src/systems/VFXSystem.js @@ -0,0 +1,104 @@ +import { System } from '../core/System.js'; +import { Palette } from '../core/Palette.js'; + +export class VFXSystem extends System { + constructor() { + super('VFXSystem'); + this.requiredComponents = []; + this.priority = 40; + this.particles = []; + } + + process(deltaTime, _entities) { + const playerController = this.engine.systems.find(s => s.name === 'PlayerControllerSystem'); + const player = playerController ? playerController.getPlayerEntity() : null; + const playerPos = player ? player.getComponent('Position') : null; + + for (let i = this.particles.length - 1; i >= 0; i--) { + const p = this.particles[i]; + + // Update lifetime + p.lifetime -= deltaTime; + if (p.lifetime <= 0) { + this.particles.splice(i, 1); + continue; + } + + // Behavior logic + if (p.type === 'absorption' && playerPos) { + // Attract to player + const dx = playerPos.x - p.x; + const dy = playerPos.y - p.y; + const dist = Math.sqrt(dx * dx + dy * dy); + + if (dist > 5) { + p.vx += (dx / dist) * 800 * deltaTime; + p.vy += (dy / dist) * 800 * deltaTime; + // Add some drag to make it smooth + p.vx *= 0.95; + p.vy *= 0.95; + } else { + // Arrived + this.particles.splice(i, 1); + continue; + } + } else { + // Basic drag for impact particles + p.vx *= 0.9; + p.vy *= 0.9; + } + + // Update position + p.x += p.vx * deltaTime; + p.y += p.vy * deltaTime; + } + } + + createImpact(x, y, color = Palette.WHITE, angle = null) { + const count = 8; + for (let i = 0; i < count; i++) { + let vx, vy; + if (angle !== null) { + // Splash in the direction of hit + some spread + const spread = (Math.random() - 0.5) * 2; + const speed = 50 + Math.random() * 150; + vx = Math.cos(angle + spread) * speed; + vy = Math.sin(angle + spread) * speed; + } else { + vx = (Math.random() - 0.5) * 150; + vy = (Math.random() - 0.5) * 150; + } + + this.particles.push({ + x, + y, + vx, + vy, + lifetime: 0.2 + Math.random() * 0.3, + size: 1 + Math.random() * 2, + color: color, + type: 'impact' + }); + } + } + + createAbsorption(x, y, color = Palette.CYAN) { + for (let i = 0; i < 12; i++) { + // Start with a small explosion then attract + this.particles.push({ + x, + y, + vx: (Math.random() - 0.5) * 100, + vy: (Math.random() - 0.5) * 100, + lifetime: 1.5, + size: 2, + color: color, + type: 'absorption' + }); + } + } + + getParticles() { + return this.particles; + } +}