feat: introduce VFXSystem to centralize visual effect management and rendering, migrating absorption effects from UISystem and AbsorptionSystem.
This commit is contained in:
parent
691bc3b8da
commit
e9db84abd1
8 changed files with 188 additions and 107 deletions
|
|
@ -14,6 +14,7 @@ import { DeathSystem } from './systems/DeathSystem.js';
|
||||||
import { MenuSystem } from './systems/MenuSystem.js';
|
import { MenuSystem } from './systems/MenuSystem.js';
|
||||||
import { RenderSystem } from './systems/RenderSystem.js';
|
import { RenderSystem } from './systems/RenderSystem.js';
|
||||||
import { UISystem } from './systems/UISystem.js';
|
import { UISystem } from './systems/UISystem.js';
|
||||||
|
import { VFXSystem } from './systems/VFXSystem.js';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import { Position } from './components/Position.js';
|
import { Position } from './components/Position.js';
|
||||||
|
|
@ -51,6 +52,7 @@ if (!canvas) {
|
||||||
engine.addSystem(new SkillEffectSystem());
|
engine.addSystem(new SkillEffectSystem());
|
||||||
engine.addSystem(new HealthRegenerationSystem());
|
engine.addSystem(new HealthRegenerationSystem());
|
||||||
engine.addSystem(new DeathSystem());
|
engine.addSystem(new DeathSystem());
|
||||||
|
engine.addSystem(new VFXSystem());
|
||||||
engine.addSystem(new RenderSystem(engine));
|
engine.addSystem(new RenderSystem(engine));
|
||||||
engine.addSystem(new UISystem(engine));
|
engine.addSystem(new UISystem(engine));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,9 @@ export class AbsorptionSystem extends System {
|
||||||
super('AbsorptionSystem');
|
super('AbsorptionSystem');
|
||||||
this.requiredComponents = ['Position', 'Absorbable'];
|
this.requiredComponents = ['Position', 'Absorbable'];
|
||||||
this.priority = 25;
|
this.priority = 25;
|
||||||
this.absorptionEffects = []; // Visual effects
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 === 'PlayerControllerSystem');
|
||||||
const player = playerController ? playerController.getPlayerEntity() : null;
|
const player = playerController ? playerController.getPlayerEntity() : null;
|
||||||
|
|
||||||
|
|
@ -62,8 +61,7 @@ export class AbsorptionSystem extends System {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update visual effects
|
// NO LOCAL Update visual effects
|
||||||
this.updateEffects(deltaTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
absorbEntity(player, entity, absorbable, evolution, skills, stats, skillProgress) {
|
absorbEntity(player, entity, absorbable, evolution, skills, stats, skillProgress) {
|
||||||
|
|
@ -113,7 +111,10 @@ export class AbsorptionSystem extends System {
|
||||||
|
|
||||||
// Visual effect
|
// Visual effect
|
||||||
if (entityPos) {
|
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
|
// 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()) {
|
if (health.isDead()) {
|
||||||
this.engine.emit(Events.ENTITY_DIED, { entity: target });
|
this.engine.emit(Events.ENTITY_DIED, { entity: target });
|
||||||
target.active = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply knockback
|
// Apply knockback
|
||||||
|
|
|
||||||
|
|
@ -35,10 +35,14 @@ export class DeathSystem extends System {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark as inactive immediately so it stops being processed by other systems
|
// Mark as inactive immediately so it stops being processed by other systems
|
||||||
|
if (entity.active || !entity.deathTime) {
|
||||||
if (entity.active) {
|
if (entity.active) {
|
||||||
entity.active = false;
|
entity.active = false;
|
||||||
|
}
|
||||||
|
if (!entity.deathTime) {
|
||||||
entity.deathTime = Date.now(); // Set death time when first marked dead
|
entity.deathTime = Date.now(); // Set death time when first marked dead
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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('Absorbable');
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import { System } from '../core/System.js';
|
import { System } from '../core/System.js';
|
||||||
|
import { Events } from '../core/EventBus.js';
|
||||||
|
import { Palette } from '../core/Palette.js';
|
||||||
|
|
||||||
export class ProjectileSystem extends System {
|
export class ProjectileSystem extends System {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -54,16 +56,23 @@ export class ProjectileSystem 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);
|
||||||
|
|
||||||
if (distance < 20) {
|
if (distance < 8) {
|
||||||
// Hit!
|
// Hit!
|
||||||
const targetHealth = target.getComponent('Health');
|
const targetHealth = target.getComponent('Health');
|
||||||
const damage = entity.damage || 10;
|
const damage = entity.damage || 10;
|
||||||
targetHealth.takeDamage(damage);
|
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()) {
|
if (targetHealth.isDead()) {
|
||||||
target.active = false;
|
this.engine.emit(Events.ENTITY_DIED, { entity: target });
|
||||||
// DeathSystem will handle removal
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove projectile
|
// Remove projectile
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ export class RenderSystem extends System {
|
||||||
|
|
||||||
// Draw skill effects
|
// Draw skill effects
|
||||||
this.drawSkillEffects();
|
this.drawSkillEffects();
|
||||||
|
this.drawVFX();
|
||||||
}
|
}
|
||||||
|
|
||||||
drawBackground() {
|
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) {
|
drawHealthBar(health, sprite) {
|
||||||
// Pixel art health bar
|
// Pixel art health bar
|
||||||
const ctx = this.ctx;
|
const ctx = this.ctx;
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,6 @@ export class UISystem extends System {
|
||||||
// REMOVED drawStats and drawSkillProgress from active gameplay
|
// REMOVED drawStats and drawSkillProgress from active gameplay
|
||||||
this.drawDamageNumbers();
|
this.drawDamageNumbers();
|
||||||
this.drawNotifications();
|
this.drawNotifications();
|
||||||
this.drawAbsorptionEffects();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
drawHUD(player) {
|
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
104
src/systems/VFXSystem.js
Normal file
104
src/systems/VFXSystem.js
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue