sigil_parser/plurality/
combat.rs

1//! # Combat System for DAEMONIORUM
2//!
3//! Combat mechanics that integrate with plurality, alter switching,
4//! and reality perception systems.
5
6use std::collections::HashMap;
7
8use super::runtime::{
9    Alter, AlterPresenceState, AnimaState, FrontingState, PluralSystem, RealityLayer, SwitchResult,
10    Trigger, TriggerCategory, TriggerResult,
11};
12
13// ============================================================================
14// COMBAT STATE
15// ============================================================================
16
17/// The current combat encounter state
18#[derive(Debug, Clone)]
19pub struct CombatState {
20    /// Active combatants
21    pub combatants: Vec<Combatant>,
22    /// Current turn order
23    pub turn_order: Vec<usize>,
24    /// Current active combatant index
25    pub current_turn: usize,
26    /// Combat round number
27    pub round: u32,
28    /// Environmental factors
29    pub environment: CombatEnvironment,
30    /// Active combat effects
31    pub effects: Vec<CombatEffect>,
32    /// Combat phase
33    pub phase: CombatPhase,
34    /// Is combat over?
35    pub is_over: bool,
36    /// Victory/defeat result (if over)
37    pub result: Option<CombatResult>,
38}
39
40impl CombatState {
41    /// Create a new combat encounter
42    pub fn new(player_system: &PluralSystem, enemies: Vec<Enemy>) -> Self {
43        let mut combatants = vec![Combatant::Player(PlayerCombatant::from_system(
44            player_system,
45        ))];
46        combatants.extend(enemies.into_iter().map(Combatant::Enemy));
47
48        let mut state = Self {
49            combatants,
50            turn_order: Vec::new(),
51            current_turn: 0,
52            round: 1,
53            environment: CombatEnvironment::default(),
54            effects: Vec::new(),
55            phase: CombatPhase::PreCombat,
56            is_over: false,
57            result: None,
58        };
59
60        state.calculate_turn_order();
61        state
62    }
63
64    /// Calculate turn order based on initiative
65    fn calculate_turn_order(&mut self) {
66        let mut order: Vec<(usize, f32)> = self
67            .combatants
68            .iter()
69            .enumerate()
70            .map(|(i, c)| (i, c.initiative()))
71            .collect();
72
73        // Sort by initiative (highest first)
74        order.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
75
76        self.turn_order = order.into_iter().map(|(i, _)| i).collect();
77    }
78
79    /// Get the current active combatant
80    pub fn current_combatant(&self) -> Option<&Combatant> {
81        self.turn_order
82            .get(self.current_turn)
83            .and_then(|&idx| self.combatants.get(idx))
84    }
85
86    /// Get the current active combatant mutably
87    pub fn current_combatant_mut(&mut self) -> Option<&mut Combatant> {
88        let idx = *self.turn_order.get(self.current_turn)?;
89        self.combatants.get_mut(idx)
90    }
91
92    /// Advance to the next turn
93    pub fn next_turn(&mut self) {
94        self.current_turn += 1;
95        if self.current_turn >= self.turn_order.len() {
96            self.current_turn = 0;
97            self.round += 1;
98            self.phase = CombatPhase::Combat;
99        }
100    }
101
102    /// Check if combat should end
103    pub fn check_combat_end(&mut self) {
104        let player_alive = self
105            .combatants
106            .iter()
107            .any(|c| matches!(c, Combatant::Player(p) if p.health > 0.0));
108
109        let enemies_alive = self
110            .combatants
111            .iter()
112            .any(|c| matches!(c, Combatant::Enemy(e) if e.health > 0.0));
113
114        if !player_alive {
115            self.is_over = true;
116            self.result = Some(CombatResult::Defeat);
117        } else if !enemies_alive {
118            self.is_over = true;
119            self.result = Some(CombatResult::Victory);
120        }
121    }
122}
123
124/// Combat phases
125#[derive(Debug, Clone, PartialEq)]
126pub enum CombatPhase {
127    /// Before combat starts (preparation)
128    PreCombat,
129    /// Active combat
130    Combat,
131    /// Special event during combat
132    Event,
133    /// Combat is ending
134    PostCombat,
135}
136
137/// Combat result
138#[derive(Debug, Clone, PartialEq)]
139pub enum CombatResult {
140    Victory,
141    Defeat,
142    Flee,
143    Negotiated,
144}
145
146// ============================================================================
147// COMBATANTS
148// ============================================================================
149
150/// A participant in combat
151#[derive(Debug, Clone)]
152pub enum Combatant {
153    Player(PlayerCombatant),
154    Enemy(Enemy),
155}
156
157impl Combatant {
158    /// Get the combatant's initiative value
159    pub fn initiative(&self) -> f32 {
160        match self {
161            Combatant::Player(p) => p.initiative(),
162            Combatant::Enemy(e) => e.initiative,
163        }
164    }
165
166    /// Get the combatant's name
167    pub fn name(&self) -> &str {
168        match self {
169            Combatant::Player(p) => &p.fronting_alter,
170            Combatant::Enemy(e) => &e.name,
171        }
172    }
173}
174
175/// Player combatant (the plural system)
176#[derive(Debug, Clone)]
177pub struct PlayerCombatant {
178    /// Currently fronting alter name
179    pub fronting_alter: String,
180    /// Current health (0.0 to max_health)
181    pub health: f32,
182    /// Maximum health
183    pub max_health: f32,
184    /// Current psyche (mental/emotional resource)
185    pub psyche: f32,
186    /// Maximum psyche
187    pub max_psyche: f32,
188    /// Current anima state
189    pub anima: AnimaState,
190    /// Current reality perception
191    pub reality: RealityLayer,
192    /// Available abilities for current fronter
193    pub abilities: Vec<CombatAbility>,
194    /// Status effects
195    pub status_effects: Vec<StatusEffect>,
196    /// Alter switching cooldown (turns remaining)
197    pub switch_cooldown: u32,
198    /// Available alters that can switch in
199    pub available_alters: Vec<String>,
200    /// Combat-specific triggers active
201    pub active_triggers: Vec<String>,
202}
203
204impl PlayerCombatant {
205    /// Create from a plural system
206    pub fn from_system(system: &PluralSystem) -> Self {
207        let fronting = match &system.fronting {
208            FrontingState::Single(id) => id.clone(),
209            FrontingState::Blended(ids) => ids.first().cloned().unwrap_or_default(),
210            _ => "Unknown".to_string(),
211        };
212
213        let fronter = system.alters.get(&fronting);
214        let abilities = fronter
215            .map(|a| generate_combat_abilities(a))
216            .unwrap_or_default();
217
218        let available: Vec<String> = system
219            .alters
220            .values()
221            .filter(|a| {
222                !matches!(
223                    a.state,
224                    AlterPresenceState::Dormant | AlterPresenceState::Dissociating
225                )
226            })
227            .map(|a| a.id.clone())
228            .collect();
229
230        Self {
231            fronting_alter: fronting,
232            health: 100.0,
233            max_health: 100.0,
234            psyche: 50.0,
235            max_psyche: 50.0,
236            anima: system.anima.clone(),
237            reality: system.reality_layer.clone(),
238            abilities,
239            status_effects: Vec::new(),
240            switch_cooldown: 0,
241            available_alters: available,
242            active_triggers: Vec::new(),
243        }
244    }
245
246    /// Calculate initiative based on current state
247    pub fn initiative(&self) -> f32 {
248        let base = 10.0;
249        let arousal_bonus = self.anima.arousal * 5.0; // Higher arousal = faster
250        let dominance_bonus = self.anima.dominance * 2.0;
251        base + arousal_bonus + dominance_bonus
252    }
253
254    /// Execute an ability
255    pub fn execute_ability(
256        &mut self,
257        ability_index: usize,
258        target: &mut Combatant,
259    ) -> AbilityResult {
260        if ability_index >= self.abilities.len() {
261            return AbilityResult::Failed("Invalid ability".to_string());
262        }
263
264        let ability = &self.abilities[ability_index].clone();
265
266        // Check psyche cost
267        if self.psyche < ability.psyche_cost {
268            return AbilityResult::Failed("Not enough psyche".to_string());
269        }
270
271        // Check reality requirements
272        if let Some(ref required) = ability.reality_requirement {
273            if &self.reality != required {
274                return AbilityResult::Failed("Wrong reality layer".to_string());
275            }
276        }
277
278        // Pay cost
279        self.psyche -= ability.psyche_cost;
280
281        // Calculate damage/effect
282        let mut damage = ability.base_damage;
283
284        // Apply anima modifiers
285        match ability.damage_type {
286            DamageType::Physical => {
287                damage *= 1.0 + self.anima.dominance * 0.3;
288            }
289            DamageType::Psychic => {
290                damage *= 1.0 + self.anima.arousal * 0.3;
291            }
292            DamageType::Emotional => {
293                damage *= 1.0 + (1.0 - self.anima.stability) * 0.5;
294            }
295            DamageType::Reality => {
296                if matches!(
297                    self.reality,
298                    RealityLayer::Fractured | RealityLayer::Shattered
299                ) {
300                    damage *= 1.5;
301                }
302            }
303        }
304
305        // Apply to target
306        match target {
307            Combatant::Enemy(e) => {
308                e.health = (e.health - damage).max(0.0);
309            }
310            Combatant::Player(p) => {
311                p.health = (p.health - damage).max(0.0);
312            }
313        }
314
315        // Apply status effects
316        for effect in &ability.applies_effects {
317            match target {
318                Combatant::Enemy(e) => e.status_effects.push(effect.clone()),
319                Combatant::Player(p) => p.status_effects.push(effect.clone()),
320            }
321        }
322
323        AbilityResult::Success {
324            damage,
325            effects: ability.applies_effects.clone(),
326        }
327    }
328
329    /// Attempt to switch alters mid-combat
330    pub fn combat_switch(
331        &mut self,
332        target_alter: &str,
333        system: &mut PluralSystem,
334    ) -> CombatSwitchResult {
335        if self.switch_cooldown > 0 {
336            return CombatSwitchResult::OnCooldown(self.switch_cooldown);
337        }
338
339        if !self.available_alters.contains(&target_alter.to_string()) {
340            return CombatSwitchResult::NotAvailable;
341        }
342
343        // Attempt the switch
344        let urgency = 0.5 + self.anima.arousal * 0.3; // Higher arousal = easier combat switches
345        let result = system.request_switch(target_alter, urgency, false);
346
347        match result {
348            SwitchResult::Success => {
349                self.fronting_alter = target_alter.to_string();
350                self.switch_cooldown = 2; // 2 turn cooldown
351
352                // Update abilities for new fronter
353                if let Some(alter) = system.alters.get(target_alter) {
354                    self.abilities = generate_combat_abilities(alter);
355                    self.anima = alter.anima.clone();
356                }
357
358                // Reality might shift based on alter preference
359                if let Some(alter) = system.alters.get(target_alter) {
360                    self.reality = alter.preferred_reality.clone();
361                }
362
363                CombatSwitchResult::Success
364            }
365            SwitchResult::Resisted { resistance } => CombatSwitchResult::Resisted(resistance),
366            SwitchResult::Failed(reason) => CombatSwitchResult::Failed(format!("{:?}", reason)),
367            SwitchResult::InProgress { eta } => CombatSwitchResult::InProgress(eta),
368        }
369    }
370
371    /// Process a combat trigger
372    pub fn process_combat_trigger(
373        &mut self,
374        trigger: Trigger,
375        system: &mut PluralSystem,
376    ) -> CombatTriggerResult {
377        let trigger_result = system.process_trigger(trigger.clone());
378
379        match trigger_result {
380            TriggerResult::ForcedSwitch(alter_id) => {
381                // Forced switch ignores cooldown
382                self.switch_cooldown = 0;
383                let switch_result = self.combat_switch(&alter_id, system);
384                CombatTriggerResult::ForcedSwitch(alter_id, switch_result)
385            }
386            TriggerResult::Activation(alters) => {
387                // Activation makes alters more available
388                for alter_id in &alters {
389                    if !self.available_alters.contains(alter_id) {
390                        self.available_alters.push(alter_id.clone());
391                    }
392                }
393                CombatTriggerResult::AltersActivated(alters)
394            }
395            TriggerResult::Dissociation => {
396                self.anima.apply_trauma_response(0.5);
397                CombatTriggerResult::Dissociation
398            }
399            TriggerResult::NoResponse => CombatTriggerResult::NoEffect,
400        }
401    }
402}
403
404/// Result of a combat switch attempt
405#[derive(Debug, Clone)]
406pub enum CombatSwitchResult {
407    Success,
408    OnCooldown(u32),
409    NotAvailable,
410    Resisted(f32),
411    Failed(String),
412    InProgress(u64),
413}
414
415/// Result of a combat trigger
416#[derive(Debug, Clone)]
417pub enum CombatTriggerResult {
418    NoEffect,
419    ForcedSwitch(String, CombatSwitchResult),
420    AltersActivated(Vec<String>),
421    Dissociation,
422}
423
424// ============================================================================
425// ENEMIES
426// ============================================================================
427
428/// An enemy combatant
429#[derive(Debug, Clone)]
430pub struct Enemy {
431    /// Enemy ID
432    pub id: String,
433    /// Display name
434    pub name: String,
435    /// Enemy type
436    pub enemy_type: EnemyType,
437    /// Current health
438    pub health: f32,
439    /// Maximum health
440    pub max_health: f32,
441    /// Initiative value
442    pub initiative: f32,
443    /// Available abilities
444    pub abilities: Vec<CombatAbility>,
445    /// Status effects
446    pub status_effects: Vec<StatusEffect>,
447    /// Trigger behaviors (what triggers this enemy causes)
448    pub trigger_behaviors: Vec<TriggerBehavior>,
449    /// Reality perception (affects which layer player sees them in)
450    pub reality_visibility: Vec<RealityLayer>,
451}
452
453/// Types of enemies
454#[derive(Debug, Clone, PartialEq)]
455pub enum EnemyType {
456    /// Normal human threat
457    Human,
458    /// Symbolic/metaphorical threat (trauma manifestation)
459    Manifestation,
460    /// Environmental hazard
461    Hazard,
462    /// Internal threat (persecutor alter, intrusive thought)
463    Internal,
464    /// Supernatural entity
465    Entity,
466}
467
468/// Behavior that creates triggers
469#[derive(Debug, Clone)]
470pub struct TriggerBehavior {
471    /// Trigger ID this enemy can cause
472    pub trigger_id: String,
473    /// Condition for triggering
474    pub condition: TriggerCondition,
475    /// Intensity when triggered
476    pub intensity: f32,
477}
478
479/// Conditions for enemy triggers
480#[derive(Debug, Clone)]
481pub enum TriggerCondition {
482    /// Always trigger at start of enemy turn
483    OnTurn,
484    /// Trigger when enemy attacks
485    OnAttack,
486    /// Trigger when enemy health drops below threshold
487    HealthBelow(f32),
488    /// Trigger when player is at low health
489    PlayerLowHealth,
490    /// Trigger when specific ability is used
491    OnAbility(String),
492    /// Random chance per turn
493    Random(f32),
494}
495
496// ============================================================================
497// ABILITIES
498// ============================================================================
499
500/// A combat ability
501#[derive(Debug, Clone)]
502pub struct CombatAbility {
503    /// Ability ID
504    pub id: String,
505    /// Display name
506    pub name: String,
507    /// Description
508    pub description: String,
509    /// Base damage/healing
510    pub base_damage: f32,
511    /// Damage type
512    pub damage_type: DamageType,
513    /// Psyche cost
514    pub psyche_cost: f32,
515    /// Cooldown in turns
516    pub cooldown: u32,
517    /// Current cooldown remaining
518    pub current_cooldown: u32,
519    /// Required reality layer (if any)
520    pub reality_requirement: Option<RealityLayer>,
521    /// Required alter category (if any)
522    pub alter_requirement: Option<String>,
523    /// Status effects applied
524    pub applies_effects: Vec<StatusEffect>,
525    /// Is this a defensive ability?
526    pub is_defensive: bool,
527}
528
529/// Types of damage
530#[derive(Debug, Clone, PartialEq)]
531pub enum DamageType {
532    Physical,
533    Psychic,
534    Emotional,
535    Reality, // Affects reality perception
536}
537
538/// Result of using an ability
539#[derive(Debug, Clone)]
540pub enum AbilityResult {
541    Success {
542        damage: f32,
543        effects: Vec<StatusEffect>,
544    },
545    Failed(String),
546    Blocked,
547}
548
549// ============================================================================
550// STATUS EFFECTS
551// ============================================================================
552
553/// A status effect that modifies combat
554#[derive(Debug, Clone)]
555pub struct StatusEffect {
556    /// Effect ID
557    pub id: String,
558    /// Display name
559    pub name: String,
560    /// Duration in turns (None = permanent)
561    pub duration: Option<u32>,
562    /// Effect type
563    pub effect_type: StatusEffectType,
564    /// Intensity/magnitude
565    pub intensity: f32,
566}
567
568/// Types of status effects
569#[derive(Debug, Clone)]
570pub enum StatusEffectType {
571    /// Damage over time
572    Bleed,
573    /// Psyche drain
574    PsycheDrain,
575    /// Cannot switch alters
576    SwitchLocked,
577    /// Reality perception forced
578    RealityLocked(RealityLayer),
579    /// Damage reduction
580    Shielded,
581    /// Increased damage output
582    Empowered,
583    /// Reduced accuracy/effectiveness
584    Disoriented,
585    /// Cannot use abilities
586    Silenced,
587    /// Specific trigger is active
588    Triggered(String),
589    /// Healing over time
590    Regenerating,
591}
592
593// ============================================================================
594// COMBAT ENVIRONMENT
595// ============================================================================
596
597/// Environmental factors in combat
598#[derive(Debug, Clone, Default)]
599pub struct CombatEnvironment {
600    /// Current reality layer visibility
601    pub reality_layers: Vec<RealityLayer>,
602    /// Environmental hazards
603    pub hazards: Vec<EnvironmentHazard>,
604    /// Cover/defensive positions available
605    pub defensive_positions: u32,
606    /// Light level (affects perception)
607    pub light_level: f32,
608    /// Ambient trigger intensity
609    pub ambient_trigger_intensity: f32,
610}
611
612/// Environmental hazards
613#[derive(Debug, Clone)]
614pub struct EnvironmentHazard {
615    pub name: String,
616    pub damage_per_turn: f32,
617    pub affects_reality: Option<RealityLayer>,
618}
619
620// ============================================================================
621// COMBAT EFFECTS
622// ============================================================================
623
624/// Active effects in combat
625#[derive(Debug, Clone)]
626pub struct CombatEffect {
627    pub name: String,
628    pub duration: u32,
629    pub effect: CombatEffectType,
630}
631
632#[derive(Debug, Clone)]
633pub enum CombatEffectType {
634    /// Reality is shifting
635    RealityFlux,
636    /// Forced switching is occurring
637    SystemInstability,
638    /// Environmental change
639    EnvironmentChange(String),
640}
641
642// ============================================================================
643// HELPER FUNCTIONS
644// ============================================================================
645
646/// Generate combat abilities for an alter based on their traits
647fn generate_combat_abilities(alter: &Alter) -> Vec<CombatAbility> {
648    let mut abilities = Vec::new();
649
650    // Basic attack available to all
651    abilities.push(CombatAbility {
652        id: "basic_attack".to_string(),
653        name: "Strike".to_string(),
654        description: "A basic physical attack".to_string(),
655        base_damage: 10.0,
656        damage_type: DamageType::Physical,
657        psyche_cost: 0.0,
658        cooldown: 0,
659        current_cooldown: 0,
660        reality_requirement: None,
661        alter_requirement: None,
662        applies_effects: Vec::new(),
663        is_defensive: false,
664    });
665
666    // Add abilities based on alter's abilities set
667    for ability_name in &alter.abilities {
668        match ability_name.as_str() {
669            "combat" | "combat_master" => {
670                abilities.push(CombatAbility {
671                    id: "power_strike".to_string(),
672                    name: "Power Strike".to_string(),
673                    description: "A devastating physical attack".to_string(),
674                    base_damage: 25.0,
675                    damage_type: DamageType::Physical,
676                    psyche_cost: 10.0,
677                    cooldown: 2,
678                    current_cooldown: 0,
679                    reality_requirement: None,
680                    alter_requirement: None,
681                    applies_effects: Vec::new(),
682                    is_defensive: false,
683                });
684            }
685            "perception" | "perceive" => {
686                abilities.push(CombatAbility {
687                    id: "reality_sight".to_string(),
688                    name: "Reality Sight".to_string(),
689                    description: "Pierce the veil between layers".to_string(),
690                    base_damage: 0.0,
691                    damage_type: DamageType::Reality,
692                    psyche_cost: 15.0,
693                    cooldown: 3,
694                    current_cooldown: 0,
695                    reality_requirement: None,
696                    alter_requirement: None,
697                    applies_effects: Vec::new(),
698                    is_defensive: true,
699                });
700            }
701            "protection" | "shield" => {
702                abilities.push(CombatAbility {
703                    id: "protective_barrier".to_string(),
704                    name: "Protective Barrier".to_string(),
705                    description: "Shield the system from harm".to_string(),
706                    base_damage: 0.0,
707                    damage_type: DamageType::Psychic,
708                    psyche_cost: 20.0,
709                    cooldown: 3,
710                    current_cooldown: 0,
711                    reality_requirement: None,
712                    alter_requirement: None,
713                    applies_effects: vec![StatusEffect {
714                        id: "shielded".to_string(),
715                        name: "Shielded".to_string(),
716                        duration: Some(2),
717                        effect_type: StatusEffectType::Shielded,
718                        intensity: 0.5,
719                    }],
720                    is_defensive: true,
721                });
722            }
723            "trauma_processing" => {
724                abilities.push(CombatAbility {
725                    id: "trauma_strike".to_string(),
726                    name: "Trauma Strike".to_string(),
727                    description: "Channel trauma into devastating force".to_string(),
728                    base_damage: 35.0,
729                    damage_type: DamageType::Emotional,
730                    psyche_cost: 25.0,
731                    cooldown: 4,
732                    current_cooldown: 0,
733                    reality_requirement: Some(RealityLayer::Fractured),
734                    alter_requirement: None,
735                    applies_effects: Vec::new(),
736                    is_defensive: false,
737                });
738            }
739            _ => {}
740        }
741    }
742
743    // Add reality-specific ability if alter prefers fractured
744    if matches!(
745        alter.preferred_reality,
746        RealityLayer::Fractured | RealityLayer::Shattered
747    ) {
748        abilities.push(CombatAbility {
749            id: "fractured_assault".to_string(),
750            name: "Fractured Assault".to_string(),
751            description: "Attack from the fractured realm".to_string(),
752            base_damage: 20.0,
753            damage_type: DamageType::Reality,
754            psyche_cost: 15.0,
755            cooldown: 2,
756            current_cooldown: 0,
757            reality_requirement: Some(RealityLayer::Fractured),
758            alter_requirement: None,
759            applies_effects: Vec::new(),
760            is_defensive: false,
761        });
762    }
763
764    abilities
765}
766
767// ============================================================================
768// TESTS
769// ============================================================================
770
771#[cfg(test)]
772mod tests {
773    use super::*;
774    use std::collections::HashSet;
775
776    fn create_test_system() -> PluralSystem {
777        let mut system = PluralSystem::new("Test System");
778
779        let alter = Alter {
780            id: "protector".to_string(),
781            name: "Protector".to_string(),
782            category: super::super::runtime::AlterCategory::Council,
783            state: AlterPresenceState::Fronting,
784            anima: AnimaState::new(0.0, 0.5, 0.7),
785            base_arousal: 0.5,
786            base_dominance: 0.7,
787            time_since_front: 0,
788            triggers: vec!["threat".to_string()],
789            abilities: HashSet::from(["combat".to_string(), "protection".to_string()]),
790            preferred_reality: RealityLayer::Grounded,
791            memory_access: super::super::runtime::MemoryAccess::Full,
792        };
793
794        system.add_alter(alter);
795        system.fronting = FrontingState::Single("protector".to_string());
796        system
797    }
798
799    #[test]
800    fn test_combat_state_creation() {
801        let system = create_test_system();
802        let enemy = Enemy {
803            id: "enemy1".to_string(),
804            name: "Shadow".to_string(),
805            enemy_type: EnemyType::Manifestation,
806            health: 50.0,
807            max_health: 50.0,
808            initiative: 8.0,
809            abilities: Vec::new(),
810            status_effects: Vec::new(),
811            trigger_behaviors: Vec::new(),
812            reality_visibility: vec![RealityLayer::Fractured],
813        };
814
815        let combat = CombatState::new(&system, vec![enemy]);
816        assert_eq!(combat.combatants.len(), 2);
817        assert_eq!(combat.round, 1);
818        assert!(!combat.is_over);
819    }
820
821    #[test]
822    fn test_ability_execution() {
823        let system = create_test_system();
824        let mut player = PlayerCombatant::from_system(&system);
825        let mut enemy = Combatant::Enemy(Enemy {
826            id: "enemy1".to_string(),
827            name: "Target".to_string(),
828            enemy_type: EnemyType::Human,
829            health: 100.0,
830            max_health: 100.0,
831            initiative: 5.0,
832            abilities: Vec::new(),
833            status_effects: Vec::new(),
834            trigger_behaviors: Vec::new(),
835            reality_visibility: vec![RealityLayer::Grounded],
836        });
837
838        // Basic attack should work
839        let result = player.execute_ability(0, &mut enemy);
840        assert!(matches!(result, AbilityResult::Success { .. }));
841
842        // Enemy should have taken damage
843        if let Combatant::Enemy(e) = &enemy {
844            assert!(e.health < 100.0);
845        }
846    }
847}