import { System } from '../core/System.js'; import { GameConfig } from '../GameConfig.js'; import { Events } from '../core/EventBus.js'; export class AbsorptionSystem extends System { constructor() { super('AbsorptionSystem'); this.requiredComponents = ['Position', 'Absorbable']; this.priority = 25; this.absorptionEffects = []; // Visual effects } process(deltaTime, _entities) { const playerController = this.engine.systems.find(s => s.name === 'PlayerControllerSystem'); const player = playerController ? playerController.getPlayerEntity() : null; if (!player) return; const playerPos = player.getComponent('Position'); const playerEvolution = player.getComponent('Evolution'); const playerSkills = player.getComponent('Skills'); const playerStats = player.getComponent('Stats'); const skillProgress = player.getComponent('SkillProgress'); if (!playerPos || !playerEvolution) return; // Get ALL entities (including inactive ones) for absorption check const allEntities = this.engine.entities; // Get raw entities array, not filtered const config = GameConfig.Absorption; // Check for absorbable entities near player allEntities.forEach(entity => { if (entity === player) return; // Allow inactive entities if they're dead and absorbable if (!entity.active) { const health = entity.getComponent('Health'); const absorbable = entity.getComponent('Absorbable'); // Only process inactive entities if they're dead and not yet absorbed if (!health || !health.isDead() || !absorbable || absorbable.absorbed) { return; } } if (!entity.hasComponent('Absorbable')) return; if (!entity.hasComponent('Health')) return; const absorbable = entity.getComponent('Absorbable'); const health = entity.getComponent('Health'); const entityPos = entity.getComponent('Position'); if (!entityPos) return; // Check if creature is dead and in absorption range if (health.isDead() && !absorbable.absorbed) { const dx = playerPos.x - entityPos.x; const dy = playerPos.y - entityPos.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance <= config.range) { this.absorbEntity(player, entity, absorbable, playerEvolution, playerSkills, playerStats, skillProgress); } } }); // Update visual effects this.updateEffects(deltaTime); } absorbEntity(player, entity, absorbable, evolution, skills, stats, skillProgress) { if (absorbable.absorbed) return; absorbable.absorbed = true; const entityPos = entity.getComponent('Position'); const health = player.getComponent('Health'); const config = GameConfig.Absorption; // Add evolution points evolution.addEvolution( absorbable.evolutionData.human, absorbable.evolutionData.beast, absorbable.evolutionData.slime ); // Track skill progress (need to absorb multiple times to learn) // Always track progress for ALL skills the enemy has, regardless of roll if (skillProgress && absorbable.skillsGranted && absorbable.skillsGranted.length > 0) { absorbable.skillsGranted.forEach(skill => { // Always add progress when absorbing an enemy with this skill const currentProgress = skillProgress.addSkillProgress(skill.id); const required = skillProgress.requiredAbsorptions; // If we've absorbed enough, learn the skill if (currentProgress >= required && !skills.hasSkill(skill.id)) { skills.addSkill(skill.id, false); this.engine.emit(Events.SKILL_LEARNED, { id: skill.id }); console.log(`Learned skill: ${skill.id}!`); } }); } // Heal from absorption (slime recovers by consuming) if (health) { const healPercent = config.healPercentMin + Math.random() * (config.healPercentMax - config.healPercentMin); const healAmount = health.maxHp * healPercent; health.heal(healAmount); } // Check for mutation if (absorbable.shouldMutate() && stats) { this.applyMutation(stats); evolution.checkMutations(stats, this.engine); } // Visual effect if (entityPos) { this.addAbsorptionEffect(entityPos.x, entityPos.y); } // Mark as absorbed - DeathSystem will handle removal after absorption window // Don't remove immediately, let DeathSystem handle it } applyMutation(stats) { // Random stat mutation const mutations = [ { stat: 'strength', amount: 5 }, { stat: 'agility', amount: 5 }, { stat: 'intelligence', amount: 5 }, { stat: 'constitution', amount: 5 }, { stat: 'perception', amount: 5 }, ]; const mutation = mutations[Math.floor(Math.random() * mutations.length)]; stats[mutation.stat] += mutation.amount; // Could also add negative mutations if (Math.random() < 0.3) { const negativeStat = mutations[Math.floor(Math.random() * mutations.length)]; stats[negativeStat.stat] = Math.max(1, stats[negativeStat.stat] - 2); } } 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; } }