Skip to main content

proof_engine/character/
mod.rs

1// src/character/mod.rs
2// Character system: stats, inventory, skills, quests, and more.
3
4pub mod stats;
5pub mod inventory;
6pub mod skills;
7pub mod quests;
8
9use std::collections::HashMap;
10use stats::{StatSheet, AllResources, LevelData, StatGrowth, ModifierRegistry, ClassArchetype, StatPreset, XpCurve};
11use inventory::{EquippedItems, Inventory, Stash};
12use skills::{SkillBook, AbilityBar, CooldownTracker, ComboSystem};
13use quests::{QuestJournal, AchievementSystem, QuestTracker};
14
15// ---------------------------------------------------------------------------
16// CharacterId
17// ---------------------------------------------------------------------------
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
20pub struct CharacterId(pub u64);
21
22impl CharacterId {
23    pub fn new(id: u64) -> Self {
24        Self(id)
25    }
26    pub fn inner(self) -> u64 {
27        self.0
28    }
29}
30
31impl std::fmt::Display for CharacterId {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        write!(f, "Char({})", self.0)
34    }
35}
36
37// ---------------------------------------------------------------------------
38// CharacterKind
39// ---------------------------------------------------------------------------
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub enum CharacterKind {
43    Player,
44    NPC,
45    Monster,
46    Boss,
47    Summon,
48    Pet,
49    Merchant,
50    QuestGiver,
51}
52
53impl CharacterKind {
54    pub fn is_hostile_to_player(&self) -> bool {
55        matches!(self, CharacterKind::Monster | CharacterKind::Boss)
56    }
57
58    pub fn is_friendly_to_player(&self) -> bool {
59        matches!(self, CharacterKind::NPC | CharacterKind::Pet | CharacterKind::Merchant | CharacterKind::QuestGiver)
60    }
61
62    pub fn display_name(&self) -> &'static str {
63        match self {
64            CharacterKind::Player => "Player",
65            CharacterKind::NPC => "NPC",
66            CharacterKind::Monster => "Monster",
67            CharacterKind::Boss => "Boss",
68            CharacterKind::Summon => "Summon",
69            CharacterKind::Pet => "Pet",
70            CharacterKind::Merchant => "Merchant",
71            CharacterKind::QuestGiver => "Quest Giver",
72        }
73    }
74}
75
76// ---------------------------------------------------------------------------
77// CharacterState — the current action/animation state
78// ---------------------------------------------------------------------------
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
81pub enum CharacterState {
82    Idle,
83    Moving,
84    Attacking,
85    Casting,
86    Stunned,
87    Dead,
88    Interacting,
89    Resting,
90    Fleeing,
91    Defending,
92    Dashing,
93    Falling,
94}
95
96impl CharacterState {
97    pub fn is_alive(&self) -> bool {
98        !matches!(self, CharacterState::Dead)
99    }
100
101    pub fn can_act(&self) -> bool {
102        matches!(self, CharacterState::Idle | CharacterState::Moving | CharacterState::Defending)
103    }
104
105    pub fn can_move(&self) -> bool {
106        matches!(self, CharacterState::Idle | CharacterState::Moving | CharacterState::Fleeing)
107    }
108
109    pub fn is_incapacitated(&self) -> bool {
110        matches!(self, CharacterState::Stunned | CharacterState::Dead | CharacterState::Falling)
111    }
112
113    pub fn display_name(&self) -> &'static str {
114        match self {
115            CharacterState::Idle => "Idle",
116            CharacterState::Moving => "Moving",
117            CharacterState::Attacking => "Attacking",
118            CharacterState::Casting => "Casting",
119            CharacterState::Stunned => "Stunned",
120            CharacterState::Dead => "Dead",
121            CharacterState::Interacting => "Interacting",
122            CharacterState::Resting => "Resting",
123            CharacterState::Fleeing => "Fleeing",
124            CharacterState::Defending => "Defending",
125            CharacterState::Dashing => "Dashing",
126            CharacterState::Falling => "Falling",
127        }
128    }
129}
130
131// ---------------------------------------------------------------------------
132// CharacterRelationship + FactionSystem
133// ---------------------------------------------------------------------------
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
136pub enum CharacterRelationship {
137    Allied,
138    Neutral,
139    Hostile,
140    Feared,
141    Worshipped,
142    Ignored,
143}
144
145impl CharacterRelationship {
146    pub fn from_reputation(rep: i32) -> Self {
147        match rep {
148            i32::MIN..=-500 => CharacterRelationship::Hostile,
149            -499..=-100 => CharacterRelationship::Feared,
150            -99..=99 => CharacterRelationship::Neutral,
151            100..=499 => CharacterRelationship::Allied,
152            500..=999 => CharacterRelationship::Allied,
153            _ => CharacterRelationship::Worshipped,
154        }
155    }
156
157    pub fn is_hostile(&self) -> bool {
158        matches!(self, CharacterRelationship::Hostile | CharacterRelationship::Feared)
159    }
160
161    pub fn is_friendly(&self) -> bool {
162        matches!(self, CharacterRelationship::Allied | CharacterRelationship::Worshipped)
163    }
164}
165
166#[derive(Debug, Clone)]
167pub struct Faction {
168    pub name: String,
169    pub description: String,
170    pub default_relationship: CharacterRelationship,
171    pub allied_factions: Vec<String>,
172    pub hostile_factions: Vec<String>,
173    pub icon: char,
174}
175
176impl Faction {
177    pub fn new(name: impl Into<String>) -> Self {
178        Self {
179            name: name.into(),
180            description: String::new(),
181            default_relationship: CharacterRelationship::Neutral,
182            allied_factions: Vec::new(),
183            hostile_factions: Vec::new(),
184            icon: '⚑',
185        }
186    }
187
188    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
189        self.description = desc.into();
190        self
191    }
192
193    pub fn set_default(mut self, rel: CharacterRelationship) -> Self {
194        self.default_relationship = rel;
195        self
196    }
197
198    pub fn allied_with(mut self, faction: impl Into<String>) -> Self {
199        self.allied_factions.push(faction.into());
200        self
201    }
202
203    pub fn hostile_to(mut self, faction: impl Into<String>) -> Self {
204        self.hostile_factions.push(faction.into());
205        self
206    }
207}
208
209#[derive(Debug, Clone, Default)]
210pub struct FactionSystem {
211    pub factions: HashMap<String, Faction>,
212    pub player_reputation: HashMap<String, i32>, // faction_name -> reputation
213}
214
215impl FactionSystem {
216    pub fn new() -> Self {
217        let mut sys = Self::default();
218        sys.register_defaults();
219        sys
220    }
221
222    fn register_defaults(&mut self) {
223        self.add_faction(Faction::new("Adventurers Guild")
224            .with_description("The guild of brave heroes.")
225            .set_default(CharacterRelationship::Neutral));
226        self.add_faction(Faction::new("Merchants League")
227            .with_description("Trade confederation.")
228            .set_default(CharacterRelationship::Neutral));
229        self.add_faction(Faction::new("Dark Brotherhood")
230            .with_description("An assassin cult.")
231            .set_default(CharacterRelationship::Hostile));
232        self.add_faction(Faction::new("Kingdom Guard")
233            .with_description("Defenders of the realm.")
234            .set_default(CharacterRelationship::Neutral)
235            .allied_with("Adventurers Guild")
236            .hostile_to("Dark Brotherhood"));
237        self.add_faction(Faction::new("Undead Horde")
238            .with_description("Shambling undead.")
239            .set_default(CharacterRelationship::Hostile));
240    }
241
242    pub fn add_faction(&mut self, faction: Faction) {
243        self.factions.insert(faction.name.clone(), faction);
244    }
245
246    pub fn get_reputation(&self, faction: &str) -> i32 {
247        *self.player_reputation.get(faction).unwrap_or(&0)
248    }
249
250    pub fn modify_reputation(&mut self, faction: &str, delta: i32) {
251        let rep = self.player_reputation.entry(faction.to_string()).or_insert(0);
252        *rep = (*rep + delta).clamp(-1000, 1000);
253    }
254
255    pub fn get_relationship(&self, faction: &str) -> CharacterRelationship {
256        let rep = self.get_reputation(faction);
257        let base = self.factions.get(faction)
258            .map(|f| f.default_relationship)
259            .unwrap_or(CharacterRelationship::Neutral);
260        if rep != 0 {
261            CharacterRelationship::from_reputation(rep)
262        } else {
263            base
264        }
265    }
266
267    pub fn all_faction_names(&self) -> Vec<&str> {
268        self.factions.keys().map(|s| s.as_str()).collect()
269    }
270
271    pub fn faction_relationship_between(&self, faction_a: &str, faction_b: &str) -> CharacterRelationship {
272        if let Some(f) = self.factions.get(faction_a) {
273            if f.allied_factions.iter().any(|x| x == faction_b) {
274                return CharacterRelationship::Allied;
275            }
276            if f.hostile_factions.iter().any(|x| x == faction_b) {
277                return CharacterRelationship::Hostile;
278            }
279        }
280        CharacterRelationship::Neutral
281    }
282}
283
284// ---------------------------------------------------------------------------
285// AppearanceData — visual representation
286// ---------------------------------------------------------------------------
287
288#[derive(Debug, Clone)]
289pub struct AppearanceData {
290    pub glyph_char: char,
291    pub color: (f32, f32, f32, f32), // RGBA [0,1]
292    pub scale: f32,
293    pub formation_preset: String,
294    pub glow_color: (f32, f32, f32),
295    pub glow_radius: f32,
296    pub title: String,
297    pub portrait_char: char,
298}
299
300impl AppearanceData {
301    pub fn new(glyph_char: char) -> Self {
302        Self {
303            glyph_char,
304            color: (1.0, 1.0, 1.0, 1.0),
305            scale: 1.0,
306            formation_preset: "diamond".to_string(),
307            glow_color: (1.0, 1.0, 1.0),
308            glow_radius: 0.5,
309            title: String::new(),
310            portrait_char: '@',
311        }
312    }
313
314    pub fn with_color(mut self, r: f32, g: f32, b: f32) -> Self {
315        self.color = (r, g, b, 1.0);
316        self
317    }
318
319    pub fn with_scale(mut self, scale: f32) -> Self {
320        self.scale = scale;
321        self
322    }
323
324    pub fn with_formation(mut self, preset: impl Into<String>) -> Self {
325        self.formation_preset = preset.into();
326        self
327    }
328
329    pub fn with_glow(mut self, r: f32, g: f32, b: f32, radius: f32) -> Self {
330        self.glow_color = (r, g, b);
331        self.glow_radius = radius;
332        self
333    }
334
335    pub fn with_title(mut self, title: impl Into<String>) -> Self {
336        self.title = title.into();
337        self
338    }
339}
340
341impl Default for AppearanceData {
342    fn default() -> Self {
343        Self::new('@')
344    }
345}
346
347// ---------------------------------------------------------------------------
348// CharacterController — movement, physics, input
349// ---------------------------------------------------------------------------
350
351#[derive(Debug, Clone)]
352pub struct CharacterController {
353    pub position: (f32, f32, f32),
354    pub velocity: (f32, f32, f32),
355    pub acceleration: (f32, f32, f32),
356    pub friction: f32,
357    pub max_speed: f32,
358    pub jump_force: f32,
359    pub is_grounded: bool,
360    pub facing_direction: f32, // radians
361    pub collision_radius: f32,
362    pub collision_height: f32,
363    pub gravity: f32,
364    pub can_fly: bool,
365    pub input_move: (f32, f32), // normalized move input
366    pub input_jump: bool,
367    pub input_dash: bool,
368    pub dash_cooldown: f32,
369    pub dash_speed: f32,
370    pub dash_duration: f32,
371    pub dash_timer: f32,
372    pub knockback: (f32, f32, f32),
373    pub knockback_decay: f32,
374}
375
376impl CharacterController {
377    pub fn new(position: (f32, f32, f32)) -> Self {
378        Self {
379            position,
380            velocity: (0.0, 0.0, 0.0),
381            acceleration: (0.0, 0.0, 0.0),
382            friction: 0.85,
383            max_speed: 5.0,
384            jump_force: 8.0,
385            is_grounded: true,
386            facing_direction: 0.0,
387            collision_radius: 0.4,
388            collision_height: 1.8,
389            gravity: -20.0,
390            can_fly: false,
391            input_move: (0.0, 0.0),
392            input_jump: false,
393            input_dash: false,
394            dash_cooldown: 0.0,
395            dash_speed: 15.0,
396            dash_duration: 0.15,
397            dash_timer: 0.0,
398            knockback: (0.0, 0.0, 0.0),
399            knockback_decay: 0.8,
400        }
401    }
402
403    pub fn tick(&mut self, dt: f32, move_speed: f32) {
404        // Apply input to velocity
405        let speed = move_speed.max(0.0);
406        let (ix, iz) = self.input_move;
407        let len = (ix * ix + iz * iz).sqrt();
408        let (nx, nz) = if len > 0.001 {
409            (ix / len, iz / len)
410        } else {
411            (0.0, 0.0)
412        };
413
414        self.velocity.0 = nx * speed;
415        self.velocity.2 = nz * speed;
416
417        // Dash
418        if self.input_dash && self.dash_cooldown <= 0.0 {
419            self.velocity.0 = nx * self.dash_speed;
420            self.velocity.2 = nz * self.dash_speed;
421            self.dash_timer = self.dash_duration;
422            self.dash_cooldown = 0.8; // 0.8s dash cooldown
423        }
424        if self.dash_timer > 0.0 {
425            self.dash_timer -= dt;
426        }
427        if self.dash_cooldown > 0.0 {
428            self.dash_cooldown -= dt;
429        }
430
431        // Gravity
432        if !self.is_grounded && !self.can_fly {
433            self.velocity.1 += self.gravity * dt;
434        }
435
436        // Jump
437        if self.input_jump && self.is_grounded {
438            self.velocity.1 = self.jump_force;
439            self.is_grounded = false;
440        }
441
442        // Apply knockback
443        self.velocity.0 += self.knockback.0;
444        self.velocity.1 += self.knockback.1;
445        self.velocity.2 += self.knockback.2;
446        self.knockback.0 *= self.knockback_decay;
447        self.knockback.1 *= self.knockback_decay;
448        self.knockback.2 *= self.knockback_decay;
449
450        // Clamp horizontal speed
451        let hspd = (self.velocity.0 * self.velocity.0 + self.velocity.2 * self.velocity.2).sqrt();
452        if hspd > self.max_speed && self.dash_timer <= 0.0 {
453            let scale = self.max_speed / hspd;
454            self.velocity.0 *= scale;
455            self.velocity.2 *= scale;
456        }
457
458        // Integrate position
459        self.position.0 += self.velocity.0 * dt;
460        self.position.1 += self.velocity.1 * dt;
461        self.position.2 += self.velocity.2 * dt;
462
463        // Ground collision (simple flat floor at y=0)
464        if self.position.1 < 0.0 {
465            self.position.1 = 0.0;
466            self.velocity.1 = 0.0;
467            self.is_grounded = true;
468        }
469
470        // Friction (horizontal)
471        if self.is_grounded {
472            self.velocity.0 *= self.friction;
473            self.velocity.2 *= self.friction;
474        }
475
476        // Update facing direction from velocity
477        if self.velocity.0.abs() > 0.01 || self.velocity.2.abs() > 0.01 {
478            self.facing_direction = self.velocity.2.atan2(self.velocity.0);
479        }
480
481        // Reset inputs
482        self.input_move = (0.0, 0.0);
483        self.input_jump = false;
484        self.input_dash = false;
485    }
486
487    pub fn apply_knockback(&mut self, direction: (f32, f32, f32), force: f32) {
488        let (dx, dy, dz) = direction;
489        let len = (dx * dx + dy * dy + dz * dz).sqrt().max(0.001);
490        self.knockback.0 += dx / len * force;
491        self.knockback.1 += dy / len * force;
492        self.knockback.2 += dz / len * force;
493    }
494
495    pub fn distance_to(&self, other: &CharacterController) -> f32 {
496        let dx = self.position.0 - other.position.0;
497        let dy = self.position.1 - other.position.1;
498        let dz = self.position.2 - other.position.2;
499        (dx * dx + dy * dy + dz * dz).sqrt()
500    }
501
502    pub fn is_colliding_with(&self, other: &CharacterController) -> bool {
503        let dist = self.distance_to(other);
504        dist < self.collision_radius + other.collision_radius
505    }
506
507    pub fn resolve_collision(&mut self, other: &mut CharacterController) {
508        if !self.is_colliding_with(other) { return; }
509        let dx = self.position.0 - other.position.0;
510        let dz = self.position.2 - other.position.2;
511        let dist = (dx * dx + dz * dz).sqrt().max(0.001);
512        let overlap = self.collision_radius + other.collision_radius - dist;
513        let nx = dx / dist;
514        let nz = dz / dist;
515        self.position.0 += nx * overlap * 0.5;
516        self.position.2 += nz * overlap * 0.5;
517        other.position.0 -= nx * overlap * 0.5;
518        other.position.2 -= nz * overlap * 0.5;
519    }
520}
521
522// ---------------------------------------------------------------------------
523// InputBinding — key bindings for player character
524// ---------------------------------------------------------------------------
525
526#[derive(Debug, Clone)]
527pub struct InputBinding {
528    pub move_up: u32,
529    pub move_down: u32,
530    pub move_left: u32,
531    pub move_right: u32,
532    pub jump: u32,
533    pub dash: u32,
534    pub interact: u32,
535    pub ability_slots: [u32; 12],
536    pub inventory: u32,
537    pub map: u32,
538    pub quest_log: u32,
539    pub character_screen: u32,
540}
541
542impl Default for InputBinding {
543    fn default() -> Self {
544        Self {
545            move_up: b'w' as u32,
546            move_down: b's' as u32,
547            move_left: b'a' as u32,
548            move_right: b'd' as u32,
549            jump: b' ' as u32,
550            dash: b'e' as u32,
551            interact: b'f' as u32,
552            ability_slots: [b'1' as u32, b'2' as u32, b'3' as u32, b'4' as u32,
553                b'5' as u32, b'6' as u32, b'7' as u32, b'8' as u32,
554                b'9' as u32, b'0' as u32, b'q' as u32, b'r' as u32],
555            inventory: b'i' as u32,
556            map: b'm' as u32,
557            quest_log: b'j' as u32,
558            character_screen: b'c' as u32,
559        }
560    }
561}
562
563// ---------------------------------------------------------------------------
564// CharacterEvents
565// ---------------------------------------------------------------------------
566
567#[derive(Debug, Clone)]
568pub enum CharacterEvent {
569    Spawned(CharacterId),
570    Died(CharacterId),
571    LevelUp { id: CharacterId, new_level: u32 },
572    SkillLearned { id: CharacterId, skill_name: String },
573    ItemPickup { id: CharacterId, item_name: String },
574    TookDamage { id: CharacterId, amount: f32, source: String },
575    Healed { id: CharacterId, amount: f32 },
576    StateChanged { id: CharacterId, old: CharacterState, new: CharacterState },
577    QuestAccepted { id: CharacterId, quest_name: String },
578    QuestCompleted { id: CharacterId, quest_name: String },
579    AchievementUnlocked { id: CharacterId, achievement_name: String },
580    FactionRepChange { id: CharacterId, faction: String, delta: i32 },
581}
582
583// ---------------------------------------------------------------------------
584// Character — the master struct combining all subsystems
585// ---------------------------------------------------------------------------
586
587#[derive(Debug, Clone)]
588pub struct Character {
589    pub id: CharacterId,
590    pub name: String,
591    pub kind: CharacterKind,
592    pub state: CharacterState,
593    pub archetype: ClassArchetype,
594
595    // Core subsystems
596    pub stats: StatSheet,
597    pub resources: AllResources,
598    pub level_data: LevelData,
599    pub modifier_registry: ModifierRegistry,
600    pub stat_growth: StatGrowth,
601
602    // Equipment and items
603    pub equipped: EquippedItems,
604    pub inventory: Inventory,
605    pub stash: Stash,
606    pub gold: u64,
607
608    // Skills
609    pub skill_book: SkillBook,
610    pub ability_bar: AbilityBar,
611    pub cooldowns: CooldownTracker,
612    pub combos: ComboSystem,
613
614    // Quests
615    pub journal: QuestJournal,
616    pub achievements: AchievementSystem,
617    pub quest_tracker: QuestTracker,
618
619    // Social
620    pub faction: Option<String>,
621    pub faction_system: FactionSystem,
622
623    // Movement / physics
624    pub controller: CharacterController,
625    pub input_binding: InputBinding,
626
627    // Visual
628    pub appearance: AppearanceData,
629
630    // Meta
631    pub is_player_controlled: bool,
632    pub event_queue: Vec<CharacterEvent>,
633    pub tags: Vec<String>,
634    pub metadata: HashMap<String, String>,
635}
636
637impl Character {
638    pub fn new(id: CharacterId, name: impl Into<String>, kind: CharacterKind, archetype: ClassArchetype) -> Self {
639        let stats = StatPreset::for_class(archetype, 1);
640        let resources = AllResources::from_sheet(&stats, 1);
641        let level_data = LevelData::new(XpCurve::default(), 100);
642        let stat_growth = StatPreset::growth_for(archetype);
643        let appearance = match kind {
644            CharacterKind::Player => AppearanceData::new('@').with_color(0.0, 0.8, 1.0),
645            CharacterKind::Monster => AppearanceData::new('M').with_color(1.0, 0.2, 0.2),
646            CharacterKind::Boss => AppearanceData::new('B').with_color(1.0, 0.5, 0.0).with_scale(1.5),
647            CharacterKind::NPC => AppearanceData::new('N').with_color(0.8, 0.8, 0.2),
648            CharacterKind::Summon => AppearanceData::new('S').with_color(0.5, 0.5, 1.0),
649            CharacterKind::Pet => AppearanceData::new('p').with_color(0.5, 1.0, 0.5),
650            CharacterKind::Merchant => AppearanceData::new('$').with_color(0.9, 0.7, 0.2),
651            CharacterKind::QuestGiver => AppearanceData::new('Q').with_color(0.2, 1.0, 0.5),
652        };
653        Self {
654            id,
655            name: name.into(),
656            kind,
657            state: CharacterState::Idle,
658            archetype,
659            stats,
660            resources,
661            level_data,
662            modifier_registry: ModifierRegistry::new(),
663            stat_growth,
664            equipped: EquippedItems::new(),
665            inventory: Inventory::new(40, 200.0),
666            stash: Stash::new(),
667            gold: 0,
668            skill_book: SkillBook::new(),
669            ability_bar: AbilityBar::new(),
670            cooldowns: CooldownTracker::new(),
671            combos: ComboSystem::new(),
672            journal: QuestJournal::new(),
673            achievements: AchievementSystem::new(),
674            quest_tracker: QuestTracker::new(5),
675            faction: None,
676            faction_system: FactionSystem::new(),
677            controller: CharacterController::new((0.0, 0.0, 0.0)),
678            input_binding: InputBinding::default(),
679            appearance,
680            is_player_controlled: matches!(kind, CharacterKind::Player),
681            event_queue: Vec::new(),
682            tags: Vec::new(),
683            metadata: HashMap::new(),
684        }
685    }
686
687    /// Tick all time-dependent subsystems by dt seconds.
688    pub fn tick(&mut self, dt: f32) {
689        if !self.state.is_alive() { return; }
690
691        // Resources regen
692        self.resources.tick(dt);
693
694        // Cooldowns
695        self.cooldowns.tick(dt);
696
697        // Combo window
698        self.combos.tick(dt);
699
700        // Quest time limits
701        let failed_quests = self.journal.tick(dt);
702        for qid in failed_quests {
703            if let Some(quest) = self.journal.failed.last() {
704                let name = quest.name.clone();
705                self.event_queue.push(CharacterEvent::QuestCompleted { id: self.id, quest_name: name });
706                let _ = qid;
707            }
708        }
709
710        // Movement
711        let move_speed = self.stats.move_speed();
712        if self.state.can_move() {
713            self.controller.tick(dt, move_speed);
714        }
715
716        // State transitions based on velocity
717        if self.state == CharacterState::Moving {
718            let (vx, _, vz) = self.controller.velocity;
719            if vx.abs() < 0.05 && vz.abs() < 0.05 {
720                self.set_state(CharacterState::Idle);
721            }
722        }
723    }
724
725    pub fn set_state(&mut self, new_state: CharacterState) {
726        if self.state == new_state { return; }
727        let old = self.state;
728        self.state = new_state;
729        self.event_queue.push(CharacterEvent::StateChanged { id: self.id, old, new: new_state });
730    }
731
732    /// Deal damage to this character. Returns true if the character died.
733    pub fn take_damage(&mut self, amount: f32, source: &str) -> bool {
734        if !self.state.is_alive() { return false; }
735
736        // Compute effective defense
737        let defense = self.stats.defense(self.equipped.total_defense());
738        let effective = (amount - defense * 0.5).max(1.0);
739
740        let actual = self.resources.hp.drain(effective);
741        self.event_queue.push(CharacterEvent::TookDamage { id: self.id, amount: actual, source: source.to_string() });
742
743        // Track in achievements
744        self.achievements.record_kill(""); // placeholder — actual kills tracked by killer
745
746        if self.resources.hp.empty() {
747            self.set_state(CharacterState::Dead);
748            self.event_queue.push(CharacterEvent::Died(self.id));
749            return true;
750        }
751        false
752    }
753
754    /// Heal this character.
755    pub fn heal(&mut self, amount: f32) -> f32 {
756        let healed = self.resources.hp.restore(amount);
757        self.event_queue.push(CharacterEvent::Healed { id: self.id, amount: healed });
758        healed
759    }
760
761    /// Gain experience, potentially levelling up.
762    pub fn gain_xp(&mut self, amount: u64) {
763        let levels = self.level_data.add_xp(amount);
764        for _ in 0..levels {
765            self.level_up();
766        }
767    }
768
769    fn level_up(&mut self) {
770        self.level_data.level_up(5, 1);
771        self.stat_growth.apply_to(&mut self.stats);
772        let new_level = self.level_data.level;
773
774        // Recalculate resource maxes
775        let max_hp = self.stats.max_hp(new_level);
776        let max_mp = self.stats.max_mp(new_level);
777        let max_st = self.stats.max_stamina(new_level);
778        self.resources.hp.set_max(max_hp, true);
779        self.resources.mp.set_max(max_mp, true);
780        self.resources.stamina.set_max(max_st, true);
781        // Restore on level up
782        self.resources.hp.fill();
783        self.resources.mp.fill();
784        self.resources.stamina.fill();
785
786        let newly_unlocked = self.achievements.check_level(new_level);
787        for ach_id in newly_unlocked {
788            if let Some(ach) = self.achievements.achievements.iter().find(|a| a.id == ach_id) {
789                let name = ach.name.clone();
790                self.event_queue.push(CharacterEvent::AchievementUnlocked { id: self.id, achievement_name: name });
791            }
792        }
793
794        self.event_queue.push(CharacterEvent::LevelUp { id: self.id, new_level });
795    }
796
797    /// Modify reputation with a faction.
798    pub fn change_reputation(&mut self, faction: &str, delta: i32) {
799        self.faction_system.modify_reputation(faction, delta);
800        self.event_queue.push(CharacterEvent::FactionRepChange { id: self.id, faction: faction.to_string(), delta });
801    }
802
803    /// Get current level.
804    pub fn level(&self) -> u32 {
805        self.level_data.level
806    }
807
808    /// Whether this character is alive.
809    pub fn is_alive(&self) -> bool {
810        self.state.is_alive()
811    }
812
813    /// Drain pending events.
814    pub fn drain_events(&mut self) -> Vec<CharacterEvent> {
815        std::mem::take(&mut self.event_queue)
816    }
817
818    pub fn add_tag(&mut self, tag: impl Into<String>) {
819        self.tags.push(tag.into());
820    }
821
822    pub fn has_tag(&self, tag: &str) -> bool {
823        self.tags.iter().any(|t| t == tag)
824    }
825
826    pub fn set_meta(&mut self, key: impl Into<String>, value: impl Into<String>) {
827        self.metadata.insert(key.into(), value.into());
828    }
829
830    pub fn get_meta(&self, key: &str) -> Option<&str> {
831        self.metadata.get(key).map(|s| s.as_str())
832    }
833}
834
835// ---------------------------------------------------------------------------
836// CharacterBundle — everything needed to spawn a character
837// ---------------------------------------------------------------------------
838
839#[derive(Debug, Clone)]
840pub struct CharacterBundle {
841    pub id: CharacterId,
842    pub name: String,
843    pub kind: CharacterKind,
844    pub archetype: ClassArchetype,
845    pub level: u32,
846    pub spawn_position: (f32, f32, f32),
847    pub faction: Option<String>,
848    pub is_player_controlled: bool,
849    pub tags: Vec<String>,
850}
851
852impl CharacterBundle {
853    pub fn player(name: impl Into<String>, archetype: ClassArchetype, pos: (f32, f32, f32)) -> Self {
854        Self {
855            id: CharacterId(1),
856            name: name.into(),
857            kind: CharacterKind::Player,
858            archetype,
859            level: 1,
860            spawn_position: pos,
861            faction: Some("Adventurers Guild".to_string()),
862            is_player_controlled: true,
863            tags: vec!["player".to_string()],
864        }
865    }
866
867    pub fn monster(name: impl Into<String>, level: u32, pos: (f32, f32, f32)) -> Self {
868        Self {
869            id: CharacterId(0), // Will be assigned by registry
870            name: name.into(),
871            kind: CharacterKind::Monster,
872            archetype: ClassArchetype::Warrior,
873            level,
874            spawn_position: pos,
875            faction: Some("Undead Horde".to_string()),
876            is_player_controlled: false,
877            tags: vec!["enemy".to_string()],
878        }
879    }
880
881    pub fn build(self, id: CharacterId) -> Character {
882        let mut ch = Character::new(id, self.name, self.kind, self.archetype);
883        ch.controller.position = self.spawn_position;
884        ch.faction = self.faction;
885        ch.is_player_controlled = self.is_player_controlled;
886        for tag in self.tags {
887            ch.tags.push(tag);
888        }
889        // Scale stats to level
890        for _ in 1..self.level {
891            ch.stat_growth.apply_to(&mut ch.stats);
892        }
893        let lv = self.level;
894        let max_hp = ch.stats.max_hp(lv);
895        let max_mp = ch.stats.max_mp(lv);
896        let max_st = ch.stats.max_stamina(lv);
897        ch.resources.hp.set_max(max_hp, false);
898        ch.resources.mp.set_max(max_mp, false);
899        ch.resources.stamina.set_max(max_st, false);
900        ch.resources.hp.fill();
901        ch.resources.mp.fill();
902        ch.resources.stamina.fill();
903        ch.level_data.level = self.level;
904        ch.event_queue.push(CharacterEvent::Spawned(id));
905        ch
906    }
907}
908
909// ---------------------------------------------------------------------------
910// CharacterRegistry — global map of all active characters
911// ---------------------------------------------------------------------------
912
913#[derive(Debug, Clone, Default)]
914pub struct CharacterRegistry {
915    pub characters: HashMap<CharacterId, Character>,
916    next_id: u64,
917}
918
919impl CharacterRegistry {
920    pub fn new() -> Self {
921        Self { characters: HashMap::new(), next_id: 1 }
922    }
923
924    pub fn next_id(&mut self) -> CharacterId {
925        let id = CharacterId(self.next_id);
926        self.next_id += 1;
927        id
928    }
929
930    pub fn spawn(&mut self, bundle: CharacterBundle) -> CharacterId {
931        let id = if bundle.id.0 == 0 {
932            self.next_id()
933        } else {
934            // Ensure next_id stays ahead of manually-assigned ids.
935            if bundle.id.0 >= self.next_id {
936                self.next_id = bundle.id.0 + 1;
937            }
938            bundle.id
939        };
940        let character = bundle.build(id);
941        self.characters.insert(id, character);
942        id
943    }
944
945    pub fn despawn(&mut self, id: CharacterId) -> Option<Character> {
946        self.characters.remove(&id)
947    }
948
949    pub fn get(&self, id: CharacterId) -> Option<&Character> {
950        self.characters.get(&id)
951    }
952
953    pub fn get_mut(&mut self, id: CharacterId) -> Option<&mut Character> {
954        self.characters.get_mut(&id)
955    }
956
957    pub fn tick_all(&mut self, dt: f32) {
958        for ch in self.characters.values_mut() {
959            ch.tick(dt);
960        }
961    }
962
963    pub fn all_alive(&self) -> impl Iterator<Item = &Character> {
964        self.characters.values().filter(|c| c.is_alive())
965    }
966
967    pub fn all_dead(&self) -> impl Iterator<Item = &Character> {
968        self.characters.values().filter(|c| !c.is_alive())
969    }
970
971    pub fn player(&self) -> Option<&Character> {
972        self.characters.values().find(|c| c.is_player_controlled)
973    }
974
975    pub fn player_mut(&mut self) -> Option<&mut Character> {
976        self.characters.values_mut().find(|c| c.is_player_controlled)
977    }
978
979    pub fn count(&self) -> usize {
980        self.characters.len()
981    }
982
983    pub fn by_kind(&self, kind: CharacterKind) -> Vec<&Character> {
984        self.characters.values().filter(|c| c.kind == kind).collect()
985    }
986
987    pub fn remove_all_dead(&mut self) {
988        self.characters.retain(|_, c| c.is_alive());
989    }
990
991    pub fn drain_all_events(&mut self) -> Vec<(CharacterId, CharacterEvent)> {
992        let mut events = Vec::new();
993        for ch in self.characters.values_mut() {
994            for ev in ch.drain_events() {
995                events.push((ch.id, ev));
996            }
997        }
998        events
999    }
1000
1001    pub fn find_in_radius(&self, center: (f32, f32, f32), radius: f32) -> Vec<CharacterId> {
1002        self.characters.values()
1003            .filter(|c| {
1004                let (cx, cy, cz) = c.controller.position;
1005                let (ox, oy, oz) = center;
1006                let dx = cx - ox; let dy = cy - oy; let dz = cz - oz;
1007                (dx*dx + dy*dy + dz*dz).sqrt() <= radius
1008            })
1009            .map(|c| c.id)
1010            .collect()
1011    }
1012}
1013
1014// ---------------------------------------------------------------------------
1015// Tests
1016// ---------------------------------------------------------------------------
1017
1018#[cfg(test)]
1019mod tests {
1020    use super::*;
1021
1022    fn make_player() -> Character {
1023        Character::new(CharacterId(1), "Hero", CharacterKind::Player, ClassArchetype::Warrior)
1024    }
1025
1026    #[test]
1027    fn test_character_creation() {
1028        let ch = make_player();
1029        assert_eq!(ch.id, CharacterId(1));
1030        assert_eq!(ch.name, "Hero");
1031        assert!(ch.is_alive());
1032    }
1033
1034    #[test]
1035    fn test_character_take_damage() {
1036        let mut ch = make_player();
1037        ch.take_damage(10.0, "test");
1038        assert!(ch.resources.hp.current < ch.resources.hp.max);
1039    }
1040
1041    #[test]
1042    fn test_character_death() {
1043        let mut ch = make_player();
1044        let died = ch.take_damage(999999.0, "one_shot");
1045        assert!(died);
1046        assert_eq!(ch.state, CharacterState::Dead);
1047    }
1048
1049    #[test]
1050    fn test_character_heal() {
1051        let mut ch = make_player();
1052        ch.resources.hp.drain(50.0);
1053        let healed = ch.heal(30.0);
1054        assert!(healed > 0.0);
1055    }
1056
1057    #[test]
1058    fn test_character_gain_xp() {
1059        let mut ch = make_player();
1060        ch.gain_xp(200);
1061        assert!(ch.level() >= 2);
1062    }
1063
1064    #[test]
1065    fn test_character_state_machine() {
1066        let mut ch = make_player();
1067        ch.set_state(CharacterState::Moving);
1068        assert_eq!(ch.state, CharacterState::Moving);
1069        ch.set_state(CharacterState::Attacking);
1070        let events = ch.drain_events();
1071        assert!(events.iter().any(|e| matches!(e, CharacterEvent::StateChanged { .. })));
1072    }
1073
1074    #[test]
1075    fn test_character_events() {
1076        let mut ch = make_player();
1077        ch.take_damage(5.0, "fire");
1078        ch.heal(3.0);
1079        let events = ch.drain_events();
1080        assert!(events.iter().any(|e| matches!(e, CharacterEvent::TookDamage { .. })));
1081        assert!(events.iter().any(|e| matches!(e, CharacterEvent::Healed { .. })));
1082    }
1083
1084    #[test]
1085    fn test_faction_system_reputation() {
1086        let mut ch = make_player();
1087        ch.change_reputation("Adventurers Guild", 100);
1088        let rep = ch.faction_system.get_reputation("Adventurers Guild");
1089        assert_eq!(rep, 100);
1090        let rel = ch.faction_system.get_relationship("Adventurers Guild");
1091        assert!(rel.is_friendly());
1092    }
1093
1094    #[test]
1095    fn test_character_registry_spawn() {
1096        let mut registry = CharacterRegistry::new();
1097        let bundle = CharacterBundle::player("Alice", ClassArchetype::Mage, (0.0, 0.0, 0.0));
1098        let id = registry.spawn(bundle);
1099        assert!(registry.get(id).is_some());
1100        assert_eq!(registry.count(), 1);
1101    }
1102
1103    #[test]
1104    fn test_character_registry_despawn() {
1105        let mut registry = CharacterRegistry::new();
1106        let bundle = CharacterBundle::monster("Goblin", 5, (10.0, 0.0, 5.0));
1107        let id = registry.spawn(bundle);
1108        let ch = registry.despawn(id);
1109        assert!(ch.is_some());
1110        assert_eq!(registry.count(), 0);
1111    }
1112
1113    #[test]
1114    fn test_character_controller_tick() {
1115        let mut controller = CharacterController::new((0.0, 0.0, 0.0));
1116        controller.input_move = (1.0, 0.0);
1117        controller.tick(0.016, 100.0);
1118        assert!(controller.position.0 > 0.0);
1119    }
1120
1121    #[test]
1122    fn test_character_controller_jump() {
1123        let mut controller = CharacterController::new((0.0, 0.0, 0.0));
1124        controller.input_jump = true;
1125        controller.tick(0.016, 100.0);
1126        assert!(!controller.is_grounded);
1127        assert!(controller.velocity.1 > 0.0 || controller.position.1 > 0.0);
1128    }
1129
1130    #[test]
1131    fn test_character_registry_find_in_radius() {
1132        let mut registry = CharacterRegistry::new();
1133        let b1 = CharacterBundle::player("Alice", ClassArchetype::Warrior, (0.0, 0.0, 0.0));
1134        let b2 = CharacterBundle::monster("Goblin", 1, (5.0, 0.0, 0.0));
1135        let b3 = CharacterBundle::monster("Far Goblin", 1, (100.0, 0.0, 0.0));
1136        registry.spawn(b1);
1137        registry.spawn(b2);
1138        registry.spawn(b3);
1139        let nearby = registry.find_in_radius((0.0, 0.0, 0.0), 10.0);
1140        assert_eq!(nearby.len(), 2);
1141    }
1142
1143    #[test]
1144    fn test_faction_relationship_between() {
1145        let fs = FactionSystem::new();
1146        let rel = fs.faction_relationship_between("Kingdom Guard", "Dark Brotherhood");
1147        assert!(rel.is_hostile());
1148    }
1149}