1use std::collections::{HashMap, HashSet};
5use crate::character::inventory::Item;
6use crate::character::skills::SkillId;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub struct QuestId(pub u64);
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub struct ItemId(pub u64);
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub struct AchievementId(pub u64);
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26pub enum QuestState {
27 Available,
28 Active,
29 Completed,
30 Failed,
31 Abandoned,
32}
33
34#[derive(Debug, Clone)]
39pub enum ObjectiveKind {
40 Kill { enemy_type: String, count: u32 },
41 Collect { item_id: ItemId, count: u32 },
42 Talk { npc_id: u64 },
43 Reach { location_name: String, x: f32, y: f32, z: f32, radius: f32 },
44 Survive { duration_secs: f32 },
45 Escort { npc_id: u64 },
46 Craft { item_id: ItemId, count: u32 },
47 UseSkill { skill_id: SkillId, count: u32 },
48 Explore { zone_name: String },
49 Protect { target_id: u64, duration_secs: f32 },
50 Deliver { item_id: ItemId, npc_id: u64 },
51 Defeat { boss_id: u64 },
52 Custom { description: String, required: u32 },
53}
54
55impl ObjectiveKind {
56 pub fn required(&self) -> u32 {
57 match self {
58 ObjectiveKind::Kill { count, .. } => *count,
59 ObjectiveKind::Collect { count, .. } => *count,
60 ObjectiveKind::Talk { .. } => 1,
61 ObjectiveKind::Reach { .. } => 1,
62 ObjectiveKind::Survive { duration_secs } => *duration_secs as u32,
63 ObjectiveKind::Escort { .. } => 1,
64 ObjectiveKind::Craft { count, .. } => *count,
65 ObjectiveKind::UseSkill { count, .. } => *count,
66 ObjectiveKind::Explore { .. } => 1,
67 ObjectiveKind::Protect { duration_secs, .. } => *duration_secs as u32,
68 ObjectiveKind::Deliver { .. } => 1,
69 ObjectiveKind::Defeat { .. } => 1,
70 ObjectiveKind::Custom { required, .. } => *required,
71 }
72 }
73}
74
75#[derive(Debug, Clone)]
80pub struct QuestObjective {
81 pub description: String,
82 pub kind: ObjectiveKind,
83 pub progress: u32,
84 pub required: u32,
85 pub optional: bool,
86 pub hidden: bool, }
88
89impl QuestObjective {
90 pub fn new(description: impl Into<String>, kind: ObjectiveKind) -> Self {
91 let required = kind.required();
92 Self {
93 description: description.into(),
94 required,
95 kind,
96 progress: 0,
97 optional: false,
98 hidden: false,
99 }
100 }
101
102 pub fn optional(mut self) -> Self {
103 self.optional = true;
104 self
105 }
106
107 pub fn hidden(mut self) -> Self {
108 self.hidden = true;
109 self
110 }
111
112 pub fn is_complete(&self) -> bool {
113 self.progress >= self.required
114 }
115
116 pub fn advance(&mut self, amount: u32) -> bool {
117 if self.is_complete() { return false; }
118 self.progress = (self.progress + amount).min(self.required);
119 self.is_complete()
120 }
121
122 pub fn fraction(&self) -> f32 {
123 if self.required == 0 { return 1.0; }
124 self.progress as f32 / self.required as f32
125 }
126}
127
128#[derive(Debug, Clone)]
133pub struct QuestReward {
134 pub xp: u64,
135 pub gold: u64,
136 pub items: Vec<(Item, u32)>,
137 pub skills: Vec<SkillId>,
138 pub reputation: Vec<(String, i32)>,
139 pub title: Option<String>,
140 pub stat_points: u32,
141 pub skill_points: u32,
142}
143
144impl QuestReward {
145 pub fn new(xp: u64, gold: u64) -> Self {
146 Self {
147 xp,
148 gold,
149 items: Vec::new(),
150 skills: Vec::new(),
151 reputation: Vec::new(),
152 title: None,
153 stat_points: 0,
154 skill_points: 0,
155 }
156 }
157
158 pub fn add_item(mut self, item: Item, count: u32) -> Self {
159 self.items.push((item, count));
160 self
161 }
162
163 pub fn add_skill(mut self, skill_id: SkillId) -> Self {
164 self.skills.push(skill_id);
165 self
166 }
167
168 pub fn add_rep(mut self, faction: impl Into<String>, amount: i32) -> Self {
169 self.reputation.push((faction.into(), amount));
170 self
171 }
172
173 pub fn with_title(mut self, title: impl Into<String>) -> Self {
174 self.title = Some(title.into());
175 self
176 }
177}
178
179impl Default for QuestReward {
180 fn default() -> Self {
181 Self::new(100, 50)
182 }
183}
184
185#[derive(Debug, Clone)]
190pub struct Quest {
191 pub id: QuestId,
192 pub name: String,
193 pub description: String,
194 pub giver_id: Option<u64>,
195 pub state: QuestState,
196 pub objectives: Vec<QuestObjective>,
197 pub reward: QuestReward,
198 pub level_requirement: u32,
199 pub chain_id: Option<u64>,
200 pub chain_position: u32,
201 pub time_limit_secs: Option<f32>,
202 pub time_elapsed: f32,
203 pub repeatable: bool,
204 pub times_completed: u32,
205 pub category: QuestCategory,
206 pub priority: QuestPriority,
207}
208
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
210pub enum QuestCategory {
211 MainStory,
212 SideQuest,
213 Daily,
214 Weekly,
215 Guild,
216 Bounty,
217 Exploration,
218 Crafting,
219 Escort,
220}
221
222#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
223pub enum QuestPriority {
224 Low,
225 Normal,
226 High,
227 Urgent,
228}
229
230impl Quest {
231 pub fn new(id: QuestId, name: impl Into<String>, reward: QuestReward) -> Self {
232 Self {
233 id,
234 name: name.into(),
235 description: String::new(),
236 giver_id: None,
237 state: QuestState::Available,
238 objectives: Vec::new(),
239 reward,
240 level_requirement: 1,
241 chain_id: None,
242 chain_position: 0,
243 time_limit_secs: None,
244 time_elapsed: 0.0,
245 repeatable: false,
246 times_completed: 0,
247 category: QuestCategory::SideQuest,
248 priority: QuestPriority::Normal,
249 }
250 }
251
252 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
253 self.description = desc.into();
254 self
255 }
256
257 pub fn with_giver(mut self, npc_id: u64) -> Self {
258 self.giver_id = Some(npc_id);
259 self
260 }
261
262 pub fn add_objective(mut self, obj: QuestObjective) -> Self {
263 self.objectives.push(obj);
264 self
265 }
266
267 pub fn with_level_req(mut self, level: u32) -> Self {
268 self.level_requirement = level;
269 self
270 }
271
272 pub fn with_time_limit(mut self, secs: f32) -> Self {
273 self.time_limit_secs = Some(secs);
274 self
275 }
276
277 pub fn repeatable(mut self) -> Self {
278 self.repeatable = true;
279 self
280 }
281
282 pub fn with_category(mut self, cat: QuestCategory) -> Self {
283 self.category = cat;
284 self
285 }
286
287 pub fn with_priority(mut self, p: QuestPriority) -> Self {
288 self.priority = p;
289 self
290 }
291
292 pub fn activate(&mut self) {
293 self.state = QuestState::Active;
294 self.time_elapsed = 0.0;
295 }
296
297 pub fn all_objectives_complete(&self) -> bool {
298 self.objectives.iter()
299 .filter(|o| !o.optional)
300 .all(|o| o.is_complete())
301 }
302
303 pub fn tick(&mut self, dt: f32) -> bool {
304 if self.state != QuestState::Active { return false; }
305 self.time_elapsed += dt;
306 if let Some(limit) = self.time_limit_secs {
307 if self.time_elapsed >= limit {
308 self.state = QuestState::Failed;
309 return true; }
311 }
312 false
313 }
314
315 pub fn time_remaining(&self) -> Option<f32> {
316 self.time_limit_secs.map(|l| (l - self.time_elapsed).max(0.0))
317 }
318
319 pub fn update_objective(&mut self, obj_idx: usize, delta: u32) -> bool {
320 if let Some(obj) = self.objectives.get_mut(obj_idx) {
321 let completed = obj.advance(delta);
322 if self.all_objectives_complete() {
323 self.state = QuestState::Completed;
324 self.times_completed += 1;
325 return true; }
327 return completed;
328 }
329 false
330 }
331
332 pub fn is_active(&self) -> bool {
333 self.state == QuestState::Active
334 }
335
336 pub fn is_done(&self) -> bool {
337 matches!(self.state, QuestState::Completed | QuestState::Failed | QuestState::Abandoned)
338 }
339}
340
341pub const MAX_ACTIVE_QUESTS: usize = 25;
346
347#[derive(Debug, Clone, Default)]
348pub struct QuestJournal {
349 pub active: HashMap<QuestId, Quest>,
350 pub completed: Vec<Quest>,
351 pub failed: Vec<Quest>,
352}
353
354impl QuestJournal {
355 pub fn new() -> Self {
356 Self {
357 active: HashMap::new(),
358 completed: Vec::new(),
359 failed: Vec::new(),
360 }
361 }
362
363 pub fn can_accept(&self) -> bool {
364 self.active.len() < MAX_ACTIVE_QUESTS
365 }
366
367 pub fn add_quest(&mut self, mut quest: Quest) -> bool {
368 if self.active.len() >= MAX_ACTIVE_QUESTS { return false; }
369 if self.active.contains_key(&quest.id) { return false; }
370 quest.activate();
371 self.active.insert(quest.id, quest);
372 true
373 }
374
375 pub fn complete_quest(&mut self, id: QuestId) -> Option<Quest> {
376 let mut quest = self.active.remove(&id)?;
377 quest.state = QuestState::Completed;
378 quest.times_completed += 1;
379 self.completed.push(quest.clone());
380 Some(quest)
381 }
382
383 pub fn fail_quest(&mut self, id: QuestId) -> Option<Quest> {
384 let mut quest = self.active.remove(&id)?;
385 quest.state = QuestState::Failed;
386 self.failed.push(quest.clone());
387 Some(quest)
388 }
389
390 pub fn abandon_quest(&mut self, id: QuestId) -> Option<Quest> {
391 let mut quest = self.active.remove(&id)?;
392 quest.state = QuestState::Abandoned;
393 Some(quest)
394 }
395
396 pub fn update_objective(&mut self, quest_id: QuestId, obj_idx: usize, delta: u32) -> Option<bool> {
397 let quest = self.active.get_mut(&quest_id)?;
398 let newly_done = quest.update_objective(obj_idx, delta);
399 let completed = quest.state == QuestState::Completed;
401 Some(newly_done || completed)
402 }
403
404 pub fn check_completion(&mut self, quest_id: QuestId) -> bool {
405 let quest = match self.active.get(&quest_id) {
406 Some(q) => q,
407 None => return false,
408 };
409 if quest.all_objectives_complete() {
410 let id = quest.id;
411 self.complete_quest(id);
412 return true;
413 }
414 false
415 }
416
417 pub fn tick(&mut self, dt: f32) -> Vec<QuestId> {
418 let mut failed = Vec::new();
419 for quest in self.active.values_mut() {
420 if quest.tick(dt) {
421 failed.push(quest.id);
422 }
423 }
424 for id in &failed {
425 self.fail_quest(*id);
426 }
427 failed
428 }
429
430 pub fn update_kill_objectives(&mut self, enemy_type: &str) -> Vec<(QuestId, usize)> {
431 let mut updates = Vec::new();
432 for quest in self.active.values_mut() {
433 for (obj_idx, obj) in quest.objectives.iter_mut().enumerate() {
434 if let ObjectiveKind::Kill { enemy_type: et, .. } = &obj.kind {
435 if et == enemy_type && !obj.is_complete() {
436 obj.advance(1);
437 updates.push((quest.id, obj_idx));
438 }
439 }
440 }
441 }
442 updates
443 }
444
445 pub fn update_collect_objectives(&mut self, item_id: ItemId, count: u32) -> Vec<(QuestId, usize)> {
446 let mut updates = Vec::new();
447 for quest in self.active.values_mut() {
448 for (obj_idx, obj) in quest.objectives.iter_mut().enumerate() {
449 if let ObjectiveKind::Collect { item_id: iid, .. } = &obj.kind {
450 if *iid == item_id && !obj.is_complete() {
451 obj.advance(count);
452 updates.push((quest.id, obj_idx));
453 }
454 }
455 }
456 }
457 updates
458 }
459
460 pub fn has_completed(&self, id: QuestId) -> bool {
461 self.completed.iter().any(|q| q.id == id)
462 }
463
464 pub fn active_count(&self) -> usize {
465 self.active.len()
466 }
467
468 pub fn get_active(&self, id: QuestId) -> Option<&Quest> {
469 self.active.get(&id)
470 }
471
472 pub fn all_active_sorted(&self) -> Vec<&Quest> {
473 let mut quests: Vec<&Quest> = self.active.values().collect();
474 quests.sort_by(|a, b| b.priority.cmp(&a.priority).then(a.name.cmp(&b.name)));
475 quests
476 }
477}
478
479#[derive(Debug, Clone)]
484pub struct QuestChain {
485 pub id: u64,
486 pub name: String,
487 pub quests: Vec<QuestId>,
488 pub auto_advance: bool,
489 pub current_index: usize,
490}
491
492impl QuestChain {
493 pub fn new(id: u64, name: impl Into<String>, quests: Vec<QuestId>, auto_advance: bool) -> Self {
494 Self { id, name: name.into(), quests, auto_advance, current_index: 0 }
495 }
496
497 pub fn current_quest(&self) -> Option<QuestId> {
498 self.quests.get(self.current_index).copied()
499 }
500
501 pub fn advance(&mut self) -> Option<QuestId> {
502 if self.current_index + 1 < self.quests.len() {
503 self.current_index += 1;
504 self.current_quest()
505 } else {
506 None
507 }
508 }
509
510 pub fn is_complete(&self) -> bool {
511 self.current_index >= self.quests.len()
512 }
513
514 pub fn progress_fraction(&self) -> f32 {
515 if self.quests.is_empty() { return 1.0; }
516 self.current_index as f32 / self.quests.len() as f32
517 }
518}
519
520#[derive(Debug, Clone)]
525pub enum QuestTrigger {
526 LevelReached(u32),
527 QuestCompleted(QuestId),
528 ItemOwned(ItemId),
529 FactionRep { faction: String, min_rep: i32 },
530 TimeElapsed(f64),
531 TalkToNpc(u64),
532 EnterZone(String),
533 AchievementUnlocked(AchievementId),
534 Always,
535}
536
537impl QuestTrigger {
538 pub fn check_level(&self, player_level: u32) -> bool {
539 match self {
540 QuestTrigger::LevelReached(req) => player_level >= *req,
541 QuestTrigger::Always => true,
542 _ => false,
543 }
544 }
545
546 pub fn check_quest_complete(&self, journal: &QuestJournal) -> bool {
547 match self {
548 QuestTrigger::QuestCompleted(id) => journal.has_completed(*id),
549 QuestTrigger::Always => true,
550 _ => false,
551 }
552 }
553}
554
555#[derive(Debug, Clone)]
560pub struct QuestBoardEntry {
561 pub quest: Quest,
562 pub trigger: QuestTrigger,
563 pub expires_at: Option<f64>,
564 pub posted: bool,
565}
566
567impl QuestBoardEntry {
568 pub fn new(quest: Quest, trigger: QuestTrigger) -> Self {
569 Self { quest, trigger, expires_at: None, posted: true }
570 }
571
572 pub fn with_expiry(mut self, time: f64) -> Self {
573 self.expires_at = Some(time);
574 self
575 }
576}
577
578#[derive(Debug, Clone, Default)]
579pub struct QuestBoard {
580 pub entries: Vec<QuestBoardEntry>,
581 pub current_time: f64,
582}
583
584impl QuestBoard {
585 pub fn new() -> Self {
586 Self { entries: Vec::new(), current_time: 0.0 }
587 }
588
589 pub fn post(&mut self, entry: QuestBoardEntry) {
590 self.entries.push(entry);
591 }
592
593 pub fn tick(&mut self, dt: f64) {
594 self.current_time += dt;
595 self.entries.retain(|e| {
596 e.expires_at.map(|exp| self.current_time < exp).unwrap_or(true)
597 });
598 }
599
600 pub fn available_for_level(&self, level: u32) -> Vec<&Quest> {
601 self.entries.iter()
602 .filter(|e| e.posted && e.quest.level_requirement <= level)
603 .map(|e| &e.quest)
604 .collect()
605 }
606
607 pub fn remove_quest(&mut self, id: QuestId) -> Option<Quest> {
608 if let Some(pos) = self.entries.iter().position(|e| e.quest.id == id) {
609 Some(self.entries.remove(pos).quest)
610 } else {
611 None
612 }
613 }
614}
615
616static ENEMY_TYPES: &[&str] = &["goblin", "skeleton", "wolf", "bandit", "orc", "vampire", "zombie", "drake", "giant_spider", "troll"];
621static LOCATIONS: &[&str] = &["Dark Forest", "Abandoned Mine", "Cursed Ruins", "Flooded Caves", "Mountain Peak", "Shadow Swamp", "Haunted Tower"];
622static NPC_NAMES: &[&str] = &["Aldric", "Theron", "Lyra", "Sable", "Mordecai", "Veran", "Kessa", "Torvin", "Aelys", "Bramwell"];
623static QUEST_TEMPLATES_KILL: &[&str] = &[
624 "Thin the Herd", "Extermination", "Clear the Path", "Bounty: {enemy}",
625 "Defend the Village", "Purge the {enemy}s",
626];
627static QUEST_TEMPLATES_COLLECT: &[&str] = &[
628 "Resource Gathering", "Supply Run", "The Missing Shipment", "Reagent Collection",
629];
630
631pub struct QuestGenerator {
632 next_id: u64,
633 seed: u64,
634}
635
636impl QuestGenerator {
637 pub fn new(seed: u64) -> Self {
638 Self { next_id: 10000, seed }
639 }
640
641 fn next_rand(&mut self) -> u64 {
642 self.seed ^= self.seed << 13;
643 self.seed ^= self.seed >> 7;
644 self.seed ^= self.seed << 17;
645 self.seed
646 }
647
648 fn rand_range(&mut self, min: u64, max: u64) -> u64 {
649 if max <= min { return min; }
650 min + self.next_rand() % (max - min)
651 }
652
653 fn next_id(&mut self) -> QuestId {
654 let id = QuestId(self.next_id);
655 self.next_id += 1;
656 id
657 }
658
659 fn pick<T>(&mut self, slice: &[T]) -> usize {
660 self.next_rand() as usize % slice.len()
661 }
662
663 fn make_name(&mut self, template: &str, enemy: &str) -> String {
664 template.replace("{enemy}", enemy)
665 }
666
667 pub fn generate_kill_quest(&mut self, player_level: u32) -> Quest {
668 let enemy_idx = self.pick(ENEMY_TYPES);
669 let enemy = ENEMY_TYPES[enemy_idx];
670 let count = self.rand_range(3, 15 + player_level as u64) as u32;
671 let tmpl_idx = self.pick(QUEST_TEMPLATES_KILL);
672 let name = self.make_name(QUEST_TEMPLATES_KILL[tmpl_idx], enemy);
673 let xp = (count as u64 * 20 + player_level as u64 * 50).max(100);
674 let gold = (count as u64 * 5 + player_level as u64 * 10).max(20);
675 let id = self.next_id();
676 let desc = format!(
677 "Kill {} {}{}. They have been terrorizing the region.",
678 count,
679 enemy,
680 if count > 1 { "s" } else { "" }
681 );
682 Quest::new(id, name, QuestReward::new(xp, gold))
683 .with_description(desc)
684 .add_objective(QuestObjective::new(
685 format!("Kill {count} {enemy}s"),
686 ObjectiveKind::Kill { enemy_type: enemy.to_string(), count },
687 ))
688 .with_level_req(player_level.saturating_sub(2))
689 .with_category(QuestCategory::Bounty)
690 }
691
692 pub fn generate_collect_quest(&mut self, player_level: u32) -> Quest {
693 let count = self.rand_range(3, 10 + player_level as u64 / 2) as u32;
694 let item_id = ItemId(self.rand_range(1000, 2000));
695 let tmpl_idx = self.pick(QUEST_TEMPLATES_COLLECT);
696 let name = QUEST_TEMPLATES_COLLECT[tmpl_idx].to_string();
697 let xp = (count as u64 * 15 + player_level as u64 * 30).max(80);
698 let gold = (count as u64 * 8 + player_level as u64 * 8).max(15);
699 let id = self.next_id();
700 Quest::new(id, name, QuestReward::new(xp, gold))
701 .with_description(format!("Collect {} rare materials for the crafters guild.", count))
702 .add_objective(QuestObjective::new(
703 format!("Collect {count} materials"),
704 ObjectiveKind::Collect { item_id, count },
705 ))
706 .with_level_req(player_level.saturating_sub(2))
707 .with_category(QuestCategory::Crafting)
708 }
709
710 pub fn generate_escort_quest(&mut self, player_level: u32) -> Quest {
711 let npc_idx = self.pick(NPC_NAMES);
712 let npc_name = NPC_NAMES[npc_idx];
713 let npc_id = self.rand_range(100, 500);
714 let loc_idx = self.pick(LOCATIONS);
715 let loc = LOCATIONS[loc_idx];
716 let xp = (player_level as u64 * 80 + 200).max(300);
717 let gold = (player_level as u64 * 20 + 100).max(100);
718 let id = self.next_id();
719 Quest::new(id, format!("Escort {npc_name} to Safety"), QuestReward::new(xp, gold))
720 .with_description(format!("Escort {} safely to {}.", npc_name, loc))
721 .add_objective(QuestObjective::new(
722 format!("Escort {npc_name}"),
723 ObjectiveKind::Escort { npc_id },
724 ))
725 .add_objective(QuestObjective::new(
726 format!("Reach {loc}"),
727 ObjectiveKind::Reach { location_name: loc.to_string(), x: 0.0, y: 0.0, z: 0.0, radius: 5.0 },
728 ))
729 .with_level_req(player_level.saturating_sub(3))
730 .with_category(QuestCategory::Escort)
731 }
732
733 pub fn generate_explore_quest(&mut self, player_level: u32) -> Quest {
734 let loc_idx = self.pick(LOCATIONS);
735 let loc = LOCATIONS[loc_idx];
736 let xp = (player_level as u64 * 60 + 150).max(200);
737 let gold = (player_level as u64 * 15 + 50).max(50);
738 let id = self.next_id();
739 Quest::new(id, format!("Explore: {loc}"), QuestReward::new(xp, gold))
740 .with_description(format!("Survey the {} area and report back.", loc))
741 .add_objective(QuestObjective::new(
742 format!("Explore {loc}"),
743 ObjectiveKind::Explore { zone_name: loc.to_string() },
744 ))
745 .with_level_req(player_level.saturating_sub(1))
746 .with_category(QuestCategory::Exploration)
747 }
748
749 pub fn generate_daily_quests(&mut self, player_level: u32, count: usize) -> Vec<Quest> {
750 let mut quests = Vec::new();
751 for i in 0..count {
752 let quest = match i % 4 {
753 0 => self.generate_kill_quest(player_level),
754 1 => self.generate_collect_quest(player_level),
755 2 => self.generate_escort_quest(player_level),
756 _ => self.generate_explore_quest(player_level),
757 };
758 quests.push(quest);
759 }
760 quests
761 }
762}
763
764#[derive(Debug, Clone)]
769pub struct DialogueChoice {
770 pub text: String,
771 pub gives_quest: Option<QuestId>,
772 pub requires_quest_completed: Option<QuestId>,
773 pub requires_item: Option<ItemId>,
774 pub requires_level: u32,
775 pub leads_to_node: Option<usize>,
776}
777
778impl DialogueChoice {
779 pub fn new(text: impl Into<String>) -> Self {
780 Self {
781 text: text.into(),
782 gives_quest: None,
783 requires_quest_completed: None,
784 requires_item: None,
785 requires_level: 0,
786 leads_to_node: None,
787 }
788 }
789
790 pub fn gives_quest(mut self, id: QuestId) -> Self {
791 self.gives_quest = Some(id);
792 self
793 }
794
795 pub fn requires_level(mut self, level: u32) -> Self {
796 self.requires_level = level;
797 self
798 }
799
800 pub fn is_available(&self, player_level: u32, journal: &QuestJournal) -> bool {
801 if player_level < self.requires_level { return false; }
802 if let Some(id) = self.requires_quest_completed {
803 if !journal.has_completed(id) { return false; }
804 }
805 true
806 }
807}
808
809#[derive(Debug, Clone)]
810pub struct DialogueNode {
811 pub npc_text: String,
812 pub choices: Vec<DialogueChoice>,
813}
814
815impl DialogueNode {
816 pub fn new(npc_text: impl Into<String>) -> Self {
817 Self { npc_text: npc_text.into(), choices: Vec::new() }
818 }
819
820 pub fn add_choice(mut self, choice: DialogueChoice) -> Self {
821 self.choices.push(choice);
822 self
823 }
824}
825
826#[derive(Debug, Clone)]
827pub struct DialogueTree {
828 pub npc_id: u64,
829 pub npc_name: String,
830 pub nodes: Vec<DialogueNode>,
831 pub root_node: usize,
832}
833
834impl DialogueTree {
835 pub fn new(npc_id: u64, npc_name: impl Into<String>) -> Self {
836 Self { npc_id, npc_name: npc_name.into(), nodes: Vec::new(), root_node: 0 }
837 }
838
839 pub fn add_node(mut self, node: DialogueNode) -> Self {
840 self.nodes.push(node);
841 self
842 }
843
844 pub fn get_root(&self) -> Option<&DialogueNode> {
845 self.nodes.get(self.root_node)
846 }
847
848 pub fn get_node(&self, idx: usize) -> Option<&DialogueNode> {
849 self.nodes.get(idx)
850 }
851
852 pub fn available_choices(&self, node_idx: usize, level: u32, journal: &QuestJournal) -> Vec<(usize, &DialogueChoice)> {
853 self.nodes.get(node_idx)
854 .map(|n| {
855 n.choices.iter().enumerate()
856 .filter(|(_, c)| c.is_available(level, journal))
857 .collect()
858 })
859 .unwrap_or_default()
860 }
861}
862
863#[derive(Debug, Clone)]
868pub struct TrackerObjective {
869 pub quest_name: String,
870 pub description: String,
871 pub progress: u32,
872 pub required: u32,
873}
874
875impl TrackerObjective {
876 pub fn fraction(&self) -> f32 {
877 if self.required == 0 { return 1.0; }
878 self.progress as f32 / self.required as f32
879 }
880}
881
882#[derive(Debug, Clone, Default)]
883pub struct QuestTracker {
884 pub tracked: Vec<(QuestId, usize)>, pub max_tracked: usize,
886}
887
888impl QuestTracker {
889 pub fn new(max: usize) -> Self {
890 Self { tracked: Vec::new(), max_tracked: max }
891 }
892
893 pub fn track(&mut self, quest_id: QuestId, obj_idx: usize) -> bool {
894 if self.tracked.len() >= self.max_tracked { return false; }
895 if self.tracked.contains(&(quest_id, obj_idx)) { return false; }
896 self.tracked.push((quest_id, obj_idx));
897 true
898 }
899
900 pub fn untrack(&mut self, quest_id: QuestId, obj_idx: usize) {
901 self.tracked.retain(|&(qid, oi)| !(qid == quest_id && oi == obj_idx));
902 }
903
904 pub fn get_display(&self, journal: &QuestJournal) -> Vec<TrackerObjective> {
905 self.tracked.iter().filter_map(|&(qid, oi)| {
906 let quest = journal.get_active(qid)?;
907 let obj = quest.objectives.get(oi)?;
908 Some(TrackerObjective {
909 quest_name: quest.name.clone(),
910 description: obj.description.clone(),
911 progress: obj.progress,
912 required: obj.required,
913 })
914 }).collect()
915 }
916}
917
918#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
923pub enum AchievementCategory {
924 Combat,
925 Exploration,
926 Crafting,
927 Social,
928 Collection,
929 Progression,
930 Secret,
931 Event,
932}
933
934#[derive(Debug, Clone)]
935pub struct Achievement {
936 pub id: AchievementId,
937 pub name: String,
938 pub description: String,
939 pub icon: char,
940 pub points: u32,
941 pub secret: bool,
942 pub category: AchievementCategory,
943 pub trigger: AchievementTrigger,
944 pub reward: Option<AchievementReward>,
945}
946
947impl Achievement {
948 pub fn new(id: AchievementId, name: impl Into<String>, category: AchievementCategory, trigger: AchievementTrigger) -> Self {
949 Self {
950 id,
951 name: name.into(),
952 description: String::new(),
953 icon: '★',
954 points: 10,
955 secret: false,
956 category,
957 trigger,
958 reward: None,
959 }
960 }
961
962 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
963 self.description = desc.into();
964 self
965 }
966
967 pub fn with_points(mut self, pts: u32) -> Self {
968 self.points = pts;
969 self
970 }
971
972 pub fn secret(mut self) -> Self {
973 self.secret = true;
974 self
975 }
976
977 pub fn with_reward(mut self, reward: AchievementReward) -> Self {
978 self.reward = Some(reward);
979 self
980 }
981}
982
983#[derive(Debug, Clone)]
984pub enum AchievementTrigger {
985 LevelReached(u32),
986 QuestCompleted(QuestId),
987 KillCount { enemy_type: String, count: u64 },
988 TotalKills(u64),
989 ItemCollected { item_id: ItemId },
990 GoldAccumulated(u64),
991 SkillRankMaxed(SkillId),
992 QuestsCompleted(u32),
993 AchievementsUnlocked(u32),
994 DeathCount(u32),
995 Manual, }
997
998impl AchievementTrigger {
999 pub fn check_level(&self, level: u32) -> bool {
1000 matches!(self, AchievementTrigger::LevelReached(req) if level >= *req)
1001 }
1002
1003 pub fn check_kill_count(&self, enemy_type: &str, count: u64) -> bool {
1004 match self {
1005 AchievementTrigger::KillCount { enemy_type: et, count: req } => {
1006 et == enemy_type && count >= *req
1007 }
1008 AchievementTrigger::TotalKills(req) => count >= *req,
1009 _ => false,
1010 }
1011 }
1012}
1013
1014#[derive(Debug, Clone)]
1015pub struct AchievementReward {
1016 pub xp: u64,
1017 pub title: Option<String>,
1018 pub cosmetic: Option<String>,
1019}
1020
1021impl AchievementReward {
1022 pub fn new(xp: u64) -> Self {
1023 Self { xp, title: None, cosmetic: None }
1024 }
1025 pub fn with_title(mut self, t: impl Into<String>) -> Self {
1026 self.title = Some(t.into());
1027 self
1028 }
1029}
1030
1031#[derive(Debug, Clone)]
1032pub struct AchievementProgress {
1033 pub achievement_id: AchievementId,
1034 pub current: u64,
1035 pub required: u64,
1036}
1037
1038impl AchievementProgress {
1039 pub fn fraction(&self) -> f32 {
1040 if self.required == 0 { return 1.0; }
1041 (self.current as f32 / self.required as f32).min(1.0)
1042 }
1043
1044 pub fn is_complete(&self) -> bool {
1045 self.current >= self.required
1046 }
1047}
1048
1049#[derive(Debug, Clone, Default)]
1050pub struct AchievementSystem {
1051 pub achievements: Vec<Achievement>,
1052 pub unlocked: HashSet<AchievementId>,
1053 pub progress: HashMap<AchievementId, AchievementProgress>,
1054 pub total_points: u32,
1055 pub kill_counts: HashMap<String, u64>,
1056 pub total_kills: u64,
1057 pub quests_completed: u32,
1058 pub gold_accumulated: u64,
1059}
1060
1061impl AchievementSystem {
1062 pub fn new() -> Self {
1063 let mut sys = Self::default();
1064 sys.register_defaults();
1065 sys
1066 }
1067
1068 fn register_defaults(&mut self) {
1069 let defaults = vec![
1070 Achievement::new(
1071 AchievementId(1), "First Blood", AchievementCategory::Combat,
1072 AchievementTrigger::TotalKills(1),
1073 ).with_description("Get your first kill.").with_points(5),
1074
1075 Achievement::new(
1076 AchievementId(2), "Slayer", AchievementCategory::Combat,
1077 AchievementTrigger::TotalKills(100),
1078 ).with_description("Kill 100 enemies.").with_points(20),
1079
1080 Achievement::new(
1081 AchievementId(3), "Centurion", AchievementCategory::Combat,
1082 AchievementTrigger::TotalKills(1000),
1083 ).with_description("Kill 1000 enemies.").with_points(50),
1084
1085 Achievement::new(
1086 AchievementId(4), "Goblin Slayer", AchievementCategory::Combat,
1087 AchievementTrigger::KillCount { enemy_type: "goblin".to_string(), count: 50 },
1088 ).with_description("Kill 50 goblins.").with_points(15),
1089
1090 Achievement::new(
1091 AchievementId(5), "Quest Beginner", AchievementCategory::Progression,
1092 AchievementTrigger::QuestsCompleted(1),
1093 ).with_description("Complete your first quest.").with_points(10),
1094
1095 Achievement::new(
1096 AchievementId(6), "Adventurer", AchievementCategory::Progression,
1097 AchievementTrigger::QuestsCompleted(25),
1098 ).with_description("Complete 25 quests.").with_points(25),
1099
1100 Achievement::new(
1101 AchievementId(7), "Veteran", AchievementCategory::Progression,
1102 AchievementTrigger::QuestsCompleted(100),
1103 ).with_description("Complete 100 quests.").with_points(75),
1104
1105 Achievement::new(
1106 AchievementId(8), "Level 10", AchievementCategory::Progression,
1107 AchievementTrigger::LevelReached(10),
1108 ).with_description("Reach level 10.").with_points(10),
1109
1110 Achievement::new(
1111 AchievementId(9), "Level 50", AchievementCategory::Progression,
1112 AchievementTrigger::LevelReached(50),
1113 ).with_description("Reach level 50.").with_points(50),
1114
1115 Achievement::new(
1116 AchievementId(10), "Max Level", AchievementCategory::Progression,
1117 AchievementTrigger::LevelReached(100),
1118 ).with_description("Reach the maximum level.").with_points(100)
1119 .with_reward(AchievementReward::new(10000).with_title("The Ascended")),
1120
1121 Achievement::new(
1122 AchievementId(11), "Wealthy", AchievementCategory::Collection,
1123 AchievementTrigger::GoldAccumulated(10000),
1124 ).with_description("Accumulate 10,000 gold.").with_points(20),
1125
1126 Achievement::new(
1127 AchievementId(12), "Secret: The Unkillable", AchievementCategory::Secret,
1128 AchievementTrigger::DeathCount(0),
1129 ).with_description("Never die. Ever.").with_points(500).secret(),
1130 ];
1131 for ach in defaults {
1132 self.register(ach);
1133 }
1134 }
1135
1136 pub fn register(&mut self, achievement: Achievement) {
1137 self.achievements.push(achievement);
1138 }
1139
1140 pub fn is_unlocked(&self, id: AchievementId) -> bool {
1141 self.unlocked.contains(&id)
1142 }
1143
1144 pub fn unlock(&mut self, id: AchievementId) -> bool {
1145 if self.unlocked.contains(&id) { return false; }
1146 if let Some(ach) = self.achievements.iter().find(|a| a.id == id) {
1147 self.total_points += ach.points;
1148 self.unlocked.insert(id);
1149 return true;
1150 }
1151 false
1152 }
1153
1154 pub fn record_kill(&mut self, enemy_type: &str) -> Vec<AchievementId> {
1155 *self.kill_counts.entry(enemy_type.to_string()).or_insert(0) += 1;
1156 self.total_kills += 1;
1157 self.check_all()
1158 }
1159
1160 pub fn record_quest_complete(&mut self) -> Vec<AchievementId> {
1161 self.quests_completed += 1;
1162 self.check_all()
1163 }
1164
1165 pub fn record_gold(&mut self, amount: u64) -> Vec<AchievementId> {
1166 self.gold_accumulated += amount;
1167 self.check_all()
1168 }
1169
1170 pub fn check_level(&mut self, level: u32) -> Vec<AchievementId> {
1171 let ids: Vec<AchievementId> = self.achievements.iter()
1172 .filter(|a| !self.unlocked.contains(&a.id) && a.trigger.check_level(level))
1173 .map(|a| a.id)
1174 .collect();
1175 let mut newly_unlocked = Vec::new();
1176 for id in ids {
1177 if self.unlock(id) { newly_unlocked.push(id); }
1178 }
1179 newly_unlocked
1180 }
1181
1182 pub fn manual_unlock(&mut self, id: AchievementId) -> bool {
1183 self.unlock(id)
1184 }
1185
1186 fn check_all(&mut self) -> Vec<AchievementId> {
1187 let total_kills = self.total_kills;
1188 let kill_counts = self.kill_counts.clone();
1189 let quests = self.quests_completed;
1190 let gold = self.gold_accumulated;
1191
1192 let ids: Vec<AchievementId> = self.achievements.iter()
1193 .filter(|a| !self.unlocked.contains(&a.id))
1194 .filter(|a| match &a.trigger {
1195 AchievementTrigger::TotalKills(req) => total_kills >= *req,
1196 AchievementTrigger::KillCount { enemy_type, count } => {
1197 kill_counts.get(enemy_type.as_str()).copied().unwrap_or(0) >= *count
1198 }
1199 AchievementTrigger::QuestsCompleted(req) => quests >= *req,
1200 AchievementTrigger::GoldAccumulated(req) => gold >= *req,
1201 _ => false,
1202 })
1203 .map(|a| a.id)
1204 .collect();
1205
1206 let mut newly_unlocked = Vec::new();
1207 for id in ids {
1208 if self.unlock(id) { newly_unlocked.push(id); }
1209 }
1210 newly_unlocked
1211 }
1212
1213 pub fn unlocked_count(&self) -> usize {
1214 self.unlocked.len()
1215 }
1216
1217 pub fn total_achievement_count(&self) -> usize {
1218 self.achievements.len()
1219 }
1220
1221 pub fn completion_fraction(&self) -> f32 {
1222 if self.achievements.is_empty() { return 0.0; }
1223 self.unlocked.len() as f32 / self.achievements.len() as f32
1224 }
1225
1226 pub fn by_category(&self, cat: AchievementCategory) -> Vec<&Achievement> {
1227 self.achievements.iter()
1228 .filter(|a| a.category == cat)
1229 .collect()
1230 }
1231
1232 pub fn recently_unlocked(&self, count: usize) -> Vec<&Achievement> {
1233 self.achievements.iter()
1236 .filter(|a| self.unlocked.contains(&a.id))
1237 .rev()
1238 .take(count)
1239 .collect()
1240 }
1241}
1242
1243#[cfg(test)]
1248mod tests {
1249 use super::*;
1250
1251 fn simple_quest(id: u64, enemy: &str, count: u32) -> Quest {
1252 Quest::new(QuestId(id), format!("Kill {enemy}"), QuestReward::new(100, 50))
1253 .add_objective(QuestObjective::new(
1254 format!("Kill {count} {enemy}s"),
1255 ObjectiveKind::Kill { enemy_type: enemy.to_string(), count },
1256 ))
1257 }
1258
1259 #[test]
1260 fn test_quest_objective_advance() {
1261 let mut obj = QuestObjective::new("Kill 5 goblins", ObjectiveKind::Kill { enemy_type: "goblin".to_string(), count: 5 });
1262 assert!(!obj.is_complete());
1263 obj.advance(3);
1264 assert!(!obj.is_complete());
1265 obj.advance(2);
1266 assert!(obj.is_complete());
1267 }
1268
1269 #[test]
1270 fn test_quest_auto_complete() {
1271 let mut quest = simple_quest(1, "goblin", 3);
1272 quest.activate();
1273 quest.update_objective(0, 3);
1274 assert_eq!(quest.state, QuestState::Completed);
1275 }
1276
1277 #[test]
1278 fn test_quest_journal_add_and_complete() {
1279 let mut journal = QuestJournal::new();
1280 let q = simple_quest(1, "wolf", 2);
1281 assert!(journal.add_quest(q));
1282 assert_eq!(journal.active_count(), 1);
1283 let done = journal.complete_quest(QuestId(1));
1284 assert!(done.is_some());
1285 assert_eq!(journal.active_count(), 0);
1286 assert!(journal.has_completed(QuestId(1)));
1287 }
1288
1289 #[test]
1290 fn test_quest_journal_fail() {
1291 let mut journal = QuestJournal::new();
1292 let q = simple_quest(2, "orc", 5);
1293 journal.add_quest(q);
1294 let failed = journal.fail_quest(QuestId(2));
1295 assert!(failed.is_some());
1296 }
1297
1298 #[test]
1299 fn test_quest_journal_max_active() {
1300 let mut journal = QuestJournal::new();
1301 for i in 0..MAX_ACTIVE_QUESTS {
1302 let q = simple_quest(i as u64, "goblin", 1);
1303 journal.add_quest(q);
1304 }
1305 let overflow = simple_quest(999, "goblin", 1);
1306 assert!(!journal.add_quest(overflow));
1307 }
1308
1309 #[test]
1310 fn test_quest_kill_objective_tracking() {
1311 let mut journal = QuestJournal::new();
1312 let q = simple_quest(1, "goblin", 5);
1313 journal.add_quest(q);
1314 let updates = journal.update_kill_objectives("goblin");
1315 assert!(!updates.is_empty());
1316 }
1317
1318 #[test]
1319 fn test_quest_time_limit_expiry() {
1320 let mut quest = Quest::new(QuestId(1), "Timed", QuestReward::default())
1321 .with_time_limit(5.0);
1322 quest.activate();
1323 let expired = quest.tick(6.0);
1324 assert!(expired);
1325 assert_eq!(quest.state, QuestState::Failed);
1326 }
1327
1328 #[test]
1329 fn test_quest_generator_kill() {
1330 let mut gen = QuestGenerator::new(42);
1331 let q = gen.generate_kill_quest(10);
1332 assert!(!q.objectives.is_empty());
1333 assert!(matches!(q.objectives[0].kind, ObjectiveKind::Kill { .. }));
1334 }
1335
1336 #[test]
1337 fn test_quest_generator_daily() {
1338 let mut gen = QuestGenerator::new(99);
1339 let quests = gen.generate_daily_quests(15, 8);
1340 assert_eq!(quests.len(), 8);
1341 }
1342
1343 #[test]
1344 fn test_achievement_system_unlock() {
1345 let mut sys = AchievementSystem::new();
1346 for _ in 0..100 {
1348 sys.record_kill("anything");
1349 }
1350 assert!(sys.is_unlocked(AchievementId(2))); }
1352
1353 #[test]
1354 fn test_achievement_kill_count() {
1355 let mut sys = AchievementSystem::new();
1356 for _ in 0..50 {
1357 sys.record_kill("goblin");
1358 }
1359 assert!(sys.is_unlocked(AchievementId(4))); }
1361
1362 #[test]
1363 fn test_achievement_quest_completion() {
1364 let mut sys = AchievementSystem::new();
1365 sys.record_quest_complete();
1366 assert!(sys.is_unlocked(AchievementId(5))); }
1368
1369 #[test]
1370 fn test_quest_chain_advance() {
1371 let mut chain = QuestChain::new(1, "Main Story",
1372 vec![QuestId(1), QuestId(2), QuestId(3)], true);
1373 assert_eq!(chain.current_quest(), Some(QuestId(1)));
1374 chain.advance();
1375 assert_eq!(chain.current_quest(), Some(QuestId(2)));
1376 chain.advance();
1377 chain.advance();
1378 assert!(chain.is_complete());
1379 }
1380
1381 #[test]
1382 fn test_quest_board_expiry() {
1383 let mut board = QuestBoard::new();
1384 let q = simple_quest(1, "troll", 1);
1385 board.post(QuestBoardEntry::new(q, QuestTrigger::Always).with_expiry(5.0));
1386 board.tick(6.0);
1387 assert!(board.entries.is_empty());
1388 }
1389}