1pub 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#[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#[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#[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#[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>, }
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#[derive(Debug, Clone)]
289pub struct AppearanceData {
290 pub glyph_char: char,
291 pub color: (f32, f32, f32, f32), 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#[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, pub collision_radius: f32,
362 pub collision_height: f32,
363 pub gravity: f32,
364 pub can_fly: bool,
365 pub input_move: (f32, f32), 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 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 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; }
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 if !self.is_grounded && !self.can_fly {
433 self.velocity.1 += self.gravity * dt;
434 }
435
436 if self.input_jump && self.is_grounded {
438 self.velocity.1 = self.jump_force;
439 self.is_grounded = false;
440 }
441
442 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 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 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 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 if self.is_grounded {
472 self.velocity.0 *= self.friction;
473 self.velocity.2 *= self.friction;
474 }
475
476 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 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#[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#[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#[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 pub stats: StatSheet,
597 pub resources: AllResources,
598 pub level_data: LevelData,
599 pub modifier_registry: ModifierRegistry,
600 pub stat_growth: StatGrowth,
601
602 pub equipped: EquippedItems,
604 pub inventory: Inventory,
605 pub stash: Stash,
606 pub gold: u64,
607
608 pub skill_book: SkillBook,
610 pub ability_bar: AbilityBar,
611 pub cooldowns: CooldownTracker,
612 pub combos: ComboSystem,
613
614 pub journal: QuestJournal,
616 pub achievements: AchievementSystem,
617 pub quest_tracker: QuestTracker,
618
619 pub faction: Option<String>,
621 pub faction_system: FactionSystem,
622
623 pub controller: CharacterController,
625 pub input_binding: InputBinding,
626
627 pub appearance: AppearanceData,
629
630 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 pub fn tick(&mut self, dt: f32) {
689 if !self.state.is_alive() { return; }
690
691 self.resources.tick(dt);
693
694 self.cooldowns.tick(dt);
696
697 self.combos.tick(dt);
699
700 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 let move_speed = self.stats.move_speed();
712 if self.state.can_move() {
713 self.controller.tick(dt, move_speed);
714 }
715
716 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 pub fn take_damage(&mut self, amount: f32, source: &str) -> bool {
734 if !self.state.is_alive() { return false; }
735
736 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 self.achievements.record_kill(""); 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 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 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 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 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 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 pub fn level(&self) -> u32 {
805 self.level_data.level
806 }
807
808 pub fn is_alive(&self) -> bool {
810 self.state.is_alive()
811 }
812
813 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#[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), 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 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#[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 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#[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}