Skip to main content

proof_engine/character/
quests.rs

1// src/character/quests.rs
2// Quest system, journal, achievements, procedural quests.
3
4use std::collections::{HashMap, HashSet};
5use crate::character::inventory::Item;
6use crate::character::skills::SkillId;
7
8// ---------------------------------------------------------------------------
9// QuestId / ItemId
10// ---------------------------------------------------------------------------
11
12#[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// ---------------------------------------------------------------------------
22// QuestState
23// ---------------------------------------------------------------------------
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26pub enum QuestState {
27    Available,
28    Active,
29    Completed,
30    Failed,
31    Abandoned,
32}
33
34// ---------------------------------------------------------------------------
35// ObjectiveKind
36// ---------------------------------------------------------------------------
37
38#[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// ---------------------------------------------------------------------------
76// QuestObjective
77// ---------------------------------------------------------------------------
78
79#[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, // revealed only when triggered
87}
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// ---------------------------------------------------------------------------
129// QuestReward
130// ---------------------------------------------------------------------------
131
132#[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// ---------------------------------------------------------------------------
186// Quest
187// ---------------------------------------------------------------------------
188
189#[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; // Signal: quest expired
310            }
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; // Quest completed!
326            }
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
341// ---------------------------------------------------------------------------
342// QuestJournal — the player's quest log (max 25 active)
343// ---------------------------------------------------------------------------
344
345pub 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        // If quest got auto-completed, move it
400        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// ---------------------------------------------------------------------------
480// QuestChain — sequential quest series
481// ---------------------------------------------------------------------------
482
483#[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// ---------------------------------------------------------------------------
519// QuestTrigger — conditions that make quests available
520// ---------------------------------------------------------------------------
521
522#[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// ---------------------------------------------------------------------------
554// QuestBoard — dynamic board of available quests
555// ---------------------------------------------------------------------------
556
557#[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
614// ---------------------------------------------------------------------------
615// QuestGenerator — procedural quest generation
616// ---------------------------------------------------------------------------
617
618static 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// ---------------------------------------------------------------------------
763// DialogueQuestIntegration
764// ---------------------------------------------------------------------------
765
766#[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// ---------------------------------------------------------------------------
862// QuestTracker — minimal HUD data for objectives
863// ---------------------------------------------------------------------------
864
865#[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)>, // (quest_id, obj_idx)
883    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// ---------------------------------------------------------------------------
917// AchievementSystem
918// ---------------------------------------------------------------------------
919
920#[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, // triggered from code
994}
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        // Returns last N unlocked (order of unlock is not tracked precisely here;
1232        // we return achievements whose ids are in unlocked, by order in the list)
1233        self.achievements.iter()
1234            .filter(|a| self.unlocked.contains(&a.id))
1235            .rev()
1236            .take(count)
1237            .collect()
1238    }
1239}
1240
1241// ---------------------------------------------------------------------------
1242// Tests
1243// ---------------------------------------------------------------------------
1244
1245#[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        // Record 100 kills
1345        for _ in 0..100 {
1346            sys.record_kill("anything");
1347        }
1348        assert!(sys.is_unlocked(AchievementId(2))); // Slayer: 100 kills
1349    }
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))); // Goblin Slayer
1358    }
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))); // Quest Beginner
1365    }
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}