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 + 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// ---------------------------------------------------------------------------
521// QuestTrigger — conditions that make quests available
522// ---------------------------------------------------------------------------
523
524#[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// ---------------------------------------------------------------------------
556// QuestBoard — dynamic board of available quests
557// ---------------------------------------------------------------------------
558
559#[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
616// ---------------------------------------------------------------------------
617// QuestGenerator — procedural quest generation
618// ---------------------------------------------------------------------------
619
620static 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// ---------------------------------------------------------------------------
765// DialogueQuestIntegration
766// ---------------------------------------------------------------------------
767
768#[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// ---------------------------------------------------------------------------
864// QuestTracker — minimal HUD data for objectives
865// ---------------------------------------------------------------------------
866
867#[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)>, // (quest_id, obj_idx)
885    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// ---------------------------------------------------------------------------
919// AchievementSystem
920// ---------------------------------------------------------------------------
921
922#[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, // triggered from code
996}
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        // Returns last N unlocked (order of unlock is not tracked precisely here;
1234        // we return achievements whose ids are in unlocked, by order in the list)
1235        self.achievements.iter()
1236            .filter(|a| self.unlocked.contains(&a.id))
1237            .rev()
1238            .take(count)
1239            .collect()
1240    }
1241}
1242
1243// ---------------------------------------------------------------------------
1244// Tests
1245// ---------------------------------------------------------------------------
1246
1247#[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        // Record 100 kills
1347        for _ in 0..100 {
1348            sys.record_kill("anything");
1349        }
1350        assert!(sys.is_unlocked(AchievementId(2))); // Slayer: 100 kills
1351    }
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))); // Goblin Slayer
1360    }
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))); // Quest Beginner
1367    }
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}