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 < self.quests.len() {
503 self.current_index += 1;
504 }
505 self.current_quest()
506 }
507
508 pub fn is_complete(&self) -> bool {
509 self.current_index >= self.quests.len()
510 }
511
512 pub fn progress_fraction(&self) -> f32 {
513 if self.quests.is_empty() { return 1.0; }
514 self.current_index as f32 / self.quests.len() as f32
515 }
516}
517
518#[derive(Debug, Clone)]
523pub enum QuestTrigger {
524 LevelReached(u32),
525 QuestCompleted(QuestId),
526 ItemOwned(ItemId),
527 FactionRep { faction: String, min_rep: i32 },
528 TimeElapsed(f64),
529 TalkToNpc(u64),
530 EnterZone(String),
531 AchievementUnlocked(AchievementId),
532 Always,
533}
534
535impl QuestTrigger {
536 pub fn check_level(&self, player_level: u32) -> bool {
537 match self {
538 QuestTrigger::LevelReached(req) => player_level >= *req,
539 QuestTrigger::Always => true,
540 _ => false,
541 }
542 }
543
544 pub fn check_quest_complete(&self, journal: &QuestJournal) -> bool {
545 match self {
546 QuestTrigger::QuestCompleted(id) => journal.has_completed(*id),
547 QuestTrigger::Always => true,
548 _ => false,
549 }
550 }
551}
552
553#[derive(Debug, Clone)]
558pub struct QuestBoardEntry {
559 pub quest: Quest,
560 pub trigger: QuestTrigger,
561 pub expires_at: Option<f64>,
562 pub posted: bool,
563}
564
565impl QuestBoardEntry {
566 pub fn new(quest: Quest, trigger: QuestTrigger) -> Self {
567 Self { quest, trigger, expires_at: None, posted: true }
568 }
569
570 pub fn with_expiry(mut self, time: f64) -> Self {
571 self.expires_at = Some(time);
572 self
573 }
574}
575
576#[derive(Debug, Clone, Default)]
577pub struct QuestBoard {
578 pub entries: Vec<QuestBoardEntry>,
579 pub current_time: f64,
580}
581
582impl QuestBoard {
583 pub fn new() -> Self {
584 Self { entries: Vec::new(), current_time: 0.0 }
585 }
586
587 pub fn post(&mut self, entry: QuestBoardEntry) {
588 self.entries.push(entry);
589 }
590
591 pub fn tick(&mut self, dt: f64) {
592 self.current_time += dt;
593 self.entries.retain(|e| {
594 e.expires_at.map(|exp| self.current_time < exp).unwrap_or(true)
595 });
596 }
597
598 pub fn available_for_level(&self, level: u32) -> Vec<&Quest> {
599 self.entries.iter()
600 .filter(|e| e.posted && e.quest.level_requirement <= level)
601 .map(|e| &e.quest)
602 .collect()
603 }
604
605 pub fn remove_quest(&mut self, id: QuestId) -> Option<Quest> {
606 if let Some(pos) = self.entries.iter().position(|e| e.quest.id == id) {
607 Some(self.entries.remove(pos).quest)
608 } else {
609 None
610 }
611 }
612}
613
614static ENEMY_TYPES: &[&str] = &["goblin", "skeleton", "wolf", "bandit", "orc", "vampire", "zombie", "drake", "giant_spider", "troll"];
619static LOCATIONS: &[&str] = &["Dark Forest", "Abandoned Mine", "Cursed Ruins", "Flooded Caves", "Mountain Peak", "Shadow Swamp", "Haunted Tower"];
620static NPC_NAMES: &[&str] = &["Aldric", "Theron", "Lyra", "Sable", "Mordecai", "Veran", "Kessa", "Torvin", "Aelys", "Bramwell"];
621static QUEST_TEMPLATES_KILL: &[&str] = &[
622 "Thin the Herd", "Extermination", "Clear the Path", "Bounty: {enemy}",
623 "Defend the Village", "Purge the {enemy}s",
624];
625static QUEST_TEMPLATES_COLLECT: &[&str] = &[
626 "Resource Gathering", "Supply Run", "The Missing Shipment", "Reagent Collection",
627];
628
629pub struct QuestGenerator {
630 next_id: u64,
631 seed: u64,
632}
633
634impl QuestGenerator {
635 pub fn new(seed: u64) -> Self {
636 Self { next_id: 10000, seed }
637 }
638
639 fn next_rand(&mut self) -> u64 {
640 self.seed ^= self.seed << 13;
641 self.seed ^= self.seed >> 7;
642 self.seed ^= self.seed << 17;
643 self.seed
644 }
645
646 fn rand_range(&mut self, min: u64, max: u64) -> u64 {
647 if max <= min { return min; }
648 min + self.next_rand() % (max - min)
649 }
650
651 fn next_id(&mut self) -> QuestId {
652 let id = QuestId(self.next_id);
653 self.next_id += 1;
654 id
655 }
656
657 fn pick<T>(&mut self, slice: &[T]) -> usize {
658 self.next_rand() as usize % slice.len()
659 }
660
661 fn make_name(&mut self, template: &str, enemy: &str) -> String {
662 template.replace("{enemy}", enemy)
663 }
664
665 pub fn generate_kill_quest(&mut self, player_level: u32) -> Quest {
666 let enemy_idx = self.pick(ENEMY_TYPES);
667 let enemy = ENEMY_TYPES[enemy_idx];
668 let count = self.rand_range(3, 15 + player_level as u64) as u32;
669 let tmpl_idx = self.pick(QUEST_TEMPLATES_KILL);
670 let name = self.make_name(QUEST_TEMPLATES_KILL[tmpl_idx], enemy);
671 let xp = (count as u64 * 20 + player_level as u64 * 50).max(100);
672 let gold = (count as u64 * 5 + player_level as u64 * 10).max(20);
673 let id = self.next_id();
674 let desc = format!(
675 "Kill {} {}{}. They have been terrorizing the region.",
676 count,
677 enemy,
678 if count > 1 { "s" } else { "" }
679 );
680 Quest::new(id, name, QuestReward::new(xp, gold))
681 .with_description(desc)
682 .add_objective(QuestObjective::new(
683 format!("Kill {count} {enemy}s"),
684 ObjectiveKind::Kill { enemy_type: enemy.to_string(), count },
685 ))
686 .with_level_req(player_level.saturating_sub(2))
687 .with_category(QuestCategory::Bounty)
688 }
689
690 pub fn generate_collect_quest(&mut self, player_level: u32) -> Quest {
691 let count = self.rand_range(3, 10 + player_level as u64 / 2) as u32;
692 let item_id = ItemId(self.rand_range(1000, 2000));
693 let tmpl_idx = self.pick(QUEST_TEMPLATES_COLLECT);
694 let name = QUEST_TEMPLATES_COLLECT[tmpl_idx].to_string();
695 let xp = (count as u64 * 15 + player_level as u64 * 30).max(80);
696 let gold = (count as u64 * 8 + player_level as u64 * 8).max(15);
697 let id = self.next_id();
698 Quest::new(id, name, QuestReward::new(xp, gold))
699 .with_description(format!("Collect {} rare materials for the crafters guild.", count))
700 .add_objective(QuestObjective::new(
701 format!("Collect {count} materials"),
702 ObjectiveKind::Collect { item_id, count },
703 ))
704 .with_level_req(player_level.saturating_sub(2))
705 .with_category(QuestCategory::Crafting)
706 }
707
708 pub fn generate_escort_quest(&mut self, player_level: u32) -> Quest {
709 let npc_idx = self.pick(NPC_NAMES);
710 let npc_name = NPC_NAMES[npc_idx];
711 let npc_id = self.rand_range(100, 500);
712 let loc_idx = self.pick(LOCATIONS);
713 let loc = LOCATIONS[loc_idx];
714 let xp = (player_level as u64 * 80 + 200).max(300);
715 let gold = (player_level as u64 * 20 + 100).max(100);
716 let id = self.next_id();
717 Quest::new(id, format!("Escort {npc_name} to Safety"), QuestReward::new(xp, gold))
718 .with_description(format!("Escort {} safely to {}.", npc_name, loc))
719 .add_objective(QuestObjective::new(
720 format!("Escort {npc_name}"),
721 ObjectiveKind::Escort { npc_id },
722 ))
723 .add_objective(QuestObjective::new(
724 format!("Reach {loc}"),
725 ObjectiveKind::Reach { location_name: loc.to_string(), x: 0.0, y: 0.0, z: 0.0, radius: 5.0 },
726 ))
727 .with_level_req(player_level.saturating_sub(3))
728 .with_category(QuestCategory::Escort)
729 }
730
731 pub fn generate_explore_quest(&mut self, player_level: u32) -> Quest {
732 let loc_idx = self.pick(LOCATIONS);
733 let loc = LOCATIONS[loc_idx];
734 let xp = (player_level as u64 * 60 + 150).max(200);
735 let gold = (player_level as u64 * 15 + 50).max(50);
736 let id = self.next_id();
737 Quest::new(id, format!("Explore: {loc}"), QuestReward::new(xp, gold))
738 .with_description(format!("Survey the {} area and report back.", loc))
739 .add_objective(QuestObjective::new(
740 format!("Explore {loc}"),
741 ObjectiveKind::Explore { zone_name: loc.to_string() },
742 ))
743 .with_level_req(player_level.saturating_sub(1))
744 .with_category(QuestCategory::Exploration)
745 }
746
747 pub fn generate_daily_quests(&mut self, player_level: u32, count: usize) -> Vec<Quest> {
748 let mut quests = Vec::new();
749 for i in 0..count {
750 let quest = match i % 4 {
751 0 => self.generate_kill_quest(player_level),
752 1 => self.generate_collect_quest(player_level),
753 2 => self.generate_escort_quest(player_level),
754 _ => self.generate_explore_quest(player_level),
755 };
756 quests.push(quest);
757 }
758 quests
759 }
760}
761
762#[derive(Debug, Clone)]
767pub struct DialogueChoice {
768 pub text: String,
769 pub gives_quest: Option<QuestId>,
770 pub requires_quest_completed: Option<QuestId>,
771 pub requires_item: Option<ItemId>,
772 pub requires_level: u32,
773 pub leads_to_node: Option<usize>,
774}
775
776impl DialogueChoice {
777 pub fn new(text: impl Into<String>) -> Self {
778 Self {
779 text: text.into(),
780 gives_quest: None,
781 requires_quest_completed: None,
782 requires_item: None,
783 requires_level: 0,
784 leads_to_node: None,
785 }
786 }
787
788 pub fn gives_quest(mut self, id: QuestId) -> Self {
789 self.gives_quest = Some(id);
790 self
791 }
792
793 pub fn requires_level(mut self, level: u32) -> Self {
794 self.requires_level = level;
795 self
796 }
797
798 pub fn is_available(&self, player_level: u32, journal: &QuestJournal) -> bool {
799 if player_level < self.requires_level { return false; }
800 if let Some(id) = self.requires_quest_completed {
801 if !journal.has_completed(id) { return false; }
802 }
803 true
804 }
805}
806
807#[derive(Debug, Clone)]
808pub struct DialogueNode {
809 pub npc_text: String,
810 pub choices: Vec<DialogueChoice>,
811}
812
813impl DialogueNode {
814 pub fn new(npc_text: impl Into<String>) -> Self {
815 Self { npc_text: npc_text.into(), choices: Vec::new() }
816 }
817
818 pub fn add_choice(mut self, choice: DialogueChoice) -> Self {
819 self.choices.push(choice);
820 self
821 }
822}
823
824#[derive(Debug, Clone)]
825pub struct DialogueTree {
826 pub npc_id: u64,
827 pub npc_name: String,
828 pub nodes: Vec<DialogueNode>,
829 pub root_node: usize,
830}
831
832impl DialogueTree {
833 pub fn new(npc_id: u64, npc_name: impl Into<String>) -> Self {
834 Self { npc_id, npc_name: npc_name.into(), nodes: Vec::new(), root_node: 0 }
835 }
836
837 pub fn add_node(mut self, node: DialogueNode) -> Self {
838 self.nodes.push(node);
839 self
840 }
841
842 pub fn get_root(&self) -> Option<&DialogueNode> {
843 self.nodes.get(self.root_node)
844 }
845
846 pub fn get_node(&self, idx: usize) -> Option<&DialogueNode> {
847 self.nodes.get(idx)
848 }
849
850 pub fn available_choices(&self, node_idx: usize, level: u32, journal: &QuestJournal) -> Vec<(usize, &DialogueChoice)> {
851 self.nodes.get(node_idx)
852 .map(|n| {
853 n.choices.iter().enumerate()
854 .filter(|(_, c)| c.is_available(level, journal))
855 .collect()
856 })
857 .unwrap_or_default()
858 }
859}
860
861#[derive(Debug, Clone)]
866pub struct TrackerObjective {
867 pub quest_name: String,
868 pub description: String,
869 pub progress: u32,
870 pub required: u32,
871}
872
873impl TrackerObjective {
874 pub fn fraction(&self) -> f32 {
875 if self.required == 0 { return 1.0; }
876 self.progress as f32 / self.required as f32
877 }
878}
879
880#[derive(Debug, Clone, Default)]
881pub struct QuestTracker {
882 pub tracked: Vec<(QuestId, usize)>, pub max_tracked: usize,
884}
885
886impl QuestTracker {
887 pub fn new(max: usize) -> Self {
888 Self { tracked: Vec::new(), max_tracked: max }
889 }
890
891 pub fn track(&mut self, quest_id: QuestId, obj_idx: usize) -> bool {
892 if self.tracked.len() >= self.max_tracked { return false; }
893 if self.tracked.contains(&(quest_id, obj_idx)) { return false; }
894 self.tracked.push((quest_id, obj_idx));
895 true
896 }
897
898 pub fn untrack(&mut self, quest_id: QuestId, obj_idx: usize) {
899 self.tracked.retain(|&(qid, oi)| !(qid == quest_id && oi == obj_idx));
900 }
901
902 pub fn get_display(&self, journal: &QuestJournal) -> Vec<TrackerObjective> {
903 self.tracked.iter().filter_map(|&(qid, oi)| {
904 let quest = journal.get_active(qid)?;
905 let obj = quest.objectives.get(oi)?;
906 Some(TrackerObjective {
907 quest_name: quest.name.clone(),
908 description: obj.description.clone(),
909 progress: obj.progress,
910 required: obj.required,
911 })
912 }).collect()
913 }
914}
915
916#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
921pub enum AchievementCategory {
922 Combat,
923 Exploration,
924 Crafting,
925 Social,
926 Collection,
927 Progression,
928 Secret,
929 Event,
930}
931
932#[derive(Debug, Clone)]
933pub struct Achievement {
934 pub id: AchievementId,
935 pub name: String,
936 pub description: String,
937 pub icon: char,
938 pub points: u32,
939 pub secret: bool,
940 pub category: AchievementCategory,
941 pub trigger: AchievementTrigger,
942 pub reward: Option<AchievementReward>,
943}
944
945impl Achievement {
946 pub fn new(id: AchievementId, name: impl Into<String>, category: AchievementCategory, trigger: AchievementTrigger) -> Self {
947 Self {
948 id,
949 name: name.into(),
950 description: String::new(),
951 icon: '★',
952 points: 10,
953 secret: false,
954 category,
955 trigger,
956 reward: None,
957 }
958 }
959
960 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
961 self.description = desc.into();
962 self
963 }
964
965 pub fn with_points(mut self, pts: u32) -> Self {
966 self.points = pts;
967 self
968 }
969
970 pub fn secret(mut self) -> Self {
971 self.secret = true;
972 self
973 }
974
975 pub fn with_reward(mut self, reward: AchievementReward) -> Self {
976 self.reward = Some(reward);
977 self
978 }
979}
980
981#[derive(Debug, Clone)]
982pub enum AchievementTrigger {
983 LevelReached(u32),
984 QuestCompleted(QuestId),
985 KillCount { enemy_type: String, count: u64 },
986 TotalKills(u64),
987 ItemCollected { item_id: ItemId },
988 GoldAccumulated(u64),
989 SkillRankMaxed(SkillId),
990 QuestsCompleted(u32),
991 AchievementsUnlocked(u32),
992 DeathCount(u32),
993 Manual, }
995
996impl AchievementTrigger {
997 pub fn check_level(&self, level: u32) -> bool {
998 matches!(self, AchievementTrigger::LevelReached(req) if level >= *req)
999 }
1000
1001 pub fn check_kill_count(&self, enemy_type: &str, count: u64) -> bool {
1002 match self {
1003 AchievementTrigger::KillCount { enemy_type: et, count: req } => {
1004 et == enemy_type && count >= *req
1005 }
1006 AchievementTrigger::TotalKills(req) => count >= *req,
1007 _ => false,
1008 }
1009 }
1010}
1011
1012#[derive(Debug, Clone)]
1013pub struct AchievementReward {
1014 pub xp: u64,
1015 pub title: Option<String>,
1016 pub cosmetic: Option<String>,
1017}
1018
1019impl AchievementReward {
1020 pub fn new(xp: u64) -> Self {
1021 Self { xp, title: None, cosmetic: None }
1022 }
1023 pub fn with_title(mut self, t: impl Into<String>) -> Self {
1024 self.title = Some(t.into());
1025 self
1026 }
1027}
1028
1029#[derive(Debug, Clone)]
1030pub struct AchievementProgress {
1031 pub achievement_id: AchievementId,
1032 pub current: u64,
1033 pub required: u64,
1034}
1035
1036impl AchievementProgress {
1037 pub fn fraction(&self) -> f32 {
1038 if self.required == 0 { return 1.0; }
1039 (self.current as f32 / self.required as f32).min(1.0)
1040 }
1041
1042 pub fn is_complete(&self) -> bool {
1043 self.current >= self.required
1044 }
1045}
1046
1047#[derive(Debug, Clone, Default)]
1048pub struct AchievementSystem {
1049 pub achievements: Vec<Achievement>,
1050 pub unlocked: HashSet<AchievementId>,
1051 pub progress: HashMap<AchievementId, AchievementProgress>,
1052 pub total_points: u32,
1053 pub kill_counts: HashMap<String, u64>,
1054 pub total_kills: u64,
1055 pub quests_completed: u32,
1056 pub gold_accumulated: u64,
1057}
1058
1059impl AchievementSystem {
1060 pub fn new() -> Self {
1061 let mut sys = Self::default();
1062 sys.register_defaults();
1063 sys
1064 }
1065
1066 fn register_defaults(&mut self) {
1067 let defaults = vec![
1068 Achievement::new(
1069 AchievementId(1), "First Blood", AchievementCategory::Combat,
1070 AchievementTrigger::TotalKills(1),
1071 ).with_description("Get your first kill.").with_points(5),
1072
1073 Achievement::new(
1074 AchievementId(2), "Slayer", AchievementCategory::Combat,
1075 AchievementTrigger::TotalKills(100),
1076 ).with_description("Kill 100 enemies.").with_points(20),
1077
1078 Achievement::new(
1079 AchievementId(3), "Centurion", AchievementCategory::Combat,
1080 AchievementTrigger::TotalKills(1000),
1081 ).with_description("Kill 1000 enemies.").with_points(50),
1082
1083 Achievement::new(
1084 AchievementId(4), "Goblin Slayer", AchievementCategory::Combat,
1085 AchievementTrigger::KillCount { enemy_type: "goblin".to_string(), count: 50 },
1086 ).with_description("Kill 50 goblins.").with_points(15),
1087
1088 Achievement::new(
1089 AchievementId(5), "Quest Beginner", AchievementCategory::Progression,
1090 AchievementTrigger::QuestsCompleted(1),
1091 ).with_description("Complete your first quest.").with_points(10),
1092
1093 Achievement::new(
1094 AchievementId(6), "Adventurer", AchievementCategory::Progression,
1095 AchievementTrigger::QuestsCompleted(25),
1096 ).with_description("Complete 25 quests.").with_points(25),
1097
1098 Achievement::new(
1099 AchievementId(7), "Veteran", AchievementCategory::Progression,
1100 AchievementTrigger::QuestsCompleted(100),
1101 ).with_description("Complete 100 quests.").with_points(75),
1102
1103 Achievement::new(
1104 AchievementId(8), "Level 10", AchievementCategory::Progression,
1105 AchievementTrigger::LevelReached(10),
1106 ).with_description("Reach level 10.").with_points(10),
1107
1108 Achievement::new(
1109 AchievementId(9), "Level 50", AchievementCategory::Progression,
1110 AchievementTrigger::LevelReached(50),
1111 ).with_description("Reach level 50.").with_points(50),
1112
1113 Achievement::new(
1114 AchievementId(10), "Max Level", AchievementCategory::Progression,
1115 AchievementTrigger::LevelReached(100),
1116 ).with_description("Reach the maximum level.").with_points(100)
1117 .with_reward(AchievementReward::new(10000).with_title("The Ascended")),
1118
1119 Achievement::new(
1120 AchievementId(11), "Wealthy", AchievementCategory::Collection,
1121 AchievementTrigger::GoldAccumulated(10000),
1122 ).with_description("Accumulate 10,000 gold.").with_points(20),
1123
1124 Achievement::new(
1125 AchievementId(12), "Secret: The Unkillable", AchievementCategory::Secret,
1126 AchievementTrigger::DeathCount(0),
1127 ).with_description("Never die. Ever.").with_points(500).secret(),
1128 ];
1129 for ach in defaults {
1130 self.register(ach);
1131 }
1132 }
1133
1134 pub fn register(&mut self, achievement: Achievement) {
1135 self.achievements.push(achievement);
1136 }
1137
1138 pub fn is_unlocked(&self, id: AchievementId) -> bool {
1139 self.unlocked.contains(&id)
1140 }
1141
1142 pub fn unlock(&mut self, id: AchievementId) -> bool {
1143 if self.unlocked.contains(&id) { return false; }
1144 if let Some(ach) = self.achievements.iter().find(|a| a.id == id) {
1145 self.total_points += ach.points;
1146 self.unlocked.insert(id);
1147 return true;
1148 }
1149 false
1150 }
1151
1152 pub fn record_kill(&mut self, enemy_type: &str) -> Vec<AchievementId> {
1153 *self.kill_counts.entry(enemy_type.to_string()).or_insert(0) += 1;
1154 self.total_kills += 1;
1155 self.check_all()
1156 }
1157
1158 pub fn record_quest_complete(&mut self) -> Vec<AchievementId> {
1159 self.quests_completed += 1;
1160 self.check_all()
1161 }
1162
1163 pub fn record_gold(&mut self, amount: u64) -> Vec<AchievementId> {
1164 self.gold_accumulated += amount;
1165 self.check_all()
1166 }
1167
1168 pub fn check_level(&mut self, level: u32) -> Vec<AchievementId> {
1169 let ids: Vec<AchievementId> = self.achievements.iter()
1170 .filter(|a| !self.unlocked.contains(&a.id) && a.trigger.check_level(level))
1171 .map(|a| a.id)
1172 .collect();
1173 let mut newly_unlocked = Vec::new();
1174 for id in ids {
1175 if self.unlock(id) { newly_unlocked.push(id); }
1176 }
1177 newly_unlocked
1178 }
1179
1180 pub fn manual_unlock(&mut self, id: AchievementId) -> bool {
1181 self.unlock(id)
1182 }
1183
1184 fn check_all(&mut self) -> Vec<AchievementId> {
1185 let total_kills = self.total_kills;
1186 let kill_counts = self.kill_counts.clone();
1187 let quests = self.quests_completed;
1188 let gold = self.gold_accumulated;
1189
1190 let ids: Vec<AchievementId> = self.achievements.iter()
1191 .filter(|a| !self.unlocked.contains(&a.id))
1192 .filter(|a| match &a.trigger {
1193 AchievementTrigger::TotalKills(req) => total_kills >= *req,
1194 AchievementTrigger::KillCount { enemy_type, count } => {
1195 kill_counts.get(enemy_type.as_str()).copied().unwrap_or(0) >= *count
1196 }
1197 AchievementTrigger::QuestsCompleted(req) => quests >= *req,
1198 AchievementTrigger::GoldAccumulated(req) => gold >= *req,
1199 _ => false,
1200 })
1201 .map(|a| a.id)
1202 .collect();
1203
1204 let mut newly_unlocked = Vec::new();
1205 for id in ids {
1206 if self.unlock(id) { newly_unlocked.push(id); }
1207 }
1208 newly_unlocked
1209 }
1210
1211 pub fn unlocked_count(&self) -> usize {
1212 self.unlocked.len()
1213 }
1214
1215 pub fn total_achievement_count(&self) -> usize {
1216 self.achievements.len()
1217 }
1218
1219 pub fn completion_fraction(&self) -> f32 {
1220 if self.achievements.is_empty() { return 0.0; }
1221 self.unlocked.len() as f32 / self.achievements.len() as f32
1222 }
1223
1224 pub fn by_category(&self, cat: AchievementCategory) -> Vec<&Achievement> {
1225 self.achievements.iter()
1226 .filter(|a| a.category == cat)
1227 .collect()
1228 }
1229
1230 pub fn recently_unlocked(&self, count: usize) -> Vec<&Achievement> {
1231 self.achievements.iter()
1234 .filter(|a| self.unlocked.contains(&a.id))
1235 .rev()
1236 .take(count)
1237 .collect()
1238 }
1239}
1240
1241#[cfg(test)]
1246mod tests {
1247 use super::*;
1248
1249 fn simple_quest(id: u64, enemy: &str, count: u32) -> Quest {
1250 Quest::new(QuestId(id), format!("Kill {enemy}"), QuestReward::new(100, 50))
1251 .add_objective(QuestObjective::new(
1252 format!("Kill {count} {enemy}s"),
1253 ObjectiveKind::Kill { enemy_type: enemy.to_string(), count },
1254 ))
1255 }
1256
1257 #[test]
1258 fn test_quest_objective_advance() {
1259 let mut obj = QuestObjective::new("Kill 5 goblins", ObjectiveKind::Kill { enemy_type: "goblin".to_string(), count: 5 });
1260 assert!(!obj.is_complete());
1261 obj.advance(3);
1262 assert!(!obj.is_complete());
1263 obj.advance(2);
1264 assert!(obj.is_complete());
1265 }
1266
1267 #[test]
1268 fn test_quest_auto_complete() {
1269 let mut quest = simple_quest(1, "goblin", 3);
1270 quest.activate();
1271 quest.update_objective(0, 3);
1272 assert_eq!(quest.state, QuestState::Completed);
1273 }
1274
1275 #[test]
1276 fn test_quest_journal_add_and_complete() {
1277 let mut journal = QuestJournal::new();
1278 let q = simple_quest(1, "wolf", 2);
1279 assert!(journal.add_quest(q));
1280 assert_eq!(journal.active_count(), 1);
1281 let done = journal.complete_quest(QuestId(1));
1282 assert!(done.is_some());
1283 assert_eq!(journal.active_count(), 0);
1284 assert!(journal.has_completed(QuestId(1)));
1285 }
1286
1287 #[test]
1288 fn test_quest_journal_fail() {
1289 let mut journal = QuestJournal::new();
1290 let q = simple_quest(2, "orc", 5);
1291 journal.add_quest(q);
1292 let failed = journal.fail_quest(QuestId(2));
1293 assert!(failed.is_some());
1294 }
1295
1296 #[test]
1297 fn test_quest_journal_max_active() {
1298 let mut journal = QuestJournal::new();
1299 for i in 0..MAX_ACTIVE_QUESTS {
1300 let q = simple_quest(i as u64, "goblin", 1);
1301 journal.add_quest(q);
1302 }
1303 let overflow = simple_quest(999, "goblin", 1);
1304 assert!(!journal.add_quest(overflow));
1305 }
1306
1307 #[test]
1308 fn test_quest_kill_objective_tracking() {
1309 let mut journal = QuestJournal::new();
1310 let q = simple_quest(1, "goblin", 5);
1311 journal.add_quest(q);
1312 let updates = journal.update_kill_objectives("goblin");
1313 assert!(!updates.is_empty());
1314 }
1315
1316 #[test]
1317 fn test_quest_time_limit_expiry() {
1318 let mut quest = Quest::new(QuestId(1), "Timed", QuestReward::default())
1319 .with_time_limit(5.0);
1320 quest.activate();
1321 let expired = quest.tick(6.0);
1322 assert!(expired);
1323 assert_eq!(quest.state, QuestState::Failed);
1324 }
1325
1326 #[test]
1327 fn test_quest_generator_kill() {
1328 let mut gen = QuestGenerator::new(42);
1329 let q = gen.generate_kill_quest(10);
1330 assert!(!q.objectives.is_empty());
1331 assert!(matches!(q.objectives[0].kind, ObjectiveKind::Kill { .. }));
1332 }
1333
1334 #[test]
1335 fn test_quest_generator_daily() {
1336 let mut gen = QuestGenerator::new(99);
1337 let quests = gen.generate_daily_quests(15, 8);
1338 assert_eq!(quests.len(), 8);
1339 }
1340
1341 #[test]
1342 fn test_achievement_system_unlock() {
1343 let mut sys = AchievementSystem::new();
1344 for _ in 0..100 {
1346 sys.record_kill("anything");
1347 }
1348 assert!(sys.is_unlocked(AchievementId(2))); }
1350
1351 #[test]
1352 fn test_achievement_kill_count() {
1353 let mut sys = AchievementSystem::new();
1354 for _ in 0..50 {
1355 sys.record_kill("goblin");
1356 }
1357 assert!(sys.is_unlocked(AchievementId(4))); }
1359
1360 #[test]
1361 fn test_achievement_quest_completion() {
1362 let mut sys = AchievementSystem::new();
1363 sys.record_quest_complete();
1364 assert!(sys.is_unlocked(AchievementId(5))); }
1366
1367 #[test]
1368 fn test_quest_chain_advance() {
1369 let mut chain = QuestChain::new(1, "Main Story",
1370 vec![QuestId(1), QuestId(2), QuestId(3)], true);
1371 assert_eq!(chain.current_quest(), Some(QuestId(1)));
1372 chain.advance();
1373 assert_eq!(chain.current_quest(), Some(QuestId(2)));
1374 chain.advance();
1375 chain.advance();
1376 assert!(chain.is_complete());
1377 }
1378
1379 #[test]
1380 fn test_quest_board_expiry() {
1381 let mut board = QuestBoard::new();
1382 let q = simple_quest(1, "troll", 1);
1383 board.post(QuestBoardEntry::new(q, QuestTrigger::Always).with_expiry(5.0));
1384 board.tick(6.0);
1385 assert!(board.entries.is_empty());
1386 }
1387}