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
40 changes: 40 additions & 0 deletions docs/architecture/OVERVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -2854,6 +2854,46 @@ Multiple build order variants exist per difficulty level (2-4 variants each), ta

Naval units are excluded from land attack operations to prevent them being sent to unreachable targets.

#### Air Unit Control System

The AI controls air units independently from ground forces through a multi-layered system:

**Army Separation** (`AITacticsManager`)
- During attacks, the army is split into `groundUnits` and `airCombatUnits`
- Ground units form the main army with concave formations
- Air units receive independent flanking commands perpendicular to the main attack vector
- If no ground units exist, air becomes the main force (no wasted air units)

**Air Formations** (`FormationControl`)
- Air units are positioned past the enemy center at a perpendicular offset
- Wide spacing (2x ground) reduces splash damage vulnerability
- Independent priority level (4) prevents air from interfering with ground formations

**Air Harassment** (`AITacticsManager.executeAirHarassment()`)
- Up to 3 air units sent to enemy worker lines
- Air bypasses terrain and ground defenses (direct paths)
- 10-second cooldown between harassment waves

**Support Air** (`AITacticsManager.commandSupportAir()`)
- Lifter and Overseer units follow the army centroid
- Positioned behind the army toward the AI's own base
- Provides healing and detection support during combat

**Air Micro** (`AIMicroSystem`)
- Health-based disengage: air units flee below 30% health
- Hit-and-run repositioning: perpendicular movement every 15 ticks while attacking
- Proactive Valkyrie transformation: lower threshold (1.5x) for mode switching

**Anti-Air Response**
- Emergency counter-air production on ALL difficulties (not just hard+)
- Preemptive anti-air when enemy air tech detected (medium+)
- Air superiority Valkyrie production (priority 75) when enemy has air units
- `enemyAirUnits` tracking feeds into production decisions

**Air Scouting** (`AIScoutingManager`)
- Flying units preferred as scouts (bypass terrain, fastest paths)
- Falls back to ground scouts, then idle workers

#### Unit Production Coverage

All Dominion combat and support units have dedicated macro rules:
Expand Down
28 changes: 28 additions & 0 deletions docs/design/GAME_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,34 @@ Units have restrictions on what they can attack based on air/ground targeting:

**AI Counter-Building**: When AI units are attacked by enemies they cannot hit (e.g., air units attacking ground-only troops), the AI urgently prioritizes building anti-air capable units.

### AI Air Unit Control

The AI manages air units as an independent tactical arm:

**Air Combat Units** (Valkyrie, Specter, Dreadnought):
- Separated from ground army during attacks
- Execute flanking maneuvers perpendicular to the main ground attack
- Perform hit-and-run micro (reposition after attacking)
- Disengage automatically when health drops below 30%
- Used for worker harassment between major attacks

**Air Support Units** (Lifter, Overseer):
- Follow the main army at a safe distance behind the centroid
- Provide healing (Lifter) and detection (Overseer) support
- Not included in combat formations

**Valkyrie Transform Intelligence**:
- Switches to Fighter mode when air threats dominate (1.5x threshold)
- Switches to Assault mode when ground threats dominate (1.5x threshold)
- More aggressive mode-switching than previous 2x threshold

**Air Production Priority**:
- Valkyrie: priority 54 (primary air unit)
- Specter: priority 52 (cloaked strike)
- Dreadnought: priority 58 (capital ship)
- Air Superiority response: priority 75 when enemy has air
- Emergency anti-air: priority 95 on all difficulties

### AI Personality System

Each AI player is assigned a personality that determines its strategic behavior, army composition, and build order selection. In multi-AI games, personalities are varied so no two AIs play identically.
Expand Down
25 changes: 20 additions & 5 deletions src/data/ai/factions/dominion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,6 @@ const DOMINION_MACRO_RULES: MacroRule[] = [
],
},
cooldownTicks: 10,
difficulties: ['hard', 'very_hard', 'insane'],
},

// === Scouting-Reactive Rules ===
Expand Down Expand Up @@ -342,7 +341,7 @@ const DOMINION_MACRO_RULES: MacroRule[] = [
],
},
cooldownTicks: 10,
difficulties: ['hard', 'very_hard', 'insane'],
difficulties: ['medium', 'hard', 'very_hard', 'insane'],
},

