slime/src/systems/AbsorptionSystem.ts

170 lines
6.1 KiB
TypeScript

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<Position>(ComponentType.POSITION);
const playerEvolution = player.getComponent<Evolution>(ComponentType.EVOLUTION);
const playerSkills = player.getComponent<Skills>(ComponentType.SKILLS);
const playerStats = player.getComponent<Stats>(ComponentType.STATS);
const skillProgress = player.getComponent<SkillProgress>(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<Health>(ComponentType.HEALTH);
const absorbable = entity.getComponent<Absorbable>(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<Absorbable>(ComponentType.ABSORBABLE);
const health = entity.getComponent<Health>(ComponentType.HEALTH);
const entityPos = entity.getComponent<Position>(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<Position>(ComponentType.POSITION);
const health = player.getComponent<Health>(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);
}
}
}