Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 37 additions & 17 deletions src/engine/systems/AbilitySystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Ability, AbilityDefinition } from '../components/Ability';
import { Selectable } from '../components/Selectable';
import { Building } from '../components/Building';
import { distance } from '@/utils/math';
import { validateEntityAlive } from '@/utils/EntityValidator';

interface AbilityCommand {
entityIds: number[];
Expand Down Expand Up @@ -48,7 +49,7 @@ export class AbilitySystem extends System {
private handleAbilityCommand(command: AbilityCommand): void {
for (const entityId of command.entityIds) {
const entity = this.world.getEntity(entityId);
if (!entity) continue;
if (!validateEntityAlive(entity, entityId, 'AbilitySystem:handleAbilityCommand')) continue;

const ability = entity.get<Ability>('Ability');
if (!ability) continue;
Expand Down Expand Up @@ -85,11 +86,19 @@ export class AbilitySystem extends System {
case 'ally':
if (command.targetEntityId === undefined) return false;
const target = this.world.getEntity(command.targetEntityId);
if (!target) return false;
if (
!validateEntityAlive(
target,
command.targetEntityId,
'AbilitySystem:validateTarget:target'
)
)
return false;

// Check range
const casterEntity = this.world.getEntity(caster.id);
if (!casterEntity) return false;
if (!validateEntityAlive(casterEntity, caster.id, 'AbilitySystem:validateTarget:caster'))
return false;

const casterTransform = casterEntity.get<Transform>('Transform');
const targetTransform = target.get<Transform>('Transform');
Expand Down Expand Up @@ -119,7 +128,7 @@ export class AbilitySystem extends System {
command: AbilityCommand
): void {
const casterEntity = this.world.getEntity(caster.id);
if (!casterEntity) return;
if (!validateEntityAlive(casterEntity, caster.id, 'AbilitySystem:executeAbility')) return;

const casterTransform = casterEntity.get<Transform>('Transform');
const casterSelectable = casterEntity.get<Selectable>('Selectable');
Expand Down Expand Up @@ -228,7 +237,7 @@ export class AbilitySystem extends System {

private executeCombatStim(caster: { id: number }): void {
const entity = this.world.getEntity(caster.id);
if (!entity) return;
if (!validateEntityAlive(entity, caster.id, 'AbilitySystem:executeCombatStim')) return;

const health = entity.get<Health>('Health');
const unit = entity.get<Unit>('Unit');
Expand Down Expand Up @@ -263,7 +272,7 @@ export class AbilitySystem extends System {

private executeBombardmentMode(caster: { id: number }): void {
const entity = this.world.getEntity(caster.id);
if (!entity) return;
if (!validateEntityAlive(entity, caster.id, 'AbilitySystem:executeBombardmentMode')) return;

const unit = entity.get<Unit>('Unit');
if (!unit) return;
Expand All @@ -277,7 +286,7 @@ export class AbilitySystem extends System {

private executeSnipe(caster: { id: number }, targetId: number, damage: number): void {
const target = this.world.getEntity(targetId);
if (!target) return;
if (!validateEntityAlive(target, targetId, 'AbilitySystem:executeSnipe:target')) return;

const targetHealth = target.get<Health>('Health');
if (!targetHealth) return;
Expand All @@ -286,7 +295,10 @@ export class AbilitySystem extends System {
targetHealth.takeDamage(damage, this.game.getGameTime());

const casterEntity = this.world.getEntity(caster.id);
const casterTransform = casterEntity?.get<Transform>('Transform');
// Caster may have been destroyed between ability use and execution
const casterTransform = casterEntity?.isDestroyed()
? undefined
: casterEntity?.get<Transform>('Transform');
const targetTransform = target.get<Transform>('Transform');

this.game.eventBus.emit('combat:attack', {
Expand Down Expand Up @@ -370,10 +382,13 @@ export class AbilitySystem extends System {

private executeNovaCannon(caster: { id: number }, targetId: number, damage: number): void {
const target = this.world.getEntity(targetId);
if (!target) return;
if (!validateEntityAlive(target, targetId, 'AbilitySystem:executeNovaCannon:target')) return;

const casterEntity = this.world.getEntity(caster.id);
const casterTransform = casterEntity?.get<Transform>('Transform');
// Caster may have been destroyed - still proceed with channel but skip position info
const casterTransform = casterEntity?.isDestroyed()
? undefined
: casterEntity?.get<Transform>('Transform');
const targetTransform = target.get<Transform>('Transform');

// Nova Cannon has a 3 second channel time
Expand Down Expand Up @@ -409,7 +424,7 @@ export class AbilitySystem extends System {

private executeTacticalJump(caster: { id: number }, position: { x: number; y: number }): void {
const entity = this.world.getEntity(caster.id);
if (!entity) return;
if (!validateEntityAlive(entity, caster.id, 'AbilitySystem:executeTacticalJump')) return;

const transform = entity.get<Transform>('Transform');
if (!transform) return;
Expand All @@ -432,10 +447,13 @@ export class AbilitySystem extends System {

private executePowerCannon(caster: { id: number }, targetId: number, damage: number): void {
const target = this.world.getEntity(targetId);
if (!target) return;
if (!validateEntityAlive(target, targetId, 'AbilitySystem:executePowerCannon:target')) return;

const casterEntity = this.world.getEntity(caster.id);
const casterTransform = casterEntity?.get<Transform>('Transform');
// Caster may have been destroyed - still proceed but skip position info
const casterTransform = casterEntity?.isDestroyed()
? undefined
: casterEntity?.get<Transform>('Transform');
const targetTransform = target.get<Transform>('Transform');
const targetHealth = target.get<Health>('Health');

Expand Down Expand Up @@ -502,7 +520,7 @@ export class AbilitySystem extends System {

private executeSupplyDrop(targetId: number): void {
const target = this.world.getEntity(targetId);
if (!target) return;
if (!validateEntityAlive(target, targetId, 'AbilitySystem:executeSupplyDrop')) return;

const building = target.get<Building>('Building');
if (!building || building.buildingId !== 'supply_cache') return;
Expand All @@ -527,7 +545,7 @@ export class AbilitySystem extends System {

private applyConcussiveShells(targetId: number): void {
const target = this.world.getEntity(targetId);
if (!target) return;
if (!validateEntityAlive(target, targetId, 'AbilitySystem:applyConcussiveShells')) return;

const unit = target.get<Unit>('Unit');
if (!unit) return;
Expand All @@ -549,7 +567,7 @@ export class AbilitySystem extends System {

private applyCombatShield(caster: { id: number }): void {
const entity = this.world.getEntity(caster.id);
if (!entity) return;
if (!validateEntityAlive(entity, caster.id, 'AbilitySystem:applyCombatShield')) return;

const health = entity.get<Health>('Health');
if (!health) return;
Expand Down Expand Up @@ -643,7 +661,9 @@ export class AbilitySystem extends System {
if (targetId === undefined || damage === undefined) return;

const currentTarget = this.world.getEntity(targetId);
if (!currentTarget) return;
// Target may have been destroyed during the channel time
if (!validateEntityAlive(currentTarget, targetId, 'AbilitySystem:executeNovaCannonImpact'))
return;

const targetHealth = currentTarget.get<Health>('Health');
if (!targetHealth) return;
Expand Down
35 changes: 24 additions & 11 deletions src/engine/systems/CombatSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { SpatialEntityData, SpatialUnitState } from '../core/SpatialGrid';
import { findBestTarget as findBestTargetShared, isEnemy } from '../combat/TargetAcquisition';
import { distance, clamp } from '@/utils/math';
import { ThrottledCache } from '@/utils/ThrottledCache';
import { validateEntityAlive } from '@/utils/EntityValidator';

// PERF: Reusable event payload objects to avoid allocation per attack
const attackEventPayload = {
Expand Down Expand Up @@ -350,7 +351,7 @@ export class CombatSystem extends System {

for (const entityId of this.combatActiveUnits) {
const entity = this.world.getEntity(entityId);
if (!entity) continue;
if (!validateEntityAlive(entity, entityId, 'CombatSystem:rebuildAttackQueue')) continue;

const unit = entity.get<Unit>('Unit');
if (!unit || unit.state === 'dead') continue;
Expand Down Expand Up @@ -394,7 +395,7 @@ export class CombatSystem extends System {
}): void {
for (const entityId of command.entityIds) {
const entity = this.world.getEntity(entityId);
if (!entity) continue;
if (!validateEntityAlive(entity, entityId, 'CombatSystem:handleAttackCommand')) continue;

const unit = entity.get<Unit>('Unit');
if (!unit) continue;
Expand Down Expand Up @@ -519,7 +520,7 @@ export class CombatSystem extends System {
// PERF OPTIMIZATION: Second pass - target acquisition for combat-active units only
for (const entityId of this.combatActiveUnits) {
const attacker = this.world.getEntity(entityId);
if (!attacker) continue;
if (!validateEntityAlive(attacker, entityId, 'CombatSystem:targetAcquisition')) continue;

const transform = attacker.get<Transform>('Transform');
const unit = attacker.get<Unit>('Unit');
Expand Down Expand Up @@ -790,7 +791,9 @@ export class CombatSystem extends System {
const resource = resourceEntity.get<Resource>('Resource');
if (resource) {
resource.extractorEntityId = null;
debugCombat.log(`CombatSystem: Extractor destroyed, plasma geyser ${buildingComp.linkedResourceId} restored`);
debugCombat.log(
`CombatSystem: Extractor destroyed, plasma geyser ${buildingComp.linkedResourceId} restored`
);
}
}
}
Expand Down Expand Up @@ -850,7 +853,11 @@ export class CombatSystem extends System {

// Get self's player ID
const selfEntity = this.world.getEntity(selfId);
const selfSelectable = selfEntity?.get<Selectable>('Selectable');
if (!validateEntityAlive(selfEntity, selfId, 'CombatSystem:checkCombatZone')) {
this.combatAwareUnits.delete(selfId);
return false;
}
const selfSelectable = selfEntity.get<Selectable>('Selectable');
if (!selfSelectable) {
this.combatAwareUnits.delete(selfId);
return false;
Expand All @@ -876,7 +883,8 @@ export class CombatSystem extends System {

for (const entityId of nearbyBuildingIds) {
const entity = this.world.getEntity(entityId);
if (!entity) continue;
if (!validateEntityAlive(entity, entityId, 'CombatSystem:checkCombatZone:buildings'))
continue;

const selectable = entity.get<Selectable>('Selectable');
const health = entity.get<Health>('Health');
Expand Down Expand Up @@ -921,7 +929,9 @@ export class CombatSystem extends System {

// Get self's player ID
const selfEntity = this.world.getEntity(selfId);
const selfSelectable = selfEntity?.get<Selectable>('Selectable');
if (!validateEntityAlive(selfEntity, selfId, 'CombatSystem:findImmediateAttackTarget'))
return null;
const selfSelectable = selfEntity.get<Selectable>('Selectable');
if (!selfSelectable) return null;

// Use shared target acquisition for attack-range search
Expand Down Expand Up @@ -1000,7 +1010,8 @@ export class CombatSystem extends System {
): number | null {
// Get self's player ID
const selfEntity = this.world.getEntity(selfId);
const selfSelectable = selfEntity?.get<Selectable>('Selectable');
if (!validateEntityAlive(selfEntity, selfId, 'CombatSystem:findBestTargetSpatial')) return null;
const selfSelectable = selfEntity.get<Selectable>('Selectable');
if (!selfSelectable) return null;

// Use shared target acquisition for sight-range search
Expand Down Expand Up @@ -1204,7 +1215,8 @@ export class CombatSystem extends System {
): void {
// Get attacker's player ID
const attackerEntity = this.world.getEntity(attackerId);
const attackerSelectable = attackerEntity?.get<Selectable>('Selectable');
if (!validateEntityAlive(attackerEntity, attackerId, 'CombatSystem:applySplashDamage')) return;
const attackerSelectable = attackerEntity.get<Selectable>('Selectable');
if (!attackerSelectable) return;

// Use spatial grid to find nearby units - much faster than checking all entities
Expand All @@ -1219,7 +1231,7 @@ export class CombatSystem extends System {
if (entityId === attackerId) continue;

const entity = this.world.getEntity(entityId);
if (!entity) continue;
if (!validateEntityAlive(entity, entityId, 'CombatSystem:applySplashDamage:units')) continue;

const transform = entity.get<Transform>('Transform');
const health = entity.get<Health>('Health');
Expand Down Expand Up @@ -1277,7 +1289,8 @@ export class CombatSystem extends System {

for (const entityId of nearbyBuildingIds) {
const entity = this.world.getEntity(entityId);
if (!entity) continue;
if (!validateEntityAlive(entity, entityId, 'CombatSystem:applySplashDamage:buildings'))
continue;

const transform = entity.get<Transform>('Transform');
const health = entity.get<Health>('Health');
Expand Down
28 changes: 22 additions & 6 deletions src/engine/systems/ProductionSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from '@/data/buildings/dominion';
import { debugProduction, debugSpawning } from '@/utils/debugLogger';
import { EnhancedAISystem } from './EnhancedAISystem';
import { validateEntityAlive } from '@/utils/EntityValidator';

export class ProductionSystem extends System {
public readonly name = 'ProductionSystem';
Expand Down Expand Up @@ -60,7 +61,14 @@ export class ProductionSystem extends System {
playerId?: string;
}): void {
const entity = this.world.getEntity(command.entityId);
if (!entity) return;
if (
!validateEntityAlive(
entity,
command.entityId,
'ProductionSystem:handleCancelProductionCommand'
)
)
return;

const building = entity.get<Building>('Building');
if (!building) return;
Expand Down Expand Up @@ -111,7 +119,10 @@ export class ProductionSystem extends System {
playerId?: string;
}): void {
const entity = this.world.getEntity(command.entityId);
if (!entity) return;
if (
!validateEntityAlive(entity, command.entityId, 'ProductionSystem:handleReorderQueueCommand')
)
return;

const building = entity.get<Building>('Building');
if (!building) return;
Expand Down Expand Up @@ -143,7 +154,7 @@ export class ProductionSystem extends System {

for (const entityId of entityIds) {
const entity = this.world.getEntity(entityId);
if (!entity) continue;
if (!validateEntityAlive(entity, entityId, 'ProductionSystem:handleTrainCommand')) continue;

const building = entity.get<Building>('Building');
if (!building || !building.isComplete()) continue;
Expand Down Expand Up @@ -387,7 +398,10 @@ export class ProductionSystem extends System {

// Get the building's owner from its Selectable component
const buildingEntity = this.world.getEntity(buildingId);
const selectable = buildingEntity?.get<Selectable>('Selectable');
// Building should still exist since we're in its production callback
const selectable = buildingEntity?.isDestroyed()
? undefined
: buildingEntity?.get<Selectable>('Selectable');
const ownerPlayerId = selectable?.playerId;

// Spawn multiple units if produceCount > 1 (reactor bonus)
Expand Down Expand Up @@ -433,7 +447,9 @@ export class ProductionSystem extends System {
} else {
// This is a research upgrade - emit for Phaser overlay
const buildingEntity = this.world.getEntity(buildingId);
const buildingSelectable = buildingEntity?.get<Selectable>('Selectable');
const buildingSelectable = buildingEntity?.isDestroyed()
? undefined
: buildingEntity?.get<Selectable>('Selectable');
if (buildingSelectable?.playerId && isLocalPlayer(buildingSelectable.playerId)) {
this.game.eventBus.emit('research:complete', {
buildingId,
Expand Down Expand Up @@ -466,7 +482,7 @@ export class ProductionSystem extends System {

// Update entity for mesh refresh
const entity = this.world.getEntity(buildingId);
if (entity) {
if (validateEntityAlive(entity, buildingId, 'ProductionSystem:handleBuildingUpgradeComplete')) {
const health = entity.get<Health>('Health');
if (health && newDef.maxHealth) {
// Keep current health percentage, apply to new max
Expand Down
Loading
Loading