// === Unit Production - Heavy Units (ALL DIFFICULTIES) ===
Expand Down Expand Up @@ -399,7 +398,7 @@ const DOMINION_MACRO_RULES: MacroRule[] = [
id: 'train_valkyrie',
name: 'Train Valkyrie',
description: 'Build anti-air fighter',
priority: 50,
priority: 54,
conditions: [
{ type: 'buildingCount', operator: '>=', value: 1, targetId: 'hangar' },
{ type: 'plasma', operator: '>=', value: 50 }, // Lowered
Expand All @@ -414,7 +413,7 @@ const DOMINION_MACRO_RULES: MacroRule[] = [
id: 'train_specter',
name: 'Train Specter',
description: 'Build cloaked air unit',
priority: 48,
priority: 52,
conditions: [
{ type: 'buildingCount', operator: '>=', value: 1, targetId: 'hangar' },
{ type: 'buildingCount', operator: '>=', value: 1, targetId: 'research_module' },
Expand All @@ -426,6 +425,22 @@ const DOMINION_MACRO_RULES: MacroRule[] = [
// NO difficulty restriction
},

// Build more air when enemy has air (air superiority)
{
id: 'train_valkyrie_air_superiority',
name: 'Air Superiority Valkyrie',
description: 'Build Valkyrie fighters when enemy has air units',
priority: 75,
conditions: [
{ type: 'enemyAirUnits', operator: '>', value: 0 },
{ type: 'buildingCount', operator: '>=', value: 1, targetId: 'hangar' },
{ type: 'plasma', operator: '>=', value: 50 },
{ type: 'minerals', operator: '>=', value: 100 },
],
action: { type: 'train', targetId: 'valkyrie' },
cooldownTicks: 30,
},

// === Unit Production - Vehicles (ALL DIFFICULTIES) ===
{
id: 'train_scorcher',
Expand Down Expand Up @@ -991,7 +1006,7 @@ export const DOMINION_AI_CONFIG: FactionAIConfig = {
scout: 'trooper',
antiAir: ['trooper', 'valkyrie', 'colossus', 'specter', 'breacher'],
siege: ['devastator', 'colossus', 'dreadnought'],
harass: ['scorcher', 'vanguard', 'valkyrie'],
harass: ['scorcher', 'vanguard', 'valkyrie', 'specter'],
baseTypes: ['headquarters', 'orbital_station', 'bastion'],
},

Expand Down
80 changes: 65 additions & 15 deletions src/engine/ai/FormationControl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,22 +268,25 @@ export class FormationControl {
});
}

// Air units hover above center
// Air units position in a wide spread targeting the enemy flank
// They approach from a perpendicular angle to the ground army
const airUnits = unitsByRole.get('air') || [];
const airSlots = this.calculateLinePositions(
group.center,
facing,
airUnits.length,
2 // Slightly behind
);

for (let i = 0; i < airUnits.length; i++) {
slots.push({
entityId: airUnits[i].entityId,
role: 'air',
targetPosition: airSlots[i],
priority: 2,
});
if (airUnits.length > 0) {
const airSlots = this.calculateAirPositions(
group.center,
facing,
enemyCenter,
airUnits.length
);

for (let i = 0; i < airUnits.length; i++) {
slots.push({
entityId: airUnits[i].entityId,
role: 'air',
targetPosition: airSlots[i],
priority: 4, // Lowest priority — air acts independently
});
}
}

group.slots = slots;
Expand Down Expand Up @@ -370,6 +373,53 @@ export class FormationControl {
return positions;
}

/**
* Calculate air unit positions for flanking attacks.
* Air units spread in a wide arc on the far side of the enemy,
* creating a pincer with the ground army.
*/
private calculateAirPositions(
_armyCenter: { x: number; y: number },
facing: { x: number; y: number },
enemyCenter: { x: number; y: number },
count: number
): Array<{ x: number; y: number }> {
if (count === 0) return [];

// Air units position past the enemy, flanking from perpendicular angle
const perpX = -facing.y;
const perpY = facing.x;

// Distance past the enemy center
const flankDepth = 8;
// Center of the air formation: offset to the side and slightly past enemy
const airCenter = {
x: enemyCenter.x + perpX * 10 + facing.x * flankDepth,
y: enemyCenter.y + perpY * 10 + facing.y * flankDepth,
};

const positions: Array<{ x: number; y: number }> = [];
const spacing = this.config.unitSpacing * 2; // Wider spread for air (anti-splash)

if (count === 1) {
return [airCenter];
}

// Spread along the perpendicular axis
const totalWidth = (count - 1) * spacing;
const startOffset = -totalWidth / 2;

for (let i = 0; i < count; i++) {
const offset = startOffset + i * spacing;
positions.push({
x: airCenter.x + perpX * offset,
y: airCenter.y + perpY * offset,
});
}

return positions;
}

/**
* Calculate spread formation (anti-splash)
*/
Expand Down
Loading
Loading