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
28 changes: 28 additions & 0 deletions src/engine/components/Unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,12 @@ export class Unit extends Component {
public isBiological: boolean;
public isMechanical: boolean;

// SC2-style Assault Mode: Persistent attack-move that doesn't go idle
// When set, unit stays aggressive even after reaching destination
public assaultDestination: { x: number; y: number } | null;
public isInAssaultMode: boolean; // True when unit is executing attack-move command
public assaultIdleTicks: number; // How long unit has been idle at assault destination

// Targeting restrictions - which types of units this unit can attack
public canAttackGround: boolean;
public canAttackAir: boolean;
Expand Down Expand Up @@ -319,6 +325,11 @@ export class Unit extends Component {
this.isBiological = definition.isBiological ?? !definition.isMechanical;
this.isMechanical = definition.isMechanical ?? false;

// SC2-style assault mode - persistent attack-move
this.assaultDestination = null;
this.isInAssaultMode = false;
this.assaultIdleTicks = 0;

// Targeting restrictions - default: can attack ground if has damage, can't attack air by default
const hasDamage = definition.attackDamage > 0;
this.canAttackGround = definition.canAttackGround ?? hasDamage;
Expand Down Expand Up @@ -400,6 +411,10 @@ export class Unit extends Component {
this.state = 'moving';
}
this.targetEntityId = null;
// SC2-style: Regular move clears assault mode (explicit move command overrides attack-move)
this.assaultDestination = null;
this.isInAssaultMode = false;
this.assaultIdleTicks = 0;
}

// Move to position while preserving current state (for gathering, etc.)
Expand Down Expand Up @@ -427,11 +442,16 @@ export class Unit extends Component {
}

// Attack-move: move toward a position while engaging enemies along the way
// SC2-style: Sets assault mode so unit stays aggressive even after arriving
public setAttackMoveTarget(x: number, y: number): void {
this.targetX = x;
this.targetY = y;
this.state = 'attackmoving';
this.targetEntityId = null;
// SC2-style: Enable assault mode - unit will keep scanning for targets
this.assaultDestination = { x, y };
this.isInAssaultMode = true;
this.assaultIdleTicks = 0;
}

public setPath(path: Array<{ x: number; y: number }>): void {
Expand All @@ -455,6 +475,10 @@ export class Unit extends Component {
this.patrolPoints = [];
this.isHoldingPosition = false;
this.currentSpeed = 0;
// SC2-style: Explicit stop clears assault mode
this.assaultDestination = null;
this.isInAssaultMode = false;
this.assaultIdleTicks = 0;
}

public holdPosition(): void {
Expand All @@ -463,6 +487,10 @@ export class Unit extends Component {
this.patrolPoints = [];
this.isHoldingPosition = true;
this.currentSpeed = 0;
// SC2-style: Hold position clears assault mode
this.assaultDestination = null;
this.isInAssaultMode = false;
this.assaultIdleTicks = 0;
}

public canAttack(gameTime: number): boolean {
Expand Down
8 changes: 6 additions & 2 deletions src/engine/systems/AIMicroSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,8 +334,10 @@ export class AIMicroSystem extends System {
if (unit.isWorker) continue;

// Process units in combat-related states
// SC2-STYLE: Also process idle units in assault mode - they need micro to find targets
const canProcessUnit = unit.state === 'attacking' || unit.state === 'moving' ||
unit.state === 'attackmoving' || (unit.canTransform && unit.state === 'idle');
unit.state === 'attackmoving' || (unit.canTransform && unit.state === 'idle') ||
(unit.isInAssaultMode && unit.state === 'idle');
if (!canProcessUnit) continue;

// Get or create micro state
Expand Down Expand Up @@ -384,8 +386,10 @@ export class AIMicroSystem extends System {

// Process units in combat-related states
// attackmoving = moving to position while engaging enemies along the way
// SC2-STYLE: Also process idle units in assault mode - they need micro to find targets
const canProcessUnit = unit.state === 'attacking' || unit.state === 'moving' ||
unit.state === 'attackmoving' || (unit.canTransform && unit.state === 'idle');
unit.state === 'attackmoving' || (unit.canTransform && unit.state === 'idle') ||
(unit.isInAssaultMode && unit.state === 'idle');
if (!canProcessUnit) continue;

// Get or create micro state
Expand Down
52 changes: 48 additions & 4 deletions src/engine/systems/CombatSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,9 @@ export class CombatSystem extends System {
/**
* PERF OPTIMIZATION: Rebuild the combat-active unit list
* Only units in hot cells or with active targets are tracked
*
* SC2-STYLE: Units in assault mode are ALWAYS combat-active and never throttled.
* They continue scanning for enemies even when idle at their destination.
*/
private updateCombatActiveUnits(currentTick: number): void {
if (currentTick - this.combatActiveLastUpdate < this.COMBAT_ACTIVE_UPDATE_INTERVAL) {
Expand All @@ -205,6 +208,13 @@ export class CombatSystem extends System {
continue;
}

// SC2-STYLE: Always include assault mode units - they never stop scanning
// This is the key fix for idle units in enemy bases
if (unit.isInAssaultMode) {
this.combatActiveUnits.add(entity.id);
continue;
}

// Include units in attack-move or patrolling states
if (unit.state === 'attackmoving' || unit.state === 'patrolling') {
this.combatActiveUnits.add(entity.id);
Expand Down Expand Up @@ -477,18 +487,21 @@ export class CombatSystem extends System {

// Auto-acquire targets for units that need them
// canAttackWhileMoving units also acquire targets while moving
// SC2-STYLE: Assault mode units ALWAYS need to acquire targets when idle
const needsTarget = unit.targetEntityId === null && (
unit.state === 'idle' ||
unit.state === 'patrolling' ||
unit.state === 'attackmoving' ||
unit.state === 'attacking' ||
unit.isHoldingPosition ||
unit.isInAssaultMode || // SC2-STYLE: Assault mode units always scan
(unit.canAttackWhileMoving && unit.state === 'moving')
);

if (needsTarget) {
// PERF: Use hot cell check instead of expensive spatial query for idle units
if (unit.state === 'idle' && !unit.isHoldingPosition) {
// SC2-STYLE: Skip this optimization for assault mode units - they always search
if (unit.state === 'idle' && !unit.isHoldingPosition && !unit.isInAssaultMode) {
// Fast check: is this unit in a hot cell?
const inHotCell = this.world.unitGrid.isInHotCell(transform.x, transform.y, this.hotCells);
if (!inHotCell) {
Expand All @@ -503,9 +516,15 @@ export class CombatSystem extends System {

let target: number | null = null;

// For idle units, do a fast check for enemies within ATTACK range
// Uses light throttle (1 tick = ~50ms) for performance while staying responsive
if (unit.state === 'idle' || unit.isHoldingPosition) {
// SC2-STYLE: Assault mode units always search at sight range, no throttling
if (unit.isInAssaultMode && unit.state === 'idle') {
// Aggressive sight-range search for assault mode units
target = this.findBestTargetSpatial(attacker.id, transform, unit);
// Track idle time for assault mode units
unit.assaultIdleTicks++;
} else if (unit.state === 'idle' || unit.isHoldingPosition) {
// For regular idle units, do a fast check for enemies within ATTACK range
// Uses light throttle (1 tick = ~50ms) for performance while staying responsive
target = this.findImmediateAttackTarget(attacker.id, transform, unit, currentTick);
}

Expand All @@ -523,6 +542,9 @@ export class CombatSystem extends System {
const savedTargetX = unit.targetX;
const savedTargetY = unit.targetY;
const wasAttackMoving = unit.state === 'attackmoving';
// SC2-STYLE: Remember assault destination
const savedAssaultDest = unit.assaultDestination;
const wasInAssaultMode = unit.isInAssaultMode;

unit.setAttackTarget(target);

Expand All @@ -531,6 +553,13 @@ export class CombatSystem extends System {
unit.targetX = savedTargetX;
unit.targetY = savedTargetY;
}

// SC2-STYLE: Preserve assault mode through target acquisition
if (wasInAssaultMode && savedAssaultDest) {
unit.assaultDestination = savedAssaultDest;
unit.isInAssaultMode = true;
unit.assaultIdleTicks = 0; // Reset idle counter - we found a target
}
}
} else if (target && unit.isHoldingPosition) {
// Holding position units only attack if in range (already confirmed by findImmediateAttackTarget)
Expand Down Expand Up @@ -558,6 +587,12 @@ export class CombatSystem extends System {
// Resume attack-move to destination
unit.state = 'attackmoving';
unit.targetEntityId = null;
} else if (unit.isInAssaultMode) {
// SC2-STYLE: Assault mode units stay in assault mode, ready to scan for new targets
// They go "idle" but with assault mode flag still set, so they keep scanning
unit.targetEntityId = null;
unit.state = 'idle';
// Don't clear assault mode - unit will immediately scan for new targets next tick
} else if (!unit.executeNextCommand()) {
unit.clearTarget();
}
Expand All @@ -578,6 +613,11 @@ export class CombatSystem extends System {
// Resume attack-move to destination
unit.state = 'attackmoving';
unit.targetEntityId = null;
} else if (unit.isInAssaultMode) {
// SC2-STYLE: Assault mode units stay aggressive and keep scanning
unit.targetEntityId = null;
unit.state = 'idle';
// Assault mode preserved - unit will scan for new targets
} else if (!unit.executeNextCommand()) {
unit.clearTarget();
}
Expand All @@ -599,6 +639,10 @@ export class CombatSystem extends System {
} else if (unit.targetX !== null && unit.targetY !== null) {
unit.state = 'attackmoving';
unit.targetEntityId = null;
} else if (unit.isInAssaultMode) {
// SC2-STYLE: Stay aggressive, find a target we CAN attack
unit.targetEntityId = null;
unit.state = 'idle';
} else if (!unit.executeNextCommand()) {
unit.clearTarget();
}
Expand Down
Loading