import { System } from '../core/System.ts'; import { GameConfig } from '../GameConfig.ts'; import { Events } from '../core/EventBus.ts'; import { SystemName, ComponentType } from '../core/Constants.ts'; import type { Entity } from '../core/Entity.ts'; import type { Position } from '../components/Position.ts'; import type { Evolution } from '../components/Evolution.ts'; import type { Skills } from '../components/Skills.ts'; import type { Stats } from '../components/Stats.ts'; import type { SkillProgress } from '../components/SkillProgress.ts'; import type { Absorbable } from '../components/Absorbable.ts'; import type { Health } from '../components/Health.ts'; import type { PlayerControllerSystem } from './PlayerControllerSystem.ts'; import type { VFXSystem } from './VFXSystem.ts'; /** * System responsible for identifying dead absorbable entities near the player and processing absorption. */ export class AbsorptionSystem extends System { constructor() { super(SystemName.ABSORPTION); this.requiredComponents = [ComponentType.POSITION, ComponentType.ABSORBABLE]; this.priority = 25; } /** * Check for absorbable entities within range of the player and initiate absorption if applicable. * @param _deltaTime - Time elapsed since last frame * @param _entities - Matching entities (not used, uses raw engine entities) */ process(_deltaTime: number, _entities: Entity[]): void { const playerController = this.engine.systems.find( (s) => s.name === SystemName.PLAYER_CONTROLLER ) as PlayerControllerSystem | undefined; const player = playerController ? playerController.getPlayerEntity() : null; if (!player) return; const playerPos = player.getComponent(ComponentType.POSITION); const playerEvolution = player.getComponent(ComponentType.EVOLUTION); const playerSkills = player.getComponent(ComponentType.SKILLS); const playerStats = player.getComponent(ComponentType.STATS); const skillProgress = player.getComponent(ComponentType.SKILL_PROGRESS); if (!playerPos || !playerEvolution) return; const allEntities = this.engine.entities; const config = GameConfig.Absorption; allEntities.forEach((entity) => { if (entity === player) return; if (!entity.active) { const health = entity.getComponent(ComponentType.HEALTH); const absorbable = entity.getComponent(ComponentType.ABSORBABLE); if (!health || !health.isDead() || !absorbable || absorbable.absorbed) { return; } } if (!entity.hasComponent(ComponentType.ABSORBABLE)) return; if (!entity.hasComponent(ComponentType.HEALTH)) return; const absorbable = entity.getComponent(ComponentType.ABSORBABLE); const health = entity.getComponent(ComponentType.HEALTH); const entityPos = entity.getComponent(ComponentType.POSITION); if (!entityPos) return; if (health && health.isDead() && absorbable && !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 ); } } }); } /** * Process the absorption of an entity by the player. */ absorbEntity( player: Entity, entity: Entity, absorbable: Absorbable, evolution: Evolution, skills: Skills | undefined, stats: Stats | undefined, skillProgress: SkillProgress | undefined ): void { if (absorbable.absorbed) return; absorbable.absorbed = true; const entityPos = entity.getComponent(ComponentType.POSITION); const health = player.getComponent(ComponentType.HEALTH); const config = GameConfig.Absorption; evolution.addEvolution( absorbable.evolutionData.human, absorbable.evolutionData.beast, absorbable.evolutionData.slime ); if (skillProgress && absorbable.skillsGranted && absorbable.skillsGranted.length > 0) { absorbable.skillsGranted.forEach((skill) => { const currentProgress = skillProgress.addSkillProgress(skill.id); const required = skillProgress.requiredAbsorptions; if (currentProgress >= required && skills && !skills.hasSkill(skill.id)) { skills.addSkill(skill.id, false); this.engine.emit(Events.SKILL_LEARNED, { id: skill.id }); console.log(`Learned skill: ${skill.id}!`); } }); } if (health) { const healPercent = config.healPercentMin + Math.random() * (config.healPercentMax - config.healPercentMin); const healAmount = health.maxHp * healPercent; health.heal(healAmount); } if (absorbable.shouldMutate() && stats) { this.applyMutation(stats); evolution.checkMutations(stats, this.engine); } if (entityPos) { const vfxSystem = this.engine.systems.find((s) => s.name === SystemName.VFX) as | VFXSystem | undefined; if (vfxSystem) { vfxSystem.createAbsorption(entityPos.x, entityPos.y); } } } /** * Apply a random stat mutation (positive or negative) to an entity's stats. * @param stats - The stats component to mutate */ applyMutation(stats: Stats): void { type StatName = 'strength' | 'agility' | 'intelligence' | 'constitution' | 'perception'; const mutations: Array<{ stat: StatName; amount: number }> = [ { 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; if (Math.random() < 0.3) { const negativeStat = mutations[Math.floor(Math.random() * mutations.length)]; stats[negativeStat.stat] = Math.max(1, stats[negativeStat.stat] - 2); } } }