Adaptive Bosses
Adaptive bosses in AstraWeave learn from player behavior and adjust their strategies in real-time. Unlike scripted boss encounters, these AI-driven enemies create unique, memorable fights that evolve with each attempt.
Bosses use the same AI architecture as companions - perception, planning, and validated tools - but with combat-focused behaviors and learning capabilities.
Boss Architecture
graph TB
subgraph "Boss AI"
Perception[Player Analysis]
Memory[Pattern Memory]
Strategy[Strategy Selection]
Phase[Phase Management]
Actions[Action Execution]
end
Player[Player Behavior] --> Perception
Perception --> Memory
Memory --> Strategy
Strategy --> Phase
Phase --> Actions
Actions --> Player
Creating an Adaptive Boss
Basic Boss Setup
#![allow(unused)]
fn main() {
use astraweave_ai::prelude::*;
use astraweave_gameplay::boss::*;
fn spawn_adaptive_boss(world: &mut World) -> Entity {
world.spawn((
Name::new("The Hollow Knight"),
Transform::from_xyz(0.0, 2.0, 0.0),
BossAi::new()
.with_phases(3)
.with_adaptation_rate(0.3)
.with_pattern_memory(50),
BossHealth {
current: 10000.0,
max: 10000.0,
phase_thresholds: vec![0.7, 0.4, 0.15],
},
AvailableTools::new(vec![
Tool::melee_combo(),
Tool::ranged_attack(),
Tool::area_attack(),
Tool::summon_minions(),
Tool::teleport(),
Tool::enrage(),
]),
PlayerAnalyzer::default(),
StrategySelector::default(),
RigidBody::Dynamic,
Collider::capsule(1.0, 4.0),
NavAgent::default(),
))
}
}
Phase Configuration
#![allow(unused)]
fn main() {
#[derive(Component)]
struct BossPhaseConfig {
phases: Vec<BossPhase>,
current_phase: usize,
}
struct BossPhase {
name: String,
health_threshold: f32,
available_attacks: Vec<AttackPattern>,
behavior_modifiers: BehaviorModifiers,
transition_animation: Option<AnimationId>,
}
let phase_config = BossPhaseConfig {
phases: vec![
BossPhase {
name: "Cautious".into(),
health_threshold: 1.0,
available_attacks: vec![
AttackPattern::SingleSlash,
AttackPattern::Thrust,
AttackPattern::Sidestep,
],
behavior_modifiers: BehaviorModifiers {
aggression: 0.3,
defense: 0.7,
patience: 0.8,
},
transition_animation: None,
},
BossPhase {
name: "Aggressive".into(),
health_threshold: 0.6,
available_attacks: vec![
AttackPattern::TripleCombo,
AttackPattern::SpinAttack,
AttackPattern::LeapSlam,
AttackPattern::Thrust,
],
behavior_modifiers: BehaviorModifiers {
aggression: 0.7,
defense: 0.3,
patience: 0.4,
},
transition_animation: Some(AnimationId::Enrage),
},
BossPhase {
name: "Desperate".into(),
health_threshold: 0.25,
available_attacks: vec![
AttackPattern::FuryCombo,
AttackPattern::ShadowClones,
AttackPattern::AreaDenial,
AttackPattern::GrabAttack,
],
behavior_modifiers: BehaviorModifiers {
aggression: 1.0,
defense: 0.1,
patience: 0.1,
},
transition_animation: Some(AnimationId::Transform),
},
],
current_phase: 0,
};
}
Player Analysis
Bosses track player behavior to counter their strategies:
#![allow(unused)]
fn main() {
#[derive(Component, Default)]
struct PlayerAnalyzer {
dodge_pattern: DodgePattern,
attack_timing: AttackTiming,
positioning_preference: PositioningStyle,
heal_threshold: f32,
aggression_level: f32,
pattern_history: VecDeque<PlayerAction>,
}
#[derive(Default)]
struct DodgePattern {
left_count: u32,
right_count: u32,
back_count: u32,
roll_timing: Vec<f32>,
}
fn analyze_player_system(
mut analyzers: Query<&mut PlayerAnalyzer, With<BossAi>>,
player_actions: EventReader<PlayerActionEvent>,
) {
for action in player_actions.read() {
for mut analyzer in analyzers.iter_mut() {
analyzer.pattern_history.push_back(action.clone());
if analyzer.pattern_history.len() > 100 {
analyzer.pattern_history.pop_front();
}
match action {
PlayerActionEvent::Dodge(direction) => {
match direction {
Direction::Left => analyzer.dodge_pattern.left_count += 1,
Direction::Right => analyzer.dodge_pattern.right_count += 1,
Direction::Back => analyzer.dodge_pattern.back_count += 1,
_ => {}
}
}
PlayerActionEvent::Attack(timing) => {
analyzer.attack_timing.record(*timing);
}
PlayerActionEvent::Heal => {
analyzer.heal_threshold = calculate_heal_threshold(&analyzer);
}
_ => {}
}
}
}
}
}
Strategy Adaptation
#![allow(unused)]
fn main() {
#[derive(Component)]
struct StrategySelector {
current_strategy: BossStrategy,
strategy_effectiveness: HashMap<BossStrategy, f32>,
adaptation_cooldown: Timer,
}
#[derive(Clone, Copy, Hash, Eq, PartialEq)]
enum BossStrategy {
Aggressive,
Defensive,
Counter,
Pressure,
Bait,
Mixed,
}
fn adapt_strategy_system(
mut bosses: Query<(&PlayerAnalyzer, &mut StrategySelector, &BossHealth)>,
time: Res<Time>,
) {
for (analyzer, mut selector, health) in bosses.iter_mut() {
selector.adaptation_cooldown.tick(time.delta());
if selector.adaptation_cooldown.finished() {
let new_strategy = select_counter_strategy(analyzer);
if new_strategy != selector.current_strategy {
let old_effectiveness = selector.strategy_effectiveness
.get(&selector.current_strategy)
.copied()
.unwrap_or(0.5);
selector.strategy_effectiveness
.insert(selector.current_strategy, old_effectiveness * 0.9);
selector.current_strategy = new_strategy;
selector.adaptation_cooldown.reset();
}
}
}
}
fn select_counter_strategy(analyzer: &PlayerAnalyzer) -> BossStrategy {
if analyzer.aggression_level > 0.7 {
BossStrategy::Counter
} else if analyzer.dodge_pattern.is_predictable() {
BossStrategy::Bait
} else if analyzer.heal_threshold > 0.5 {
BossStrategy::Pressure
} else {
BossStrategy::Mixed
}
}
}
Attack Patterns
#![allow(unused)]
fn main() {
fn boss_attack_system(
mut bosses: Query<(
&BossAi,
&StrategySelector,
&PlayerAnalyzer,
&Transform,
&mut ActionQueue,
)>,
player: Query<&Transform, With<Player>>,
) {
let player_pos = player.single().translation;
for (boss, strategy, analyzer, transform, mut actions) in bosses.iter_mut() {
let distance = transform.translation.distance(player_pos);
let attack = match strategy.current_strategy {
BossStrategy::Aggressive => {
select_aggressive_attack(distance, boss.current_phase)
}
BossStrategy::Counter => {
if analyzer.is_player_attacking() {
Some(AttackPattern::Parry)
} else {
Some(AttackPattern::Wait)
}
}
BossStrategy::Bait => {
let predicted_dodge = analyzer.predict_dodge_direction();
Some(AttackPattern::DelayedStrike(predicted_dodge.opposite()))
}
BossStrategy::Pressure => {
Some(AttackPattern::RelentlessCombo)
}
_ => {
select_random_attack(boss.current_phase)
}
};
if let Some(pattern) = attack {
actions.push(Action::ExecutePattern(pattern));
}
}
}
}
Learning Between Attempts
Bosses can remember strategies across player deaths:
#![allow(unused)]
fn main() {
#[derive(Resource)]
struct BossMemory {
player_deaths: u32,
successful_attacks: HashMap<AttackPattern, u32>,
failed_attacks: HashMap<AttackPattern, u32>,
player_weaknesses: Vec<Weakness>,
}
impl BossMemory {
fn record_attack_result(&mut self, pattern: AttackPattern, hit: bool) {
if hit {
*self.successful_attacks.entry(pattern).or_default() += 1;
} else {
*self.failed_attacks.entry(pattern).or_default() += 1;
}
}
fn get_attack_priority(&self, pattern: AttackPattern) -> f32 {
let successes = self.successful_attacks.get(&pattern).copied().unwrap_or(0) as f32;
let failures = self.failed_attacks.get(&pattern).copied().unwrap_or(0) as f32;
if successes + failures < 3.0 {
return 0.5;
}
successes / (successes + failures)
}
}
}
Example: The Hollow Knight
A complete adaptive boss implementation:
#![allow(unused)]
fn main() {
pub fn spawn_hollow_knight(world: &mut World) -> Entity {
let boss = world.spawn((
Name::new("The Hollow Knight"),
Transform::from_xyz(0.0, 2.0, -20.0),
BossAi::new()
.with_phases(4)
.with_adaptation_rate(0.25),
BossHealth::new(15000.0)
.with_phases(vec![0.75, 0.5, 0.25]),
PlayerAnalyzer::default(),
StrategySelector::new(BossStrategy::Defensive),
AttackPatterns::new(vec![
("slash", AttackPattern::Slash { damage: 80.0, range: 3.0 }),
("thrust", AttackPattern::Thrust { damage: 100.0, range: 5.0 }),
("spin", AttackPattern::Spin { damage: 60.0, radius: 4.0 }),
("leap", AttackPattern::LeapSlam { damage: 120.0, radius: 6.0 }),
("shadow", AttackPattern::ShadowDash { damage: 50.0, distance: 10.0 }),
]),
RigidBody::Dynamic,
Collider::capsule(1.2, 3.5),
NavAgent::default(),
));
world.send_event(BossSpawnedEvent { boss });
boss
}
}
Best Practices
- Start with simpler patterns, add complexity in later phases
- Ensure all attacks are telegraphed and fair
- Test adaptation rates - too fast feels unfair, too slow feels scripted
- Provide recovery windows between attack chains
- Adaptation should challenge, not frustrate
- Preserve attack patterns that players can learn
- Don't punish the same mistake indefinitely
- Allow multiple valid strategies
See Also
- AI System - Core AI architecture
- AI Companions - Friendly AI patterns
- Combat System - Combat mechanics