Skip to main content

proof_engine/game/
bosses.rs

1//! Boss Encounter Orchestration System
2//!
3//! Ten unique boss encounters for the Chaos RPG, each with multi-phase mechanics,
4//! special abilities, and emergent behaviors. Bosses are driven by a phase controller
5//! that monitors HP thresholds and triggers transitions with visual animations.
6//!
7//! # Bosses
8//! - **Mirror**: copies player abilities with delay
9//! - **Null**: progressively erases game elements
10//! - **Committee**: five judges vote on actions
11//! - **FibonacciHydra**: splits recursively on death
12//! - **Eigenstate**: quantum superposition of forms
13//! - **Ouroboros**: reversed healing/damage semantics
14//! - **AlgorithmReborn**: learns and predicts player patterns (final boss)
15//! - **ChaosWeaver**: manipulates game rules
16//! - **VoidSerpent**: consumes the arena
17//! - **PrimeFactorial**: arithmetic puzzle boss
18
19use std::collections::HashMap;
20use crate::combat::{Element, CombatStats, ResistanceProfile};
21use crate::entity::AmorphousEntity;
22
23// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
24// Boss Type Registry
25// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
26
27/// All boss types in the Chaos RPG.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29pub enum BossType {
30    Mirror,
31    Null,
32    Committee,
33    FibonacciHydra,
34    Eigenstate,
35    Ouroboros,
36    AlgorithmReborn,
37    ChaosWeaver,
38    VoidSerpent,
39    PrimeFactorial,
40}
41
42impl BossType {
43    /// All boss types in order.
44    pub fn all() -> &'static [BossType] {
45        &[
46            BossType::Mirror,
47            BossType::Null,
48            BossType::Committee,
49            BossType::FibonacciHydra,
50            BossType::Eigenstate,
51            BossType::Ouroboros,
52            BossType::AlgorithmReborn,
53            BossType::ChaosWeaver,
54            BossType::VoidSerpent,
55            BossType::PrimeFactorial,
56        ]
57    }
58
59    /// Display name.
60    pub fn name(self) -> &'static str {
61        match self {
62            BossType::Mirror => "The Mirror",
63            BossType::Null => "The Null",
64            BossType::Committee => "The Committee",
65            BossType::FibonacciHydra => "Fibonacci Hydra",
66            BossType::Eigenstate => "The Eigenstate",
67            BossType::Ouroboros => "Ouroboros",
68            BossType::AlgorithmReborn => "Algorithm Reborn",
69            BossType::ChaosWeaver => "Chaos Weaver",
70            BossType::VoidSerpent => "Void Serpent",
71            BossType::PrimeFactorial => "Prime Factorial",
72        }
73    }
74
75    /// Boss subtitle/title.
76    pub fn title(self) -> &'static str {
77        match self {
78            BossType::Mirror => "Reflection of Self",
79            BossType::Null => "The Eraser of Meaning",
80            BossType::Committee => "Democracy of Violence",
81            BossType::FibonacciHydra => "The Golden Recursion",
82            BossType::Eigenstate => "Collapsed Possibility",
83            BossType::Ouroboros => "The Serpent That Devours",
84            BossType::AlgorithmReborn => "Final Proof",
85            BossType::ChaosWeaver => "Unraveler of Rules",
86            BossType::VoidSerpent => "Consumer of Arenas",
87            BossType::PrimeFactorial => "The Indivisible Explosion",
88        }
89    }
90
91    /// Boss tier (1 = earliest, 5 = final boss).
92    pub fn tier(self) -> u32 {
93        match self {
94            BossType::Mirror => 1,
95            BossType::Null => 2,
96            BossType::Committee => 2,
97            BossType::FibonacciHydra => 3,
98            BossType::Eigenstate => 3,
99            BossType::Ouroboros => 3,
100            BossType::ChaosWeaver => 4,
101            BossType::VoidSerpent => 4,
102            BossType::PrimeFactorial => 4,
103            BossType::AlgorithmReborn => 5,
104        }
105    }
106}
107
108// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
109// Music & Arena
110// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
111
112/// Music style for a boss encounter.
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub enum MusicType {
115    Ominous,
116    Frenetic,
117    Orchestral,
118    Glitch,
119    Silence,
120    Reversed,
121    Algorithmic,
122    Chaotic,
123    Crescendo,
124    MinimalDrone,
125}
126
127/// Modifications applied to the arena during the boss fight.
128#[derive(Debug, Clone, PartialEq)]
129pub enum ArenaMod {
130    /// Shrink arena by removing edge tiles.
131    ShrinkEdges { rate_per_turn: u32 },
132    /// Add hazard tiles of the given element.
133    HazardTiles { element: Element, count: u32 },
134    /// Darken vision range.
135    DarkenVision { radius_reduction: f32 },
136    /// Invert movement controls.
137    InvertControls,
138    /// Tiles become slippery (momentum).
139    SlipperyFloor { friction: f32 },
140    /// Random teleport traps.
141    TeleportTraps { count: u32 },
142    /// No arena modifications.
143    None,
144}
145
146// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
147// Boss Phase System
148// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
149
150/// How a phase transition is visually animated.
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub enum PhaseTransition {
153    /// Glyphs rearrange into new formation.
154    GlyphReorganize,
155    /// Entity dissolves and reforms.
156    Dissolve,
157    /// Entity splits into multiple parts.
158    Split,
159    /// Multiple parts merge into one.
160    Merge,
161    /// Entity teleports to new position.
162    Teleport,
163    /// Entity charges up with visual energy.
164    PowerUp,
165}
166
167/// A special ability usable during a boss phase.
168#[derive(Debug, Clone, PartialEq)]
169pub enum SpecialAbility {
170    /// Copy the player's last N abilities.
171    MirrorCopy { depth: usize },
172    /// Erase a specific game element.
173    Erase(EraseTarget),
174    /// Summon helper entities.
175    Summon { count: u32, hp_each: f32 },
176    /// Area-of-effect blast.
177    AoeBlast { radius: f32, damage: f32, element: Element },
178    /// Self-heal.
179    SelfHeal { amount: f32 },
180    /// Lock one of the player's abilities.
181    LockAbility,
182    /// Quantum collapse into a specific form.
183    QuantumCollapse,
184    /// Reverse damage/heal semantics.
185    ReverseSemantic,
186    /// Counter the player's most-used action.
187    CounterPredict,
188    /// Markov-chain based prediction of next player action.
189    MarkovPredict,
190    /// Randomize game rules.
191    RuleRandomize,
192    /// Consume arena tiles.
193    ConsumeArena { columns: u32 },
194    /// Factorial damage sequence.
195    FactorialStrike { sequence_index: u32 },
196    /// Arithmetic puzzle: player must deal specific damage.
197    ArithmeticPuzzle { target_factors: Vec<u32> },
198    /// Split into fibonacci sub-entities.
199    FibonacciSplit,
200    /// Vote-based action selection.
201    CommitteeVote,
202    /// Entangle with a copy.
203    Entangle,
204    /// No special ability.
205    None,
206}
207
208/// What the Null boss can erase.
209#[derive(Debug, Clone, Copy, PartialEq, Eq)]
210pub enum EraseTarget {
211    PlayerBuffs,
212    HpBar,
213    MiniMap,
214    AbilitySlot,
215    InventorySlot,
216    DamageNumbers,
217    BossHpBar,
218}
219
220/// Behavior pattern for a boss during a phase.
221#[derive(Debug, Clone, Copy, PartialEq, Eq)]
222pub enum BehaviorPattern {
223    /// Standard attack cycle.
224    Standard,
225    /// Aggressive: shorter cooldowns, higher damage.
226    Aggressive,
227    /// Defensive: heals, blocks, retreats.
228    Defensive,
229    /// Erratic: random action selection.
230    Erratic,
231    /// Calculated: optimal counter-play.
232    Calculated,
233    /// Passive: minimal attacks, focus on mechanics.
234    Passive,
235    /// Berserk: maximum aggression, ignores defense.
236    Berserk,
237}
238
239/// A single phase of a boss encounter.
240#[derive(Debug, Clone, PartialEq)]
241pub struct BossPhase {
242    /// Phase index (1-based).
243    pub phase_number: u32,
244    /// HP threshold (as percentage) at which this phase activates.
245    /// e.g., 0.75 means phase activates when boss drops below 75% HP.
246    pub hp_threshold_pct: f32,
247    /// Behavior pattern during this phase.
248    pub behavior_pattern: BehaviorPattern,
249    /// Speed multiplier for this phase.
250    pub speed_mult: f32,
251    /// Damage multiplier for this phase.
252    pub damage_mult: f32,
253    /// Special ability available during this phase.
254    pub special_ability: SpecialAbility,
255    /// How the transition into this phase is animated.
256    pub transition_animation: PhaseTransition,
257    /// Dialogue spoken when entering this phase.
258    pub dialogue_on_enter: String,
259}
260
261impl BossPhase {
262    pub fn new(phase_number: u32, hp_threshold_pct: f32) -> Self {
263        Self {
264            phase_number,
265            hp_threshold_pct,
266            behavior_pattern: BehaviorPattern::Standard,
267            speed_mult: 1.0,
268            damage_mult: 1.0,
269            special_ability: SpecialAbility::None,
270            transition_animation: PhaseTransition::PowerUp,
271            dialogue_on_enter: String::new(),
272        }
273    }
274
275    pub fn with_behavior(mut self, pattern: BehaviorPattern) -> Self {
276        self.behavior_pattern = pattern;
277        self
278    }
279
280    pub fn with_speed(mut self, mult: f32) -> Self {
281        self.speed_mult = mult;
282        self
283    }
284
285    pub fn with_damage(mut self, mult: f32) -> Self {
286        self.damage_mult = mult;
287        self
288    }
289
290    pub fn with_ability(mut self, ability: SpecialAbility) -> Self {
291        self.special_ability = ability;
292        self
293    }
294
295    pub fn with_transition(mut self, anim: PhaseTransition) -> Self {
296        self.transition_animation = anim;
297        self
298    }
299
300    pub fn with_dialogue(mut self, dialogue: impl Into<String>) -> Self {
301        self.dialogue_on_enter = dialogue.into();
302        self
303    }
304}
305
306// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
307// Boss Profile
308// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
309
310/// Loot drop definition for a boss.
311#[derive(Debug, Clone)]
312pub struct BossLootEntry {
313    pub item_name: String,
314    pub drop_chance: f32,
315    pub min_quantity: u32,
316    pub max_quantity: u32,
317}
318
319/// Complete profile describing a boss encounter.
320#[derive(Debug, Clone)]
321pub struct BossProfile {
322    pub boss_type: BossType,
323    pub name: String,
324    pub title: String,
325    pub hp_base: f32,
326    pub damage_base: f32,
327    pub tier: u32,
328    pub phases: Vec<BossPhase>,
329    pub special_mechanics: Vec<String>,
330    pub loot_table: Vec<BossLootEntry>,
331    pub music_type: MusicType,
332    pub arena_mods: Vec<ArenaMod>,
333    pub resistance: ResistanceProfile,
334}
335
336impl BossProfile {
337    /// Scale boss stats for a given floor level.
338    pub fn scaled_hp(&self, floor: u32) -> f32 {
339        self.hp_base * (1.0 + 0.15 * floor as f32)
340    }
341
342    pub fn scaled_damage(&self, floor: u32) -> f32 {
343        self.damage_base * (1.0 + 0.10 * floor as f32)
344    }
345}
346
347// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
348// Boss Phase Controller
349// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
350
351/// Controls phase transitions based on HP thresholds.
352#[derive(Debug, Clone)]
353pub struct BossPhaseController {
354    /// All phases, sorted by hp_threshold_pct descending.
355    phases: Vec<BossPhase>,
356    /// Index of the currently active phase.
357    current_phase_idx: usize,
358    /// Whether a transition is currently animating.
359    transitioning: bool,
360    /// Timer for transition animation.
361    transition_timer: f32,
362    /// Duration of transition animations.
363    transition_duration: f32,
364}
365
366impl BossPhaseController {
367    pub fn new(mut phases: Vec<BossPhase>) -> Self {
368        // Sort phases by HP threshold descending (phase 1 = highest threshold).
369        phases.sort_by(|a, b| b.hp_threshold_pct.partial_cmp(&a.hp_threshold_pct).unwrap());
370        Self {
371            phases,
372            current_phase_idx: 0,
373            transitioning: false,
374            transition_timer: 0.0,
375            transition_duration: 1.5,
376        }
377    }
378
379    /// Current active phase.
380    pub fn current_phase(&self) -> Option<&BossPhase> {
381        self.phases.get(self.current_phase_idx)
382    }
383
384    /// Current phase number (1-based).
385    pub fn current_phase_number(&self) -> u32 {
386        self.current_phase()
387            .map(|p| p.phase_number)
388            .unwrap_or(1)
389    }
390
391    /// Check HP fraction and potentially trigger a phase transition.
392    /// Returns `Some(BossPhase)` if a new phase was entered.
393    pub fn check_transition(&mut self, hp_fraction: f32) -> Option<&BossPhase> {
394        if self.transitioning {
395            return None;
396        }
397
398        // Find the deepest phase whose threshold we've crossed.
399        let mut target_idx = self.current_phase_idx;
400        for (i, phase) in self.phases.iter().enumerate() {
401            if i > self.current_phase_idx && hp_fraction <= phase.hp_threshold_pct {
402                target_idx = i;
403            }
404        }
405
406        if target_idx != self.current_phase_idx {
407            self.current_phase_idx = target_idx;
408            self.transitioning = true;
409            self.transition_timer = 0.0;
410            return self.phases.get(self.current_phase_idx);
411        }
412
413        None
414    }
415
416    /// Update transition animation timer. Returns true if transition just completed.
417    pub fn update_transition(&mut self, dt: f32) -> bool {
418        if !self.transitioning {
419            return false;
420        }
421        self.transition_timer += dt;
422        if self.transition_timer >= self.transition_duration {
423            self.transitioning = false;
424            return true;
425        }
426        false
427    }
428
429    /// Whether a transition is in progress.
430    pub fn is_transitioning(&self) -> bool {
431        self.transitioning
432    }
433
434    /// Transition progress [0, 1].
435    pub fn transition_progress(&self) -> f32 {
436        if !self.transitioning {
437            return 0.0;
438        }
439        (self.transition_timer / self.transition_duration).clamp(0.0, 1.0)
440    }
441
442    /// Total number of phases.
443    pub fn phase_count(&self) -> usize {
444        self.phases.len()
445    }
446
447    /// Get the speed multiplier for the current phase.
448    pub fn speed_mult(&self) -> f32 {
449        self.current_phase().map(|p| p.speed_mult).unwrap_or(1.0)
450    }
451
452    /// Get the damage multiplier for the current phase.
453    pub fn damage_mult(&self) -> f32 {
454        self.current_phase().map(|p| p.damage_mult).unwrap_or(1.0)
455    }
456}
457
458// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
459// Player Action Tracking (used by several bosses)
460// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
461
462/// A recorded player action for boss mechanics that react to player behavior.
463#[derive(Debug, Clone, PartialEq, Eq, Hash)]
464pub enum PlayerActionType {
465    Attack,
466    Defend,
467    Heal,
468    UseAbility(u32),
469    UseItem,
470    Move,
471    Wait,
472}
473
474/// Recorded player action with metadata.
475#[derive(Debug, Clone)]
476pub struct RecordedAction {
477    pub action_type: PlayerActionType,
478    pub turn: u32,
479    pub damage_dealt: f32,
480    pub element: Option<Element>,
481}
482
483// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
484// Individual Boss Mechanic States
485// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
486
487// ── Mirror Boss ──────────────────────────────────────────────────────────────────
488
489/// State for the Mirror Boss.
490/// Copies player's last 3 abilities with a 1-turn delay.
491/// Phase 2: also copies player stats.
492/// Phase 3: acts simultaneously with copied action.
493#[derive(Debug, Clone)]
494pub struct MirrorBossState {
495    /// Buffer of player actions to copy (FIFO, max 3).
496    pub mirror_buffer: Vec<RecordedAction>,
497    /// Maximum buffer depth.
498    pub buffer_depth: usize,
499    /// Delay in turns before copied action is used.
500    pub copy_delay: u32,
501    /// Whether stats are also being copied (phase 2+).
502    pub copying_stats: bool,
503    /// Whether actions are simultaneous (phase 3).
504    pub simultaneous: bool,
505    /// Copied player stats (if copying_stats is true).
506    pub copied_attack: f32,
507    pub copied_defense: f32,
508}
509
510impl MirrorBossState {
511    pub fn new() -> Self {
512        Self {
513            mirror_buffer: Vec::new(),
514            buffer_depth: 3,
515            copy_delay: 1,
516            copying_stats: false,
517            simultaneous: false,
518            copied_attack: 0.0,
519            copied_defense: 0.0,
520        }
521    }
522
523    /// Record a player action into the mirror buffer.
524    pub fn record_action(&mut self, action: RecordedAction) {
525        self.mirror_buffer.push(action);
526        if self.mirror_buffer.len() > self.buffer_depth {
527            self.mirror_buffer.remove(0);
528        }
529    }
530
531    /// Get the action to mirror this turn (with delay).
532    pub fn get_mirrored_action(&self, current_turn: u32) -> Option<&RecordedAction> {
533        self.mirror_buffer
534            .iter()
535            .find(|a| a.turn + self.copy_delay == current_turn)
536    }
537
538    /// Copy player stats (for phase 2+).
539    pub fn copy_stats(&mut self, player_stats: &CombatStats) {
540        self.copied_attack = player_stats.attack;
541        self.copied_defense = player_stats.armor;
542        self.copying_stats = true;
543    }
544
545    /// Transition to phase 2: enable stat copying.
546    pub fn enter_phase2(&mut self) {
547        self.copying_stats = true;
548    }
549
550    /// Transition to phase 3: enable simultaneous action.
551    pub fn enter_phase3(&mut self) {
552        self.simultaneous = true;
553    }
554}
555
556impl Default for MirrorBossState {
557    fn default() -> Self { Self::new() }
558}
559
560// ── Null Boss ────────────────────────────────────────────────────────────────────
561
562/// State for the Null Boss.
563/// Phase 1: erases player buffs.
564/// Phase 2: erases UI elements (HP bar, map).
565/// Phase 3: erases player abilities (random lock each turn).
566/// Death: everything restores.
567#[derive(Debug, Clone)]
568pub struct NullBossState {
569    /// Which UI elements have been erased.
570    pub erased_ui: Vec<EraseTarget>,
571    /// Which ability slots are currently locked.
572    pub locked_abilities: Vec<u32>,
573    /// Maximum abilities that can be locked simultaneously.
574    pub max_locked: usize,
575    /// How many buffs have been erased total.
576    pub buffs_erased: u32,
577    /// Pseudo-random state for selecting targets.
578    pub rng_state: u64,
579}
580
581impl NullBossState {
582    pub fn new() -> Self {
583        Self {
584            erased_ui: Vec::new(),
585            locked_abilities: Vec::new(),
586            max_locked: 3,
587            buffs_erased: 0,
588            rng_state: 0xDEAD_BEEF_CAFE_1234,
589        }
590    }
591
592    /// Simple xorshift PRNG.
593    fn next_rng(&mut self) -> u64 {
594        self.rng_state ^= self.rng_state << 13;
595        self.rng_state ^= self.rng_state >> 7;
596        self.rng_state ^= self.rng_state << 17;
597        self.rng_state
598    }
599
600    /// Erase a random UI element (phase 2).
601    pub fn erase_ui_element(&mut self) -> EraseTarget {
602        let targets = [
603            EraseTarget::HpBar,
604            EraseTarget::MiniMap,
605            EraseTarget::DamageNumbers,
606        ];
607        let idx = (self.next_rng() as usize) % targets.len();
608        let target = targets[idx];
609        if !self.erased_ui.contains(&target) {
610            self.erased_ui.push(target);
611        }
612        target
613    }
614
615    /// Lock a random ability slot (phase 3).
616    pub fn lock_random_ability(&mut self, max_slots: u32) -> Option<u32> {
617        if self.locked_abilities.len() >= self.max_locked || max_slots == 0 {
618            return None;
619        }
620        let slot = (self.next_rng() as u32) % max_slots;
621        if !self.locked_abilities.contains(&slot) {
622            self.locked_abilities.push(slot);
623            Some(slot)
624        } else {
625            None
626        }
627    }
628
629    /// Erase player buffs (phase 1). Returns number erased.
630    pub fn erase_buffs(&mut self, active_buff_count: u32) -> u32 {
631        let to_erase = active_buff_count.min(2); // erase up to 2 per turn
632        self.buffs_erased += to_erase;
633        to_erase
634    }
635
636    /// On death: restore everything.
637    pub fn restore_all(&mut self) -> Vec<EraseTarget> {
638        let restored = self.erased_ui.clone();
639        self.erased_ui.clear();
640        self.locked_abilities.clear();
641        restored
642    }
643}
644
645impl Default for NullBossState {
646    fn default() -> Self { Self::new() }
647}
648
649// ── Committee Boss ───────────────────────────────────────────────────────────────
650
651/// Personality of a committee judge.
652#[derive(Debug, Clone, Copy, PartialEq, Eq)]
653pub enum JudgePersonality {
654    Aggressive,
655    Defensive,
656    Random,
657    Strategic,
658    Chaotic,
659}
660
661/// A single judge in the Committee boss.
662#[derive(Debug, Clone)]
663pub struct Judge {
664    pub id: u32,
665    pub personality: JudgePersonality,
666    pub hp: f32,
667    pub max_hp: f32,
668    pub alive: bool,
669    /// Dead judges become ghosts that still vote (phase 2+).
670    pub ghost: bool,
671    /// Whether this judge is currently lit up (voting).
672    pub voting: bool,
673    /// The action this judge voted for.
674    pub current_vote: Option<CommitteeAction>,
675}
676
677/// Actions the committee can vote on.
678#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
679pub enum CommitteeAction {
680    Attack,
681    Defend,
682    HeavyStrike,
683    Heal,
684    Buff,
685    Summon,
686    SpecialAttack,
687}
688
689impl Judge {
690    pub fn new(id: u32, personality: JudgePersonality, hp: f32) -> Self {
691        Self {
692            id,
693            personality,
694            hp,
695            max_hp: hp,
696            alive: true,
697            ghost: false,
698            voting: false,
699            current_vote: None,
700        }
701    }
702
703    /// Cast a vote based on personality and context.
704    pub fn cast_vote(&mut self, boss_hp_frac: f32, rng_val: u64) -> CommitteeAction {
705        let vote = match self.personality {
706            JudgePersonality::Aggressive => {
707                if boss_hp_frac < 0.3 {
708                    CommitteeAction::HeavyStrike
709                } else {
710                    CommitteeAction::Attack
711                }
712            }
713            JudgePersonality::Defensive => {
714                if boss_hp_frac < 0.5 {
715                    CommitteeAction::Heal
716                } else {
717                    CommitteeAction::Defend
718                }
719            }
720            JudgePersonality::Random => {
721                let actions = [
722                    CommitteeAction::Attack,
723                    CommitteeAction::Defend,
724                    CommitteeAction::HeavyStrike,
725                    CommitteeAction::Heal,
726                    CommitteeAction::Buff,
727                    CommitteeAction::Summon,
728                    CommitteeAction::SpecialAttack,
729                ];
730                actions[(rng_val as usize) % actions.len()]
731            }
732            JudgePersonality::Strategic => {
733                if boss_hp_frac < 0.3 {
734                    CommitteeAction::Heal
735                } else if boss_hp_frac < 0.6 {
736                    CommitteeAction::Buff
737                } else {
738                    CommitteeAction::SpecialAttack
739                }
740            }
741            JudgePersonality::Chaotic => {
742                // Chaotic flips between extremes.
743                if rng_val % 2 == 0 {
744                    CommitteeAction::HeavyStrike
745                } else {
746                    CommitteeAction::Summon
747                }
748            }
749        };
750        self.current_vote = Some(vote);
751        self.voting = true;
752        vote
753    }
754
755    /// Take damage. Returns true if killed.
756    pub fn take_damage(&mut self, amount: f32) -> bool {
757        if !self.alive {
758            return false;
759        }
760        self.hp = (self.hp - amount).max(0.0);
761        if self.hp <= 0.0 {
762            self.alive = false;
763            true
764        } else {
765            false
766        }
767    }
768
769    /// Convert to ghost (phase 2).
770    pub fn become_ghost(&mut self) {
771        self.ghost = true;
772    }
773}
774
775/// State for the Committee Boss.
776#[derive(Debug, Clone)]
777pub struct CommitteeBossState {
778    pub judges: Vec<Judge>,
779    /// Whether dead judges vote as ghosts.
780    pub ghost_voting: bool,
781    /// Whether remaining judges have merged (phase 3).
782    pub merged: bool,
783    /// RNG state for random/chaotic votes.
784    pub rng_state: u64,
785}
786
787impl CommitteeBossState {
788    pub fn new() -> Self {
789        let judges = vec![
790            Judge::new(0, JudgePersonality::Aggressive, 200.0),
791            Judge::new(1, JudgePersonality::Defensive, 200.0),
792            Judge::new(2, JudgePersonality::Random, 200.0),
793            Judge::new(3, JudgePersonality::Strategic, 200.0),
794            Judge::new(4, JudgePersonality::Chaotic, 200.0),
795        ];
796        Self {
797            judges,
798            ghost_voting: false,
799            merged: false,
800            rng_state: 0xC0FF_EE42,
801        }
802    }
803
804    fn next_rng(&mut self) -> u64 {
805        self.rng_state ^= self.rng_state << 13;
806        self.rng_state ^= self.rng_state >> 7;
807        self.rng_state ^= self.rng_state << 17;
808        self.rng_state
809    }
810
811    /// Conduct a vote. All alive judges (and ghosts if enabled) vote.
812    /// Returns the winning action by majority.
813    pub fn conduct_vote(&mut self, boss_hp_frac: f32) -> CommitteeAction {
814        let mut tallies: HashMap<CommitteeAction, u32> = HashMap::new();
815
816        for judge in &mut self.judges {
817            let can_vote = judge.alive || (judge.ghost && self.ghost_voting);
818            if can_vote {
819                let rng_val = self.rng_state;
820                self.rng_state ^= self.rng_state << 13;
821                self.rng_state ^= self.rng_state >> 7;
822                self.rng_state ^= self.rng_state << 17;
823                let vote = judge.cast_vote(boss_hp_frac, rng_val);
824                *tallies.entry(vote).or_insert(0) += 1;
825            }
826        }
827
828        // Find majority winner.
829        tallies
830            .into_iter()
831            .max_by_key(|&(_, count)| count)
832            .map(|(action, _)| action)
833            .unwrap_or(CommitteeAction::Attack)
834    }
835
836    /// Count alive judges.
837    pub fn alive_count(&self) -> usize {
838        self.judges.iter().filter(|j| j.alive).count()
839    }
840
841    /// Enable ghost voting (phase 2).
842    pub fn enable_ghost_voting(&mut self) {
843        self.ghost_voting = true;
844        for judge in &mut self.judges {
845            if !judge.alive {
846                judge.become_ghost();
847            }
848        }
849    }
850
851    /// Merge remaining judges (phase 3). Returns combined HP.
852    pub fn merge_judges(&mut self) -> f32 {
853        let combined_hp: f32 = self.judges.iter().filter(|j| j.alive).map(|j| j.hp).sum();
854        self.merged = true;
855        combined_hp
856    }
857}
858
859impl Default for CommitteeBossState {
860    fn default() -> Self { Self::new() }
861}
862
863// ── Fibonacci Hydra ──────────────────────────────────────────────────────────────
864
865/// A single head of the Fibonacci Hydra.
866#[derive(Debug, Clone)]
867pub struct HydraHead {
868    pub id: u32,
869    pub hp: f32,
870    pub max_hp: f32,
871    pub depth: u32,
872    pub alive: bool,
873    pub parent_id: Option<u32>,
874}
875
876impl HydraHead {
877    pub fn new(id: u32, hp: f32, depth: u32, parent_id: Option<u32>) -> Self {
878        Self {
879            id,
880            hp,
881            max_hp: hp,
882            depth,
883            alive: true,
884            parent_id,
885        }
886    }
887
888    pub fn take_damage(&mut self, amount: f32) -> bool {
889        self.hp = (self.hp - amount).max(0.0);
890        if self.hp <= 0.0 {
891            self.alive = false;
892            true
893        } else {
894            false
895        }
896    }
897}
898
899/// State for the Fibonacci Hydra.
900/// Starts as 1. On death, splits into 2 at 61.8% original HP each.
901/// Max depth 5 (up to 32 instances). All share a damage pool.
902#[derive(Debug, Clone)]
903pub struct FibonacciHydraState {
904    pub heads: Vec<HydraHead>,
905    /// Maximum split depth.
906    pub max_depth: u32,
907    /// Next head ID to assign.
908    pub next_id: u32,
909    /// Golden ratio factor for child HP.
910    pub split_hp_ratio: f32,
911    /// Total damage dealt to all heads (shared pool).
912    pub total_damage_pool: f32,
913}
914
915impl FibonacciHydraState {
916    pub fn new(base_hp: f32) -> Self {
917        Self {
918            heads: vec![HydraHead::new(0, base_hp, 0, None)],
919            max_depth: 5,
920            next_id: 1,
921            split_hp_ratio: 0.618,
922            total_damage_pool: 0.0,
923        }
924    }
925
926    /// Count alive heads.
927    pub fn alive_count(&self) -> usize {
928        self.heads.iter().filter(|h| h.alive).count()
929    }
930
931    /// Split a dead head into two children. Returns new head IDs if split occurred.
932    pub fn try_split(&mut self, dead_head_id: u32) -> Option<(u32, u32)> {
933        let (depth, parent_hp) = {
934            let head = self.heads.iter().find(|h| h.id == dead_head_id)?;
935            if head.alive || head.depth >= self.max_depth {
936                return None;
937            }
938            (head.depth, head.max_hp)
939        };
940
941        let child_hp = parent_hp * self.split_hp_ratio;
942        let id_a = self.next_id;
943        let id_b = self.next_id + 1;
944        self.next_id += 2;
945
946        self.heads.push(HydraHead::new(id_a, child_hp, depth + 1, Some(dead_head_id)));
947        self.heads.push(HydraHead::new(id_b, child_hp, depth + 1, Some(dead_head_id)));
948
949        // Mark parent as already split so it cannot split again
950        if let Some(head) = self.heads.iter_mut().find(|h| h.id == dead_head_id) {
951            head.depth = self.max_depth;
952        }
953
954        Some((id_a, id_b))
955    }
956
957    /// Apply damage to a specific head. Returns true if it died.
958    pub fn damage_head(&mut self, head_id: u32, amount: f32) -> bool {
959        self.total_damage_pool += amount;
960        if let Some(head) = self.heads.iter_mut().find(|h| h.id == head_id) {
961            head.take_damage(amount)
962        } else {
963            false
964        }
965    }
966
967    /// Check if the hydra is fully defeated (no alive heads and no more splits possible).
968    pub fn is_defeated(&self) -> bool {
969        let alive = self.alive_count();
970        if alive > 0 {
971            return false;
972        }
973        // Check if any dead head can still split.
974        !self.heads.iter().any(|h| !h.alive && h.depth < self.max_depth)
975    }
976
977    /// Maximum possible heads at full depth.
978    pub fn max_possible_heads(&self) -> u32 {
979        1 << self.max_depth // 2^max_depth = 32 at depth 5
980    }
981}
982
983impl Default for FibonacciHydraState {
984    fn default() -> Self { Self::new(1000.0) }
985}
986
987// ── Eigenstate Boss ──────────────────────────────────────────────────────────────
988
989/// A quantum form the Eigenstate boss can take.
990#[derive(Debug, Clone, Copy, PartialEq, Eq)]
991pub enum QuantumForm {
992    Attack,
993    Defense,
994    /// Phase 2 adds a third form.
995    Evasion,
996}
997
998/// State for the Eigenstate Boss.
999/// Exists in superposition. Observing (targeting) collapses to one form.
1000/// Phase 2: 3 forms. Phase 3: entangled with a copy.
1001#[derive(Debug, Clone)]
1002pub struct EigenstateBossState {
1003    /// Available forms in the superposition.
1004    pub forms: Vec<QuantumForm>,
1005    /// Currently collapsed form (None = still in superposition).
1006    pub collapsed_form: Option<QuantumForm>,
1007    /// Whether the boss is being observed/targeted.
1008    pub observed: bool,
1009    /// Whether an entangled copy exists (phase 3).
1010    pub entangled: bool,
1011    /// HP of the entangled copy.
1012    pub entangled_hp: f32,
1013    /// Turns since last observation.
1014    pub turns_unobserved: u32,
1015    /// RNG for collapse.
1016    pub rng_state: u64,
1017}
1018
1019impl EigenstateBossState {
1020    pub fn new() -> Self {
1021        Self {
1022            forms: vec![QuantumForm::Attack, QuantumForm::Defense],
1023            collapsed_form: None,
1024            observed: false,
1025            entangled: false,
1026            entangled_hp: 0.0,
1027            turns_unobserved: 0,
1028            rng_state: 0xABCD_0042,
1029        }
1030    }
1031
1032    /// Observe the boss, collapsing it to a random form.
1033    pub fn observe(&mut self) -> QuantumForm {
1034        self.observed = true;
1035        self.turns_unobserved = 0;
1036        self.rng_state ^= self.rng_state << 13;
1037        self.rng_state ^= self.rng_state >> 7;
1038        self.rng_state ^= self.rng_state << 17;
1039        let idx = (self.rng_state as usize) % self.forms.len();
1040        let form = self.forms[idx];
1041        self.collapsed_form = Some(form);
1042        form
1043    }
1044
1045    /// End observation — return to superposition.
1046    pub fn unobserve(&mut self) {
1047        self.observed = false;
1048        self.collapsed_form = None;
1049        self.turns_unobserved += 1;
1050    }
1051
1052    /// Add evasion form (phase 2).
1053    pub fn add_evasion_form(&mut self) {
1054        if !self.forms.contains(&QuantumForm::Evasion) {
1055            self.forms.push(QuantumForm::Evasion);
1056        }
1057    }
1058
1059    /// Create entangled copy (phase 3). Returns initial copy HP.
1060    pub fn entangle(&mut self, boss_hp: f32) -> f32 {
1061        self.entangled = true;
1062        self.entangled_hp = boss_hp;
1063        self.entangled_hp
1064    }
1065
1066    /// When entangled, damage to one is mirrored to the other.
1067    pub fn mirror_damage(&mut self, amount: f32) -> f32 {
1068        if self.entangled {
1069            self.entangled_hp = (self.entangled_hp - amount).max(0.0);
1070            amount // damage also applied to main boss
1071        } else {
1072            0.0
1073        }
1074    }
1075
1076    /// Is the boss in superposition (not collapsed)?
1077    pub fn in_superposition(&self) -> bool {
1078        self.collapsed_form.is_none()
1079    }
1080
1081    /// Get damage multiplier based on current form.
1082    pub fn damage_mult(&self) -> f32 {
1083        match self.collapsed_form {
1084            Some(QuantumForm::Attack) => 1.8,
1085            Some(QuantumForm::Defense) => 0.5,
1086            Some(QuantumForm::Evasion) => 1.0,
1087            None => 1.2, // superposition: moderate from both
1088        }
1089    }
1090
1091    /// Get defense multiplier based on current form.
1092    pub fn defense_mult(&self) -> f32 {
1093        match self.collapsed_form {
1094            Some(QuantumForm::Attack) => 0.5,
1095            Some(QuantumForm::Defense) => 2.0,
1096            Some(QuantumForm::Evasion) => 0.8,
1097            None => 1.0,
1098        }
1099    }
1100}
1101
1102impl Default for EigenstateBossState {
1103    fn default() -> Self { Self::new() }
1104}
1105
1106// ── Ouroboros Boss ───────────────────────────────────────────────────────────────
1107
1108/// State for the Ouroboros Boss.
1109/// Heals by dealing damage to player. Player heals by dealing damage to boss.
1110/// Bleed heals the bleeder. Healing damages the healer.
1111/// Phase 2: rules randomly swap back to normal for 2 turns.
1112#[derive(Debug, Clone)]
1113pub struct OuroborosBossState {
1114    /// Whether damage/heal semantics are currently reversed.
1115    pub reversed: bool,
1116    /// Countdown for temporary normal-rules window.
1117    pub normal_turns_remaining: u32,
1118    /// How many turns between rule swaps (phase 2).
1119    pub swap_interval: u32,
1120    /// Turn counter since last swap.
1121    pub turns_since_swap: u32,
1122    /// Total HP healed from dealing damage.
1123    pub total_self_heal: f32,
1124    /// Total HP player healed from dealing damage.
1125    pub total_player_heal: f32,
1126    /// Whether phase 2 random swapping is active.
1127    pub phase2_active: bool,
1128    /// RNG for swap timing.
1129    pub rng_state: u64,
1130}
1131
1132impl OuroborosBossState {
1133    pub fn new() -> Self {
1134        Self {
1135            reversed: true, // starts reversed
1136            normal_turns_remaining: 0,
1137            swap_interval: 5,
1138            turns_since_swap: 0,
1139            total_self_heal: 0.0,
1140            total_player_heal: 0.0,
1141            phase2_active: false,
1142            rng_state: 0xB0B0_0007,
1143        }
1144    }
1145
1146    /// Process a damage event under Ouroboros rules.
1147    /// Returns (actual_damage_to_target, heal_to_source).
1148    pub fn process_damage(&mut self, raw_damage: f32, is_boss_attacking: bool) -> (f32, f32) {
1149        if self.reversed {
1150            // Reversed: dealing damage heals the attacker.
1151            let heal = raw_damage * 0.5;
1152            if is_boss_attacking {
1153                self.total_self_heal += heal;
1154            } else {
1155                self.total_player_heal += heal;
1156            }
1157            (raw_damage, heal)
1158        } else {
1159            // Normal rules.
1160            (raw_damage, 0.0)
1161        }
1162    }
1163
1164    /// Process a heal event under Ouroboros rules.
1165    /// Returns actual healing (negative means damage).
1166    pub fn process_heal(&self, raw_heal: f32) -> f32 {
1167        if self.reversed {
1168            -raw_heal // healing becomes damage
1169        } else {
1170            raw_heal
1171        }
1172    }
1173
1174    /// Advance a turn. May trigger rule swap in phase 2.
1175    pub fn advance_turn(&mut self) -> bool {
1176        if self.normal_turns_remaining > 0 {
1177            self.normal_turns_remaining -= 1;
1178            if self.normal_turns_remaining == 0 {
1179                self.reversed = true;
1180                return true; // rules swapped back
1181            }
1182        }
1183
1184        if self.phase2_active {
1185            self.turns_since_swap += 1;
1186            self.rng_state ^= self.rng_state << 13;
1187            self.rng_state ^= self.rng_state >> 7;
1188            self.rng_state ^= self.rng_state << 17;
1189
1190            // Random chance to swap to normal for 2 turns.
1191            if self.turns_since_swap >= self.swap_interval
1192                && (self.rng_state % 3 == 0)
1193            {
1194                self.reversed = false;
1195                self.normal_turns_remaining = 2;
1196                self.turns_since_swap = 0;
1197                return true; // rules temporarily normal
1198            }
1199        }
1200        false
1201    }
1202
1203    /// Activate phase 2 swapping.
1204    pub fn enter_phase2(&mut self) {
1205        self.phase2_active = true;
1206    }
1207}
1208
1209impl Default for OuroborosBossState {
1210    fn default() -> Self { Self::new() }
1211}
1212
1213// ── Algorithm Reborn Boss (Final Boss) ───────────────────────────────────────────
1214
1215/// State for the Algorithm Reborn Boss.
1216/// Phase 1: learns player patterns (tracks action frequency).
1217/// Phase 2: counters player's most-used action type.
1218/// Phase 3: predicts next action via Markov chain.
1219/// Unique: no HP bar shown.
1220#[derive(Debug, Clone)]
1221pub struct AlgorithmRebornState {
1222    /// Frequency count of each player action type.
1223    pub action_frequency: HashMap<PlayerActionType, u32>,
1224    /// Markov chain: transition probabilities from action A to action B.
1225    /// Key: (from_action, to_action), Value: count.
1226    pub markov_transitions: HashMap<(PlayerActionType, PlayerActionType), u32>,
1227    /// Last player action (for Markov chain).
1228    pub last_action: Option<PlayerActionType>,
1229    /// The predicted next player action (phase 3).
1230    pub predicted_action: Option<PlayerActionType>,
1231    /// Whether the boss is in counter mode (phase 2+).
1232    pub counter_mode: bool,
1233    /// Whether Markov prediction is active (phase 3).
1234    pub markov_mode: bool,
1235    /// Visual degradation level [0, 1]. 0 = pristine, 1 = nearly dead.
1236    pub degradation: f32,
1237    /// Actual HP fraction (hidden from player).
1238    pub hidden_hp_frac: f32,
1239}
1240
1241impl AlgorithmRebornState {
1242    pub fn new() -> Self {
1243        Self {
1244            action_frequency: HashMap::new(),
1245            markov_transitions: HashMap::new(),
1246            last_action: None,
1247            predicted_action: None,
1248            counter_mode: false,
1249            markov_mode: false,
1250            degradation: 0.0,
1251            hidden_hp_frac: 1.0,
1252        }
1253    }
1254
1255    /// Record a player action and update the frequency and Markov tables.
1256    pub fn record_action(&mut self, action: PlayerActionType) {
1257        *self.action_frequency.entry(action.clone()).or_insert(0) += 1;
1258
1259        if let Some(ref last) = self.last_action {
1260            *self
1261                .markov_transitions
1262                .entry((last.clone(), action.clone()))
1263                .or_insert(0) += 1;
1264        }
1265        self.last_action = Some(action);
1266    }
1267
1268    /// Get the player's most frequently used action.
1269    pub fn most_used_action(&self) -> Option<PlayerActionType> {
1270        self.action_frequency
1271            .iter()
1272            .max_by_key(|&(_, count)| count)
1273            .map(|(action, _)| action.clone())
1274    }
1275
1276    /// Get the counter-action for a given player action.
1277    pub fn counter_for(action: &PlayerActionType) -> PlayerActionType {
1278        match action {
1279            PlayerActionType::Attack => PlayerActionType::Defend,
1280            PlayerActionType::Defend => PlayerActionType::UseAbility(0), // piercing
1281            PlayerActionType::Heal => PlayerActionType::Attack,           // punish
1282            PlayerActionType::UseAbility(_) => PlayerActionType::Defend,
1283            PlayerActionType::UseItem => PlayerActionType::Attack,
1284            PlayerActionType::Move => PlayerActionType::UseAbility(1),    // AoE
1285            PlayerActionType::Wait => PlayerActionType::Attack,
1286        }
1287    }
1288
1289    /// Predict the next player action using the Markov chain.
1290    pub fn predict_next(&mut self) -> Option<PlayerActionType> {
1291        let last = self.last_action.as_ref()?;
1292        let mut best_action = None;
1293        let mut best_count = 0u32;
1294
1295        for ((from, to), &count) in &self.markov_transitions {
1296            if from == last && count > best_count {
1297                best_count = count;
1298                best_action = Some(to.clone());
1299            }
1300        }
1301
1302        self.predicted_action = best_action.clone();
1303        best_action
1304    }
1305
1306    /// Update visual degradation based on actual HP fraction.
1307    pub fn update_degradation(&mut self, hp_frac: f32) {
1308        self.hidden_hp_frac = hp_frac;
1309        self.degradation = 1.0 - hp_frac;
1310    }
1311
1312    /// Enter phase 2: enable counter mode.
1313    pub fn enter_phase2(&mut self) {
1314        self.counter_mode = true;
1315    }
1316
1317    /// Enter phase 3: enable Markov prediction.
1318    pub fn enter_phase3(&mut self) {
1319        self.markov_mode = true;
1320    }
1321}
1322
1323impl Default for AlgorithmRebornState {
1324    fn default() -> Self { Self::new() }
1325}
1326
1327// ── Chaos Weaver Boss ────────────────────────────────────────────────────────────
1328
1329/// State for the Chaos Weaver Boss.
1330/// Manipulates game rules: element weakness chart, ability slots, damage display.
1331/// Phase 2: randomizes tile effects.
1332#[derive(Debug, Clone)]
1333pub struct ChaosWeaverState {
1334    /// Scrambled element weakness overrides. Key: element, Value: new weakness.
1335    pub weakness_overrides: HashMap<Element, Element>,
1336    /// Mapping of swapped ability slots (original -> new position).
1337    pub slot_swaps: HashMap<u32, u32>,
1338    /// Whether damage numbers are visually randomized.
1339    pub randomize_damage_display: bool,
1340    /// Whether tile effects are randomized (phase 2).
1341    pub randomize_tiles: bool,
1342    /// How many rule changes have occurred.
1343    pub chaos_count: u32,
1344    /// RNG state.
1345    pub rng_state: u64,
1346}
1347
1348impl ChaosWeaverState {
1349    pub fn new() -> Self {
1350        Self {
1351            weakness_overrides: HashMap::new(),
1352            slot_swaps: HashMap::new(),
1353            randomize_damage_display: false,
1354            randomize_tiles: false,
1355            chaos_count: 0,
1356            rng_state: 0xC4A0_5555,
1357        }
1358    }
1359
1360    fn next_rng(&mut self) -> u64 {
1361        self.rng_state ^= self.rng_state << 13;
1362        self.rng_state ^= self.rng_state >> 7;
1363        self.rng_state ^= self.rng_state << 17;
1364        self.rng_state
1365    }
1366
1367    /// Scramble the element weakness chart.
1368    pub fn scramble_weaknesses(&mut self) {
1369        let elements = [
1370            Element::Physical, Element::Fire, Element::Ice, Element::Lightning,
1371            Element::Void, Element::Entropy, Element::Gravity, Element::Radiant,
1372            Element::Shadow, Element::Temporal,
1373        ];
1374
1375        self.weakness_overrides.clear();
1376        for &elem in &elements {
1377            let rng = self.next_rng();
1378            let target_idx = (rng as usize) % elements.len();
1379            self.weakness_overrides.insert(elem, elements[target_idx]);
1380        }
1381        self.chaos_count += 1;
1382    }
1383
1384    /// Swap two ability slots.
1385    pub fn swap_ability_slots(&mut self, max_slots: u32) -> (u32, u32) {
1386        let a = (self.next_rng() as u32) % max_slots;
1387        let mut b = (self.next_rng() as u32) % max_slots;
1388        if b == a {
1389            b = (a + 1) % max_slots;
1390        }
1391        self.slot_swaps.insert(a, b);
1392        self.slot_swaps.insert(b, a);
1393        self.chaos_count += 1;
1394        (a, b)
1395    }
1396
1397    /// Get a fake damage number for display (actual damage is correct).
1398    pub fn fake_damage_number(&mut self, _actual: f32) -> f32 {
1399        if self.randomize_damage_display {
1400            let rng = self.next_rng();
1401            (rng % 9999) as f32 + 1.0
1402        } else {
1403            _actual
1404        }
1405    }
1406
1407    /// Enable damage display randomization.
1408    pub fn enable_damage_randomization(&mut self) {
1409        self.randomize_damage_display = true;
1410        self.chaos_count += 1;
1411    }
1412
1413    /// Enable tile randomization (phase 2).
1414    pub fn enable_tile_randomization(&mut self) {
1415        self.randomize_tiles = true;
1416        self.chaos_count += 1;
1417    }
1418
1419    /// Lookup what element a given element is now weak to (after scramble).
1420    pub fn effective_weakness(&self, element: Element) -> Element {
1421        self.weakness_overrides
1422            .get(&element)
1423            .copied()
1424            .unwrap_or(element)
1425    }
1426}
1427
1428impl Default for ChaosWeaverState {
1429    fn default() -> Self { Self::new() }
1430}
1431
1432// ── Void Serpent Boss ────────────────────────────────────────────────────────────
1433
1434/// State for the Void Serpent Boss.
1435/// Consumes the arena: each turn, edge tiles become void (instant death).
1436/// Phase 2: void tiles spit projectiles.
1437/// Phase 3: serpent emerges for direct attacks.
1438#[derive(Debug, Clone)]
1439pub struct VoidSerpentState {
1440    /// Current arena width (shrinks over time).
1441    pub arena_width: u32,
1442    /// Current arena height.
1443    pub arena_height: u32,
1444    /// Original arena width.
1445    pub original_width: u32,
1446    /// Original arena height.
1447    pub original_height: u32,
1448    /// Which edges have been consumed (layers consumed from each side).
1449    pub consumed_north: u32,
1450    pub consumed_south: u32,
1451    pub consumed_east: u32,
1452    pub consumed_west: u32,
1453    /// Whether void tiles spit projectiles (phase 2).
1454    pub void_projectiles: bool,
1455    /// Number of projectiles per turn.
1456    pub projectiles_per_turn: u32,
1457    /// Whether the serpent has emerged (phase 3).
1458    pub serpent_emerged: bool,
1459    /// Serpent direct attack damage.
1460    pub serpent_attack_damage: f32,
1461    /// Turn counter.
1462    pub turn_count: u32,
1463}
1464
1465impl VoidSerpentState {
1466    pub fn new(arena_w: u32, arena_h: u32) -> Self {
1467        Self {
1468            arena_width: arena_w,
1469            arena_height: arena_h,
1470            original_width: arena_w,
1471            original_height: arena_h,
1472            consumed_north: 0,
1473            consumed_south: 0,
1474            consumed_east: 0,
1475            consumed_west: 0,
1476            void_projectiles: false,
1477            projectiles_per_turn: 2,
1478            serpent_emerged: false,
1479            serpent_attack_damage: 50.0,
1480            turn_count: 0,
1481        }
1482    }
1483
1484    /// Consume one edge row/column. Returns which direction was consumed.
1485    pub fn consume_edge(&mut self) -> Option<&'static str> {
1486        self.turn_count += 1;
1487        // Cycle through directions.
1488        let direction = self.turn_count % 4;
1489        match direction {
1490            0 => {
1491                if self.consumed_north < self.original_height / 2 {
1492                    self.consumed_north += 1;
1493                    self.arena_height = self.arena_height.saturating_sub(1);
1494                    Some("north")
1495                } else {
1496                    None
1497                }
1498            }
1499            1 => {
1500                if self.consumed_east < self.original_width / 2 {
1501                    self.consumed_east += 1;
1502                    self.arena_width = self.arena_width.saturating_sub(1);
1503                    Some("east")
1504                } else {
1505                    None
1506                }
1507            }
1508            2 => {
1509                if self.consumed_south < self.original_height / 2 {
1510                    self.consumed_south += 1;
1511                    self.arena_height = self.arena_height.saturating_sub(1);
1512                    Some("south")
1513                } else {
1514                    None
1515                }
1516            }
1517            3 => {
1518                if self.consumed_west < self.original_width / 2 {
1519                    self.consumed_west += 1;
1520                    self.arena_width = self.arena_width.saturating_sub(1);
1521                    Some("west")
1522                } else {
1523                    None
1524                }
1525            }
1526            _ => None,
1527        }
1528    }
1529
1530    /// Remaining safe arena area.
1531    pub fn safe_area(&self) -> u32 {
1532        self.arena_width.saturating_mul(self.arena_height)
1533    }
1534
1535    /// Fraction of arena remaining.
1536    pub fn arena_fraction(&self) -> f32 {
1537        let original = self.original_width * self.original_height;
1538        if original == 0 { return 0.0; }
1539        self.safe_area() as f32 / original as f32
1540    }
1541
1542    /// Is a position within the safe zone?
1543    pub fn is_safe(&self, x: u32, y: u32) -> bool {
1544        x >= self.consumed_west
1545            && x < self.original_width - self.consumed_east
1546            && y >= self.consumed_north
1547            && y < self.original_height - self.consumed_south
1548    }
1549
1550    /// Enable void projectiles (phase 2).
1551    pub fn enable_projectiles(&mut self) {
1552        self.void_projectiles = true;
1553    }
1554
1555    /// Serpent emerges (phase 3).
1556    pub fn emerge_serpent(&mut self, damage: f32) {
1557        self.serpent_emerged = true;
1558        self.serpent_attack_damage = damage;
1559    }
1560}
1561
1562impl Default for VoidSerpentState {
1563    fn default() -> Self { Self::new(20, 20) }
1564}
1565
1566// ── Prime Factorial Boss ─────────────────────────────────────────────────────────
1567
1568/// State for the Prime Factorial Boss.
1569/// HP is a large prime. Deals damage in factorial sequences.
1570/// Can only be damaged by prime-numbered damage values.
1571/// Phase 2: arithmetic puzzle mechanic.
1572#[derive(Debug, Clone)]
1573pub struct PrimeFactorialState {
1574    /// Current position in the factorial damage sequence.
1575    pub factorial_index: u32,
1576    /// Cached factorial values.
1577    pub factorial_cache: Vec<f32>,
1578    /// Whether the arithmetic puzzle mode is active (phase 2).
1579    pub puzzle_active: bool,
1580    /// Current puzzle target factors.
1581    pub puzzle_target_factors: Vec<u32>,
1582    /// How many puzzles solved.
1583    pub puzzles_solved: u32,
1584    /// RNG for puzzle generation.
1585    pub rng_state: u64,
1586}
1587
1588impl PrimeFactorialState {
1589    pub fn new() -> Self {
1590        // Pre-compute factorials: 1!, 2!, 3!, 4!, 5!, 6!, 7!
1591        let factorials = vec![1.0, 2.0, 6.0, 24.0, 120.0, 720.0, 5040.0];
1592        Self {
1593            factorial_index: 0,
1594            factorial_cache: factorials,
1595            puzzle_active: false,
1596            puzzle_target_factors: Vec::new(),
1597            puzzles_solved: 0,
1598            rng_state: 0xA01E_0013,
1599        }
1600    }
1601
1602    fn next_rng(&mut self) -> u64 {
1603        self.rng_state ^= self.rng_state << 13;
1604        self.rng_state ^= self.rng_state >> 7;
1605        self.rng_state ^= self.rng_state << 17;
1606        self.rng_state
1607    }
1608
1609    /// Get the next damage value in the factorial sequence.
1610    pub fn next_factorial_damage(&mut self) -> f32 {
1611        let idx = self.factorial_index as usize;
1612        let damage = if idx < self.factorial_cache.len() {
1613            self.factorial_cache[idx]
1614        } else {
1615            // Cap at last cached value.
1616            *self.factorial_cache.last().unwrap_or(&1.0)
1617        };
1618        self.factorial_index += 1;
1619        // Cycle back after reaching the end.
1620        if self.factorial_index as usize >= self.factorial_cache.len() {
1621            self.factorial_index = 0;
1622        }
1623        damage
1624    }
1625
1626    /// Check if a damage value is prime.
1627    pub fn is_prime(n: u32) -> bool {
1628        if n < 2 {
1629            return false;
1630        }
1631        if n == 2 || n == 3 {
1632            return true;
1633        }
1634        if n % 2 == 0 || n % 3 == 0 {
1635            return false;
1636        }
1637        let mut i = 5u32;
1638        while i.saturating_mul(i) <= n {
1639            if n % i == 0 || n % (i + 2) == 0 {
1640                return false;
1641            }
1642            i += 6;
1643        }
1644        true
1645    }
1646
1647    /// Filter incoming damage: only prime values deal damage.
1648    pub fn filter_damage(&self, raw_damage: f32) -> f32 {
1649        let rounded = raw_damage.round() as u32;
1650        if Self::is_prime(rounded) {
1651            raw_damage
1652        } else {
1653            0.0 // non-prime damage is nullified
1654        }
1655    }
1656
1657    /// Generate an arithmetic puzzle (phase 2).
1658    /// Player must deal damage that factors to these specific numbers.
1659    pub fn generate_puzzle(&mut self) -> Vec<u32> {
1660        let small_primes = [2, 3, 5, 7, 11, 13];
1661        let count = 2 + (self.puzzles_solved.min(3) as usize); // 2-5 factors
1662        let mut factors = Vec::new();
1663        for _ in 0..count {
1664            let idx = (self.next_rng() as usize) % small_primes.len();
1665            factors.push(small_primes[idx]);
1666        }
1667        self.puzzle_target_factors = factors.clone();
1668        self.puzzle_active = true;
1669        factors
1670    }
1671
1672    /// Check if player's damage solves the current puzzle.
1673    pub fn check_puzzle_solution(&mut self, damage: u32) -> bool {
1674        if !self.puzzle_active || self.puzzle_target_factors.is_empty() {
1675            return false;
1676        }
1677
1678        // Check if damage equals the product of target factors.
1679        let target: u32 = self.puzzle_target_factors.iter().product();
1680        if damage == target {
1681            self.puzzles_solved += 1;
1682            self.puzzle_active = false;
1683            self.puzzle_target_factors.clear();
1684            true
1685        } else {
1686            false
1687        }
1688    }
1689
1690    /// Get a large prime for boss HP based on tier.
1691    pub fn prime_hp(tier: u32) -> f32 {
1692        match tier {
1693            1 => 997.0,
1694            2 => 4999.0,
1695            3 => 10_007.0,
1696            4 => 49_999.0,  // not actually prime, but close
1697            _ => 99_991.0,
1698        }
1699    }
1700}
1701
1702impl Default for PrimeFactorialState {
1703    fn default() -> Self { Self::new() }
1704}
1705
1706// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1707// Boss Mechanic State (union of all boss-specific states)
1708// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1709
1710/// Union of all boss-specific mechanic states.
1711#[derive(Debug, Clone)]
1712pub enum BossMechanicState {
1713    Mirror(MirrorBossState),
1714    Null(NullBossState),
1715    Committee(CommitteeBossState),
1716    FibonacciHydra(FibonacciHydraState),
1717    Eigenstate(EigenstateBossState),
1718    Ouroboros(OuroborosBossState),
1719    AlgorithmReborn(AlgorithmRebornState),
1720    ChaosWeaver(ChaosWeaverState),
1721    VoidSerpent(VoidSerpentState),
1722    PrimeFactorial(PrimeFactorialState),
1723}
1724
1725// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1726// Boss Events
1727// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1728
1729/// Events emitted during a boss encounter.
1730#[derive(Debug, Clone)]
1731pub enum BossEvent {
1732    /// Boss entered a new phase.
1733    PhaseChange {
1734        new_phase: u32,
1735        transition: PhaseTransition,
1736        dialogue: String,
1737    },
1738    /// Boss used a special ability.
1739    SpecialAbility {
1740        ability: SpecialAbility,
1741        description: String,
1742    },
1743    /// Boss speaks dialogue.
1744    Dialogue(String),
1745    /// Music should change.
1746    MusicChange(MusicType),
1747    /// Arena was modified.
1748    ArenaModification(ArenaMod),
1749    /// Boss is defeated — victory rewards.
1750    VictoryReward {
1751        boss_type: BossType,
1752        loot: Vec<BossLootEntry>,
1753        xp_reward: u64,
1754    },
1755    /// UI element erased (Null boss).
1756    UiErased(EraseTarget),
1757    /// UI elements restored (Null boss death).
1758    UiRestored(Vec<EraseTarget>),
1759    /// Hydra split into new heads.
1760    HydraSplit { parent_id: u32, child_ids: (u32, u32) },
1761    /// Quantum form collapsed.
1762    QuantumCollapse(QuantumForm),
1763    /// Rules changed (Ouroboros/ChaosWeaver).
1764    RulesChanged(String),
1765    /// Ability locked (Null boss).
1766    AbilityLocked(u32),
1767    /// Committee vote result.
1768    CommitteeVoteResult(CommitteeAction),
1769    /// Arena shrunk (Void Serpent).
1770    ArenaShrunk { direction: String, remaining_fraction: f32 },
1771    /// Puzzle generated (PrimeFactorial).
1772    PuzzleGenerated(Vec<u32>),
1773    /// Puzzle solved.
1774    PuzzleSolved,
1775    /// Boss defeated.
1776    BossDefeated(BossType),
1777}
1778
1779// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1780// Boss Encounter
1781// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1782
1783/// A live boss encounter.
1784#[derive(Clone)]
1785pub struct BossEncounter {
1786    /// The boss entity.
1787    pub entity: AmorphousEntity,
1788    /// Boss profile data.
1789    pub profile: BossProfile,
1790    /// Phase controller.
1791    pub phase_controller: BossPhaseController,
1792    /// Boss-specific mechanic state.
1793    pub mechanic_state: BossMechanicState,
1794    /// Turn counter.
1795    pub turn_count: u32,
1796    /// Cumulative damage log.
1797    pub damage_log: Vec<f32>,
1798    /// Floor this encounter is on (for scaling).
1799    pub floor: u32,
1800    /// Whether the encounter is finished.
1801    pub finished: bool,
1802    /// Combat stats for the boss.
1803    pub boss_stats: CombatStats,
1804}
1805
1806impl BossEncounter {
1807    /// Drive the encounter forward. Processes AI, checks phases, applies mechanics.
1808    pub fn update(&mut self, dt: f32, player_actions: &[RecordedAction]) -> Vec<BossEvent> {
1809        let mut events = Vec::new();
1810
1811        if self.finished {
1812            return events;
1813        }
1814
1815        // Record player actions for bosses that care.
1816        for action in player_actions {
1817            self.record_player_action(action.clone());
1818        }
1819
1820        // Update phase transition animation.
1821        if self.phase_controller.is_transitioning() {
1822            self.phase_controller.update_transition(dt);
1823            return events; // skip AI during transitions
1824        }
1825
1826        // Check for phase transition.
1827        let hp_frac = self.entity.hp_frac();
1828        if let Some(phase) = self.phase_controller.check_transition(hp_frac) {
1829            let phase_num = phase.phase_number;
1830            let transition = phase.transition_animation;
1831            let dialogue = phase.dialogue_on_enter.clone();
1832
1833            events.push(BossEvent::PhaseChange {
1834                new_phase: phase_num,
1835                transition,
1836                dialogue: dialogue.clone(),
1837            });
1838
1839            if !dialogue.is_empty() {
1840                events.push(BossEvent::Dialogue(dialogue));
1841            }
1842
1843            // Trigger phase-specific state changes.
1844            self.on_phase_enter(phase_num, &mut events);
1845        }
1846
1847        // Run boss-specific mechanic logic.
1848        self.tick_mechanic(dt, &mut events);
1849
1850        // Check for death.
1851        if self.entity.is_dead() {
1852            self.on_death(&mut events);
1853        }
1854
1855        // Update entity visuals.
1856        self.entity.tick(dt, self.entity.age);
1857
1858        self.turn_count += 1;
1859        events
1860    }
1861
1862    /// Record a player action for boss mechanics.
1863    fn record_player_action(&mut self, action: RecordedAction) {
1864        match &mut self.mechanic_state {
1865            BossMechanicState::Mirror(state) => {
1866                state.record_action(action);
1867            }
1868            BossMechanicState::AlgorithmReborn(state) => {
1869                state.record_action(action.action_type);
1870            }
1871            _ => {}
1872        }
1873    }
1874
1875    /// Handle phase-specific state changes when entering a new phase.
1876    fn on_phase_enter(&mut self, phase_num: u32, events: &mut Vec<BossEvent>) {
1877        match &mut self.mechanic_state {
1878            BossMechanicState::Mirror(state) => {
1879                if phase_num == 2 {
1880                    state.enter_phase2();
1881                } else if phase_num == 3 {
1882                    state.enter_phase3();
1883                }
1884            }
1885            BossMechanicState::Null(_state) => {
1886                // Null boss phases are handled in tick_mechanic.
1887            }
1888            BossMechanicState::Committee(state) => {
1889                if phase_num == 2 {
1890                    state.enable_ghost_voting();
1891                } else if phase_num == 3 {
1892                    let combined_hp = state.merge_judges();
1893                    events.push(BossEvent::Dialogue(
1894                        format!("The judges merge! Combined HP: {:.0}", combined_hp),
1895                    ));
1896                }
1897            }
1898            BossMechanicState::Eigenstate(state) => {
1899                if phase_num == 2 {
1900                    state.add_evasion_form();
1901                } else if phase_num == 3 {
1902                    let copy_hp = state.entangle(self.entity.hp);
1903                    events.push(BossEvent::SpecialAbility {
1904                        ability: SpecialAbility::Entangle,
1905                        description: format!("Entangled copy spawned with {:.0} HP", copy_hp),
1906                    });
1907                }
1908            }
1909            BossMechanicState::Ouroboros(state) => {
1910                if phase_num == 2 {
1911                    state.enter_phase2();
1912                    events.push(BossEvent::RulesChanged(
1913                        "The rules of damage and healing flicker...".into(),
1914                    ));
1915                }
1916            }
1917            BossMechanicState::AlgorithmReborn(state) => {
1918                if phase_num == 2 {
1919                    state.enter_phase2();
1920                    events.push(BossEvent::Dialogue(
1921                        "I have studied your every move.".into(),
1922                    ));
1923                } else if phase_num == 3 {
1924                    state.enter_phase3();
1925                    events.push(BossEvent::Dialogue(
1926                        "I know what you will do before you do it.".into(),
1927                    ));
1928                }
1929            }
1930            BossMechanicState::ChaosWeaver(state) => {
1931                if phase_num == 2 {
1932                    state.enable_tile_randomization();
1933                    events.push(BossEvent::ArenaModification(
1934                        ArenaMod::HazardTiles { element: Element::Entropy, count: 10 },
1935                    ));
1936                }
1937            }
1938            BossMechanicState::VoidSerpent(state) => {
1939                if phase_num == 2 {
1940                    state.enable_projectiles();
1941                } else if phase_num == 3 {
1942                    state.emerge_serpent(80.0);
1943                    events.push(BossEvent::Dialogue(
1944                        "The Void Serpent emerges from the darkness!".into(),
1945                    ));
1946                }
1947            }
1948            BossMechanicState::PrimeFactorial(state) => {
1949                if phase_num == 2 {
1950                    let factors = state.generate_puzzle();
1951                    events.push(BossEvent::PuzzleGenerated(factors));
1952                }
1953            }
1954            _ => {}
1955        }
1956    }
1957
1958    /// Tick boss-specific mechanic logic each turn.
1959    fn tick_mechanic(&mut self, _dt: f32, events: &mut Vec<BossEvent>) {
1960        let phase_num = self.phase_controller.current_phase_number();
1961
1962        match &mut self.mechanic_state {
1963            BossMechanicState::Mirror(state) => {
1964                if let Some(action) = state.get_mirrored_action(self.turn_count) {
1965                    events.push(BossEvent::SpecialAbility {
1966                        ability: SpecialAbility::MirrorCopy { depth: state.buffer_depth },
1967                        description: format!("Mirror copies: {:?}", action.action_type),
1968                    });
1969                }
1970            }
1971            BossMechanicState::Null(state) => {
1972                match phase_num {
1973                    1 => {
1974                        let erased = state.erase_buffs(3); // assume 3 active buffs
1975                        if erased > 0 {
1976                            events.push(BossEvent::SpecialAbility {
1977                                ability: SpecialAbility::Erase(EraseTarget::PlayerBuffs),
1978                                description: format!("Erased {} buff(s)", erased),
1979                            });
1980                        }
1981                    }
1982                    2 => {
1983                        let target = state.erase_ui_element();
1984                        events.push(BossEvent::UiErased(target));
1985                    }
1986                    3 => {
1987                        if let Some(slot) = state.lock_random_ability(6) {
1988                            events.push(BossEvent::AbilityLocked(slot));
1989                        }
1990                    }
1991                    _ => {}
1992                }
1993            }
1994            BossMechanicState::Committee(state) => {
1995                if !state.merged {
1996                    let hp_frac = self.entity.hp_frac();
1997                    let action = state.conduct_vote(hp_frac);
1998                    events.push(BossEvent::CommitteeVoteResult(action));
1999                }
2000            }
2001            BossMechanicState::FibonacciHydra(state) => {
2002                // Check for dead heads that can split.
2003                let dead_heads: Vec<u32> = state
2004                    .heads
2005                    .iter()
2006                    .filter(|h| !h.alive && h.depth < state.max_depth)
2007                    .map(|h| h.id)
2008                    .collect();
2009
2010                for head_id in dead_heads {
2011                    if let Some((a, b)) = state.try_split(head_id) {
2012                        events.push(BossEvent::HydraSplit {
2013                            parent_id: head_id,
2014                            child_ids: (a, b),
2015                        });
2016                    }
2017                }
2018
2019                if state.is_defeated() {
2020                    self.entity.hp = 0.0; // ensure entity death triggers
2021                }
2022            }
2023            BossMechanicState::Eigenstate(state) => {
2024                // Auto-unobserve after a turn.
2025                if state.observed {
2026                    state.unobserve();
2027                }
2028            }
2029            BossMechanicState::Ouroboros(state) => {
2030                let swapped = state.advance_turn();
2031                if swapped {
2032                    let msg = if state.reversed {
2033                        "The rules twist back... damage heals, healing harms."
2034                    } else {
2035                        "For a brief moment, the rules return to normal..."
2036                    };
2037                    events.push(BossEvent::RulesChanged(msg.into()));
2038                }
2039            }
2040            BossMechanicState::AlgorithmReborn(state) => {
2041                let hp_frac = self.entity.hp_frac();
2042                state.update_degradation(hp_frac);
2043
2044                if state.markov_mode {
2045                    if let Some(predicted) = state.predict_next() {
2046                        let counter = AlgorithmRebornState::counter_for(&predicted);
2047                        events.push(BossEvent::SpecialAbility {
2048                            ability: SpecialAbility::MarkovPredict,
2049                            description: format!(
2050                                "Algorithm predicts {:?}, counters with {:?}",
2051                                predicted, counter
2052                            ),
2053                        });
2054                    }
2055                } else if state.counter_mode {
2056                    if let Some(most_used) = state.most_used_action() {
2057                        let counter = AlgorithmRebornState::counter_for(&most_used);
2058                        events.push(BossEvent::SpecialAbility {
2059                            ability: SpecialAbility::CounterPredict,
2060                            description: format!(
2061                                "Algorithm counters your favorite: {:?} with {:?}",
2062                                most_used, counter
2063                            ),
2064                        });
2065                    }
2066                }
2067            }
2068            BossMechanicState::ChaosWeaver(state) => {
2069                // Scramble weaknesses every 3 turns.
2070                if self.turn_count % 3 == 0 {
2071                    state.scramble_weaknesses();
2072                    events.push(BossEvent::RulesChanged(
2073                        "Element weaknesses have been scrambled!".into(),
2074                    ));
2075                }
2076                // Swap ability slots every 5 turns.
2077                if self.turn_count % 5 == 0 {
2078                    let (a, b) = state.swap_ability_slots(6);
2079                    events.push(BossEvent::RulesChanged(
2080                        format!("Ability slots {} and {} swapped!", a, b),
2081                    ));
2082                }
2083            }
2084            BossMechanicState::VoidSerpent(state) => {
2085                if let Some(direction) = state.consume_edge() {
2086                    let frac = state.arena_fraction();
2087                    events.push(BossEvent::ArenaShrunk {
2088                        direction: direction.to_string(),
2089                        remaining_fraction: frac,
2090                    });
2091                }
2092            }
2093            BossMechanicState::PrimeFactorial(state) => {
2094                let damage = state.next_factorial_damage();
2095                events.push(BossEvent::SpecialAbility {
2096                    ability: SpecialAbility::FactorialStrike {
2097                        sequence_index: state.factorial_index,
2098                    },
2099                    description: format!("Factorial strike: {:.0} damage!", damage),
2100                });
2101            }
2102        }
2103    }
2104
2105    /// Handle boss death.
2106    fn on_death(&mut self, events: &mut Vec<BossEvent>) {
2107        self.finished = true;
2108
2109        // Boss-specific death effects.
2110        if let BossMechanicState::Null(state) = &mut self.mechanic_state {
2111            let restored = state.restore_all();
2112            events.push(BossEvent::UiRestored(restored));
2113        }
2114
2115        events.push(BossEvent::BossDefeated(self.profile.boss_type));
2116
2117        // Loot reward.
2118        events.push(BossEvent::VictoryReward {
2119            boss_type: self.profile.boss_type,
2120            loot: self.profile.loot_table.clone(),
2121            xp_reward: (self.profile.tier as u64) * 500 + (self.floor as u64) * 100,
2122        });
2123    }
2124}
2125
2126// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2127// Boss Encounter Manager
2128// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2129
2130/// Factory and manager for boss encounters.
2131pub struct BossEncounterManager;
2132
2133impl BossEncounterManager {
2134    /// Create a new boss encounter.
2135    pub fn start_encounter(
2136        boss_type: BossType,
2137        floor: u32,
2138        player_stats: &CombatStats,
2139    ) -> BossEncounter {
2140        let profile = Self::build_profile(boss_type);
2141        let scaled_hp = profile.scaled_hp(floor);
2142        let scaled_damage = profile.scaled_damage(floor);
2143
2144        let mut entity = AmorphousEntity::new(profile.name.clone(), glam::Vec3::ZERO);
2145        entity.hp = scaled_hp;
2146        entity.max_hp = scaled_hp;
2147
2148        let mut boss_stats = CombatStats {
2149            attack: scaled_damage,
2150            max_hp: scaled_hp,
2151            hp: scaled_hp,
2152            level: floor,
2153            crit_chance: 0.1,
2154            crit_mult: 2.5,
2155            ..CombatStats::default()
2156        };
2157
2158        // Scale boss slightly based on player stats.
2159        boss_stats.armor = player_stats.attack * 0.3;
2160
2161        let phase_controller = BossPhaseController::new(profile.phases.clone());
2162        let mechanic_state = Self::create_mechanic_state(boss_type, scaled_hp);
2163
2164        BossEncounter {
2165            entity,
2166            profile,
2167            phase_controller,
2168            mechanic_state,
2169            turn_count: 0,
2170            damage_log: Vec::new(),
2171            floor,
2172            finished: false,
2173            boss_stats,
2174        }
2175    }
2176
2177    /// Build the full profile for a boss type.
2178    fn build_profile(boss_type: BossType) -> BossProfile {
2179        match boss_type {
2180            BossType::Mirror => Self::mirror_profile(),
2181            BossType::Null => Self::null_profile(),
2182            BossType::Committee => Self::committee_profile(),
2183            BossType::FibonacciHydra => Self::fibonacci_hydra_profile(),
2184            BossType::Eigenstate => Self::eigenstate_profile(),
2185            BossType::Ouroboros => Self::ouroboros_profile(),
2186            BossType::AlgorithmReborn => Self::algorithm_reborn_profile(),
2187            BossType::ChaosWeaver => Self::chaos_weaver_profile(),
2188            BossType::VoidSerpent => Self::void_serpent_profile(),
2189            BossType::PrimeFactorial => Self::prime_factorial_profile(),
2190        }
2191    }
2192
2193    /// Create the appropriate mechanic state for a boss type.
2194    fn create_mechanic_state(boss_type: BossType, base_hp: f32) -> BossMechanicState {
2195        match boss_type {
2196            BossType::Mirror => BossMechanicState::Mirror(MirrorBossState::new()),
2197            BossType::Null => BossMechanicState::Null(NullBossState::new()),
2198            BossType::Committee => BossMechanicState::Committee(CommitteeBossState::new()),
2199            BossType::FibonacciHydra => {
2200                BossMechanicState::FibonacciHydra(FibonacciHydraState::new(base_hp))
2201            }
2202            BossType::Eigenstate => BossMechanicState::Eigenstate(EigenstateBossState::new()),
2203            BossType::Ouroboros => BossMechanicState::Ouroboros(OuroborosBossState::new()),
2204            BossType::AlgorithmReborn => {
2205                BossMechanicState::AlgorithmReborn(AlgorithmRebornState::new())
2206            }
2207            BossType::ChaosWeaver => BossMechanicState::ChaosWeaver(ChaosWeaverState::new()),
2208            BossType::VoidSerpent => {
2209                BossMechanicState::VoidSerpent(VoidSerpentState::new(20, 20))
2210            }
2211            BossType::PrimeFactorial => {
2212                BossMechanicState::PrimeFactorial(PrimeFactorialState::new())
2213            }
2214        }
2215    }
2216
2217    // ── Individual boss profiles ──────────────────────────────────────────────
2218
2219    fn mirror_profile() -> BossProfile {
2220        BossProfile {
2221            boss_type: BossType::Mirror,
2222            name: "The Mirror".into(),
2223            title: "Reflection of Self".into(),
2224            hp_base: 800.0,
2225            damage_base: 15.0,
2226            tier: 1,
2227            phases: vec![
2228                BossPhase::new(1, 1.0)
2229                    .with_behavior(BehaviorPattern::Standard)
2230                    .with_ability(SpecialAbility::MirrorCopy { depth: 3 })
2231                    .with_dialogue("I am you, delayed."),
2232                BossPhase::new(2, 0.5)
2233                    .with_behavior(BehaviorPattern::Calculated)
2234                    .with_damage(1.3)
2235                    .with_ability(SpecialAbility::MirrorCopy { depth: 3 })
2236                    .with_transition(PhaseTransition::GlyphReorganize)
2237                    .with_dialogue("Now I wear your strength as well."),
2238                BossPhase::new(3, 0.25)
2239                    .with_behavior(BehaviorPattern::Aggressive)
2240                    .with_speed(1.5)
2241                    .with_damage(1.5)
2242                    .with_ability(SpecialAbility::MirrorCopy { depth: 3 })
2243                    .with_transition(PhaseTransition::PowerUp)
2244                    .with_dialogue("We act as one. There is no delay."),
2245            ],
2246            special_mechanics: vec![
2247                "Copies player abilities with 1-turn delay".into(),
2248                "Phase 2: copies player stats".into(),
2249                "Phase 3: simultaneous mirrored actions".into(),
2250            ],
2251            loot_table: vec![BossLootEntry {
2252                item_name: "Shard of Reflection".into(),
2253                drop_chance: 1.0,
2254                min_quantity: 1,
2255                max_quantity: 1,
2256            }],
2257            music_type: MusicType::Ominous,
2258            arena_mods: vec![ArenaMod::None],
2259            resistance: ResistanceProfile::boss_resist(),
2260        }
2261    }
2262
2263    fn null_profile() -> BossProfile {
2264        BossProfile {
2265            boss_type: BossType::Null,
2266            name: "The Null".into(),
2267            title: "The Eraser of Meaning".into(),
2268            hp_base: 1200.0,
2269            damage_base: 12.0,
2270            tier: 2,
2271            phases: vec![
2272                BossPhase::new(1, 1.0)
2273                    .with_behavior(BehaviorPattern::Passive)
2274                    .with_ability(SpecialAbility::Erase(EraseTarget::PlayerBuffs))
2275                    .with_dialogue("Let us subtract."),
2276                BossPhase::new(2, 0.6)
2277                    .with_behavior(BehaviorPattern::Standard)
2278                    .with_damage(1.2)
2279                    .with_ability(SpecialAbility::Erase(EraseTarget::HpBar))
2280                    .with_transition(PhaseTransition::Dissolve)
2281                    .with_dialogue("Your interface is a luxury I revoke."),
2282                BossPhase::new(3, 0.3)
2283                    .with_behavior(BehaviorPattern::Aggressive)
2284                    .with_speed(1.3)
2285                    .with_damage(1.4)
2286                    .with_ability(SpecialAbility::LockAbility)
2287                    .with_transition(PhaseTransition::Dissolve)
2288                    .with_dialogue("Even your skills are expendable."),
2289            ],
2290            special_mechanics: vec![
2291                "Phase 1: erases player buffs".into(),
2292                "Phase 2: erases UI elements".into(),
2293                "Phase 3: locks random abilities each turn".into(),
2294                "On death: all erased elements restored".into(),
2295            ],
2296            loot_table: vec![BossLootEntry {
2297                item_name: "Void Fragment".into(),
2298                drop_chance: 1.0,
2299                min_quantity: 1,
2300                max_quantity: 2,
2301            }],
2302            music_type: MusicType::Silence,
2303            arena_mods: vec![ArenaMod::DarkenVision { radius_reduction: 3.0 }],
2304            resistance: ResistanceProfile::void_entity(),
2305        }
2306    }
2307
2308    fn committee_profile() -> BossProfile {
2309        BossProfile {
2310            boss_type: BossType::Committee,
2311            name: "The Committee".into(),
2312            title: "Democracy of Violence".into(),
2313            hp_base: 1500.0,
2314            damage_base: 18.0,
2315            tier: 2,
2316            phases: vec![
2317                BossPhase::new(1, 1.0)
2318                    .with_behavior(BehaviorPattern::Standard)
2319                    .with_ability(SpecialAbility::CommitteeVote)
2320                    .with_dialogue("The vote is called. All in favor?"),
2321                BossPhase::new(2, 0.5)
2322                    .with_behavior(BehaviorPattern::Calculated)
2323                    .with_damage(1.2)
2324                    .with_ability(SpecialAbility::CommitteeVote)
2325                    .with_transition(PhaseTransition::GlyphReorganize)
2326                    .with_dialogue("The dead still have a voice here."),
2327                BossPhase::new(3, 0.2)
2328                    .with_behavior(BehaviorPattern::Berserk)
2329                    .with_speed(1.4)
2330                    .with_damage(1.8)
2331                    .with_ability(SpecialAbility::CommitteeVote)
2332                    .with_transition(PhaseTransition::Merge)
2333                    .with_dialogue("We are ONE. The motion carries unanimously."),
2334            ],
2335            special_mechanics: vec![
2336                "5 judges vote on each action".into(),
2337                "Kill judges to change vote balance".into(),
2338                "Phase 2: dead judges vote as ghosts".into(),
2339                "Phase 3: remaining judges merge".into(),
2340            ],
2341            loot_table: vec![BossLootEntry {
2342                item_name: "Gavel of Authority".into(),
2343                drop_chance: 0.8,
2344                min_quantity: 1,
2345                max_quantity: 1,
2346            }],
2347            music_type: MusicType::Orchestral,
2348            arena_mods: vec![ArenaMod::None],
2349            resistance: ResistanceProfile::neutral(),
2350        }
2351    }
2352
2353    fn fibonacci_hydra_profile() -> BossProfile {
2354        BossProfile {
2355            boss_type: BossType::FibonacciHydra,
2356            name: "Fibonacci Hydra".into(),
2357            title: "The Golden Recursion".into(),
2358            hp_base: 1000.0,
2359            damage_base: 14.0,
2360            tier: 3,
2361            phases: vec![
2362                BossPhase::new(1, 1.0)
2363                    .with_behavior(BehaviorPattern::Standard)
2364                    .with_ability(SpecialAbility::FibonacciSplit)
2365                    .with_dialogue("Cut one, and two shall grow."),
2366                BossPhase::new(2, 0.618)
2367                    .with_behavior(BehaviorPattern::Aggressive)
2368                    .with_speed(1.2)
2369                    .with_damage(1.3)
2370                    .with_ability(SpecialAbility::FibonacciSplit)
2371                    .with_transition(PhaseTransition::Split)
2372                    .with_dialogue("The golden ratio demands expansion!"),
2373                BossPhase::new(3, 0.3)
2374                    .with_behavior(BehaviorPattern::Berserk)
2375                    .with_speed(1.5)
2376                    .with_damage(1.5)
2377                    .with_ability(SpecialAbility::FibonacciSplit)
2378                    .with_transition(PhaseTransition::Split)
2379                    .with_dialogue("We are legion! 1, 1, 2, 3, 5, 8, 13..."),
2380            ],
2381            special_mechanics: vec![
2382                "Splits into 2 on death at 61.8% HP each".into(),
2383                "Max depth 5 (up to 32 heads)".into(),
2384                "All heads share a damage pool".into(),
2385            ],
2386            loot_table: vec![BossLootEntry {
2387                item_name: "Golden Spiral Shell".into(),
2388                drop_chance: 1.0,
2389                min_quantity: 1,
2390                max_quantity: 1,
2391            }],
2392            music_type: MusicType::Frenetic,
2393            arena_mods: vec![ArenaMod::None],
2394            resistance: ResistanceProfile::neutral(),
2395        }
2396    }
2397
2398    fn eigenstate_profile() -> BossProfile {
2399        BossProfile {
2400            boss_type: BossType::Eigenstate,
2401            name: "The Eigenstate".into(),
2402            title: "Collapsed Possibility".into(),
2403            hp_base: 1100.0,
2404            damage_base: 20.0,
2405            tier: 3,
2406            phases: vec![
2407                BossPhase::new(1, 1.0)
2408                    .with_behavior(BehaviorPattern::Erratic)
2409                    .with_ability(SpecialAbility::QuantumCollapse)
2410                    .with_dialogue("Observe me and I become certain. Look away and I am everything."),
2411                BossPhase::new(2, 0.5)
2412                    .with_behavior(BehaviorPattern::Calculated)
2413                    .with_speed(1.3)
2414                    .with_damage(1.4)
2415                    .with_ability(SpecialAbility::QuantumCollapse)
2416                    .with_transition(PhaseTransition::Dissolve)
2417                    .with_dialogue("A third possibility emerges."),
2418                BossPhase::new(3, 0.25)
2419                    .with_behavior(BehaviorPattern::Aggressive)
2420                    .with_speed(1.5)
2421                    .with_damage(1.6)
2422                    .with_ability(SpecialAbility::Entangle)
2423                    .with_transition(PhaseTransition::Split)
2424                    .with_dialogue("We are entangled now. Harm one, harm both."),
2425            ],
2426            special_mechanics: vec![
2427                "Exists in superposition of Attack/Defense".into(),
2428                "Targeting collapses to one form".into(),
2429                "Phase 2: 3 forms (adds Evasion)".into(),
2430                "Phase 3: entangled copy mirrors all damage".into(),
2431            ],
2432            loot_table: vec![BossLootEntry {
2433                item_name: "Quantum Shard".into(),
2434                drop_chance: 1.0,
2435                min_quantity: 1,
2436                max_quantity: 1,
2437            }],
2438            music_type: MusicType::Glitch,
2439            arena_mods: vec![ArenaMod::None],
2440            resistance: ResistanceProfile::neutral(),
2441        }
2442    }
2443
2444    fn ouroboros_profile() -> BossProfile {
2445        BossProfile {
2446            boss_type: BossType::Ouroboros,
2447            name: "Ouroboros".into(),
2448            title: "The Serpent That Devours".into(),
2449            hp_base: 1300.0,
2450            damage_base: 16.0,
2451            tier: 3,
2452            phases: vec![
2453                BossPhase::new(1, 1.0)
2454                    .with_behavior(BehaviorPattern::Standard)
2455                    .with_ability(SpecialAbility::ReverseSemantic)
2456                    .with_dialogue("What heals you, harms me. What harms you, heals me. Or is it the other way?"),
2457                BossPhase::new(2, 0.5)
2458                    .with_behavior(BehaviorPattern::Erratic)
2459                    .with_speed(1.2)
2460                    .with_damage(1.3)
2461                    .with_ability(SpecialAbility::ReverseSemantic)
2462                    .with_transition(PhaseTransition::GlyphReorganize)
2463                    .with_dialogue("The rules flicker between truth and lies."),
2464            ],
2465            special_mechanics: vec![
2466                "Damage/heal semantics reversed".into(),
2467                "Boss heals by dealing damage".into(),
2468                "Player heals by dealing damage to boss".into(),
2469                "Phase 2: rules randomly swap to normal for 2 turns".into(),
2470            ],
2471            loot_table: vec![BossLootEntry {
2472                item_name: "Ouroboros Ring".into(),
2473                drop_chance: 1.0,
2474                min_quantity: 1,
2475                max_quantity: 1,
2476            }],
2477            music_type: MusicType::Reversed,
2478            arena_mods: vec![ArenaMod::None],
2479            resistance: ResistanceProfile::boss_resist(),
2480        }
2481    }
2482
2483    fn algorithm_reborn_profile() -> BossProfile {
2484        BossProfile {
2485            boss_type: BossType::AlgorithmReborn,
2486            name: "Algorithm Reborn".into(),
2487            title: "Final Proof".into(),
2488            hp_base: 3000.0,
2489            damage_base: 25.0,
2490            tier: 5,
2491            phases: vec![
2492                BossPhase::new(1, 1.0)
2493                    .with_behavior(BehaviorPattern::Standard)
2494                    .with_ability(SpecialAbility::None)
2495                    .with_dialogue("Begin the final computation."),
2496                BossPhase::new(2, 0.65)
2497                    .with_behavior(BehaviorPattern::Calculated)
2498                    .with_speed(1.2)
2499                    .with_damage(1.4)
2500                    .with_ability(SpecialAbility::CounterPredict)
2501                    .with_transition(PhaseTransition::PowerUp)
2502                    .with_dialogue("I have studied your every move."),
2503                BossPhase::new(3, 0.3)
2504                    .with_behavior(BehaviorPattern::Calculated)
2505                    .with_speed(1.5)
2506                    .with_damage(1.8)
2507                    .with_ability(SpecialAbility::MarkovPredict)
2508                    .with_transition(PhaseTransition::GlyphReorganize)
2509                    .with_dialogue("I know what you will do before you do it."),
2510            ],
2511            special_mechanics: vec![
2512                "Tracks player action frequency".into(),
2513                "Phase 2: counters most-used action".into(),
2514                "Phase 3: Markov-chain prediction of next action".into(),
2515                "No HP bar: judge by visual degradation".into(),
2516            ],
2517            loot_table: vec![
2518                BossLootEntry {
2519                    item_name: "Core of the Algorithm".into(),
2520                    drop_chance: 1.0,
2521                    min_quantity: 1,
2522                    max_quantity: 1,
2523                },
2524                BossLootEntry {
2525                    item_name: "Proof of Completion".into(),
2526                    drop_chance: 1.0,
2527                    min_quantity: 1,
2528                    max_quantity: 1,
2529                },
2530            ],
2531            music_type: MusicType::Algorithmic,
2532            arena_mods: vec![
2533                ArenaMod::HazardTiles { element: Element::Entropy, count: 5 },
2534            ],
2535            resistance: ResistanceProfile::boss_resist(),
2536        }
2537    }
2538
2539    fn chaos_weaver_profile() -> BossProfile {
2540        BossProfile {
2541            boss_type: BossType::ChaosWeaver,
2542            name: "Chaos Weaver".into(),
2543            title: "Unraveler of Rules".into(),
2544            hp_base: 1400.0,
2545            damage_base: 17.0,
2546            tier: 4,
2547            phases: vec![
2548                BossPhase::new(1, 1.0)
2549                    .with_behavior(BehaviorPattern::Erratic)
2550                    .with_ability(SpecialAbility::RuleRandomize)
2551                    .with_dialogue("The rules are merely suggestions."),
2552                BossPhase::new(2, 0.45)
2553                    .with_behavior(BehaviorPattern::Erratic)
2554                    .with_speed(1.3)
2555                    .with_damage(1.5)
2556                    .with_ability(SpecialAbility::RuleRandomize)
2557                    .with_transition(PhaseTransition::Teleport)
2558                    .with_dialogue("Even the ground beneath you obeys me now."),
2559            ],
2560            special_mechanics: vec![
2561                "Scrambles element weakness chart".into(),
2562                "Swaps player ability slots".into(),
2563                "Randomizes damage number display".into(),
2564                "Phase 2: randomizes tile effects".into(),
2565            ],
2566            loot_table: vec![BossLootEntry {
2567                item_name: "Thread of Chaos".into(),
2568                drop_chance: 1.0,
2569                min_quantity: 1,
2570                max_quantity: 3,
2571            }],
2572            music_type: MusicType::Chaotic,
2573            arena_mods: vec![ArenaMod::None],
2574            resistance: ResistanceProfile::chaos_rift(),
2575        }
2576    }
2577
2578    fn void_serpent_profile() -> BossProfile {
2579        BossProfile {
2580            boss_type: BossType::VoidSerpent,
2581            name: "Void Serpent".into(),
2582            title: "Consumer of Arenas".into(),
2583            hp_base: 1600.0,
2584            damage_base: 20.0,
2585            tier: 4,
2586            phases: vec![
2587                BossPhase::new(1, 1.0)
2588                    .with_behavior(BehaviorPattern::Passive)
2589                    .with_ability(SpecialAbility::ConsumeArena { columns: 1 })
2590                    .with_dialogue("The void hungers."),
2591                BossPhase::new(2, 0.55)
2592                    .with_behavior(BehaviorPattern::Standard)
2593                    .with_speed(1.2)
2594                    .with_damage(1.3)
2595                    .with_ability(SpecialAbility::ConsumeArena { columns: 2 })
2596                    .with_transition(PhaseTransition::Dissolve)
2597                    .with_dialogue("The void spits back what it cannot digest."),
2598                BossPhase::new(3, 0.25)
2599                    .with_behavior(BehaviorPattern::Berserk)
2600                    .with_speed(1.5)
2601                    .with_damage(1.8)
2602                    .with_ability(SpecialAbility::ConsumeArena { columns: 3 })
2603                    .with_transition(PhaseTransition::Teleport)
2604                    .with_dialogue("I emerge from the nothing!"),
2605            ],
2606            special_mechanics: vec![
2607                "Each turn, edge tiles become void".into(),
2608                "Phase 2: void tiles spit projectiles".into(),
2609                "Phase 3: serpent emerges for direct attacks".into(),
2610                "Arena shrinks continuously".into(),
2611            ],
2612            loot_table: vec![BossLootEntry {
2613                item_name: "Void Scale".into(),
2614                drop_chance: 1.0,
2615                min_quantity: 1,
2616                max_quantity: 2,
2617            }],
2618            music_type: MusicType::MinimalDrone,
2619            arena_mods: vec![
2620                ArenaMod::ShrinkEdges { rate_per_turn: 1 },
2621                ArenaMod::DarkenVision { radius_reduction: 2.0 },
2622            ],
2623            resistance: ResistanceProfile::void_entity(),
2624        }
2625    }
2626
2627    fn prime_factorial_profile() -> BossProfile {
2628        BossProfile {
2629            boss_type: BossType::PrimeFactorial,
2630            name: "Prime Factorial".into(),
2631            title: "The Indivisible Explosion".into(),
2632            hp_base: PrimeFactorialState::prime_hp(4),
2633            damage_base: 22.0,
2634            tier: 4,
2635            phases: vec![
2636                BossPhase::new(1, 1.0)
2637                    .with_behavior(BehaviorPattern::Standard)
2638                    .with_ability(SpecialAbility::FactorialStrike { sequence_index: 0 })
2639                    .with_dialogue("Only primes can wound me."),
2640                BossPhase::new(2, 0.5)
2641                    .with_behavior(BehaviorPattern::Calculated)
2642                    .with_speed(1.2)
2643                    .with_damage(1.5)
2644                    .with_ability(SpecialAbility::ArithmeticPuzzle {
2645                        target_factors: vec![2, 3, 5],
2646                    })
2647                    .with_transition(PhaseTransition::PowerUp)
2648                    .with_dialogue("Solve the equation or perish in the factorial!"),
2649            ],
2650            special_mechanics: vec![
2651                "HP is a large prime number".into(),
2652                "Damage in factorial sequences: 1, 2, 6, 24, 120...".into(),
2653                "Only prime-numbered damage hurts this boss".into(),
2654                "Phase 2: arithmetic puzzle mechanic".into(),
2655            ],
2656            loot_table: vec![BossLootEntry {
2657                item_name: "Prime Gemstone".into(),
2658                drop_chance: 1.0,
2659                min_quantity: 1,
2660                max_quantity: 1,
2661            }],
2662            music_type: MusicType::Crescendo,
2663            arena_mods: vec![ArenaMod::None],
2664            resistance: ResistanceProfile::neutral(),
2665        }
2666    }
2667}
2668
2669// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2670// Tests
2671// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2672
2673#[cfg(test)]
2674mod tests {
2675    use super::*;
2676
2677    // ── Phase Controller ──
2678
2679    #[test]
2680    fn phase_controller_starts_at_first_phase() {
2681        let phases = vec![
2682            BossPhase::new(1, 1.0),
2683            BossPhase::new(2, 0.5),
2684            BossPhase::new(3, 0.25),
2685        ];
2686        let ctrl = BossPhaseController::new(phases);
2687        assert_eq!(ctrl.current_phase_number(), 1);
2688    }
2689
2690    #[test]
2691    fn phase_controller_transitions_on_hp_drop() {
2692        let phases = vec![
2693            BossPhase::new(1, 1.0),
2694            BossPhase::new(2, 0.5),
2695            BossPhase::new(3, 0.25),
2696        ];
2697        let mut ctrl = BossPhaseController::new(phases);
2698
2699        // At full HP, no transition.
2700        assert!(ctrl.check_transition(0.8).is_none());
2701
2702        // Drop below 50%: transition to phase 2.
2703        let result = ctrl.check_transition(0.4);
2704        assert!(result.is_some());
2705        assert_eq!(ctrl.current_phase_number(), 2);
2706    }
2707
2708    #[test]
2709    fn phase_controller_skips_to_deepest_crossed_phase() {
2710        let phases = vec![
2711            BossPhase::new(1, 1.0),
2712            BossPhase::new(2, 0.5),
2713            BossPhase::new(3, 0.25),
2714        ];
2715        let mut ctrl = BossPhaseController::new(phases);
2716
2717        // Drop below 25% in one hit: should skip to phase 3.
2718        let result = ctrl.check_transition(0.1);
2719        assert!(result.is_some());
2720        assert_eq!(ctrl.current_phase_number(), 3);
2721    }
2722
2723    #[test]
2724    fn phase_controller_transition_animation() {
2725        let phases = vec![
2726            BossPhase::new(1, 1.0),
2727            BossPhase::new(2, 0.5),
2728        ];
2729        let mut ctrl = BossPhaseController::new(phases);
2730        ctrl.check_transition(0.4);
2731
2732        assert!(ctrl.is_transitioning());
2733        assert!(!ctrl.update_transition(0.5)); // not done
2734        assert!(ctrl.is_transitioning());
2735        assert!(ctrl.update_transition(1.5)); // done (total > 1.5)
2736        assert!(!ctrl.is_transitioning());
2737    }
2738
2739    // ── Mirror Boss ──
2740
2741    #[test]
2742    fn mirror_records_and_retrieves_actions() {
2743        let mut state = MirrorBossState::new();
2744        state.record_action(RecordedAction {
2745            action_type: PlayerActionType::Attack,
2746            turn: 5,
2747            damage_dealt: 20.0,
2748            element: Some(Element::Fire),
2749        });
2750
2751        // With delay of 1, action from turn 5 is available at turn 6.
2752        let mirrored = state.get_mirrored_action(6);
2753        assert!(mirrored.is_some());
2754        assert_eq!(mirrored.unwrap().action_type, PlayerActionType::Attack);
2755    }
2756
2757    #[test]
2758    fn mirror_buffer_limited_to_depth() {
2759        let mut state = MirrorBossState::new();
2760        for i in 0..10 {
2761            state.record_action(RecordedAction {
2762                action_type: PlayerActionType::Attack,
2763                turn: i,
2764                damage_dealt: 10.0,
2765                element: None,
2766            });
2767        }
2768        assert_eq!(state.mirror_buffer.len(), 3); // max depth
2769    }
2770
2771    #[test]
2772    fn mirror_phase2_copies_stats() {
2773        let mut state = MirrorBossState::new();
2774        let stats = CombatStats { attack: 50.0, armor: 30.0, ..CombatStats::default() };
2775        state.enter_phase2();
2776        state.copy_stats(&stats);
2777        assert!(state.copying_stats);
2778        assert!((state.copied_attack - 50.0).abs() < f32::EPSILON);
2779    }
2780
2781    #[test]
2782    fn mirror_phase3_simultaneous() {
2783        let mut state = MirrorBossState::new();
2784        assert!(!state.simultaneous);
2785        state.enter_phase3();
2786        assert!(state.simultaneous);
2787    }
2788
2789    // ── Null Boss ──
2790
2791    #[test]
2792    fn null_erase_buffs() {
2793        let mut state = NullBossState::new();
2794        let erased = state.erase_buffs(5);
2795        assert_eq!(erased, 2); // max 2 per turn
2796        assert_eq!(state.buffs_erased, 2);
2797    }
2798
2799    #[test]
2800    fn null_erase_ui_element() {
2801        let mut state = NullBossState::new();
2802        let target = state.erase_ui_element();
2803        assert!(!state.erased_ui.is_empty());
2804        assert!(state.erased_ui.contains(&target));
2805    }
2806
2807    #[test]
2808    fn null_lock_ability() {
2809        let mut state = NullBossState::new();
2810        let slot = state.lock_random_ability(6);
2811        assert!(slot.is_some());
2812        assert_eq!(state.locked_abilities.len(), 1);
2813    }
2814
2815    #[test]
2816    fn null_restore_all() {
2817        let mut state = NullBossState::new();
2818        state.erase_ui_element();
2819        state.lock_random_ability(6);
2820        let restored = state.restore_all();
2821        assert!(!restored.is_empty());
2822        assert!(state.erased_ui.is_empty());
2823        assert!(state.locked_abilities.is_empty());
2824    }
2825
2826    // ── Committee Boss ──
2827
2828    #[test]
2829    fn committee_has_five_judges() {
2830        let state = CommitteeBossState::new();
2831        assert_eq!(state.judges.len(), 5);
2832        assert_eq!(state.alive_count(), 5);
2833    }
2834
2835    #[test]
2836    fn committee_vote_returns_action() {
2837        let mut state = CommitteeBossState::new();
2838        let action = state.conduct_vote(0.8);
2839        // Just verify it returns a valid action.
2840        let _ = action;
2841    }
2842
2843    #[test]
2844    fn committee_killing_judges() {
2845        let mut state = CommitteeBossState::new();
2846        let killed = state.judges[0].take_damage(500.0);
2847        assert!(killed);
2848        assert_eq!(state.alive_count(), 4);
2849    }
2850
2851    #[test]
2852    fn committee_ghost_voting() {
2853        let mut state = CommitteeBossState::new();
2854        state.judges[0].take_damage(500.0);
2855        state.enable_ghost_voting();
2856        assert!(state.judges[0].ghost);
2857        // Ghost should still participate in vote.
2858        let _action = state.conduct_vote(0.5);
2859    }
2860
2861    #[test]
2862    fn committee_merge() {
2863        let mut state = CommitteeBossState::new();
2864        let combined = state.merge_judges();
2865        assert!(combined > 0.0);
2866        assert!(state.merged);
2867    }
2868
2869    // ── Fibonacci Hydra ──
2870
2871    #[test]
2872    fn fibonacci_hydra_starts_with_one_head() {
2873        let state = FibonacciHydraState::new(1000.0);
2874        assert_eq!(state.alive_count(), 1);
2875    }
2876
2877    #[test]
2878    fn fibonacci_hydra_split_on_death() {
2879        let mut state = FibonacciHydraState::new(1000.0);
2880        // Kill head 0.
2881        state.damage_head(0, 1000.0);
2882        assert_eq!(state.alive_count(), 0);
2883
2884        // Split.
2885        let result = state.try_split(0);
2886        assert!(result.is_some());
2887        let (a, b) = result.unwrap();
2888
2889        // Two new heads at 61.8% HP.
2890        assert_eq!(state.alive_count(), 2);
2891        let head_a = state.heads.iter().find(|h| h.id == a).unwrap();
2892        assert!((head_a.max_hp - 618.0).abs() < 1.0);
2893        let head_b = state.heads.iter().find(|h| h.id == b).unwrap();
2894        assert!((head_b.max_hp - 618.0).abs() < 1.0);
2895    }
2896
2897    #[test]
2898    fn fibonacci_hydra_max_depth() {
2899        let mut state = FibonacciHydraState::new(1000.0);
2900        state.max_depth = 2; // limit for test speed
2901
2902        // Kill and split head 0.
2903        state.damage_head(0, 1000.0);
2904        let (a, b) = state.try_split(0).unwrap();
2905
2906        // Kill and split children.
2907        state.damage_head(a, 1000.0);
2908        let (c, d) = state.try_split(a).unwrap();
2909
2910        state.damage_head(b, 1000.0);
2911        let (e, f) = state.try_split(b).unwrap();
2912
2913        // Kill depth-2 heads: they should NOT split further.
2914        state.damage_head(c, 1000.0);
2915        assert!(state.try_split(c).is_none());
2916        state.damage_head(d, 1000.0);
2917        state.damage_head(e, 1000.0);
2918        state.damage_head(f, 1000.0);
2919
2920        assert!(state.is_defeated());
2921    }
2922
2923    #[test]
2924    fn fibonacci_hydra_max_possible_heads() {
2925        let state = FibonacciHydraState::new(1000.0);
2926        assert_eq!(state.max_possible_heads(), 32); // 2^5
2927    }
2928
2929    // ── Eigenstate Boss ──
2930
2931    #[test]
2932    fn eigenstate_superposition_by_default() {
2933        let state = EigenstateBossState::new();
2934        assert!(state.in_superposition());
2935        assert!(state.collapsed_form.is_none());
2936    }
2937
2938    #[test]
2939    fn eigenstate_observation_collapses() {
2940        let mut state = EigenstateBossState::new();
2941        let form = state.observe();
2942        assert!(!state.in_superposition());
2943        assert_eq!(state.collapsed_form, Some(form));
2944    }
2945
2946    #[test]
2947    fn eigenstate_unobserve_returns_to_superposition() {
2948        let mut state = EigenstateBossState::new();
2949        state.observe();
2950        state.unobserve();
2951        assert!(state.in_superposition());
2952    }
2953
2954    #[test]
2955    fn eigenstate_phase2_adds_evasion() {
2956        let mut state = EigenstateBossState::new();
2957        assert_eq!(state.forms.len(), 2);
2958        state.add_evasion_form();
2959        assert_eq!(state.forms.len(), 3);
2960    }
2961
2962    #[test]
2963    fn eigenstate_entangle() {
2964        let mut state = EigenstateBossState::new();
2965        let copy_hp = state.entangle(500.0);
2966        assert!(state.entangled);
2967        assert!((copy_hp - 500.0).abs() < f32::EPSILON);
2968    }
2969
2970    #[test]
2971    fn eigenstate_mirror_damage() {
2972        let mut state = EigenstateBossState::new();
2973        state.entangle(500.0);
2974        let mirrored = state.mirror_damage(100.0);
2975        assert!((mirrored - 100.0).abs() < f32::EPSILON);
2976        assert!((state.entangled_hp - 400.0).abs() < f32::EPSILON);
2977    }
2978
2979    // ── Ouroboros Boss ──
2980
2981    #[test]
2982    fn ouroboros_starts_reversed() {
2983        let state = OuroborosBossState::new();
2984        assert!(state.reversed);
2985    }
2986
2987    #[test]
2988    fn ouroboros_reversed_damage_heals_attacker() {
2989        let mut state = OuroborosBossState::new();
2990        let (damage, heal) = state.process_damage(100.0, true);
2991        assert!((damage - 100.0).abs() < f32::EPSILON);
2992        assert!((heal - 50.0).abs() < f32::EPSILON);
2993    }
2994
2995    #[test]
2996    fn ouroboros_reversed_heal_damages() {
2997        let state = OuroborosBossState::new();
2998        let result = state.process_heal(50.0);
2999        assert!((result - (-50.0)).abs() < f32::EPSILON);
3000    }
3001
3002    #[test]
3003    fn ouroboros_normal_rules_no_heal() {
3004        let mut state = OuroborosBossState::new();
3005        state.reversed = false;
3006        let (damage, heal) = state.process_damage(100.0, true);
3007        assert!((damage - 100.0).abs() < f32::EPSILON);
3008        assert!((heal - 0.0).abs() < f32::EPSILON);
3009    }
3010
3011    // ── Algorithm Reborn ──
3012
3013    #[test]
3014    fn algorithm_records_frequency() {
3015        let mut state = AlgorithmRebornState::new();
3016        state.record_action(PlayerActionType::Attack);
3017        state.record_action(PlayerActionType::Attack);
3018        state.record_action(PlayerActionType::Heal);
3019
3020        assert_eq!(state.action_frequency[&PlayerActionType::Attack], 2);
3021        assert_eq!(state.action_frequency[&PlayerActionType::Heal], 1);
3022    }
3023
3024    #[test]
3025    fn algorithm_most_used() {
3026        let mut state = AlgorithmRebornState::new();
3027        state.record_action(PlayerActionType::Attack);
3028        state.record_action(PlayerActionType::Attack);
3029        state.record_action(PlayerActionType::Heal);
3030
3031        assert_eq!(state.most_used_action(), Some(PlayerActionType::Attack));
3032    }
3033
3034    #[test]
3035    fn algorithm_markov_prediction() {
3036        let mut state = AlgorithmRebornState::new();
3037        // Build pattern: Attack -> Heal -> Attack -> Heal.
3038        state.record_action(PlayerActionType::Attack);
3039        state.record_action(PlayerActionType::Heal);
3040        state.record_action(PlayerActionType::Attack);
3041        state.record_action(PlayerActionType::Heal);
3042
3043        // Last action is Heal. After Heal, Attack appeared twice.
3044        let prediction = state.predict_next();
3045        assert_eq!(prediction, Some(PlayerActionType::Attack));
3046    }
3047
3048    #[test]
3049    fn algorithm_counter_for() {
3050        assert_eq!(
3051            AlgorithmRebornState::counter_for(&PlayerActionType::Attack),
3052            PlayerActionType::Defend
3053        );
3054        assert_eq!(
3055            AlgorithmRebornState::counter_for(&PlayerActionType::Heal),
3056            PlayerActionType::Attack
3057        );
3058    }
3059
3060    #[test]
3061    fn algorithm_degradation_tracks_hp() {
3062        let mut state = AlgorithmRebornState::new();
3063        state.update_degradation(0.3);
3064        assert!((state.degradation - 0.7).abs() < f32::EPSILON);
3065        assert!((state.hidden_hp_frac - 0.3).abs() < f32::EPSILON);
3066    }
3067
3068    // ── Chaos Weaver ──
3069
3070    #[test]
3071    fn chaos_weaver_scramble_weaknesses() {
3072        let mut state = ChaosWeaverState::new();
3073        state.scramble_weaknesses();
3074        assert!(!state.weakness_overrides.is_empty());
3075        assert_eq!(state.chaos_count, 1);
3076    }
3077
3078    #[test]
3079    fn chaos_weaver_swap_slots() {
3080        let mut state = ChaosWeaverState::new();
3081        let (a, b) = state.swap_ability_slots(6);
3082        assert_ne!(a, b);
3083        assert_eq!(state.slot_swaps[&a], b);
3084        assert_eq!(state.slot_swaps[&b], a);
3085    }
3086
3087    #[test]
3088    fn chaos_weaver_fake_damage() {
3089        let mut state = ChaosWeaverState::new();
3090        state.enable_damage_randomization();
3091        let fake = state.fake_damage_number(100.0);
3092        // Fake number should be different from actual (with high probability).
3093        // Just verify it's positive.
3094        assert!(fake > 0.0);
3095    }
3096
3097    // ── Void Serpent ──
3098
3099    #[test]
3100    fn void_serpent_starts_full() {
3101        let state = VoidSerpentState::new(20, 20);
3102        assert!((state.arena_fraction() - 1.0).abs() < f32::EPSILON);
3103    }
3104
3105    #[test]
3106    fn void_serpent_consume_edge_shrinks_arena() {
3107        let mut state = VoidSerpentState::new(20, 20);
3108        let dir = state.consume_edge();
3109        assert!(dir.is_some());
3110        assert!(state.arena_fraction() < 1.0);
3111    }
3112
3113    #[test]
3114    fn void_serpent_safe_zone() {
3115        let mut state = VoidSerpentState::new(20, 20);
3116        assert!(state.is_safe(10, 10));
3117        // Consume north edge.
3118        state.consumed_north = 5;
3119        state.arena_height = 15;
3120        assert!(!state.is_safe(10, 2)); // in consumed zone
3121        assert!(state.is_safe(10, 10)); // still safe
3122    }
3123
3124    #[test]
3125    fn void_serpent_emerge() {
3126        let mut state = VoidSerpentState::new(20, 20);
3127        state.emerge_serpent(100.0);
3128        assert!(state.serpent_emerged);
3129        assert!((state.serpent_attack_damage - 100.0).abs() < f32::EPSILON);
3130    }
3131
3132    // ── Prime Factorial ──
3133
3134    #[test]
3135    fn prime_check() {
3136        assert!(!PrimeFactorialState::is_prime(0));
3137        assert!(!PrimeFactorialState::is_prime(1));
3138        assert!(PrimeFactorialState::is_prime(2));
3139        assert!(PrimeFactorialState::is_prime(3));
3140        assert!(!PrimeFactorialState::is_prime(4));
3141        assert!(PrimeFactorialState::is_prime(5));
3142        assert!(PrimeFactorialState::is_prime(7));
3143        assert!(!PrimeFactorialState::is_prime(9));
3144        assert!(PrimeFactorialState::is_prime(97));
3145        assert!(PrimeFactorialState::is_prime(997));
3146    }
3147
3148    #[test]
3149    fn prime_filter_damage() {
3150        let state = PrimeFactorialState::new();
3151        assert!((state.filter_damage(7.0) - 7.0).abs() < f32::EPSILON); // prime
3152        assert!((state.filter_damage(8.0) - 0.0).abs() < f32::EPSILON); // not prime
3153    }
3154
3155    #[test]
3156    fn factorial_sequence() {
3157        let mut state = PrimeFactorialState::new();
3158        assert!((state.next_factorial_damage() - 1.0).abs() < f32::EPSILON);
3159        assert!((state.next_factorial_damage() - 2.0).abs() < f32::EPSILON);
3160        assert!((state.next_factorial_damage() - 6.0).abs() < f32::EPSILON);
3161        assert!((state.next_factorial_damage() - 24.0).abs() < f32::EPSILON);
3162        assert!((state.next_factorial_damage() - 120.0).abs() < f32::EPSILON);
3163        assert!((state.next_factorial_damage() - 720.0).abs() < f32::EPSILON);
3164        assert!((state.next_factorial_damage() - 5040.0).abs() < f32::EPSILON);
3165        // Wraps around.
3166        assert!((state.next_factorial_damage() - 1.0).abs() < f32::EPSILON);
3167    }
3168
3169    #[test]
3170    fn puzzle_generation() {
3171        let mut state = PrimeFactorialState::new();
3172        let factors = state.generate_puzzle();
3173        assert!(factors.len() >= 2);
3174        assert!(state.puzzle_active);
3175    }
3176
3177    #[test]
3178    fn puzzle_solution() {
3179        let mut state = PrimeFactorialState::new();
3180        state.puzzle_target_factors = vec![2, 3, 5];
3181        state.puzzle_active = true;
3182
3183        assert!(!state.check_puzzle_solution(29)); // wrong
3184        assert!(state.check_puzzle_solution(30));   // 2*3*5 = 30
3185        assert_eq!(state.puzzles_solved, 1);
3186        assert!(!state.puzzle_active);
3187    }
3188
3189    // ── Boss Encounter Manager ──
3190
3191    #[test]
3192    fn encounter_creates_all_boss_types() {
3193        let player_stats = CombatStats::default();
3194        for &boss_type in BossType::all() {
3195            let encounter = BossEncounterManager::start_encounter(boss_type, 1, &player_stats);
3196            assert!(!encounter.finished);
3197            assert!(encounter.entity.hp > 0.0);
3198            assert!(!encounter.profile.name.is_empty());
3199        }
3200    }
3201
3202    #[test]
3203    fn encounter_scales_with_floor() {
3204        let player_stats = CombatStats::default();
3205        let e1 = BossEncounterManager::start_encounter(BossType::Mirror, 1, &player_stats);
3206        let e10 = BossEncounterManager::start_encounter(BossType::Mirror, 10, &player_stats);
3207        assert!(e10.entity.max_hp > e1.entity.max_hp);
3208    }
3209
3210    #[test]
3211    fn encounter_update_emits_events() {
3212        let player_stats = CombatStats::default();
3213        let mut encounter = BossEncounterManager::start_encounter(
3214            BossType::ChaosWeaver, 1, &player_stats,
3215        );
3216        // Turn 0 is divisible by 3 (ChaosWeaver scrambles on turn % 3 == 0).
3217        let events = encounter.update(0.016, &[]);
3218        assert!(!events.is_empty());
3219    }
3220
3221    #[test]
3222    fn encounter_boss_death_emits_events() {
3223        let player_stats = CombatStats::default();
3224        let mut encounter = BossEncounterManager::start_encounter(
3225            BossType::Mirror, 1, &player_stats,
3226        );
3227        encounter.entity.hp = 0.0;
3228        let events = encounter.update(0.016, &[]);
3229
3230        let has_defeat = events.iter().any(|e| matches!(e, BossEvent::BossDefeated(_)));
3231        assert!(has_defeat);
3232        assert!(encounter.finished);
3233    }
3234
3235    #[test]
3236    fn encounter_null_death_restores_ui() {
3237        let player_stats = CombatStats::default();
3238        let mut encounter = BossEncounterManager::start_encounter(
3239            BossType::Null, 1, &player_stats,
3240        );
3241
3242        // Manually erase some UI elements.
3243        if let BossMechanicState::Null(ref mut state) = encounter.mechanic_state {
3244            state.erase_ui_element();
3245        }
3246
3247        encounter.entity.hp = 0.0;
3248        let events = encounter.update(0.016, &[]);
3249
3250        let has_restore = events.iter().any(|e| matches!(e, BossEvent::UiRestored(_)));
3251        assert!(has_restore);
3252    }
3253}