1#[allow(dead_code, unused_variables, unused_mut, unused_imports)]
2
3use glam::{Vec2, Vec3, Vec4, Quat, Mat4};
4use std::collections::{HashMap, VecDeque, HashSet, BTreeMap};
5
6#[derive(Debug, Clone, PartialEq)]
11pub enum QuestNodeType {
12 QuestStart,
13 Objective,
14 ConditionCheck,
15 Reward,
16 Branch,
17 Fail,
18 Completion,
19 Timer,
20 Trigger,
21}
22
23#[derive(Debug, Clone)]
24pub struct QuestStartNode {
25 pub id: u64,
26 pub position: Vec2,
27 pub quest_id: String,
28 pub quest_name: String,
29 pub description: String,
30 pub quest_giver_id: String,
31 pub prerequisites: Vec<QuestPrerequisite>,
32 pub initial_journal_entry: String,
33 pub start_map_markers: Vec<MapMarker>,
34 pub on_start_events: Vec<String>,
35 pub output_port: u64,
36 pub tags: Vec<String>,
37 pub comment: String,
38 pub auto_start: bool,
39 pub recommended_level: u32,
40 pub difficulty_rating: f32,
41}
42
43impl QuestStartNode {
44 pub fn new(id: u64) -> Self {
45 QuestStartNode {
46 id,
47 position: Vec2::ZERO,
48 quest_id: format!("quest_{}", id),
49 quest_name: String::new(),
50 description: String::new(),
51 quest_giver_id: String::new(),
52 prerequisites: Vec::new(),
53 initial_journal_entry: String::new(),
54 start_map_markers: Vec::new(),
55 on_start_events: Vec::new(),
56 output_port: 0,
57 tags: Vec::new(),
58 comment: String::new(),
59 auto_start: false,
60 recommended_level: 1,
61 difficulty_rating: 1.0,
62 }
63 }
64
65 pub fn check_prerequisites(&self, state: &QuestStateStore) -> bool {
66 for prereq in &self.prerequisites {
67 if !prereq.is_satisfied(state) {
68 return false;
69 }
70 }
71 true
72 }
73}
74
75#[derive(Debug, Clone)]
76pub struct QuestPrerequisite {
77 pub prereq_type: PrerequisiteType,
78 pub quest_id: Option<String>,
79 pub level_required: Option<u32>,
80 pub faction_id: Option<String>,
81 pub faction_rep_required: Option<i32>,
82 pub flag_name: Option<String>,
83 pub negated: bool,
84}
85
86#[derive(Debug, Clone, PartialEq)]
87pub enum PrerequisiteType {
88 QuestCompleted,
89 QuestActive,
90 QuestNotStarted,
91 PlayerLevel,
92 FactionRep,
93 Flag,
94 ItemOwned,
95}
96
97impl QuestPrerequisite {
98 pub fn is_satisfied(&self, state: &QuestStateStore) -> bool {
99 let result = match &self.prereq_type {
100 PrerequisiteType::QuestCompleted => {
101 if let Some(qid) = &self.quest_id {
102 state.get_quest_state(qid) == QuestState::Completed
103 } else { false }
104 }
105 PrerequisiteType::QuestActive => {
106 if let Some(qid) = &self.quest_id {
107 state.get_quest_state(qid) == QuestState::Active
108 } else { false }
109 }
110 PrerequisiteType::QuestNotStarted => {
111 if let Some(qid) = &self.quest_id {
112 state.get_quest_state(qid) == QuestState::NotStarted
113 } else { true }
114 }
115 PrerequisiteType::PlayerLevel => {
116 if let Some(level) = self.level_required {
117 state.player_level >= level
118 } else { true }
119 }
120 PrerequisiteType::FactionRep => {
121 if let (Some(fid), Some(req)) = (&self.faction_id, &self.faction_rep_required) {
122 state.get_faction_rep(fid) >= *req
123 } else { false }
124 }
125 PrerequisiteType::Flag => {
126 if let Some(fname) = &self.flag_name {
127 state.get_flag(fname)
128 } else { false }
129 }
130 PrerequisiteType::ItemOwned => {
131 false }
133 };
134 if self.negated { !result } else { result }
135 }
136}
137
138#[derive(Debug, Clone)]
139pub struct ObjectiveNode {
140 pub id: u64,
141 pub position: Vec2,
142 pub objective_id: String,
143 pub objective: QuestObjective,
144 pub input_port: u64,
145 pub success_port: u64,
146 pub failure_port: Option<u64>,
147 pub optional: bool,
148 pub secret: bool,
149 pub journal_entry_on_start: Option<String>,
150 pub journal_entry_on_complete: Option<String>,
151 pub map_markers: Vec<MapMarker>,
152 pub time_limit: Option<f32>,
153 pub elapsed_time: f32,
154 pub tags: Vec<String>,
155 pub comment: String,
156 pub completion_xp_bonus: u32,
157}
158
159impl ObjectiveNode {
160 pub fn new(id: u64, objective: QuestObjective) -> Self {
161 ObjectiveNode {
162 id,
163 position: Vec2::ZERO,
164 objective_id: format!("obj_{}", id),
165 objective,
166 input_port: 0,
167 success_port: 1,
168 failure_port: None,
169 optional: false,
170 secret: false,
171 journal_entry_on_start: None,
172 journal_entry_on_complete: None,
173 map_markers: Vec::new(),
174 time_limit: None,
175 elapsed_time: 0.0,
176 tags: Vec::new(),
177 comment: String::new(),
178 completion_xp_bonus: 0,
179 }
180 }
181
182 pub fn update(&mut self, delta: f32, events: &[QuestEvent]) -> ObjectiveNodeResult {
183 if let Some(limit) = self.time_limit {
184 self.elapsed_time += delta;
185 if self.elapsed_time >= limit {
186 return ObjectiveNodeResult::TimedOut;
187 }
188 }
189 for event in events {
190 self.objective.process_event(event);
191 }
192 match self.objective.get_status() {
193 ObjectiveStatus::Completed => ObjectiveNodeResult::Succeeded,
194 ObjectiveStatus::Failed => ObjectiveNodeResult::Failed,
195 ObjectiveStatus::Active => ObjectiveNodeResult::InProgress(self.objective.get_progress()),
196 ObjectiveStatus::NotStarted => ObjectiveNodeResult::NotStarted,
197 }
198 }
199
200 pub fn time_remaining(&self) -> Option<f32> {
201 self.time_limit.map(|limit| (limit - self.elapsed_time).max(0.0))
202 }
203
204 pub fn progress_display(&self) -> String {
205 self.objective.get_progress_display()
206 }
207}
208
209#[derive(Debug, Clone)]
210pub enum ObjectiveNodeResult {
211 NotStarted,
212 InProgress(f32),
213 Succeeded,
214 Failed,
215 TimedOut,
216}
217
218#[derive(Debug, Clone)]
219pub struct ConditionCheckNode {
220 pub id: u64,
221 pub position: Vec2,
222 pub conditions: Vec<QuestCondition>,
223 pub logic: ConditionLogic,
224 pub true_port: u64,
225 pub false_port: u64,
226 pub input_port: u64,
227 pub check_label: String,
228 pub tags: Vec<String>,
229 pub comment: String,
230}
231
232#[derive(Debug, Clone, PartialEq)]
233pub enum ConditionLogic {
234 All,
235 Any,
236 None,
237 Majority,
238}
239
240impl ConditionCheckNode {
241 pub fn new(id: u64) -> Self {
242 ConditionCheckNode {
243 id,
244 position: Vec2::ZERO,
245 conditions: Vec::new(),
246 logic: ConditionLogic::All,
247 true_port: 0,
248 false_port: 1,
249 input_port: 0,
250 check_label: String::new(),
251 tags: Vec::new(),
252 comment: String::new(),
253 }
254 }
255
256 pub fn evaluate(&self, state: &QuestStateStore) -> bool {
257 let results: Vec<bool> = self.conditions.iter()
258 .map(|c| c.evaluate(state))
259 .collect();
260 match self.logic {
261 ConditionLogic::All => results.iter().all(|&r| r),
262 ConditionLogic::Any => results.iter().any(|&r| r),
263 ConditionLogic::None => results.iter().all(|&r| !r),
264 ConditionLogic::Majority => {
265 let true_count = results.iter().filter(|&&r| r).count();
266 true_count * 2 > results.len()
267 }
268 }
269 }
270}
271
272#[derive(Debug, Clone)]
273pub struct RewardNode {
274 pub id: u64,
275 pub position: Vec2,
276 pub reward_table: RewardTable,
277 pub input_port: u64,
278 pub output_port: u64,
279 pub reward_label: String,
280 pub announce_rewards: bool,
281 pub delay_secs: f32,
282 pub tags: Vec<String>,
283 pub comment: String,
284}
285
286impl RewardNode {
287 pub fn new(id: u64) -> Self {
288 RewardNode {
289 id,
290 position: Vec2::ZERO,
291 reward_table: RewardTable::new(),
292 input_port: 0,
293 output_port: 1,
294 reward_label: "Quest Reward".to_string(),
295 announce_rewards: true,
296 delay_secs: 0.0,
297 tags: Vec::new(),
298 comment: String::new(),
299 }
300 }
301
302 pub fn roll_rewards(&self, player_level: u32, rng: &mut SimpleRng) -> Vec<RolledReward> {
303 self.reward_table.roll(player_level, rng)
304 }
305}
306
307#[derive(Debug, Clone)]
308pub struct BranchNode {
309 pub id: u64,
310 pub position: Vec2,
311 pub branch_type: BranchType,
312 pub conditions: Vec<(QuestCondition, u64)>,
313 pub default_port: u64,
314 pub input_port: u64,
315 pub tags: Vec<String>,
316 pub comment: String,
317}
318
319#[derive(Debug, Clone, PartialEq)]
320pub enum BranchType {
321 FirstTrue,
322 AllTrue,
323 Random,
324 PlayerChoice,
325 ValueSwitch(String),
326}
327
328impl BranchNode {
329 pub fn new(id: u64) -> Self {
330 BranchNode {
331 id,
332 position: Vec2::ZERO,
333 branch_type: BranchType::FirstTrue,
334 conditions: Vec::new(),
335 default_port: 0,
336 input_port: 0,
337 tags: Vec::new(),
338 comment: String::new(),
339 }
340 }
341
342 pub fn evaluate(&self, state: &QuestStateStore, rng: &mut SimpleRng) -> u64 {
343 match &self.branch_type {
344 BranchType::FirstTrue => {
345 for (cond, port) in &self.conditions {
346 if cond.evaluate(state) {
347 return *port;
348 }
349 }
350 self.default_port
351 }
352 BranchType::AllTrue => {
353 let all_true = self.conditions.iter().all(|(c, _)| c.evaluate(state));
354 if all_true && !self.conditions.is_empty() {
355 self.conditions[0].1
356 } else {
357 self.default_port
358 }
359 }
360 BranchType::Random => {
361 if self.conditions.is_empty() { return self.default_port; }
362 let idx = rng.next_usize(self.conditions.len());
363 self.conditions[idx].1
364 }
365 BranchType::PlayerChoice => {
366 self.default_port }
368 BranchType::ValueSwitch(var_name) => {
369 self.default_port
371 }
372 }
373 }
374}
375
376#[derive(Debug, Clone)]
377pub struct FailNode {
378 pub id: u64,
379 pub position: Vec2,
380 pub fail_reason: String,
381 pub fail_message: String,
382 pub on_fail_events: Vec<String>,
383 pub penalty_reputation: Vec<(String, i32)>,
384 pub penalty_xp: i32,
385 pub allow_retry: bool,
386 pub retry_cooldown: f32,
387 pub input_port: u64,
388 pub tags: Vec<String>,
389 pub comment: String,
390}
391
392impl FailNode {
393 pub fn new(id: u64) -> Self {
394 FailNode {
395 id,
396 position: Vec2::ZERO,
397 fail_reason: String::new(),
398 fail_message: "Quest Failed".to_string(),
399 on_fail_events: Vec::new(),
400 penalty_reputation: Vec::new(),
401 penalty_xp: 0,
402 allow_retry: false,
403 retry_cooldown: 0.0,
404 input_port: 0,
405 tags: Vec::new(),
406 comment: String::new(),
407 }
408 }
409
410 pub fn get_penalty_description(&self) -> String {
411 let mut parts = Vec::new();
412 if self.penalty_xp < 0 {
413 parts.push(format!("{} XP", self.penalty_xp));
414 }
415 for (faction, delta) in &self.penalty_reputation {
416 if *delta != 0 {
417 parts.push(format!("{}: {:+}", faction, delta));
418 }
419 }
420 parts.join(", ")
421 }
422}
423
424#[derive(Debug, Clone)]
425pub struct CompletionNode {
426 pub id: u64,
427 pub position: Vec2,
428 pub completion_type: CompletionType,
429 pub completion_message: String,
430 pub journal_entry: String,
431 pub on_complete_events: Vec<String>,
432 pub unlock_quests: Vec<String>,
433 pub set_flags: Vec<String>,
434 pub mark_map_markers_complete: bool,
435 pub credits_sequence: bool,
436 pub input_port: u64,
437 pub tags: Vec<String>,
438 pub comment: String,
439}
440
441#[derive(Debug, Clone, PartialEq)]
442pub enum CompletionType {
443 Success,
444 PartialSuccess,
445 Bittersweet,
446 PyrrhicVictory,
447}
448
449impl CompletionNode {
450 pub fn new(id: u64) -> Self {
451 CompletionNode {
452 id,
453 position: Vec2::ZERO,
454 completion_type: CompletionType::Success,
455 completion_message: "Quest Complete!".to_string(),
456 journal_entry: String::new(),
457 on_complete_events: Vec::new(),
458 unlock_quests: Vec::new(),
459 set_flags: Vec::new(),
460 mark_map_markers_complete: true,
461 credits_sequence: false,
462 input_port: 0,
463 tags: Vec::new(),
464 comment: String::new(),
465 }
466 }
467}
468
469#[derive(Debug, Clone)]
470pub struct TimerNode {
471 pub id: u64,
472 pub position: Vec2,
473 pub duration_secs: f32,
474 pub elapsed: f32,
475 pub on_expire_port: u64,
476 pub on_complete_port: u64,
477 pub show_timer_hud: bool,
478 pub timer_label: String,
479 pub timer_color: Vec4,
480 pub pause_when_inactive: bool,
481 pub running: bool,
482 pub input_port: u64,
483 pub tags: Vec<String>,
484 pub comment: String,
485}
486
487impl TimerNode {
488 pub fn new(id: u64) -> Self {
489 TimerNode {
490 id,
491 position: Vec2::ZERO,
492 duration_secs: 60.0,
493 elapsed: 0.0,
494 on_expire_port: 0,
495 on_complete_port: 1,
496 show_timer_hud: true,
497 timer_label: String::new(),
498 timer_color: Vec4::new(1.0, 0.5, 0.0, 1.0),
499 pause_when_inactive: false,
500 running: false,
501 input_port: 0,
502 tags: Vec::new(),
503 comment: String::new(),
504 }
505 }
506
507 pub fn update(&mut self, delta: f32) -> Option<bool> {
508 if !self.running { return None; }
509 self.elapsed += delta;
510 if self.elapsed >= self.duration_secs {
511 self.running = false;
512 Some(true) } else {
514 None
515 }
516 }
517
518 pub fn reset(&mut self) { self.elapsed = 0.0; self.running = false; }
519 pub fn start(&mut self) { self.running = true; }
520 pub fn remaining(&self) -> f32 { (self.duration_secs - self.elapsed).max(0.0) }
521 pub fn progress(&self) -> f32 { (self.elapsed / self.duration_secs).clamp(0.0, 1.0) }
522
523 pub fn format_remaining(&self) -> String {
524 let rem = self.remaining();
525 let mins = (rem / 60.0) as u32;
526 let secs = (rem % 60.0) as u32;
527 format!("{}:{:02}", mins, secs)
528 }
529}
530
531#[derive(Debug, Clone)]
532pub struct TriggerNode {
533 pub id: u64,
534 pub position: Vec2,
535 pub trigger_type: QuestTriggerType,
536 pub trigger_data: HashMap<String, String>,
537 pub output_port: u64,
538 pub one_shot: bool,
539 pub triggered: bool,
540 pub cooldown: f32,
541 pub last_triggered: f32,
542 pub input_port: u64,
543 pub tags: Vec<String>,
544 pub comment: String,
545}
546
547#[derive(Debug, Clone, PartialEq)]
548pub enum QuestTriggerType {
549 EnterArea(Vec3, f32),
550 KillTarget(String),
551 InteractObject(String),
552 ItemPickup(String),
553 TimePassed(f32),
554 Custom(String),
555 PlayerNearNPC(String, f32),
556 QuestStateChange(String, QuestState),
557}
558
559impl TriggerNode {
560 pub fn new(id: u64) -> Self {
561 TriggerNode {
562 id,
563 position: Vec2::ZERO,
564 trigger_type: QuestTriggerType::Custom("default".to_string()),
565 trigger_data: HashMap::new(),
566 output_port: 0,
567 one_shot: true,
568 triggered: false,
569 cooldown: 0.0,
570 last_triggered: -f32::MAX,
571 input_port: 0,
572 tags: Vec::new(),
573 comment: String::new(),
574 }
575 }
576
577 pub fn check_trigger(&self, event: &QuestEvent, current_time: f32) -> bool {
578 if self.one_shot && self.triggered { return false; }
579 if current_time - self.last_triggered < self.cooldown { return false; }
580 match (&self.trigger_type, event) {
581 (QuestTriggerType::KillTarget(target_id), QuestEvent::EnemyKilled { enemy_id, .. }) => {
582 Some(target_id.as_str()) == enemy_id.as_deref()
583 }
584 (QuestTriggerType::ItemPickup(item_id), QuestEvent::ItemPickedUp { item_id: picked_id, .. }) => {
585 item_id == picked_id
586 }
587 (QuestTriggerType::InteractObject(obj_id), QuestEvent::ObjectInteracted { object_id }) => {
588 obj_id == object_id
589 }
590 (QuestTriggerType::Custom(name), QuestEvent::CustomEvent { name: event_name, .. }) => {
591 name == event_name
592 }
593 _ => false,
594 }
595 }
596}
597
598#[derive(Debug, Clone, PartialEq)]
603pub enum ObjectiveStatus {
604 NotStarted,
605 Active,
606 Completed,
607 Failed,
608}
609
610#[derive(Debug, Clone)]
611pub enum QuestObjective {
612 KillEnemy(KillEnemyObjective),
613 CollectItem(CollectItemObjective),
614 ReachLocation(ReachLocationObjective),
615 TalkToNPC(TalkToNPCObjective),
616 ProtectTarget(ProtectTargetObjective),
617 EscortTarget(EscortTargetObjective),
618 SurviveWaves(SurviveWavesObjective),
619 SolvePuzzle(SolvePuzzleObjective),
620 CraftItem(CraftItemObjective),
621 UseAbility(UseAbilityObjective),
622 ExploreArea(ExploreAreaObjective),
623 TakePhoto(TakePhotoObjective),
624 BuildStructure(BuildStructureObjective),
625 DefeatBoss(DefeatBossObjective),
626 FindSecret(FindSecretObjective),
627 DeliverItem(DeliverItemObjective),
628 InvestigateClue(InvestigateClueObjective),
629 RepairObject(RepairObjectObjective),
630 PlantDevice(PlantDeviceObjective),
631 HackTerminal(HackTerminalObjective),
632 SneakPast(SneakPastObjective),
633 PickpocketTarget(PickpocketTargetObjective),
634}
635
636impl QuestObjective {
637 pub fn get_status(&self) -> ObjectiveStatus {
638 match self {
639 QuestObjective::KillEnemy(o) => o.status(),
640 QuestObjective::CollectItem(o) => o.status(),
641 QuestObjective::ReachLocation(o) => o.status(),
642 QuestObjective::TalkToNPC(o) => o.status(),
643 QuestObjective::ProtectTarget(o) => o.status(),
644 QuestObjective::EscortTarget(o) => o.status(),
645 QuestObjective::SurviveWaves(o) => o.status(),
646 QuestObjective::SolvePuzzle(o) => o.status(),
647 QuestObjective::CraftItem(o) => o.status(),
648 QuestObjective::UseAbility(o) => o.status(),
649 QuestObjective::ExploreArea(o) => o.status(),
650 QuestObjective::TakePhoto(o) => o.status(),
651 QuestObjective::BuildStructure(o) => o.status(),
652 QuestObjective::DefeatBoss(o) => o.status(),
653 QuestObjective::FindSecret(o) => o.status(),
654 QuestObjective::DeliverItem(o) => o.status(),
655 QuestObjective::InvestigateClue(o) => o.status(),
656 QuestObjective::RepairObject(o) => o.status(),
657 QuestObjective::PlantDevice(o) => o.status(),
658 QuestObjective::HackTerminal(o) => o.status(),
659 QuestObjective::SneakPast(o) => o.status(),
660 QuestObjective::PickpocketTarget(o) => o.status(),
661 }
662 }
663
664 pub fn get_progress(&self) -> f32 {
665 match self {
666 QuestObjective::KillEnemy(o) => o.progress(),
667 QuestObjective::CollectItem(o) => o.progress(),
668 QuestObjective::ReachLocation(o) => o.progress(),
669 QuestObjective::TalkToNPC(o) => o.progress(),
670 QuestObjective::ProtectTarget(o) => o.progress(),
671 QuestObjective::EscortTarget(o) => o.progress(),
672 QuestObjective::SurviveWaves(o) => o.progress(),
673 QuestObjective::SolvePuzzle(o) => o.progress(),
674 QuestObjective::CraftItem(o) => o.progress(),
675 QuestObjective::UseAbility(o) => o.progress(),
676 QuestObjective::ExploreArea(o) => o.progress(),
677 QuestObjective::TakePhoto(o) => o.progress(),
678 QuestObjective::BuildStructure(o) => o.progress(),
679 QuestObjective::DefeatBoss(o) => o.progress(),
680 QuestObjective::FindSecret(o) => o.progress(),
681 QuestObjective::DeliverItem(o) => o.progress(),
682 QuestObjective::InvestigateClue(o) => o.progress(),
683 QuestObjective::RepairObject(o) => o.progress(),
684 QuestObjective::PlantDevice(o) => o.progress(),
685 QuestObjective::HackTerminal(o) => o.progress(),
686 QuestObjective::SneakPast(o) => o.progress(),
687 QuestObjective::PickpocketTarget(o) => o.progress(),
688 }
689 }
690
691 pub fn get_progress_display(&self) -> String {
692 match self {
693 QuestObjective::KillEnemy(o) => format!("{}/{}", o.killed_count, o.required_count),
694 QuestObjective::CollectItem(o) => format!("{}/{}", o.collected_count, o.required_count),
695 QuestObjective::SurviveWaves(o) => format!("Wave {}/{}", o.waves_survived, o.total_waves),
696 QuestObjective::ExploreArea(o) => {
697 let visited = o.areas.iter().filter(|a| a.visited).count();
698 format!("{}/{}", visited, o.areas.len())
699 }
700 QuestObjective::BuildStructure(o) => format!("{:.0}%", o.build_progress * 100.0),
701 QuestObjective::HackTerminal(o) => format!("{:.0}%", o.hack_progress * 100.0),
702 _ => String::new(),
703 }
704 }
705
706 pub fn process_event(&mut self, event: &QuestEvent) {
707 match self {
708 QuestObjective::KillEnemy(o) => o.process_event(event),
709 QuestObjective::CollectItem(o) => o.process_event(event),
710 QuestObjective::ReachLocation(o) => o.process_event(event),
711 QuestObjective::TalkToNPC(o) => o.process_event(event),
712 QuestObjective::ProtectTarget(o) => o.process_event(event),
713 QuestObjective::EscortTarget(o) => o.process_event(event),
714 QuestObjective::SurviveWaves(o) => o.process_event(event),
715 QuestObjective::SolvePuzzle(o) => o.process_event(event),
716 QuestObjective::CraftItem(o) => o.process_event(event),
717 QuestObjective::UseAbility(o) => o.process_event(event),
718 QuestObjective::ExploreArea(o) => o.process_event(event),
719 QuestObjective::TakePhoto(o) => o.process_event(event),
720 QuestObjective::BuildStructure(o) => o.process_event(event),
721 QuestObjective::DefeatBoss(o) => o.process_event(event),
722 QuestObjective::FindSecret(o) => o.process_event(event),
723 QuestObjective::DeliverItem(o) => o.process_event(event),
724 QuestObjective::InvestigateClue(o) => o.process_event(event),
725 QuestObjective::RepairObject(o) => o.process_event(event),
726 QuestObjective::PlantDevice(o) => o.process_event(event),
727 QuestObjective::HackTerminal(o) => o.process_event(event),
728 QuestObjective::SneakPast(o) => o.process_event(event),
729 QuestObjective::PickpocketTarget(o) => o.process_event(event),
730 }
731 }
732
733 pub fn description(&self) -> String {
734 match self {
735 QuestObjective::KillEnemy(o) => format!("Kill {} {}", o.required_count, o.enemy_type),
736 QuestObjective::CollectItem(o) => format!("Collect {} {}", o.required_count, o.item_id),
737 QuestObjective::ReachLocation(o) => format!("Reach {}", o.location_name),
738 QuestObjective::TalkToNPC(o) => format!("Talk to {}", o.npc_name),
739 QuestObjective::ProtectTarget(o) => format!("Protect {} for {}s", o.target_id, o.duration_secs),
740 QuestObjective::EscortTarget(o) => format!("Escort {} to {}", o.target_id, o.destination_name),
741 QuestObjective::SurviveWaves(o) => format!("Survive {} waves", o.total_waves),
742 QuestObjective::SolvePuzzle(o) => format!("Solve the {}", o.puzzle_id),
743 QuestObjective::CraftItem(o) => format!("Craft {}", o.item_id),
744 QuestObjective::UseAbility(o) => format!("Use {} {} times", o.ability_id, o.required_count),
745 QuestObjective::ExploreArea(o) => format!("Explore {}", o.area_name),
746 QuestObjective::TakePhoto(o) => format!("Take photo of {}", o.subject_id),
747 QuestObjective::BuildStructure(o) => format!("Build {}", o.structure_id),
748 QuestObjective::DefeatBoss(o) => format!("Defeat {}", o.boss_id),
749 QuestObjective::FindSecret(o) => format!("Find the secret"),
750 QuestObjective::DeliverItem(o) => format!("Deliver {} to {}", o.item_id, o.recipient_id),
751 QuestObjective::InvestigateClue(o) => format!("Investigate {}", o.location_name),
752 QuestObjective::RepairObject(o) => format!("Repair {}", o.object_id),
753 QuestObjective::PlantDevice(o) => format!("Plant device at {}", o.location_name),
754 QuestObjective::HackTerminal(o) => format!("Hack terminal {}", o.terminal_id),
755 QuestObjective::SneakPast(o) => format!("Sneak past {} guards", o.guard_count),
756 QuestObjective::PickpocketTarget(o) => format!("Pickpocket {}", o.target_id),
757 }
758 }
759}
760
761#[derive(Debug, Clone)]
763pub struct KillEnemyObjective {
764 pub enemy_type: String,
765 pub enemy_id: Option<String>,
766 pub required_count: u32,
767 pub killed_count: u32,
768 pub in_area: Option<String>,
769 pub with_weapon_type: Option<String>,
770 pub require_stealth_kill: bool,
771 pub allow_assists: bool,
772}
773
774impl KillEnemyObjective {
775 pub fn status(&self) -> ObjectiveStatus {
776 if self.killed_count >= self.required_count { ObjectiveStatus::Completed } else { ObjectiveStatus::Active }
777 }
778 pub fn progress(&self) -> f32 {
779 if self.required_count == 0 { return 1.0; }
780 (self.killed_count as f32 / self.required_count as f32).min(1.0)
781 }
782 pub fn process_event(&mut self, event: &QuestEvent) {
783 if let QuestEvent::EnemyKilled { enemy_type, enemy_id, weapon_type, is_stealth } = event {
784 if self.killed_count >= self.required_count { return; }
785 let type_match = &self.enemy_type == enemy_type || self.enemy_type == "*";
786 let id_match = self.enemy_id.as_ref().map(|id| id == enemy_id.as_deref().unwrap_or("")).unwrap_or(true);
787 let weapon_match = self.with_weapon_type.as_ref().map(|w| Some(w) == weapon_type.as_ref()).unwrap_or(true);
788 let stealth_match = !self.require_stealth_kill || *is_stealth;
789 if type_match && id_match && weapon_match && stealth_match {
790 self.killed_count += 1;
791 }
792 }
793 }
794}
795
796#[derive(Debug, Clone)]
798pub struct CollectItemObjective {
799 pub item_id: String,
800 pub item_name: String,
801 pub required_count: u32,
802 pub collected_count: u32,
803 pub quality_requirement: Option<f32>,
804 pub allow_partial: bool,
805 pub consume_on_completion: bool,
806}
807
808impl CollectItemObjective {
809 pub fn status(&self) -> ObjectiveStatus {
810 if self.collected_count >= self.required_count { ObjectiveStatus::Completed } else { ObjectiveStatus::Active }
811 }
812 pub fn progress(&self) -> f32 {
813 if self.required_count == 0 { return 1.0; }
814 (self.collected_count as f32 / self.required_count as f32).min(1.0)
815 }
816 pub fn process_event(&mut self, event: &QuestEvent) {
817 if let QuestEvent::ItemPickedUp { item_id, quantity, quality } = event {
818 if item_id == &self.item_id {
819 let qual_ok = self.quality_requirement.map(|q| *quality >= q).unwrap_or(true);
820 if qual_ok {
821 self.collected_count = (self.collected_count + quantity).min(self.required_count);
822 }
823 }
824 }
825 }
826}
827
828#[derive(Debug, Clone)]
830pub struct ReachLocationObjective {
831 pub location_id: String,
832 pub location_name: String,
833 pub position: Vec3,
834 pub radius: f32,
835 pub required_time_secs: Option<f32>,
836 pub time_at_location: f32,
837 pub reached: bool,
838 pub require_no_combat: bool,
839}
840
841impl ReachLocationObjective {
842 pub fn status(&self) -> ObjectiveStatus {
843 if self.reached { ObjectiveStatus::Completed } else { ObjectiveStatus::Active }
844 }
845 pub fn progress(&self) -> f32 { if self.reached { 1.0 } else { 0.0 } }
846 pub fn process_event(&mut self, event: &QuestEvent) {
847 if let QuestEvent::PlayerMoved { position, is_in_combat } = event {
848 if self.require_no_combat && *is_in_combat { return; }
849 let dist = (*position - self.position).length();
850 if dist <= self.radius {
851 if let Some(req_time) = self.required_time_secs {
852 self.time_at_location += 0.016;
854 if self.time_at_location >= req_time { self.reached = true; }
855 } else {
856 self.reached = true;
857 }
858 } else {
859 self.time_at_location = 0.0;
860 }
861 }
862 }
863}
864
865#[derive(Debug, Clone)]
867pub struct TalkToNPCObjective {
868 pub npc_id: String,
869 pub npc_name: String,
870 pub required_topic: Option<String>,
871 pub talked: bool,
872 pub dialogue_completed: bool,
873 pub require_specific_ending: Option<String>,
874}
875
876impl TalkToNPCObjective {
877 pub fn status(&self) -> ObjectiveStatus {
878 if self.talked && self.dialogue_completed { ObjectiveStatus::Completed } else { ObjectiveStatus::Active }
879 }
880 pub fn progress(&self) -> f32 {
881 match (self.talked, self.dialogue_completed) {
882 (false, _) => 0.0,
883 (true, false) => 0.5,
884 (true, true) => 1.0,
885 }
886 }
887 pub fn process_event(&mut self, event: &QuestEvent) {
888 match event {
889 QuestEvent::DialogueStarted { npc_id } => {
890 if npc_id == &self.npc_id { self.talked = true; }
891 }
892 QuestEvent::DialogueEnded { npc_id, ending } => {
893 if npc_id == &self.npc_id {
894 let ending_ok = self.require_specific_ending.as_ref()
895 .map(|req| req == ending)
896 .unwrap_or(true);
897 if ending_ok { self.dialogue_completed = true; }
898 }
899 }
900 _ => {}
901 }
902 }
903}
904
905#[derive(Debug, Clone)]
907pub struct ProtectTargetObjective {
908 pub target_id: String,
909 pub target_name: String,
910 pub duration_secs: f32,
911 pub protected_secs: f32,
912 pub target_health_min: f32,
913 pub failed: bool,
914 pub running: bool,
915}
916
917impl ProtectTargetObjective {
918 pub fn status(&self) -> ObjectiveStatus {
919 if self.failed { return ObjectiveStatus::Failed; }
920 if self.protected_secs >= self.duration_secs { ObjectiveStatus::Completed } else { ObjectiveStatus::Active }
921 }
922 pub fn progress(&self) -> f32 {
923 if self.failed { return 0.0; }
924 (self.protected_secs / self.duration_secs).min(1.0)
925 }
926 pub fn process_event(&mut self, event: &QuestEvent) {
927 match event {
928 QuestEvent::NPCDied { npc_id } => {
929 if npc_id == &self.target_id { self.failed = true; }
930 }
931 QuestEvent::NPCDamaged { npc_id, health_percent } => {
932 if npc_id == &self.target_id && *health_percent < self.target_health_min {
933 self.failed = true;
934 }
935 }
936 QuestEvent::TimePassed { delta } => {
937 if self.running && !self.failed {
938 self.protected_secs += delta;
939 }
940 }
941 _ => {}
942 }
943 }
944}
945
946#[derive(Debug, Clone)]
948pub struct EscortTargetObjective {
949 pub target_id: String,
950 pub destination_name: String,
951 pub destination_pos: Vec3,
952 pub destination_radius: f32,
953 pub reached: bool,
954 pub target_died: bool,
955 pub must_stay_near: bool,
956 pub stay_near_radius: f32,
957 pub player_too_far: bool,
958}
959
960impl EscortTargetObjective {
961 pub fn status(&self) -> ObjectiveStatus {
962 if self.target_died { ObjectiveStatus::Failed }
963 else if self.reached { ObjectiveStatus::Completed }
964 else { ObjectiveStatus::Active }
965 }
966 pub fn progress(&self) -> f32 { if self.reached { 1.0 } else if self.target_died { 0.0 } else { 0.5 } }
967 pub fn process_event(&mut self, event: &QuestEvent) {
968 match event {
969 QuestEvent::NPCDied { npc_id } => {
970 if npc_id == &self.target_id { self.target_died = true; }
971 }
972 QuestEvent::NPCMoved { npc_id, position } => {
973 if npc_id == &self.target_id {
974 let dist = (*position - self.destination_pos).length();
975 if dist <= self.destination_radius { self.reached = true; }
976 }
977 }
978 _ => {}
979 }
980 }
981}
982
983#[derive(Debug, Clone)]
985pub struct SurviveWavesObjective {
986 pub total_waves: u32,
987 pub waves_survived: u32,
988 pub current_wave_enemies_remaining: u32,
989 pub player_died: bool,
990 pub between_waves: bool,
991 pub wave_countdown: f32,
992 pub wave_interval: f32,
993 pub enemy_spawn_table: Vec<WaveEnemy>,
994}
995
996#[derive(Debug, Clone)]
997pub struct WaveEnemy {
998 pub enemy_type: String,
999 pub count: u32,
1000 pub wave_start: u32,
1001}
1002
1003impl SurviveWavesObjective {
1004 pub fn status(&self) -> ObjectiveStatus {
1005 if self.player_died { ObjectiveStatus::Failed }
1006 else if self.waves_survived >= self.total_waves { ObjectiveStatus::Completed }
1007 else { ObjectiveStatus::Active }
1008 }
1009 pub fn progress(&self) -> f32 {
1010 if self.total_waves == 0 { return 1.0; }
1011 self.waves_survived as f32 / self.total_waves as f32
1012 }
1013 pub fn process_event(&mut self, event: &QuestEvent) {
1014 match event {
1015 QuestEvent::EnemyKilled { .. } => {
1016 if self.current_wave_enemies_remaining > 0 {
1017 self.current_wave_enemies_remaining -= 1;
1018 if self.current_wave_enemies_remaining == 0 {
1019 self.waves_survived += 1;
1020 self.between_waves = true;
1021 self.wave_countdown = self.wave_interval;
1022 }
1023 }
1024 }
1025 QuestEvent::PlayerDied => { self.player_died = true; }
1026 QuestEvent::TimePassed { delta } => {
1027 if self.between_waves {
1028 self.wave_countdown -= delta;
1029 if self.wave_countdown <= 0.0 { self.between_waves = false; }
1030 }
1031 }
1032 _ => {}
1033 }
1034 }
1035}
1036
1037#[derive(Debug, Clone)]
1039pub struct SolvePuzzleObjective {
1040 pub puzzle_id: String,
1041 pub puzzle_name: String,
1042 pub solved: bool,
1043 pub attempts: u32,
1044 pub max_attempts: Option<u32>,
1045 pub failed: bool,
1046 pub hint_unlocked: bool,
1047}
1048
1049impl SolvePuzzleObjective {
1050 pub fn status(&self) -> ObjectiveStatus {
1051 if self.failed { ObjectiveStatus::Failed }
1052 else if self.solved { ObjectiveStatus::Completed }
1053 else { ObjectiveStatus::Active }
1054 }
1055 pub fn progress(&self) -> f32 { if self.solved { 1.0 } else { 0.0 } }
1056 pub fn process_event(&mut self, event: &QuestEvent) {
1057 if let QuestEvent::PuzzleSolved { puzzle_id } = event {
1058 if puzzle_id == &self.puzzle_id { self.solved = true; }
1059 }
1060 if let QuestEvent::PuzzleAttempted { puzzle_id, success } = event {
1061 if puzzle_id == &self.puzzle_id {
1062 self.attempts += 1;
1063 if *success { self.solved = true; }
1064 else if let Some(max) = self.max_attempts {
1065 if self.attempts >= max { self.failed = true; }
1066 }
1067 }
1068 }
1069 }
1070}
1071
1072#[derive(Debug, Clone)]
1074pub struct CraftItemObjective {
1075 pub item_id: String,
1076 pub item_name: String,
1077 pub required_count: u32,
1078 pub crafted_count: u32,
1079 pub quality_min: Option<f32>,
1080 pub at_station_id: Option<String>,
1081}
1082
1083impl CraftItemObjective {
1084 pub fn status(&self) -> ObjectiveStatus {
1085 if self.crafted_count >= self.required_count { ObjectiveStatus::Completed } else { ObjectiveStatus::Active }
1086 }
1087 pub fn progress(&self) -> f32 {
1088 (self.crafted_count as f32 / self.required_count.max(1) as f32).min(1.0)
1089 }
1090 pub fn process_event(&mut self, event: &QuestEvent) {
1091 if let QuestEvent::ItemCrafted { item_id, quality, station_id } = event {
1092 if item_id == &self.item_id {
1093 let qual_ok = self.quality_min.map(|q| *quality >= q).unwrap_or(true);
1094 let station_ok = self.at_station_id.as_ref().map(|s| Some(s) == station_id.as_ref()).unwrap_or(true);
1095 if qual_ok && station_ok && self.crafted_count < self.required_count {
1096 self.crafted_count += 1;
1097 }
1098 }
1099 }
1100 }
1101}
1102
1103#[derive(Debug, Clone)]
1105pub struct UseAbilityObjective {
1106 pub ability_id: String,
1107 pub ability_name: String,
1108 pub required_count: u32,
1109 pub use_count: u32,
1110 pub on_enemy_type: Option<String>,
1111 pub require_hit: bool,
1112}
1113
1114impl UseAbilityObjective {
1115 pub fn status(&self) -> ObjectiveStatus {
1116 if self.use_count >= self.required_count { ObjectiveStatus::Completed } else { ObjectiveStatus::Active }
1117 }
1118 pub fn progress(&self) -> f32 { (self.use_count as f32 / self.required_count.max(1) as f32).min(1.0) }
1119 pub fn process_event(&mut self, event: &QuestEvent) {
1120 if let QuestEvent::AbilityUsed { ability_id, hit, target_type } = event {
1121 if ability_id == &self.ability_id {
1122 let hit_ok = !self.require_hit || *hit;
1123 let target_ok = self.on_enemy_type.as_ref().map(|t| Some(t) == target_type.as_ref()).unwrap_or(true);
1124 if hit_ok && target_ok && self.use_count < self.required_count {
1125 self.use_count += 1;
1126 }
1127 }
1128 }
1129 }
1130}
1131
1132#[derive(Debug, Clone)]
1134pub struct ExploreAreaObjective {
1135 pub area_name: String,
1136 pub areas: Vec<AreaPoint>,
1137 pub require_all: bool,
1138 pub discover_radius: f32,
1139}
1140
1141#[derive(Debug, Clone)]
1142pub struct AreaPoint {
1143 pub id: String,
1144 pub name: String,
1145 pub position: Vec3,
1146 pub visited: bool,
1147 pub optional: bool,
1148}
1149
1150impl ExploreAreaObjective {
1151 pub fn status(&self) -> ObjectiveStatus {
1152 let required: Vec<&AreaPoint> = self.areas.iter().filter(|a| !a.optional).collect();
1153 let visited = required.iter().filter(|a| a.visited).count();
1154 if self.require_all {
1155 if visited >= required.len() { ObjectiveStatus::Completed } else { ObjectiveStatus::Active }
1156 } else {
1157 if visited > 0 { ObjectiveStatus::Completed } else { ObjectiveStatus::Active }
1158 }
1159 }
1160 pub fn progress(&self) -> f32 {
1161 let required: Vec<&AreaPoint> = self.areas.iter().filter(|a| !a.optional).collect();
1162 if required.is_empty() { return 1.0; }
1163 let visited = required.iter().filter(|a| a.visited).count();
1164 visited as f32 / required.len() as f32
1165 }
1166 pub fn process_event(&mut self, event: &QuestEvent) {
1167 if let QuestEvent::PlayerMoved { position, .. } = event {
1168 for area in &mut self.areas {
1169 if !area.visited {
1170 let dist = (*position - area.position).length();
1171 if dist <= self.discover_radius { area.visited = true; }
1172 }
1173 }
1174 }
1175 }
1176}
1177
1178#[derive(Debug, Clone)]
1180pub struct TakePhotoObjective {
1181 pub subject_id: String,
1182 pub subject_name: String,
1183 pub required_count: u32,
1184 pub taken_count: u32,
1185 pub min_quality: f32,
1186 pub must_be_undetected: bool,
1187 pub subject_behavior: Option<String>,
1188}
1189
1190impl TakePhotoObjective {
1191 pub fn status(&self) -> ObjectiveStatus {
1192 if self.taken_count >= self.required_count { ObjectiveStatus::Completed } else { ObjectiveStatus::Active }
1193 }
1194 pub fn progress(&self) -> f32 { (self.taken_count as f32 / self.required_count.max(1) as f32).min(1.0) }
1195 pub fn process_event(&mut self, event: &QuestEvent) {
1196 if let QuestEvent::PhotoTaken { subject_id, quality, undetected } = event {
1197 if subject_id == &self.subject_id {
1198 let qual_ok = *quality >= self.min_quality;
1199 let detect_ok = !self.must_be_undetected || *undetected;
1200 if qual_ok && detect_ok && self.taken_count < self.required_count {
1201 self.taken_count += 1;
1202 }
1203 }
1204 }
1205 }
1206}
1207
1208#[derive(Debug, Clone)]
1210pub struct BuildStructureObjective {
1211 pub structure_id: String,
1212 pub structure_name: String,
1213 pub build_progress: f32,
1214 pub required_progress: f32,
1215 pub at_location: Option<Vec3>,
1216 pub required_resources: Vec<(String, u32)>,
1217 pub resources_placed: HashMap<String, u32>,
1218}
1219
1220impl BuildStructureObjective {
1221 pub fn status(&self) -> ObjectiveStatus {
1222 if self.build_progress >= self.required_progress { ObjectiveStatus::Completed } else { ObjectiveStatus::Active }
1223 }
1224 pub fn progress(&self) -> f32 { (self.build_progress / self.required_progress.max(0.001)).min(1.0) }
1225 pub fn process_event(&mut self, event: &QuestEvent) {
1226 if let QuestEvent::StructureBuilt { structure_id, progress } = event {
1227 if structure_id == &self.structure_id { self.build_progress = *progress; }
1228 }
1229 if let QuestEvent::ResourcePlaced { structure_id, resource_id, amount } = event {
1230 if structure_id == &self.structure_id {
1231 *self.resources_placed.entry(resource_id.clone()).or_insert(0) += amount;
1232 }
1233 }
1234 }
1235}
1236
1237#[derive(Debug, Clone)]
1239pub struct DefeatBossObjective {
1240 pub boss_id: String,
1241 pub boss_name: String,
1242 pub defeated: bool,
1243 pub phases_completed: u32,
1244 pub total_phases: u32,
1245 pub require_no_deaths: bool,
1246 pub player_died: bool,
1247 pub time_limit: Option<f32>,
1248 pub elapsed: f32,
1249}
1250
1251impl DefeatBossObjective {
1252 pub fn status(&self) -> ObjectiveStatus {
1253 if self.require_no_deaths && self.player_died { return ObjectiveStatus::Failed; }
1254 if self.time_limit.map(|t| self.elapsed >= t).unwrap_or(false) { return ObjectiveStatus::Failed; }
1255 if self.defeated { ObjectiveStatus::Completed } else { ObjectiveStatus::Active }
1256 }
1257 pub fn progress(&self) -> f32 {
1258 if self.defeated { return 1.0; }
1259 if self.total_phases == 0 { return 0.0; }
1260 self.phases_completed as f32 / self.total_phases as f32
1261 }
1262 pub fn process_event(&mut self, event: &QuestEvent) {
1263 match event {
1264 QuestEvent::EnemyKilled { enemy_id, .. } => {
1265 if enemy_id.as_deref() == Some(&self.boss_id) { self.defeated = true; }
1266 }
1267 QuestEvent::BossPhaseCompleted { boss_id, phase } => {
1268 if boss_id == &self.boss_id { self.phases_completed = *phase; }
1269 }
1270 QuestEvent::PlayerDied => { self.player_died = true; }
1271 QuestEvent::TimePassed { delta } => { self.elapsed += delta; }
1272 _ => {}
1273 }
1274 }
1275}
1276
1277#[derive(Debug, Clone)]
1279pub struct FindSecretObjective {
1280 pub secret_id: String,
1281 pub location_hint: String,
1282 pub found: bool,
1283 pub reveal_on_find: bool,
1284}
1285
1286impl FindSecretObjective {
1287 pub fn status(&self) -> ObjectiveStatus { if self.found { ObjectiveStatus::Completed } else { ObjectiveStatus::Active } }
1288 pub fn progress(&self) -> f32 { if self.found { 1.0 } else { 0.0 } }
1289 pub fn process_event(&mut self, event: &QuestEvent) {
1290 if let QuestEvent::SecretFound { secret_id } = event {
1291 if secret_id == &self.secret_id { self.found = true; }
1292 }
1293 }
1294}
1295
1296#[derive(Debug, Clone)]
1298pub struct DeliverItemObjective {
1299 pub item_id: String,
1300 pub item_name: String,
1301 pub recipient_id: String,
1302 pub recipient_name: String,
1303 pub quantity: u32,
1304 pub delivered: u32,
1305 pub item_can_be_damaged: bool,
1306 pub item_condition: f32,
1307 pub condition_min: f32,
1308}
1309
1310impl DeliverItemObjective {
1311 pub fn status(&self) -> ObjectiveStatus {
1312 if self.delivered >= self.quantity { ObjectiveStatus::Completed }
1313 else if self.item_can_be_damaged && self.item_condition < self.condition_min { ObjectiveStatus::Failed }
1314 else { ObjectiveStatus::Active }
1315 }
1316 pub fn progress(&self) -> f32 { (self.delivered as f32 / self.quantity.max(1) as f32).min(1.0) }
1317 pub fn process_event(&mut self, event: &QuestEvent) {
1318 if let QuestEvent::ItemDelivered { item_id, recipient_id, quantity } = event {
1319 if item_id == &self.item_id && recipient_id == &self.recipient_id {
1320 self.delivered += quantity;
1321 }
1322 }
1323 }
1324}
1325
1326#[derive(Debug, Clone)]
1328pub struct InvestigateClueObjective {
1329 pub location_name: String,
1330 pub clue_ids: Vec<String>,
1331 pub clues_found: HashSet<String>,
1332 pub required_clues: u32,
1333 pub all_clues_required: bool,
1334}
1335
1336impl InvestigateClueObjective {
1337 pub fn status(&self) -> ObjectiveStatus {
1338 let found = self.clues_found.len() as u32;
1339 let needed = if self.all_clues_required { self.clue_ids.len() as u32 } else { self.required_clues };
1340 if found >= needed { ObjectiveStatus::Completed } else { ObjectiveStatus::Active }
1341 }
1342 pub fn progress(&self) -> f32 {
1343 let needed = if self.all_clues_required { self.clue_ids.len() } else { self.required_clues as usize };
1344 if needed == 0 { return 1.0; }
1345 (self.clues_found.len() as f32 / needed as f32).min(1.0)
1346 }
1347 pub fn process_event(&mut self, event: &QuestEvent) {
1348 if let QuestEvent::ClueFound { clue_id } = event {
1349 if self.clue_ids.contains(clue_id) { self.clues_found.insert(clue_id.clone()); }
1350 }
1351 }
1352}
1353
1354#[derive(Debug, Clone)]
1356pub struct RepairObjectObjective {
1357 pub object_id: String,
1358 pub object_name: String,
1359 pub repair_progress: f32,
1360 pub required_progress: f32,
1361 pub requires_item: Option<String>,
1362 pub item_consumed: bool,
1363}
1364
1365impl RepairObjectObjective {
1366 pub fn status(&self) -> ObjectiveStatus {
1367 if self.repair_progress >= self.required_progress { ObjectiveStatus::Completed } else { ObjectiveStatus::Active }
1368 }
1369 pub fn progress(&self) -> f32 { (self.repair_progress / self.required_progress.max(0.001)).min(1.0) }
1370 pub fn process_event(&mut self, event: &QuestEvent) {
1371 if let QuestEvent::ObjectRepaired { object_id, progress } = event {
1372 if object_id == &self.object_id { self.repair_progress = *progress; }
1373 }
1374 }
1375}
1376
1377#[derive(Debug, Clone)]
1379pub struct PlantDeviceObjective {
1380 pub device_id: String,
1381 pub location_name: String,
1382 pub target_objects: Vec<String>,
1383 pub planted_on: Vec<String>,
1384 pub required_count: u32,
1385 pub undetected: bool,
1386 pub was_detected: bool,
1387}
1388
1389impl PlantDeviceObjective {
1390 pub fn status(&self) -> ObjectiveStatus {
1391 if self.undetected && self.was_detected { return ObjectiveStatus::Failed; }
1392 if self.planted_on.len() >= self.required_count as usize { ObjectiveStatus::Completed } else { ObjectiveStatus::Active }
1393 }
1394 pub fn progress(&self) -> f32 { (self.planted_on.len() as f32 / self.required_count.max(1) as f32).min(1.0) }
1395 pub fn process_event(&mut self, event: &QuestEvent) {
1396 match event {
1397 QuestEvent::DevicePlanted { device_id, object_id } => {
1398 if device_id == &self.device_id && self.target_objects.contains(object_id) {
1399 if !self.planted_on.contains(object_id) { self.planted_on.push(object_id.clone()); }
1400 }
1401 }
1402 QuestEvent::PlayerDetected => { self.was_detected = true; }
1403 _ => {}
1404 }
1405 }
1406}
1407
1408#[derive(Debug, Clone)]
1410pub struct HackTerminalObjective {
1411 pub terminal_id: String,
1412 pub terminal_name: String,
1413 pub hack_progress: f32,
1414 pub required_progress: f32,
1415 pub difficulty: f32,
1416 pub failed_attempts: u32,
1417 pub max_attempts: Option<u32>,
1418 pub locked_out: bool,
1419}
1420
1421impl HackTerminalObjective {
1422 pub fn status(&self) -> ObjectiveStatus {
1423 if self.locked_out { ObjectiveStatus::Failed }
1424 else if self.hack_progress >= self.required_progress { ObjectiveStatus::Completed }
1425 else { ObjectiveStatus::Active }
1426 }
1427 pub fn progress(&self) -> f32 { (self.hack_progress / self.required_progress.max(0.001)).min(1.0) }
1428 pub fn process_event(&mut self, event: &QuestEvent) {
1429 match event {
1430 QuestEvent::TerminalHacked { terminal_id, progress, success } => {
1431 if terminal_id == &self.terminal_id {
1432 if *success {
1433 self.hack_progress = *progress;
1434 } else {
1435 self.failed_attempts += 1;
1436 if let Some(max) = self.max_attempts {
1437 if self.failed_attempts >= max { self.locked_out = true; }
1438 }
1439 }
1440 }
1441 }
1442 _ => {}
1443 }
1444 }
1445}
1446
1447#[derive(Debug, Clone)]
1449pub struct SneakPastObjective {
1450 pub area_id: String,
1451 pub guard_count: u32,
1452 pub guards_passed: u32,
1453 pub was_detected: bool,
1454 pub destination_reached: bool,
1455}
1456
1457impl SneakPastObjective {
1458 pub fn status(&self) -> ObjectiveStatus {
1459 if self.was_detected { ObjectiveStatus::Failed }
1460 else if self.destination_reached { ObjectiveStatus::Completed }
1461 else { ObjectiveStatus::Active }
1462 }
1463 pub fn progress(&self) -> f32 { if self.destination_reached { 1.0 } else { 0.0 } }
1464 pub fn process_event(&mut self, event: &QuestEvent) {
1465 match event {
1466 QuestEvent::PlayerDetected => { self.was_detected = true; }
1467 QuestEvent::PlayerMoved { position, .. } => {}
1468 _ => {}
1469 }
1470 }
1471}
1472
1473#[derive(Debug, Clone)]
1475pub struct PickpocketTargetObjective {
1476 pub target_id: String,
1477 pub target_name: String,
1478 pub item_id: String,
1479 pub stolen: bool,
1480 pub without_detection: bool,
1481 pub detected: bool,
1482}
1483
1484impl PickpocketTargetObjective {
1485 pub fn status(&self) -> ObjectiveStatus {
1486 if self.without_detection && self.detected { ObjectiveStatus::Failed }
1487 else if self.stolen { ObjectiveStatus::Completed }
1488 else { ObjectiveStatus::Active }
1489 }
1490 pub fn progress(&self) -> f32 { if self.stolen { 1.0 } else { 0.0 } }
1491 pub fn process_event(&mut self, event: &QuestEvent) {
1492 match event {
1493 QuestEvent::Pickpocketed { target_id, item_id, detected } => {
1494 if target_id == &self.target_id && item_id == &self.item_id {
1495 self.stolen = true;
1496 if *detected { self.detected = true; }
1497 }
1498 }
1499 _ => {}
1500 }
1501 }
1502}
1503
1504#[derive(Debug, Clone)]
1509pub enum QuestEvent {
1510 EnemyKilled { enemy_type: String, enemy_id: Option<String>, weapon_type: Option<String>, is_stealth: bool },
1511 ItemPickedUp { item_id: String, quantity: u32, quality: f32 },
1512 PlayerMoved { position: Vec3, is_in_combat: bool },
1513 DialogueStarted { npc_id: String },
1514 DialogueEnded { npc_id: String, ending: String },
1515 NPCDied { npc_id: String },
1516 NPCDamaged { npc_id: String, health_percent: f32 },
1517 NPCMoved { npc_id: String, position: Vec3 },
1518 PuzzleSolved { puzzle_id: String },
1519 PuzzleAttempted { puzzle_id: String, success: bool },
1520 ItemCrafted { item_id: String, quality: f32, station_id: Option<String> },
1521 AbilityUsed { ability_id: String, hit: bool, target_type: Option<String> },
1522 PhotoTaken { subject_id: String, quality: f32, undetected: bool },
1523 StructureBuilt { structure_id: String, progress: f32 },
1524 ResourcePlaced { structure_id: String, resource_id: String, amount: u32 },
1525 BossPhaseCompleted { boss_id: String, phase: u32 },
1526 SecretFound { secret_id: String },
1527 ItemDelivered { item_id: String, recipient_id: String, quantity: u32 },
1528 ClueFound { clue_id: String },
1529 ObjectRepaired { object_id: String, progress: f32 },
1530 DevicePlanted { device_id: String, object_id: String },
1531 PlayerDetected,
1532 PlayerDied,
1533 TerminalHacked { terminal_id: String, progress: f32, success: bool },
1534 Pickpocketed { target_id: String, item_id: String, detected: bool },
1535 TimePassed { delta: f32 },
1536 ObjectInteracted { object_id: String },
1537 CustomEvent { name: String, data: HashMap<String, String> },
1538}
1539
1540#[derive(Debug, Clone)]
1545pub struct QuestCondition {
1546 pub condition_type: QuestConditionType,
1547 pub negated: bool,
1548}
1549
1550#[derive(Debug, Clone)]
1551pub enum QuestConditionType {
1552 QuestState { quest_id: String, state: QuestState },
1553 PlayerLevel { min: u32, max: Option<u32> },
1554 FactionRep { faction_id: String, min: i32, max: Option<i32> },
1555 HasItem { item_id: String, quantity: u32 },
1556 FlagSet(String),
1557 TimeSince { event: String, secs: f32 },
1558 ObjectiveComplete { quest_id: String, objective_id: String },
1559 PlayerStat { stat: String, min: f32, max: Option<f32> },
1560}
1561
1562impl QuestCondition {
1563 pub fn evaluate(&self, state: &QuestStateStore) -> bool {
1564 let result = match &self.condition_type {
1565 QuestConditionType::QuestState { quest_id, state: expected } => {
1566 state.get_quest_state(quest_id) == *expected
1567 }
1568 QuestConditionType::PlayerLevel { min, max } => {
1569 let level = state.player_level;
1570 level >= *min && max.map(|m| level <= m).unwrap_or(true)
1571 }
1572 QuestConditionType::FactionRep { faction_id, min, max } => {
1573 let rep = state.get_faction_rep(faction_id);
1574 rep >= *min && max.map(|m| rep <= m).unwrap_or(true)
1575 }
1576 QuestConditionType::HasItem { item_id, quantity } => {
1577 state.get_item_count(item_id) >= *quantity
1578 }
1579 QuestConditionType::FlagSet(flag) => {
1580 state.get_flag(flag)
1581 }
1582 QuestConditionType::TimeSince { event, secs } => {
1583 state.get_time_since(event) >= *secs
1584 }
1585 QuestConditionType::ObjectiveComplete { quest_id, objective_id } => {
1586 state.is_objective_complete(quest_id, objective_id)
1587 }
1588 QuestConditionType::PlayerStat { stat, min, max } => {
1589 let val = state.get_player_stat(stat);
1590 val >= *min && max.map(|m| val <= m).unwrap_or(true)
1591 }
1592 };
1593 if self.negated { !result } else { result }
1594 }
1595}
1596
1597#[derive(Debug, Clone)]
1598pub struct RewardTable {
1599 pub xp_reward: XpReward,
1600 pub item_rewards: Vec<ItemRewardEntry>,
1601 pub currency_rewards: Vec<CurrencyReward>,
1602 pub reputation_rewards: Vec<ReputationReward>,
1603 pub unlock_rewards: Vec<UnlockReward>,
1604 pub skill_rewards: Vec<SkillReward>,
1605}
1606
1607impl RewardTable {
1608 pub fn new() -> Self {
1609 RewardTable {
1610 xp_reward: XpReward::default(),
1611 item_rewards: Vec::new(),
1612 currency_rewards: Vec::new(),
1613 reputation_rewards: Vec::new(),
1614 unlock_rewards: Vec::new(),
1615 skill_rewards: Vec::new(),
1616 }
1617 }
1618
1619 pub fn roll(&self, player_level: u32, rng: &mut SimpleRng) -> Vec<RolledReward> {
1620 let mut rewards = Vec::new();
1621 let xp = self.xp_reward.calculate(player_level);
1623 if xp > 0 {
1624 rewards.push(RolledReward::Xp(xp));
1625 }
1626 for item_entry in &self.item_rewards {
1628 if rng.next_f32() <= item_entry.drop_chance {
1629 let quantity = item_entry.roll_quantity(rng);
1630 let quality = item_entry.roll_quality(rng);
1631 rewards.push(RolledReward::Item {
1632 item_id: item_entry.item_id.clone(),
1633 quantity,
1634 quality,
1635 rarity: item_entry.rarity.clone(),
1636 });
1637 }
1638 }
1639 for currency in &self.currency_rewards {
1641 let amount = currency.roll_amount(player_level, rng);
1642 if amount > 0 {
1643 rewards.push(RolledReward::Currency {
1644 currency_id: currency.currency_id.clone(),
1645 amount,
1646 });
1647 }
1648 }
1649 for rep in &self.reputation_rewards {
1651 rewards.push(RolledReward::Reputation {
1652 faction_id: rep.faction_id.clone(),
1653 delta: rep.calculate_delta(player_level),
1654 });
1655 }
1656 for unlock in &self.unlock_rewards {
1658 rewards.push(RolledReward::Unlock { unlock_id: unlock.unlock_id.clone(), unlock_type: unlock.unlock_type.clone() });
1659 }
1660 rewards
1661 }
1662
1663 pub fn add_item(&mut self, item_id: &str, quantity_min: u32, quantity_max: u32, drop_chance: f32, rarity: ItemRarity) {
1664 self.item_rewards.push(ItemRewardEntry {
1665 item_id: item_id.to_string(),
1666 quantity_min,
1667 quantity_max,
1668 drop_chance,
1669 rarity,
1670 quality_min: 0.5,
1671 quality_max: 1.0,
1672 quality_weights: Vec::new(),
1673 });
1674 }
1675
1676 pub fn add_xp(&mut self, base: u32, level_scale: f32) {
1677 self.xp_reward = XpReward { base, level_scale_factor: level_scale, level_cap: 100, diminish_above_level: None };
1678 }
1679
1680 pub fn add_currency(&mut self, currency_id: &str, base_min: u32, base_max: u32, level_scale: f32) {
1681 self.currency_rewards.push(CurrencyReward {
1682 currency_id: currency_id.to_string(),
1683 base_amount_min: base_min,
1684 base_amount_max: base_max,
1685 level_scale_factor: level_scale,
1686 });
1687 }
1688
1689 pub fn total_estimated_xp(&self, player_level: u32) -> u32 {
1690 self.xp_reward.calculate(player_level)
1691 }
1692}
1693
1694#[derive(Debug, Clone, Default)]
1695pub struct XpReward {
1696 pub base: u32,
1697 pub level_scale_factor: f32,
1698 pub level_cap: u32,
1699 pub diminish_above_level: Option<u32>,
1700}
1701
1702impl XpReward {
1703 pub fn calculate(&self, player_level: u32) -> u32 {
1704 let level = player_level.min(self.level_cap);
1705 let scaled = self.base as f32 * (1.0 + level as f32 * self.level_scale_factor);
1706 if let Some(cap_level) = self.diminish_above_level {
1707 if player_level > cap_level {
1708 let excess = (player_level - cap_level) as f32;
1709 let diminish = 1.0 / (1.0 + excess * 0.1);
1710 return (scaled * diminish) as u32;
1711 }
1712 }
1713 scaled as u32
1714 }
1715}
1716
1717#[derive(Debug, Clone)]
1718pub struct ItemRewardEntry {
1719 pub item_id: String,
1720 pub quantity_min: u32,
1721 pub quantity_max: u32,
1722 pub drop_chance: f32,
1723 pub rarity: ItemRarity,
1724 pub quality_min: f32,
1725 pub quality_max: f32,
1726 pub quality_weights: Vec<(f32, f32)>,
1727}
1728
1729impl ItemRewardEntry {
1730 pub fn roll_quantity(&self, rng: &mut SimpleRng) -> u32 {
1731 if self.quantity_min >= self.quantity_max { return self.quantity_min; }
1732 self.quantity_min + rng.next_u32(self.quantity_max - self.quantity_min + 1)
1733 }
1734 pub fn roll_quality(&self, rng: &mut SimpleRng) -> f32 {
1735 let t = rng.next_f32();
1736 self.quality_min + t * (self.quality_max - self.quality_min)
1737 }
1738}
1739
1740#[derive(Debug, Clone, PartialEq)]
1741pub enum ItemRarity {
1742 Common,
1743 Uncommon,
1744 Rare,
1745 Epic,
1746 Legendary,
1747 Mythic,
1748}
1749
1750impl ItemRarity {
1751 pub fn display_color(&self) -> Vec4 {
1752 match self {
1753 ItemRarity::Common => Vec4::new(0.8, 0.8, 0.8, 1.0),
1754 ItemRarity::Uncommon => Vec4::new(0.2, 0.8, 0.2, 1.0),
1755 ItemRarity::Rare => Vec4::new(0.2, 0.4, 1.0, 1.0),
1756 ItemRarity::Epic => Vec4::new(0.6, 0.1, 0.9, 1.0),
1757 ItemRarity::Legendary => Vec4::new(1.0, 0.6, 0.0, 1.0),
1758 ItemRarity::Mythic => Vec4::new(1.0, 0.0, 0.3, 1.0),
1759 }
1760 }
1761
1762 pub fn drop_chance_modifier(&self) -> f32 {
1763 match self {
1764 ItemRarity::Common => 1.0,
1765 ItemRarity::Uncommon => 0.5,
1766 ItemRarity::Rare => 0.2,
1767 ItemRarity::Epic => 0.05,
1768 ItemRarity::Legendary => 0.01,
1769 ItemRarity::Mythic => 0.001,
1770 }
1771 }
1772}
1773
1774#[derive(Debug, Clone)]
1775pub struct CurrencyReward {
1776 pub currency_id: String,
1777 pub base_amount_min: u32,
1778 pub base_amount_max: u32,
1779 pub level_scale_factor: f32,
1780}
1781
1782impl CurrencyReward {
1783 pub fn roll_amount(&self, player_level: u32, rng: &mut SimpleRng) -> u32 {
1784 let range = self.base_amount_max - self.base_amount_min;
1785 let base = if range > 0 { self.base_amount_min + rng.next_u32(range + 1) } else { self.base_amount_min };
1786 let scaled = base as f32 * (1.0 + player_level as f32 * self.level_scale_factor);
1787 scaled as u32
1788 }
1789}
1790
1791#[derive(Debug, Clone)]
1792pub struct ReputationReward {
1793 pub faction_id: String,
1794 pub base_delta: i32,
1795 pub level_scale: f32,
1796 pub related_factions: Vec<(String, f32)>,
1797}
1798
1799impl ReputationReward {
1800 pub fn calculate_delta(&self, player_level: u32) -> i32 {
1801 let scaled = self.base_delta as f32 * (1.0 + player_level as f32 * self.level_scale);
1802 scaled as i32
1803 }
1804}
1805
1806#[derive(Debug, Clone)]
1807pub struct UnlockReward {
1808 pub unlock_id: String,
1809 pub unlock_type: UnlockType,
1810 pub display_name: String,
1811}
1812
1813#[derive(Debug, Clone, PartialEq)]
1814pub enum UnlockType {
1815 Ability,
1816 Recipe,
1817 Area,
1818 Item,
1819 NPC,
1820 Quest,
1821}
1822
1823#[derive(Debug, Clone)]
1824pub struct SkillReward {
1825 pub skill_id: String,
1826 pub skill_name: String,
1827 pub points: u32,
1828}
1829
1830#[derive(Debug, Clone)]
1831pub enum RolledReward {
1832 Xp(u32),
1833 Item { item_id: String, quantity: u32, quality: f32, rarity: ItemRarity },
1834 Currency { currency_id: String, amount: u32 },
1835 Reputation { faction_id: String, delta: i32 },
1836 Unlock { unlock_id: String, unlock_type: UnlockType },
1837 Skill { skill_id: String, points: u32 },
1838}
1839
1840impl RolledReward {
1841 pub fn display_text(&self) -> String {
1842 match self {
1843 RolledReward::Xp(amount) => format!("+{} XP", amount),
1844 RolledReward::Item { item_id, quantity, rarity, .. } => {
1845 format!("{} x{} ({:?})", item_id, quantity, rarity)
1846 }
1847 RolledReward::Currency { currency_id, amount } => format!("+{} {}", amount, currency_id),
1848 RolledReward::Reputation { faction_id, delta } => {
1849 format!("{}: {:+}", faction_id, delta)
1850 }
1851 RolledReward::Unlock { unlock_id, .. } => format!("Unlocked: {}", unlock_id),
1852 RolledReward::Skill { skill_id, points } => format!("{} +{} pts", skill_id, points),
1853 }
1854 }
1855}
1856
1857#[derive(Debug, Clone)]
1862pub struct QuestChain {
1863 pub chain_id: String,
1864 pub chain_name: String,
1865 pub chain_type: ChainType,
1866 pub quests: Vec<ChainQuestEntry>,
1867 pub description: String,
1868 pub icon: String,
1869 pub completed: bool,
1870 pub active_quest_idx: usize,
1871}
1872
1873#[derive(Debug, Clone, PartialEq)]
1874pub enum ChainType {
1875 Sequential,
1876 Parallel,
1877 Mutex,
1878 Branching,
1879 Repeatable { cooldown: f32 },
1880}
1881
1882#[derive(Debug, Clone)]
1883pub struct ChainQuestEntry {
1884 pub quest_id: String,
1885 pub required: bool,
1886 pub auto_start: bool,
1887 pub completion_condition: Option<QuestCondition>,
1888 pub unlock_condition: Option<QuestCondition>,
1889 pub branch_id: Option<String>,
1890 pub order: u32,
1891}
1892
1893impl QuestChain {
1894 pub fn new(chain_id: String, name: String, chain_type: ChainType) -> Self {
1895 QuestChain {
1896 chain_id,
1897 chain_name: name,
1898 chain_type,
1899 quests: Vec::new(),
1900 description: String::new(),
1901 icon: String::new(),
1902 completed: false,
1903 active_quest_idx: 0,
1904 }
1905 }
1906
1907 pub fn add_quest(&mut self, quest_id: &str, required: bool) {
1908 let order = self.quests.len() as u32;
1909 self.quests.push(ChainQuestEntry {
1910 quest_id: quest_id.to_string(),
1911 required,
1912 auto_start: false,
1913 completion_condition: None,
1914 unlock_condition: None,
1915 branch_id: None,
1916 order,
1917 });
1918 }
1919
1920 pub fn get_next_quests(&self, state: &QuestStateStore) -> Vec<&str> {
1921 match &self.chain_type {
1922 ChainType::Sequential => {
1923 let mut result = Vec::new();
1924 for entry in &self.quests {
1925 let quest_state = state.get_quest_state(&entry.quest_id);
1926 if quest_state == QuestState::NotStarted {
1927 let prev_done = self.quests.iter()
1929 .filter(|e| e.order < entry.order && e.required)
1930 .all(|e| state.get_quest_state(&e.quest_id) == QuestState::Completed);
1931 if prev_done {
1932 result.push(entry.quest_id.as_str());
1933 }
1934 break;
1935 }
1936 }
1937 result
1938 }
1939 ChainType::Parallel => {
1940 self.quests.iter()
1941 .filter(|e| state.get_quest_state(&e.quest_id) == QuestState::NotStarted)
1942 .map(|e| e.quest_id.as_str())
1943 .collect()
1944 }
1945 ChainType::Mutex => {
1946 let any_active = self.quests.iter().any(|e| state.get_quest_state(&e.quest_id) == QuestState::Active);
1947 if any_active { return Vec::new(); }
1948 self.quests.iter()
1949 .filter(|e| state.get_quest_state(&e.quest_id) == QuestState::NotStarted)
1950 .map(|e| e.quest_id.as_str())
1951 .take(1)
1952 .collect()
1953 }
1954 _ => Vec::new(),
1955 }
1956 }
1957
1958 pub fn is_chain_complete(&self, state: &QuestStateStore) -> bool {
1959 match &self.chain_type {
1960 ChainType::Sequential | ChainType::Parallel => {
1961 self.quests.iter()
1962 .filter(|e| e.required)
1963 .all(|e| state.get_quest_state(&e.quest_id) == QuestState::Completed)
1964 }
1965 ChainType::Mutex => {
1966 self.quests.iter()
1967 .any(|e| state.get_quest_state(&e.quest_id) == QuestState::Completed)
1968 }
1969 _ => false,
1970 }
1971 }
1972
1973 pub fn get_completion_percentage(&self, state: &QuestStateStore) -> f32 {
1974 if self.quests.is_empty() { return 0.0; }
1975 let completed = self.quests.iter()
1976 .filter(|e| state.get_quest_state(&e.quest_id) == QuestState::Completed)
1977 .count();
1978 completed as f32 / self.quests.len() as f32
1979 }
1980
1981 pub fn get_cooldown_remaining(&self, state: &QuestStateStore, current_time: f32) -> Option<f32> {
1982 if let ChainType::Repeatable { cooldown } = &self.chain_type {
1983 let last = state.get_chain_last_completion(&self.chain_id);
1984 let remaining = cooldown - (current_time - last);
1985 if remaining > 0.0 { Some(remaining) } else { None }
1986 } else {
1987 None
1988 }
1989 }
1990}
1991
1992#[derive(Debug, Clone)]
1997pub struct FactionSystem {
1998 pub factions: HashMap<String, Faction>,
1999 pub relationships: HashMap<(String, String), FactionRelationship>,
2000 pub rep_tiers: Vec<RepTier>,
2001 pub reputation_events: Vec<RepEvent>,
2002}
2003
2004#[derive(Debug, Clone)]
2005pub struct Faction {
2006 pub id: String,
2007 pub name: String,
2008 pub description: String,
2009 pub reputation: i32,
2010 pub reputation_min: i32,
2011 pub reputation_max: i32,
2012 pub current_tier: ReputationTier,
2013 pub allied_factions: Vec<String>,
2014 pub enemy_factions: Vec<String>,
2015 pub neutral_factions: Vec<String>,
2016 pub color: Vec4,
2017 pub icon: String,
2018 pub is_player_faction: bool,
2019}
2020
2021#[derive(Debug, Clone, PartialEq)]
2022pub enum ReputationTier {
2023 Hated,
2024 Hostile,
2025 Unfriendly,
2026 Neutral,
2027 Friendly,
2028 Honored,
2029 Revered,
2030 Exalted,
2031}
2032
2033impl ReputationTier {
2034 pub fn from_rep(rep: i32) -> Self {
2035 match rep {
2036 i32::MIN..=-3000 => ReputationTier::Hated,
2037 -2999..=-2000 => ReputationTier::Hostile,
2038 -1999..=-1000 => ReputationTier::Unfriendly,
2039 -999..=999 => ReputationTier::Neutral,
2040 1000..=1999 => ReputationTier::Friendly,
2041 2000..=2999 => ReputationTier::Honored,
2042 3000..=3999 => ReputationTier::Revered,
2043 _ => ReputationTier::Exalted,
2044 }
2045 }
2046
2047 pub fn display_name(&self) -> &str {
2048 match self {
2049 ReputationTier::Hated => "Hated",
2050 ReputationTier::Hostile => "Hostile",
2051 ReputationTier::Unfriendly => "Unfriendly",
2052 ReputationTier::Neutral => "Neutral",
2053 ReputationTier::Friendly => "Friendly",
2054 ReputationTier::Honored => "Honored",
2055 ReputationTier::Revered => "Revered",
2056 ReputationTier::Exalted => "Exalted",
2057 }
2058 }
2059
2060 pub fn next_tier_threshold(&self) -> Option<i32> {
2061 match self {
2062 ReputationTier::Hated => Some(-3000),
2063 ReputationTier::Hostile => Some(-2000),
2064 ReputationTier::Unfriendly => Some(-1000),
2065 ReputationTier::Neutral => Some(1000),
2066 ReputationTier::Friendly => Some(2000),
2067 ReputationTier::Honored => Some(3000),
2068 ReputationTier::Revered => Some(4000),
2069 ReputationTier::Exalted => None,
2070 }
2071 }
2072
2073 pub fn color(&self) -> Vec4 {
2074 match self {
2075 ReputationTier::Hated => Vec4::new(0.8, 0.0, 0.0, 1.0),
2076 ReputationTier::Hostile => Vec4::new(0.9, 0.2, 0.1, 1.0),
2077 ReputationTier::Unfriendly => Vec4::new(0.9, 0.5, 0.1, 1.0),
2078 ReputationTier::Neutral => Vec4::new(0.8, 0.8, 0.8, 1.0),
2079 ReputationTier::Friendly => Vec4::new(0.4, 0.8, 0.4, 1.0),
2080 ReputationTier::Honored => Vec4::new(0.2, 0.9, 0.3, 1.0),
2081 ReputationTier::Revered => Vec4::new(0.2, 0.6, 1.0, 1.0),
2082 ReputationTier::Exalted => Vec4::new(1.0, 0.9, 0.0, 1.0),
2083 }
2084 }
2085}
2086
2087#[derive(Debug, Clone)]
2088pub struct FactionRelationship {
2089 pub faction_a: String,
2090 pub faction_b: String,
2091 pub relationship_type: RelationshipType,
2092 pub rep_share_factor: f32,
2093}
2094
2095#[derive(Debug, Clone, PartialEq)]
2096pub enum RelationshipType {
2097 Allied,
2098 Enemy,
2099 Neutral,
2100 Rival,
2101}
2102
2103#[derive(Debug, Clone)]
2104pub struct RepTier {
2105 pub tier: ReputationTier,
2106 pub min_rep: i32,
2107 pub unlocks: Vec<String>,
2108 pub icon: String,
2109 pub description: String,
2110}
2111
2112#[derive(Debug, Clone)]
2113pub struct RepEvent {
2114 pub faction_id: String,
2115 pub delta: i32,
2116 pub reason: String,
2117 pub timestamp: f64,
2118 pub quest_id: Option<String>,
2119}
2120
2121impl FactionSystem {
2122 pub fn new() -> Self {
2123 let mut sys = FactionSystem {
2124 factions: HashMap::new(),
2125 relationships: HashMap::new(),
2126 rep_tiers: Self::default_tiers(),
2127 reputation_events: Vec::new(),
2128 };
2129 sys
2130 }
2131
2132 fn default_tiers() -> Vec<RepTier> {
2133 vec![
2134 RepTier { tier: ReputationTier::Hated, min_rep: i32::MIN, unlocks: Vec::new(), icon: "hated".into(), description: "They want you dead".into() },
2135 RepTier { tier: ReputationTier::Hostile, min_rep: -3000, unlocks: Vec::new(), icon: "hostile".into(), description: "Attack on sight".into() },
2136 RepTier { tier: ReputationTier::Unfriendly, min_rep: -2000, unlocks: Vec::new(), icon: "unfriendly".into(), description: "Uncomfortable".into() },
2137 RepTier { tier: ReputationTier::Neutral, min_rep: -1000, unlocks: Vec::new(), icon: "neutral".into(), description: "Neither ally nor enemy".into() },
2138 RepTier { tier: ReputationTier::Friendly, min_rep: 1000, unlocks: vec!["basic_trade".into()], icon: "friendly".into(), description: "Warmly received".into() },
2139 RepTier { tier: ReputationTier::Honored, min_rep: 2000, unlocks: vec!["discount_trade".into()], icon: "honored".into(), description: "Deep respect".into() },
2140 RepTier { tier: ReputationTier::Revered, min_rep: 3000, unlocks: vec!["special_items".into()], icon: "revered".into(), description: "Near legendary status".into() },
2141 RepTier { tier: ReputationTier::Exalted, min_rep: 4000, unlocks: vec!["exclusive_rewards".into(), "faction_title".into()], icon: "exalted".into(), description: "A true champion".into() },
2142 ]
2143 }
2144
2145 pub fn add_faction(&mut self, faction: Faction) {
2146 self.factions.insert(faction.id.clone(), faction);
2147 }
2148
2149 pub fn apply_rep_delta(&mut self, faction_id: &str, delta: i32, reason: &str, quest_id: Option<&str>) {
2150 if let Some(faction) = self.factions.get_mut(faction_id) {
2152 let new_rep = (faction.reputation + delta).clamp(faction.reputation_min, faction.reputation_max);
2153 let old_tier = faction.current_tier.clone();
2154 faction.reputation = new_rep;
2155 faction.current_tier = ReputationTier::from_rep(new_rep);
2156 self.reputation_events.push(RepEvent {
2157 faction_id: faction_id.to_string(),
2158 delta,
2159 reason: reason.to_string(),
2160 timestamp: 0.0,
2161 quest_id: quest_id.map(|s| s.to_string()),
2162 });
2163 }
2164 let related: Vec<(String, f32)> = self.relationships.iter()
2166 .filter(|((a, b), _)| a == faction_id || b == faction_id)
2167 .map(|((a, b), rel)| {
2168 let other = if a == faction_id { b.clone() } else { a.clone() };
2169 (other, rel.rep_share_factor * if rel.relationship_type == RelationshipType::Enemy { -1.0 } else { 1.0 })
2170 })
2171 .collect();
2172 for (other_id, factor) in related {
2173 if let Some(other_faction) = self.factions.get_mut(&other_id) {
2174 let shared_delta = (delta as f32 * factor) as i32;
2175 let new_rep = (other_faction.reputation + shared_delta).clamp(other_faction.reputation_min, other_faction.reputation_max);
2176 other_faction.reputation = new_rep;
2177 other_faction.current_tier = ReputationTier::from_rep(new_rep);
2178 }
2179 }
2180 }
2181
2182 pub fn get_tier_unlocks(&self, faction_id: &str) -> Vec<String> {
2183 if let Some(faction) = self.factions.get(faction_id) {
2184 let tier = &faction.current_tier;
2185 for rep_tier in &self.rep_tiers {
2186 if &rep_tier.tier == tier {
2187 return rep_tier.unlocks.clone();
2188 }
2189 }
2190 }
2191 Vec::new()
2192 }
2193
2194 pub fn get_relationship(&self, faction_a: &str, faction_b: &str) -> Option<&FactionRelationship> {
2195 self.relationships.get(&(faction_a.to_string(), faction_b.to_string()))
2196 .or_else(|| self.relationships.get(&(faction_b.to_string(), faction_a.to_string())))
2197 }
2198
2199 pub fn add_relationship(&mut self, faction_a: &str, faction_b: &str, rel_type: RelationshipType, share_factor: f32) {
2200 self.relationships.insert(
2201 (faction_a.to_string(), faction_b.to_string()),
2202 FactionRelationship {
2203 faction_a: faction_a.to_string(),
2204 faction_b: faction_b.to_string(),
2205 relationship_type: rel_type,
2206 rep_share_factor: share_factor,
2207 }
2208 );
2209 }
2210
2211 pub fn progress_to_next_tier(&self, faction_id: &str) -> Option<f32> {
2212 if let Some(faction) = self.factions.get(faction_id) {
2213 let current_threshold = match &faction.current_tier {
2214 ReputationTier::Hated => i32::MIN,
2215 ReputationTier::Hostile => -3000,
2216 ReputationTier::Unfriendly => -2000,
2217 ReputationTier::Neutral => -1000,
2218 ReputationTier::Friendly => 1000,
2219 ReputationTier::Honored => 2000,
2220 ReputationTier::Revered => 3000,
2221 ReputationTier::Exalted => return None,
2222 };
2223 if let Some(next) = faction.current_tier.next_tier_threshold() {
2224 let range = next - current_threshold;
2225 let progress = faction.reputation - current_threshold;
2226 return Some((progress as f32 / range as f32).clamp(0.0, 1.0));
2227 }
2228 }
2229 None
2230 }
2231}
2232
2233#[derive(Debug, Clone)]
2238pub struct JournalSystem {
2239 pub entries: Vec<JournalEntry>,
2240 pub active_quest_entries: HashMap<String, usize>,
2241 pub clues: Vec<ClueEntry>,
2242 pub map_markers: Vec<MapMarker>,
2243 pub notification_queue: VecDeque<JournalNotification>,
2244 pub max_entries: usize,
2245}
2246
2247#[derive(Debug, Clone)]
2248pub struct JournalEntry {
2249 pub entry_id: String,
2250 pub quest_id: String,
2251 pub timestamp: f64,
2252 pub entry_type: JournalEntryType,
2253 pub title: String,
2254 pub content: String,
2255 pub read: bool,
2256 pub important: bool,
2257 pub category: JournalCategory,
2258 pub media: Vec<JournalMedia>,
2259 pub linked_entries: Vec<String>,
2260 pub linked_map_markers: Vec<String>,
2261}
2262
2263#[derive(Debug, Clone, PartialEq)]
2264pub enum JournalEntryType {
2265 QuestStart,
2266 QuestUpdate,
2267 QuestComplete,
2268 QuestFail,
2269 ObjectiveComplete,
2270 ClueDiscovered,
2271 NPCEncounter,
2272 LocationDiscovered,
2273 LoreEntry,
2274 PlayerNote,
2275}
2276
2277#[derive(Debug, Clone, PartialEq)]
2278pub enum JournalCategory {
2279 MainQuest,
2280 SideQuest,
2281 WorldEvent,
2282 Character,
2283 Location,
2284 Lore,
2285 Bestiary,
2286 Crafting,
2287}
2288
2289#[derive(Debug, Clone)]
2290pub struct JournalMedia {
2291 pub media_type: JournalMediaType,
2292 pub path: String,
2293 pub caption: String,
2294}
2295
2296#[derive(Debug, Clone)]
2297pub enum JournalMediaType {
2298 Image,
2299 Map,
2300 Portrait,
2301 Codex,
2302}
2303
2304#[derive(Debug, Clone)]
2305pub struct ClueEntry {
2306 pub clue_id: String,
2307 pub quest_id: String,
2308 pub title: String,
2309 pub description: String,
2310 pub found_at: String,
2311 pub connects_to: Vec<String>,
2312 pub is_red_herring: bool,
2313 pub solved: bool,
2314 pub location: Option<Vec3>,
2315}
2316
2317#[derive(Debug, Clone)]
2318pub struct MapMarker {
2319 pub marker_id: String,
2320 pub quest_id: Option<String>,
2321 pub marker_type: MapMarkerType,
2322 pub position: Vec3,
2323 pub label: String,
2324 pub description: String,
2325 pub icon: String,
2326 pub color: Vec4,
2327 pub visible: bool,
2328 pub completed: bool,
2329 pub tracked: bool,
2330 pub sub_markers: Vec<MapMarker>,
2331 pub priority: i32,
2332}
2333
2334impl MapMarker {
2335 pub fn new(id: &str, marker_type: MapMarkerType, position: Vec3, label: &str) -> Self {
2336 MapMarker {
2337 marker_id: id.to_string(),
2338 quest_id: None,
2339 marker_type,
2340 position,
2341 label: label.to_string(),
2342 description: String::new(),
2343 icon: String::new(),
2344 color: Vec4::new(1.0, 0.9, 0.0, 1.0),
2345 visible: true,
2346 completed: false,
2347 tracked: false,
2348 sub_markers: Vec::new(),
2349 priority: 0,
2350 }
2351 }
2352}
2353
2354#[derive(Debug, Clone, PartialEq)]
2355pub enum MapMarkerType {
2356 QuestObjective,
2357 QuestStart,
2358 QuestEnd,
2359 NPCLocation,
2360 ItemLocation,
2361 DangerZone,
2362 Discoverable,
2363 POI,
2364 FastTravel,
2365 Custom,
2366}
2367
2368#[derive(Debug, Clone)]
2369pub struct JournalNotification {
2370 pub notification_type: JournalNotificationType,
2371 pub title: String,
2372 pub description: String,
2373 pub icon: String,
2374 pub duration: f32,
2375 pub timestamp: f64,
2376}
2377
2378#[derive(Debug, Clone)]
2379pub enum JournalNotificationType {
2380 QuestAdded,
2381 QuestCompleted,
2382 QuestFailed,
2383 ObjectiveUpdated,
2384 ClueFound,
2385 ReputationChange { faction: String, delta: i32 },
2386 RewardReceived { rewards: Vec<String> },
2387}
2388
2389impl JournalSystem {
2390 pub fn new() -> Self {
2391 JournalSystem {
2392 entries: Vec::new(),
2393 active_quest_entries: HashMap::new(),
2394 clues: Vec::new(),
2395 map_markers: Vec::new(),
2396 notification_queue: VecDeque::new(),
2397 max_entries: 1000,
2398 }
2399 }
2400
2401 pub fn add_entry(&mut self, quest_id: &str, entry_type: JournalEntryType, title: &str, content: &str, category: JournalCategory) -> String {
2402 let entry_id = format!("journal_{}_{}", quest_id, self.entries.len());
2403 let entry = JournalEntry {
2404 entry_id: entry_id.clone(),
2405 quest_id: quest_id.to_string(),
2406 timestamp: 0.0,
2407 entry_type: entry_type.clone(),
2408 title: title.to_string(),
2409 content: content.to_string(),
2410 read: false,
2411 important: false,
2412 category,
2413 media: Vec::new(),
2414 linked_entries: Vec::new(),
2415 linked_map_markers: Vec::new(),
2416 };
2417 let idx = self.entries.len();
2418 self.active_quest_entries.insert(quest_id.to_string(), idx);
2419 self.entries.push(entry);
2420 if self.entries.len() > self.max_entries {
2421 self.entries.remove(0);
2422 }
2423 self.notification_queue.push_back(JournalNotification {
2424 notification_type: match entry_type {
2425 JournalEntryType::QuestStart => JournalNotificationType::QuestAdded,
2426 JournalEntryType::QuestComplete => JournalNotificationType::QuestCompleted,
2427 JournalEntryType::QuestFail => JournalNotificationType::QuestFailed,
2428 _ => JournalNotificationType::ObjectiveUpdated,
2429 },
2430 title: title.to_string(),
2431 description: content.chars().take(80).collect(),
2432 icon: String::new(),
2433 duration: 5.0,
2434 timestamp: 0.0,
2435 });
2436 entry_id
2437 }
2438
2439 pub fn add_clue(&mut self, clue: ClueEntry) {
2440 self.clues.push(clue);
2441 }
2442
2443 pub fn add_map_marker(&mut self, marker: MapMarker) {
2444 self.map_markers.push(marker);
2445 }
2446
2447 pub fn remove_marker(&mut self, marker_id: &str) {
2448 self.map_markers.retain(|m| m.marker_id != marker_id);
2449 }
2450
2451 pub fn complete_marker(&mut self, marker_id: &str) {
2452 if let Some(marker) = self.map_markers.iter_mut().find(|m| m.marker_id == marker_id) {
2453 marker.completed = true;
2454 }
2455 }
2456
2457 pub fn get_entries_for_quest(&self, quest_id: &str) -> Vec<&JournalEntry> {
2458 self.entries.iter().filter(|e| e.quest_id == quest_id).collect()
2459 }
2460
2461 pub fn get_active_markers(&self) -> Vec<&MapMarker> {
2462 self.map_markers.iter().filter(|m| m.visible && !m.completed).collect()
2463 }
2464
2465 pub fn get_clue_web_for_quest(&self, quest_id: &str) -> Vec<(&ClueEntry, Vec<&ClueEntry>)> {
2466 let quest_clues: Vec<&ClueEntry> = self.clues.iter().filter(|c| c.quest_id == quest_id).collect();
2467 quest_clues.iter().map(|clue| {
2468 let connected: Vec<&ClueEntry> = clue.connects_to.iter()
2469 .filter_map(|id| self.clues.iter().find(|c| &c.clue_id == id))
2470 .collect();
2471 (*clue, connected)
2472 }).collect()
2473 }
2474
2475 pub fn mark_as_read(&mut self, entry_id: &str) {
2476 if let Some(entry) = self.entries.iter_mut().find(|e| e.entry_id == entry_id) {
2477 entry.read = true;
2478 }
2479 }
2480
2481 pub fn unread_count(&self) -> usize {
2482 self.entries.iter().filter(|e| !e.read).count()
2483 }
2484
2485 pub fn get_notification(&mut self) -> Option<JournalNotification> {
2486 self.notification_queue.pop_front()
2487 }
2488
2489 pub fn sort_entries_by_time(&mut self) {
2490 self.entries.sort_by(|a, b| b.timestamp.partial_cmp(&a.timestamp).unwrap_or(std::cmp::Ordering::Equal));
2491 }
2492}
2493
2494#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2499pub enum QuestState {
2500 NotStarted,
2501 Active,
2502 Paused,
2503 Completed,
2504 Failed,
2505 Abandoned,
2506}
2507
2508impl QuestState {
2509 pub fn can_transition_to(&self, new_state: &QuestState) -> bool {
2510 match (self, new_state) {
2511 (QuestState::NotStarted, QuestState::Active) => true,
2512 (QuestState::Active, QuestState::Paused) => true,
2513 (QuestState::Active, QuestState::Completed) => true,
2514 (QuestState::Active, QuestState::Failed) => true,
2515 (QuestState::Active, QuestState::Abandoned) => true,
2516 (QuestState::Paused, QuestState::Active) => true,
2517 (QuestState::Paused, QuestState::Abandoned) => true,
2518 (QuestState::Failed, QuestState::Active) => true, _ => false,
2520 }
2521 }
2522
2523 pub fn display_name(&self) -> &str {
2524 match self {
2525 QuestState::NotStarted => "Not Started",
2526 QuestState::Active => "Active",
2527 QuestState::Paused => "Paused",
2528 QuestState::Completed => "Completed",
2529 QuestState::Failed => "Failed",
2530 QuestState::Abandoned => "Abandoned",
2531 }
2532 }
2533
2534 pub fn color(&self) -> Vec4 {
2535 match self {
2536 QuestState::NotStarted => Vec4::new(0.5, 0.5, 0.5, 1.0),
2537 QuestState::Active => Vec4::new(0.2, 0.6, 1.0, 1.0),
2538 QuestState::Paused => Vec4::new(0.8, 0.7, 0.1, 1.0),
2539 QuestState::Completed => Vec4::new(0.2, 0.8, 0.2, 1.0),
2540 QuestState::Failed => Vec4::new(0.9, 0.2, 0.2, 1.0),
2541 QuestState::Abandoned => Vec4::new(0.4, 0.4, 0.4, 1.0),
2542 }
2543 }
2544}
2545
2546#[derive(Debug, Clone)]
2547pub struct QuestInstance {
2548 pub quest_id: String,
2549 pub state: QuestState,
2550 pub started_at: f64,
2551 pub completed_at: Option<f64>,
2552 pub failed_at: Option<f64>,
2553 pub paused_at: Option<f64>,
2554 pub active_objective_ids: Vec<String>,
2555 pub completed_objective_ids: Vec<String>,
2556 pub failed_objective_ids: Vec<String>,
2557 pub objective_order: VecDeque<String>,
2558 pub variables: HashMap<String, String>,
2559 pub retry_count: u32,
2560 pub partial_score: f32,
2561 pub tracking: bool,
2562 pub state_history: Vec<(QuestState, f64)>,
2563}
2564
2565impl QuestInstance {
2566 pub fn new(quest_id: &str) -> Self {
2567 QuestInstance {
2568 quest_id: quest_id.to_string(),
2569 state: QuestState::NotStarted,
2570 started_at: 0.0,
2571 completed_at: None,
2572 failed_at: None,
2573 paused_at: None,
2574 active_objective_ids: Vec::new(),
2575 completed_objective_ids: Vec::new(),
2576 failed_objective_ids: Vec::new(),
2577 objective_order: VecDeque::new(),
2578 variables: HashMap::new(),
2579 retry_count: 0,
2580 partial_score: 0.0,
2581 tracking: false,
2582 state_history: Vec::new(),
2583 }
2584 }
2585
2586 pub fn transition(&mut self, new_state: QuestState, timestamp: f64) -> Result<(), String> {
2587 if !self.state.can_transition_to(&new_state) {
2588 return Err(format!("Cannot transition from {:?} to {:?}", self.state, new_state));
2589 }
2590 self.state_history.push((self.state.clone(), timestamp));
2591 match &new_state {
2592 QuestState::Active => {
2593 if self.started_at == 0.0 { self.started_at = timestamp; }
2594 self.paused_at = None;
2595 }
2596 QuestState::Completed => { self.completed_at = Some(timestamp); }
2597 QuestState::Failed => { self.failed_at = Some(timestamp); }
2598 QuestState::Paused => { self.paused_at = Some(timestamp); }
2599 _ => {}
2600 }
2601 self.state = new_state;
2602 Ok(())
2603 }
2604
2605 pub fn complete_objective(&mut self, obj_id: &str) {
2606 self.active_objective_ids.retain(|id| id != obj_id);
2607 if !self.completed_objective_ids.contains(&obj_id.to_string()) {
2608 self.completed_objective_ids.push(obj_id.to_string());
2609 }
2610 }
2611
2612 pub fn fail_objective(&mut self, obj_id: &str) {
2613 self.active_objective_ids.retain(|id| id != obj_id);
2614 if !self.failed_objective_ids.contains(&obj_id.to_string()) {
2615 self.failed_objective_ids.push(obj_id.to_string());
2616 }
2617 }
2618
2619 pub fn add_objective(&mut self, obj_id: &str) {
2620 if !self.active_objective_ids.contains(&obj_id.to_string()) {
2621 self.active_objective_ids.push(obj_id.to_string());
2622 self.objective_order.push_back(obj_id.to_string());
2623 }
2624 }
2625
2626 pub fn calculate_completion_score(&self) -> f32 {
2627 let total = self.completed_objective_ids.len() + self.failed_objective_ids.len();
2628 if total == 0 { return 0.0; }
2629 self.completed_objective_ids.len() as f32 / total as f32
2630 }
2631
2632 pub fn elapsed_time(&self, current_time: f64) -> f64 {
2633 if self.started_at == 0.0 { return 0.0; }
2634 current_time - self.started_at
2635 }
2636}
2637
2638#[derive(Debug, Clone)]
2639pub struct QuestStateStore {
2640 pub quest_instances: HashMap<String, QuestInstance>,
2641 pub faction_reps: HashMap<String, i32>,
2642 pub player_level: u32,
2643 pub player_stats: HashMap<String, f32>,
2644 pub flags: HashSet<String>,
2645 pub item_inventory: HashMap<String, u32>,
2646 pub event_times: HashMap<String, f64>,
2647 pub chain_completions: HashMap<String, f64>,
2648 pub current_time: f64,
2649}
2650
2651impl QuestStateStore {
2652 pub fn new() -> Self {
2653 QuestStateStore {
2654 quest_instances: HashMap::new(),
2655 faction_reps: HashMap::new(),
2656 player_level: 1,
2657 player_stats: HashMap::new(),
2658 flags: HashSet::new(),
2659 item_inventory: HashMap::new(),
2660 event_times: HashMap::new(),
2661 chain_completions: HashMap::new(),
2662 current_time: 0.0,
2663 }
2664 }
2665
2666 pub fn get_quest_state(&self, quest_id: &str) -> QuestState {
2667 self.quest_instances.get(quest_id)
2668 .map(|q| q.state.clone())
2669 .unwrap_or(QuestState::NotStarted)
2670 }
2671
2672 pub fn start_quest(&mut self, quest_id: &str) -> Result<(), String> {
2673 let instance = self.quest_instances.entry(quest_id.to_string()).or_insert_with(|| QuestInstance::new(quest_id));
2674 instance.transition(QuestState::Active, self.current_time)
2675 }
2676
2677 pub fn complete_quest(&mut self, quest_id: &str) -> Result<(), String> {
2678 let time = self.current_time;
2679 if let Some(instance) = self.quest_instances.get_mut(quest_id) {
2680 instance.transition(QuestState::Completed, time)
2681 } else {
2682 Err(format!("Quest {} not found", quest_id))
2683 }
2684 }
2685
2686 pub fn fail_quest(&mut self, quest_id: &str) -> Result<(), String> {
2687 let time = self.current_time;
2688 if let Some(instance) = self.quest_instances.get_mut(quest_id) {
2689 instance.transition(QuestState::Failed, time)
2690 } else {
2691 Err(format!("Quest {} not found", quest_id))
2692 }
2693 }
2694
2695 pub fn get_faction_rep(&self, faction_id: &str) -> i32 {
2696 self.faction_reps.get(faction_id).copied().unwrap_or(0)
2697 }
2698
2699 pub fn modify_faction_rep(&mut self, faction_id: &str, delta: i32) {
2700 let rep = self.faction_reps.entry(faction_id.to_string()).or_insert(0);
2701 *rep += delta;
2702 }
2703
2704 pub fn get_flag(&self, name: &str) -> bool {
2705 self.flags.contains(name)
2706 }
2707
2708 pub fn set_flag(&mut self, name: &str) {
2709 self.flags.insert(name.to_string());
2710 }
2711
2712 pub fn clear_flag(&mut self, name: &str) {
2713 self.flags.remove(name);
2714 }
2715
2716 pub fn get_item_count(&self, item_id: &str) -> u32 {
2717 self.item_inventory.get(item_id).copied().unwrap_or(0)
2718 }
2719
2720 pub fn add_item(&mut self, item_id: &str, quantity: u32) {
2721 *self.item_inventory.entry(item_id.to_string()).or_insert(0) += quantity;
2722 }
2723
2724 pub fn get_player_stat(&self, stat: &str) -> f32 {
2725 self.player_stats.get(stat).copied().unwrap_or(0.0)
2726 }
2727
2728 pub fn get_time_since(&self, event: &str) -> f32 {
2729 if let Some(&t) = self.event_times.get(event) {
2730 (self.current_time - t) as f32
2731 } else {
2732 f32::MAX
2733 }
2734 }
2735
2736 pub fn record_event(&mut self, event: &str) {
2737 self.event_times.insert(event.to_string(), self.current_time);
2738 }
2739
2740 pub fn is_objective_complete(&self, quest_id: &str, obj_id: &str) -> bool {
2741 self.quest_instances.get(quest_id)
2742 .map(|q| q.completed_objective_ids.contains(&obj_id.to_string()))
2743 .unwrap_or(false)
2744 }
2745
2746 pub fn get_chain_last_completion(&self, chain_id: &str) -> f32 {
2747 self.chain_completions.get(chain_id).copied().unwrap_or(-f64::MAX) as f32
2748 }
2749
2750 pub fn get_active_quests(&self) -> Vec<&QuestInstance> {
2751 self.quest_instances.values().filter(|q| q.state == QuestState::Active).collect()
2752 }
2753
2754 pub fn get_completed_quests(&self) -> Vec<&QuestInstance> {
2755 self.quest_instances.values().filter(|q| q.state == QuestState::Completed).collect()
2756 }
2757}
2758
2759#[derive(Debug, Clone)]
2764pub struct QuestDebugTools {
2765 pub log: VecDeque<DebugLogEntry>,
2766 pub max_log: usize,
2767 pub breakpoints: HashSet<String>,
2768 pub watch_objectives: Vec<String>,
2769 pub force_complete_queue: VecDeque<String>,
2770 pub force_fail_queue: VecDeque<String>,
2771 pub override_conditions: HashMap<String, bool>,
2772 pub reputation_inspector: ReputationInspector,
2773 pub condition_tester: ConditionTester,
2774 pub objective_overrides: HashMap<String, ObjectiveOverride>,
2775 pub slow_motion: bool,
2776 pub slow_motion_scale: f32,
2777 pub record_events: bool,
2778 pub recorded_events: Vec<QuestEvent>,
2779 pub current_scope_dump: Vec<(String, String)>,
2780}
2781
2782#[derive(Debug, Clone)]
2783pub struct DebugLogEntry {
2784 pub level: LogLevel,
2785 pub timestamp: f64,
2786 pub quest_id: Option<String>,
2787 pub message: String,
2788 pub source: String,
2789}
2790
2791#[derive(Debug, Clone, PartialEq)]
2792pub enum LogLevel {
2793 Info,
2794 Warning,
2795 Error,
2796 Verbose,
2797}
2798
2799#[derive(Debug, Clone)]
2800pub struct ReputationInspector {
2801 pub selected_faction: Option<String>,
2802 pub show_history: bool,
2803 pub history_count: usize,
2804 pub filter_quest_id: Option<String>,
2805}
2806
2807#[derive(Debug, Clone)]
2808pub struct ConditionTester {
2809 pub test_conditions: Vec<(String, QuestCondition)>,
2810 pub results: Vec<(String, bool)>,
2811 pub test_state: QuestStateStore,
2812 pub running: bool,
2813}
2814
2815impl ConditionTester {
2816 pub fn new() -> Self {
2817 ConditionTester {
2818 test_conditions: Vec::new(),
2819 results: Vec::new(),
2820 test_state: QuestStateStore::new(),
2821 running: false,
2822 }
2823 }
2824
2825 pub fn run_all(&mut self) {
2826 self.results.clear();
2827 for (name, cond) in &self.test_conditions.clone() {
2828 let result = cond.evaluate(&self.test_state);
2829 self.results.push((name.clone(), result));
2830 }
2831 }
2832
2833 pub fn add_test(&mut self, name: &str, condition: QuestCondition) {
2834 self.test_conditions.push((name.to_string(), condition));
2835 }
2836
2837 pub fn get_pass_rate(&self) -> f32 {
2838 if self.results.is_empty() { return 0.0; }
2839 let passed = self.results.iter().filter(|(_, r)| *r).count();
2840 passed as f32 / self.results.len() as f32
2841 }
2842}
2843
2844#[derive(Debug, Clone)]
2845pub struct ObjectiveOverride {
2846 pub objective_id: String,
2847 pub force_status: ObjectiveStatus,
2848 pub force_progress: Option<f32>,
2849 pub active: bool,
2850}
2851
2852impl QuestDebugTools {
2853 pub fn new() -> Self {
2854 QuestDebugTools {
2855 log: VecDeque::new(),
2856 max_log: 500,
2857 breakpoints: HashSet::new(),
2858 watch_objectives: Vec::new(),
2859 force_complete_queue: VecDeque::new(),
2860 force_fail_queue: VecDeque::new(),
2861 override_conditions: HashMap::new(),
2862 reputation_inspector: ReputationInspector {
2863 selected_faction: None,
2864 show_history: false,
2865 history_count: 20,
2866 filter_quest_id: None,
2867 },
2868 condition_tester: ConditionTester::new(),
2869 objective_overrides: HashMap::new(),
2870 slow_motion: false,
2871 slow_motion_scale: 0.1,
2872 record_events: false,
2873 recorded_events: Vec::new(),
2874 current_scope_dump: Vec::new(),
2875 }
2876 }
2877
2878 pub fn log(&mut self, level: LogLevel, message: &str, quest_id: Option<&str>, source: &str) {
2879 self.log.push_back(DebugLogEntry {
2880 level,
2881 timestamp: 0.0,
2882 quest_id: quest_id.map(|s| s.to_string()),
2883 message: message.to_string(),
2884 source: source.to_string(),
2885 });
2886 if self.log.len() > self.max_log {
2887 self.log.pop_front();
2888 }
2889 }
2890
2891 pub fn force_complete_quest(&mut self, quest_id: &str) {
2892 self.force_complete_queue.push_back(quest_id.to_string());
2893 self.log(LogLevel::Warning, &format!("Force completing: {}", quest_id), Some(quest_id), "debug");
2894 }
2895
2896 pub fn force_fail_quest(&mut self, quest_id: &str) {
2897 self.force_fail_queue.push_back(quest_id.to_string());
2898 self.log(LogLevel::Warning, &format!("Force failing: {}", quest_id), Some(quest_id), "debug");
2899 }
2900
2901 pub fn set_condition_override(&mut self, condition_id: &str, value: bool) {
2902 self.override_conditions.insert(condition_id.to_string(), value);
2903 }
2904
2905 pub fn clear_condition_override(&mut self, condition_id: &str) {
2906 self.override_conditions.remove(condition_id);
2907 }
2908
2909 pub fn override_objective(&mut self, obj_id: &str, status: ObjectiveStatus, progress: Option<f32>) {
2910 self.objective_overrides.insert(obj_id.to_string(), ObjectiveOverride {
2911 objective_id: obj_id.to_string(),
2912 force_status: status,
2913 force_progress: progress,
2914 active: true,
2915 });
2916 }
2917
2918 pub fn clear_override(&mut self, obj_id: &str) {
2919 if let Some(ov) = self.objective_overrides.get_mut(obj_id) {
2920 ov.active = false;
2921 }
2922 }
2923
2924 pub fn dump_state(&mut self, state: &QuestStateStore) {
2925 self.current_scope_dump.clear();
2926 self.current_scope_dump.push(("player_level".to_string(), state.player_level.to_string()));
2927 for (faction, rep) in &state.faction_reps {
2928 self.current_scope_dump.push((format!("rep_{}", faction), rep.to_string()));
2929 }
2930 for (item, qty) in &state.item_inventory {
2931 self.current_scope_dump.push((format!("item_{}", item), qty.to_string()));
2932 }
2933 for flag in &state.flags {
2934 self.current_scope_dump.push((format!("flag_{}", flag), "true".to_string()));
2935 }
2936 self.current_scope_dump.sort_by(|a, b| a.0.cmp(&b.0));
2937 }
2938
2939 pub fn get_log_for_quest(&self, quest_id: &str) -> Vec<&DebugLogEntry> {
2940 self.log.iter().filter(|e| e.quest_id.as_deref() == Some(quest_id)).collect()
2941 }
2942
2943 pub fn clear_log(&mut self) { self.log.clear(); }
2944
2945 pub fn get_error_count(&self) -> usize {
2946 self.log.iter().filter(|e| e.level == LogLevel::Error).count()
2947 }
2948
2949 pub fn get_warning_count(&self) -> usize {
2950 self.log.iter().filter(|e| e.level == LogLevel::Warning).count()
2951 }
2952}
2953
2954#[derive(Debug, Clone)]
2959pub enum QuestNodeEnum {
2960 QuestStart(QuestStartNode),
2961 Objective(ObjectiveNode),
2962 ConditionCheck(ConditionCheckNode),
2963 Reward(RewardNode),
2964 Branch(BranchNode),
2965 Fail(FailNode),
2966 Completion(CompletionNode),
2967 Timer(TimerNode),
2968 Trigger(TriggerNode),
2969}
2970
2971impl QuestNodeEnum {
2972 pub fn id(&self) -> u64 {
2973 match self {
2974 QuestNodeEnum::QuestStart(n) => n.id,
2975 QuestNodeEnum::Objective(n) => n.id,
2976 QuestNodeEnum::ConditionCheck(n) => n.id,
2977 QuestNodeEnum::Reward(n) => n.id,
2978 QuestNodeEnum::Branch(n) => n.id,
2979 QuestNodeEnum::Fail(n) => n.id,
2980 QuestNodeEnum::Completion(n) => n.id,
2981 QuestNodeEnum::Timer(n) => n.id,
2982 QuestNodeEnum::Trigger(n) => n.id,
2983 }
2984 }
2985
2986 pub fn position(&self) -> Vec2 {
2987 match self {
2988 QuestNodeEnum::QuestStart(n) => n.position,
2989 QuestNodeEnum::Objective(n) => n.position,
2990 QuestNodeEnum::ConditionCheck(n) => n.position,
2991 QuestNodeEnum::Reward(n) => n.position,
2992 QuestNodeEnum::Branch(n) => n.position,
2993 QuestNodeEnum::Fail(n) => n.position,
2994 QuestNodeEnum::Completion(n) => n.position,
2995 QuestNodeEnum::Timer(n) => n.position,
2996 QuestNodeEnum::Trigger(n) => n.position,
2997 }
2998 }
2999
3000 pub fn set_position(&mut self, pos: Vec2) {
3001 match self {
3002 QuestNodeEnum::QuestStart(n) => n.position = pos,
3003 QuestNodeEnum::Objective(n) => n.position = pos,
3004 QuestNodeEnum::ConditionCheck(n) => n.position = pos,
3005 QuestNodeEnum::Reward(n) => n.position = pos,
3006 QuestNodeEnum::Branch(n) => n.position = pos,
3007 QuestNodeEnum::Fail(n) => n.position = pos,
3008 QuestNodeEnum::Completion(n) => n.position = pos,
3009 QuestNodeEnum::Timer(n) => n.position = pos,
3010 QuestNodeEnum::Trigger(n) => n.position = pos,
3011 }
3012 }
3013
3014 pub fn node_type(&self) -> QuestNodeType {
3015 match self {
3016 QuestNodeEnum::QuestStart(_) => QuestNodeType::QuestStart,
3017 QuestNodeEnum::Objective(_) => QuestNodeType::Objective,
3018 QuestNodeEnum::ConditionCheck(_) => QuestNodeType::ConditionCheck,
3019 QuestNodeEnum::Reward(_) => QuestNodeType::Reward,
3020 QuestNodeEnum::Branch(_) => QuestNodeType::Branch,
3021 QuestNodeEnum::Fail(_) => QuestNodeType::Fail,
3022 QuestNodeEnum::Completion(_) => QuestNodeType::Completion,
3023 QuestNodeEnum::Timer(_) => QuestNodeType::Timer,
3024 QuestNodeEnum::Trigger(_) => QuestNodeType::Trigger,
3025 }
3026 }
3027
3028 pub fn tags(&self) -> &[String] {
3029 match self {
3030 QuestNodeEnum::QuestStart(n) => &n.tags,
3031 QuestNodeEnum::Objective(n) => &n.tags,
3032 QuestNodeEnum::ConditionCheck(n) => &n.tags,
3033 QuestNodeEnum::Reward(n) => &n.tags,
3034 QuestNodeEnum::Branch(n) => &n.tags,
3035 QuestNodeEnum::Fail(n) => &n.tags,
3036 QuestNodeEnum::Completion(n) => &n.tags,
3037 QuestNodeEnum::Timer(n) => &n.tags,
3038 QuestNodeEnum::Trigger(n) => &n.tags,
3039 }
3040 }
3041}
3042
3043#[derive(Debug, Clone)]
3044pub struct QuestNodeConnection {
3045 pub from_node: u64,
3046 pub from_port: usize,
3047 pub to_node: u64,
3048 pub to_port: usize,
3049 pub label: Option<String>,
3050 pub condition: Option<QuestCondition>,
3051}
3052
3053#[derive(Debug, Clone)]
3054pub struct QuestGraph {
3055 pub id: String,
3056 pub quest_id: String,
3057 pub name: String,
3058 pub nodes: HashMap<u64, QuestNodeEnum>,
3059 pub connections: Vec<QuestNodeConnection>,
3060 pub start_node_id: Option<u64>,
3061 pub metadata: QuestGraphMetadata,
3062 next_id: u64,
3063}
3064
3065#[derive(Debug, Clone, Default)]
3066pub struct QuestGraphMetadata {
3067 pub author: String,
3068 pub version: String,
3069 pub created: String,
3070 pub modified: String,
3071 pub description: String,
3072 pub tags: Vec<String>,
3073 pub category: String,
3074 pub reward_summary: String,
3075}
3076
3077impl QuestGraph {
3078 pub fn new(id: String, quest_id: String, name: String) -> Self {
3079 QuestGraph {
3080 id,
3081 quest_id,
3082 name,
3083 nodes: HashMap::new(),
3084 connections: Vec::new(),
3085 start_node_id: None,
3086 metadata: QuestGraphMetadata::default(),
3087 next_id: 1,
3088 }
3089 }
3090
3091 pub fn allocate_id(&mut self) -> u64 {
3092 let id = self.next_id;
3093 self.next_id += 1;
3094 id
3095 }
3096
3097 pub fn add_node(&mut self, node: QuestNodeEnum) -> u64 {
3098 let id = node.id();
3099 if let QuestNodeEnum::QuestStart(_) = &node {
3100 self.start_node_id = Some(id);
3101 }
3102 self.nodes.insert(id, node);
3103 id
3104 }
3105
3106 pub fn remove_node(&mut self, id: u64) {
3107 self.nodes.remove(&id);
3108 self.connections.retain(|c| c.from_node != id && c.to_node != id);
3109 if self.start_node_id == Some(id) { self.start_node_id = None; }
3110 }
3111
3112 pub fn connect(&mut self, from: u64, from_port: usize, to: u64, to_port: usize, label: Option<String>) {
3113 self.connections.retain(|c| !(c.from_node == from && c.from_port == from_port));
3114 self.connections.push(QuestNodeConnection {
3115 from_node: from, from_port,
3116 to_node: to, to_port,
3117 label, condition: None,
3118 });
3119 }
3120
3121 pub fn get_outputs(&self, node_id: u64) -> Vec<&QuestNodeConnection> {
3122 self.connections.iter().filter(|c| c.from_node == node_id).collect()
3123 }
3124
3125 pub fn get_inputs(&self, node_id: u64) -> Vec<&QuestNodeConnection> {
3126 self.connections.iter().filter(|c| c.to_node == node_id).collect()
3127 }
3128
3129 pub fn next_node(&self, from: u64, port: usize) -> Option<u64> {
3130 self.connections.iter()
3131 .find(|c| c.from_node == from && c.from_port == port)
3132 .map(|c| c.to_node)
3133 }
3134
3135 pub fn validate(&self) -> Vec<QuestValidationError> {
3136 let mut errors = Vec::new();
3137 if self.start_node_id.is_none() {
3138 errors.push(QuestValidationError {
3139 node_id: None,
3140 severity: QuestValidationSeverity::Error,
3141 message: "Quest has no start node".into(),
3142 });
3143 }
3144 for (id, node) in &self.nodes {
3145 match node {
3146 QuestNodeEnum::Objective(n) => {
3147 if self.get_outputs(*id).is_empty() {
3148 errors.push(QuestValidationError {
3149 node_id: Some(*id),
3150 severity: QuestValidationSeverity::Warning,
3151 message: "Objective node has no output".into(),
3152 });
3153 }
3154 }
3155 QuestNodeEnum::ConditionCheck(n) => {
3156 let has_true = self.connections.iter().any(|c| c.from_node == *id && c.from_port == 0);
3157 let has_false = self.connections.iter().any(|c| c.from_node == *id && c.from_port == 1);
3158 if !has_true {
3159 errors.push(QuestValidationError {
3160 node_id: Some(*id),
3161 severity: QuestValidationSeverity::Warning,
3162 message: "ConditionCheck missing true branch".into(),
3163 });
3164 }
3165 if !has_false {
3166 errors.push(QuestValidationError {
3167 node_id: Some(*id),
3168 severity: QuestValidationSeverity::Warning,
3169 message: "ConditionCheck missing false branch".into(),
3170 });
3171 }
3172 }
3173 QuestNodeEnum::Reward(n) => {
3174 if n.reward_table.xp_reward.base == 0 && n.reward_table.item_rewards.is_empty() {
3175 errors.push(QuestValidationError {
3176 node_id: Some(*id),
3177 severity: QuestValidationSeverity::Info,
3178 message: "Reward node has no rewards defined".into(),
3179 });
3180 }
3181 }
3182 _ => {}
3183 }
3184 }
3185 if let Some(start) = self.start_node_id {
3187 let reachable = self.find_reachable(start);
3188 for id in self.nodes.keys() {
3189 if !reachable.contains(id) {
3190 errors.push(QuestValidationError {
3191 node_id: Some(*id),
3192 severity: QuestValidationSeverity::Warning,
3193 message: "Unreachable node".into(),
3194 });
3195 }
3196 }
3197 }
3198 errors
3199 }
3200
3201 pub fn find_reachable(&self, start: u64) -> HashSet<u64> {
3202 let mut visited = HashSet::new();
3203 let mut queue = VecDeque::new();
3204 queue.push_back(start);
3205 while let Some(id) = queue.pop_front() {
3206 if visited.contains(&id) { continue; }
3207 visited.insert(id);
3208 for conn in self.get_outputs(id) {
3209 queue.push_back(conn.to_node);
3210 }
3211 }
3212 visited
3213 }
3214
3215 pub fn auto_layout(&mut self) {
3216 if let Some(start) = self.start_node_id {
3217 let node_w = 220.0_f32;
3218 let node_h = 120.0_f32;
3219 let x_gap = 60.0_f32;
3220 let y_gap = 30.0_f32;
3221 let mut queue = VecDeque::new();
3222 let mut visited = HashSet::new();
3223 let mut col_row: HashMap<usize, usize> = HashMap::new();
3224 let mut positions: HashMap<u64, Vec2> = HashMap::new();
3225 queue.push_back((start, 0usize));
3226 while let Some((id, col)) = queue.pop_front() {
3227 if visited.contains(&id) { continue; }
3228 visited.insert(id);
3229 let row = *col_row.entry(col).or_insert(0);
3230 *col_row.entry(col).or_insert(0) += 1;
3231 positions.insert(id, Vec2::new(
3232 col as f32 * (node_w + x_gap),
3233 row as f32 * (node_h + y_gap),
3234 ));
3235 for conn in self.get_outputs(id) {
3236 queue.push_back((conn.to_node, col + 1));
3237 }
3238 }
3239 for (id, pos) in positions {
3240 if let Some(node) = self.nodes.get_mut(&id) {
3241 node.set_position(pos);
3242 }
3243 }
3244 }
3245 }
3246
3247 pub fn collect_all_objectives(&self) -> Vec<&ObjectiveNode> {
3248 self.nodes.values().filter_map(|n| {
3249 if let QuestNodeEnum::Objective(obj) = n { Some(obj) } else { None }
3250 }).collect()
3251 }
3252
3253 pub fn collect_all_rewards(&self) -> Vec<&RewardNode> {
3254 self.nodes.values().filter_map(|n| {
3255 if let QuestNodeEnum::Reward(r) = n { Some(r) } else { None }
3256 }).collect()
3257 }
3258}
3259
3260#[derive(Debug, Clone)]
3261pub struct QuestValidationError {
3262 pub node_id: Option<u64>,
3263 pub severity: QuestValidationSeverity,
3264 pub message: String,
3265}
3266
3267#[derive(Debug, Clone, PartialEq)]
3268pub enum QuestValidationSeverity {
3269 Info,
3270 Warning,
3271 Error,
3272}
3273
3274#[derive(Debug, Clone, Default)]
3279pub struct QuestEditorGraphState {
3280 pub pan_offset: Vec2,
3281 pub zoom: f32,
3282 pub canvas_size: Vec2,
3283 pub selected_nodes: HashSet<u64>,
3284 pub hovered_node: Option<u64>,
3285 pub dragging_node: Option<u64>,
3286 pub drag_start_pos: Vec2,
3287 pub drag_node_start: Vec2,
3288 pub connection_drag_from: Option<(u64, usize)>,
3289 pub connection_drag_pos: Vec2,
3290 pub selection_box_start: Option<Vec2>,
3291 pub selection_box_end: Vec2,
3292 pub grid_size: f32,
3293 pub snap_to_grid: bool,
3294 pub show_grid: bool,
3295 pub context_menu_pos: Option<Vec2>,
3296 pub context_menu_node: Option<u64>,
3297 pub bezier_smoothness: f32,
3298}
3299
3300impl QuestEditorGraphState {
3301 pub fn new() -> Self {
3302 QuestEditorGraphState {
3303 zoom: 1.0,
3304 canvas_size: Vec2::new(1600.0, 900.0),
3305 grid_size: 20.0,
3306 snap_to_grid: true,
3307 show_grid: true,
3308 bezier_smoothness: 0.5,
3309 ..Default::default()
3310 }
3311 }
3312
3313 pub fn world_to_screen(&self, world: Vec2) -> Vec2 { (world + self.pan_offset) * self.zoom }
3314 pub fn screen_to_world(&self, screen: Vec2) -> Vec2 { screen / self.zoom - self.pan_offset }
3315
3316 pub fn snap(&self, pos: Vec2) -> Vec2 {
3317 if self.snap_to_grid {
3318 Vec2::new(
3319 (pos.x / self.grid_size).round() * self.grid_size,
3320 (pos.y / self.grid_size).round() * self.grid_size,
3321 )
3322 } else { pos }
3323 }
3324
3325 pub fn zoom_at(&mut self, point: Vec2, delta: f32) {
3326 let world = self.screen_to_world(point);
3327 self.zoom = (self.zoom * (1.0 + delta * 0.1)).clamp(0.05, 5.0);
3328 let new_world = self.screen_to_world(point);
3329 self.pan_offset += new_world - world;
3330 }
3331}
3332
3333#[derive(Debug)]
3334pub struct QuestEditor {
3335 pub graphs: HashMap<String, QuestGraph>,
3336 pub active_graph_id: Option<String>,
3337 pub graph_state: QuestEditorGraphState,
3338 pub quest_state_store: QuestStateStore,
3339 pub faction_system: FactionSystem,
3340 pub journal: JournalSystem,
3341 pub chains: HashMap<String, QuestChain>,
3342 pub debug_tools: QuestDebugTools,
3343 pub undo_stack: VecDeque<QuestEditorAction>,
3344 pub redo_stack: VecDeque<QuestEditorAction>,
3345 pub max_undo: usize,
3346 pub validation_errors: Vec<QuestValidationError>,
3347 pub search_query: String,
3348 pub search_results: Vec<QuestSearchResult>,
3349 pub selected_chain_id: Option<String>,
3350 pub show_journal: bool,
3351 pub show_debug: bool,
3352 pub show_factions: bool,
3353 pub show_chains: bool,
3354 pub show_validation: bool,
3355 pub show_reward_preview: bool,
3356 pub show_statistics: bool,
3357 pub node_id_counter: u64,
3358 pub dirty: bool,
3359 pub status_message: String,
3360 pub status_timer: f32,
3361 pub rng: SimpleRng,
3362 pub panel_config: QuestEditorPanels,
3363 pub recent_files: Vec<String>,
3364 pub last_save_path: Option<String>,
3365 pub event_log: VecDeque<QuestEvent>,
3366 pub max_event_log: usize,
3367 pub preview_player_level: u32,
3368 pub reward_preview_results: Vec<RolledReward>,
3369 pub graph_annotations: Vec<QuestAnnotation>,
3370}
3371
3372#[derive(Debug, Clone)]
3373pub struct QuestEditorPanels {
3374 pub left_width: f32,
3375 pub right_width: f32,
3376 pub bottom_height: f32,
3377 pub show_left: bool,
3378 pub show_right: bool,
3379 pub show_bottom: bool,
3380}
3381
3382impl Default for QuestEditorPanels {
3383 fn default() -> Self {
3384 QuestEditorPanels {
3385 left_width: 260.0,
3386 right_width: 320.0,
3387 bottom_height: 180.0,
3388 show_left: true,
3389 show_right: true,
3390 show_bottom: true,
3391 }
3392 }
3393}
3394
3395#[derive(Debug, Clone)]
3396pub struct QuestSearchResult {
3397 pub graph_id: String,
3398 pub node_id: u64,
3399 pub field: String,
3400 pub excerpt: String,
3401}
3402
3403#[derive(Debug, Clone)]
3404pub struct QuestAnnotation {
3405 pub id: u64,
3406 pub position: Vec2,
3407 pub size: Vec2,
3408 pub title: String,
3409 pub text: String,
3410 pub color: Vec4,
3411 pub collapsed: bool,
3412 pub graph_id: String,
3413}
3414
3415impl QuestAnnotation {
3416 pub fn new(id: u64, graph_id: &str, position: Vec2) -> Self {
3417 QuestAnnotation {
3418 id,
3419 position,
3420 size: Vec2::new(200.0, 100.0),
3421 title: "Note".to_string(),
3422 text: String::new(),
3423 color: Vec4::new(0.8, 0.9, 0.6, 0.5),
3424 collapsed: false,
3425 graph_id: graph_id.to_string(),
3426 }
3427 }
3428}
3429
3430#[derive(Debug, Clone)]
3431pub enum QuestEditorAction {
3432 AddNode { graph_id: String, node: QuestNodeEnum },
3433 RemoveNode { graph_id: String, node: QuestNodeEnum, connections: Vec<QuestNodeConnection> },
3434 MoveNode { graph_id: String, node_id: u64, old_pos: Vec2, new_pos: Vec2 },
3435 AddConnection { graph_id: String, connection: QuestNodeConnection },
3436 RemoveConnection { graph_id: String, connection: QuestNodeConnection },
3437 EditObjective { graph_id: String, node_id: u64, old: QuestObjective, new: QuestObjective },
3438 AddReward { graph_id: String, node_id: u64, reward: ItemRewardEntry },
3439 EditStartNode { graph_id: String, node_id: u64, old: QuestStartNode, new: QuestStartNode },
3440 BatchAction(Vec<QuestEditorAction>),
3441}
3442
3443impl QuestEditor {
3444 pub fn new() -> Self {
3445 let mut faction_system = FactionSystem::new();
3446 faction_system.add_faction(Faction {
3448 id: "merchants_guild".to_string(),
3449 name: "Merchants Guild".to_string(),
3450 description: "A powerful trading consortium".to_string(),
3451 reputation: 0,
3452 reputation_min: -5000,
3453 reputation_max: 5000,
3454 current_tier: ReputationTier::Neutral,
3455 allied_factions: vec!["craftsmen".to_string()],
3456 enemy_factions: vec!["thieves_guild".to_string()],
3457 neutral_factions: vec!["city_guard".to_string()],
3458 color: Vec4::new(0.8, 0.6, 0.1, 1.0),
3459 icon: "merchant_icon".to_string(),
3460 is_player_faction: false,
3461 });
3462 QuestEditor {
3463 graphs: HashMap::new(),
3464 active_graph_id: None,
3465 graph_state: QuestEditorGraphState::new(),
3466 quest_state_store: QuestStateStore::new(),
3467 faction_system,
3468 journal: JournalSystem::new(),
3469 chains: HashMap::new(),
3470 debug_tools: QuestDebugTools::new(),
3471 undo_stack: VecDeque::new(),
3472 redo_stack: VecDeque::new(),
3473 max_undo: 100,
3474 validation_errors: Vec::new(),
3475 search_query: String::new(),
3476 search_results: Vec::new(),
3477 selected_chain_id: None,
3478 show_journal: false,
3479 show_debug: false,
3480 show_factions: false,
3481 show_chains: false,
3482 show_validation: false,
3483 show_reward_preview: false,
3484 show_statistics: false,
3485 node_id_counter: 1000,
3486 dirty: false,
3487 status_message: "Ready".to_string(),
3488 status_timer: 0.0,
3489 rng: SimpleRng::new(12345),
3490 panel_config: QuestEditorPanels::default(),
3491 recent_files: Vec::new(),
3492 last_save_path: None,
3493 event_log: VecDeque::new(),
3494 max_event_log: 500,
3495 preview_player_level: 10,
3496 reward_preview_results: Vec::new(),
3497 graph_annotations: Vec::new(),
3498 }
3499 }
3500
3501 pub fn allocate_id(&mut self) -> u64 {
3502 self.node_id_counter += 1;
3503 self.node_id_counter
3504 }
3505
3506 pub fn active_graph(&self) -> Option<&QuestGraph> {
3507 self.active_graph_id.as_ref().and_then(|id| self.graphs.get(id))
3508 }
3509
3510 pub fn active_graph_mut(&mut self) -> Option<&mut QuestGraph> {
3511 self.active_graph_id.as_ref().and_then(|id| self.graphs.get_mut(id))
3512 }
3513
3514 pub fn new_quest(&mut self, quest_name: &str, quest_id: &str) -> String {
3515 let graph_id = format!("graph_{}", self.allocate_id());
3516 let mut graph = QuestGraph::new(graph_id.clone(), quest_id.to_string(), quest_name.to_string());
3517 let start_id = self.allocate_id();
3519 let mut start = QuestStartNode::new(start_id);
3520 start.quest_id = quest_id.to_string();
3521 start.quest_name = quest_name.to_string();
3522 start.position = Vec2::new(100.0, 200.0);
3523 graph.add_node(QuestNodeEnum::QuestStart(start));
3524 self.graphs.insert(graph_id.clone(), graph);
3525 self.active_graph_id = Some(graph_id.clone());
3526 self.dirty = true;
3527 graph_id
3528 }
3529
3530 pub fn open_graph(&mut self, id: &str) {
3531 if self.graphs.contains_key(id) {
3532 self.active_graph_id = Some(id.to_string());
3533 self.validate_active();
3534 }
3535 }
3536
3537 pub fn add_node(&mut self, node_type: QuestNodeType, position: Vec2) -> Option<u64> {
3538 let id = self.allocate_id();
3539 let node = self.create_node(node_type, id, position);
3540 let nid = node.id();
3541 let graph_id = self.active_graph_id.clone();
3542 if let Some(gid) = graph_id {
3543 let action = QuestEditorAction::AddNode { graph_id: gid, node: node.clone() };
3544 self.push_undo(action);
3545 if let Some(graph) = self.active_graph_mut() {
3546 graph.add_node(node);
3547 self.dirty = true;
3548 }
3549 Some(nid)
3550 } else {
3551 None
3552 }
3553 }
3554
3555 fn create_node(&self, node_type: QuestNodeType, id: u64, position: Vec2) -> QuestNodeEnum {
3556 match node_type {
3557 QuestNodeType::QuestStart => {
3558 let mut n = QuestStartNode::new(id);
3559 n.position = position;
3560 QuestNodeEnum::QuestStart(n)
3561 }
3562 QuestNodeType::Objective => {
3563 let obj = QuestObjective::KillEnemy(KillEnemyObjective {
3564 enemy_type: "any".to_string(),
3565 enemy_id: None,
3566 required_count: 1,
3567 killed_count: 0,
3568 in_area: None,
3569 with_weapon_type: None,
3570 require_stealth_kill: false,
3571 allow_assists: true,
3572 });
3573 let mut n = ObjectiveNode::new(id, obj);
3574 n.position = position;
3575 QuestNodeEnum::Objective(n)
3576 }
3577 QuestNodeType::ConditionCheck => {
3578 let mut n = ConditionCheckNode::new(id);
3579 n.position = position;
3580 QuestNodeEnum::ConditionCheck(n)
3581 }
3582 QuestNodeType::Reward => {
3583 let mut n = RewardNode::new(id);
3584 n.position = position;
3585 QuestNodeEnum::Reward(n)
3586 }
3587 QuestNodeType::Branch => {
3588 let mut n = BranchNode::new(id);
3589 n.position = position;
3590 QuestNodeEnum::Branch(n)
3591 }
3592 QuestNodeType::Fail => {
3593 let mut n = FailNode::new(id);
3594 n.position = position;
3595 QuestNodeEnum::Fail(n)
3596 }
3597 QuestNodeType::Completion => {
3598 let mut n = CompletionNode::new(id);
3599 n.position = position;
3600 QuestNodeEnum::Completion(n)
3601 }
3602 QuestNodeType::Timer => {
3603 let mut n = TimerNode::new(id);
3604 n.position = position;
3605 QuestNodeEnum::Timer(n)
3606 }
3607 QuestNodeType::Trigger => {
3608 let mut n = TriggerNode::new(id);
3609 n.position = position;
3610 QuestNodeEnum::Trigger(n)
3611 }
3612 }
3613 }
3614
3615 pub fn remove_selected_nodes(&mut self) {
3616 let selected: Vec<u64> = self.graph_state.selected_nodes.iter().cloned().collect();
3617 for id in &selected {
3618 let action_opt = if let Some(graph) = self.active_graph_mut() {
3619 if let Some(node) = graph.nodes.get(id).cloned() {
3620 let conns: Vec<QuestNodeConnection> = graph.connections.iter()
3621 .filter(|c| c.from_node == *id || c.to_node == *id)
3622 .cloned()
3623 .collect();
3624 Some(QuestEditorAction::RemoveNode { graph_id: graph.id.clone(), node, connections: conns })
3625 } else { None }
3626 } else { None };
3627 if let Some(action) = action_opt {
3628 self.push_undo(action);
3629 if let Some(graph) = self.active_graph_mut() {
3630 graph.remove_node(*id);
3631 }
3632 }
3633 }
3634 self.graph_state.selected_nodes.clear();
3635 self.dirty = true;
3636 }
3637
3638 pub fn connect_nodes(&mut self, from: u64, from_port: usize, to: u64, to_port: usize) {
3639 let conn = QuestNodeConnection { from_node: from, from_port, to_node: to, to_port, label: None, condition: None };
3640 if let Some(gid) = self.active_graph_id.clone() {
3641 let action = QuestEditorAction::AddConnection { graph_id: gid, connection: conn.clone() };
3642 self.push_undo(action);
3643 if let Some(graph) = self.active_graph_mut() {
3644 graph.connect(from, from_port, to, to_port, None);
3645 self.dirty = true;
3646 }
3647 }
3648 }
3649
3650 fn push_undo(&mut self, action: QuestEditorAction) {
3651 self.redo_stack.clear();
3652 self.undo_stack.push_back(action);
3653 if self.undo_stack.len() > self.max_undo {
3654 self.undo_stack.pop_front();
3655 }
3656 }
3657
3658 pub fn undo(&mut self) {
3659 if let Some(action) = self.undo_stack.pop_back() {
3660 self.redo_stack.push_back(action.clone());
3661 self.apply_undo(action);
3662 }
3663 }
3664
3665 pub fn redo(&mut self) {
3666 if let Some(action) = self.redo_stack.pop_back() {
3667 self.undo_stack.push_back(action.clone());
3668 self.apply_redo(action);
3669 }
3670 }
3671
3672 fn apply_undo(&mut self, action: QuestEditorAction) {
3673 match action {
3674 QuestEditorAction::AddNode { graph_id, node } => {
3675 if let Some(g) = self.graphs.get_mut(&graph_id) { g.remove_node(node.id()); }
3676 }
3677 QuestEditorAction::RemoveNode { graph_id, node, connections } => {
3678 if let Some(g) = self.graphs.get_mut(&graph_id) {
3679 g.add_node(node);
3680 for conn in connections { g.connections.push(conn); }
3681 }
3682 }
3683 QuestEditorAction::MoveNode { graph_id, node_id, old_pos, .. } => {
3684 if let Some(g) = self.graphs.get_mut(&graph_id) {
3685 if let Some(n) = g.nodes.get_mut(&node_id) { n.set_position(old_pos); }
3686 }
3687 }
3688 QuestEditorAction::AddConnection { graph_id, connection } => {
3689 if let Some(g) = self.graphs.get_mut(&graph_id) {
3690 g.connections.retain(|c| !(c.from_node == connection.from_node && c.from_port == connection.from_port));
3691 }
3692 }
3693 QuestEditorAction::RemoveConnection { graph_id, connection } => {
3694 if let Some(g) = self.graphs.get_mut(&graph_id) { g.connections.push(connection); }
3695 }
3696 QuestEditorAction::BatchAction(actions) => {
3697 for a in actions.into_iter().rev() { self.apply_undo(a); }
3698 }
3699 _ => {}
3700 }
3701 self.dirty = true;
3702 }
3703
3704 fn apply_redo(&mut self, action: QuestEditorAction) {
3705 match action {
3706 QuestEditorAction::AddNode { graph_id, node } => {
3707 if let Some(g) = self.graphs.get_mut(&graph_id) { g.add_node(node); }
3708 }
3709 QuestEditorAction::RemoveNode { graph_id, node, .. } => {
3710 if let Some(g) = self.graphs.get_mut(&graph_id) { g.remove_node(node.id()); }
3711 }
3712 QuestEditorAction::MoveNode { graph_id, node_id, new_pos, .. } => {
3713 if let Some(g) = self.graphs.get_mut(&graph_id) {
3714 if let Some(n) = g.nodes.get_mut(&node_id) { n.set_position(new_pos); }
3715 }
3716 }
3717 QuestEditorAction::AddConnection { graph_id, connection } => {
3718 if let Some(g) = self.graphs.get_mut(&graph_id) {
3719 g.connect(connection.from_node, connection.from_port, connection.to_node, connection.to_port, connection.label);
3720 }
3721 }
3722 QuestEditorAction::RemoveConnection { graph_id, connection } => {
3723 if let Some(g) = self.graphs.get_mut(&graph_id) {
3724 g.connections.retain(|c| !(c.from_node == connection.from_node && c.from_port == connection.from_port));
3725 }
3726 }
3727 QuestEditorAction::BatchAction(actions) => {
3728 for a in actions { self.apply_redo(a); }
3729 }
3730 _ => {}
3731 }
3732 self.dirty = true;
3733 }
3734
3735 pub fn validate_active(&mut self) {
3736 if let Some(graph) = self.active_graph() {
3737 self.validation_errors = graph.validate();
3738 }
3739 }
3740
3741 pub fn update(&mut self, delta: f32) {
3742 self.status_timer -= delta;
3743 if self.status_timer < 0.0 { self.status_timer = 0.0; }
3744 while let Some(quest_id) = self.debug_tools.force_complete_queue.pop_front() {
3746 let _ = self.quest_state_store.complete_quest(&quest_id);
3747 self.journal.add_entry(&quest_id, JournalEntryType::QuestComplete,
3748 "Quest Complete", "Force completed by debug tools", JournalCategory::MainQuest);
3749 }
3750 while let Some(quest_id) = self.debug_tools.force_fail_queue.pop_front() {
3751 let _ = self.quest_state_store.fail_quest(&quest_id);
3752 self.journal.add_entry(&quest_id, JournalEntryType::QuestFail,
3753 "Quest Failed", "Force failed by debug tools", JournalCategory::MainQuest);
3754 }
3755 self.quest_state_store.current_time += delta as f64;
3756 }
3757
3758 pub fn process_event(&mut self, event: QuestEvent) {
3759 if self.debug_tools.record_events {
3760 self.debug_tools.recorded_events.push(event.clone());
3761 }
3762 self.event_log.push_back(event.clone());
3763 if self.event_log.len() > self.max_event_log {
3764 self.event_log.pop_front();
3765 }
3766 self.debug_tools.log(LogLevel::Verbose, &format!("Event: {:?}", std::mem::discriminant(&event)), None, "event_system");
3767 }
3768
3769 pub fn set_status(&mut self, msg: &str, duration: f32) {
3770 self.status_message = msg.to_string();
3771 self.status_timer = duration;
3772 }
3773
3774 pub fn preview_rewards(&mut self, node_id: u64) {
3775 let reward_node_clone = self.active_graph()
3776 .and_then(|g| g.nodes.get(&node_id))
3777 .and_then(|n| if let QuestNodeEnum::Reward(r) = n { Some(r.clone()) } else { None });
3778 if let Some(reward_node) = reward_node_clone {
3779 let results = reward_node.roll_rewards(self.preview_player_level, &mut self.rng);
3780 self.reward_preview_results = results;
3781 self.show_reward_preview = true;
3782 }
3783 }
3784
3785 pub fn search(&mut self, query: &str) {
3786 self.search_query = query.to_string();
3787 self.search_results.clear();
3788 let query_lower = query.to_lowercase();
3789 for (graph_id, graph) in &self.graphs {
3790 for (node_id, node) in &graph.nodes {
3791 match node {
3792 QuestNodeEnum::QuestStart(n) => {
3793 if n.quest_name.to_lowercase().contains(&query_lower) {
3794 self.search_results.push(QuestSearchResult {
3795 graph_id: graph_id.clone(),
3796 node_id: *node_id,
3797 field: "quest_name".to_string(),
3798 excerpt: n.quest_name.clone(),
3799 });
3800 }
3801 if n.description.to_lowercase().contains(&query_lower) {
3802 self.search_results.push(QuestSearchResult {
3803 graph_id: graph_id.clone(),
3804 node_id: *node_id,
3805 field: "description".to_string(),
3806 excerpt: n.description.chars().take(60).collect(),
3807 });
3808 }
3809 }
3810 QuestNodeEnum::Objective(n) => {
3811 let desc = n.objective.description();
3812 if desc.to_lowercase().contains(&query_lower) {
3813 self.search_results.push(QuestSearchResult {
3814 graph_id: graph_id.clone(),
3815 node_id: *node_id,
3816 field: "objective".to_string(),
3817 excerpt: desc,
3818 });
3819 }
3820 }
3821 QuestNodeEnum::Fail(n) => {
3822 if n.fail_message.to_lowercase().contains(&query_lower) {
3823 self.search_results.push(QuestSearchResult {
3824 graph_id: graph_id.clone(),
3825 node_id: *node_id,
3826 field: "fail_message".to_string(),
3827 excerpt: n.fail_message.clone(),
3828 });
3829 }
3830 }
3831 _ => {}
3832 }
3833 for tag in node.tags() {
3834 if tag.to_lowercase().contains(&query_lower) {
3835 self.search_results.push(QuestSearchResult {
3836 graph_id: graph_id.clone(),
3837 node_id: *node_id,
3838 field: "tag".to_string(),
3839 excerpt: tag.clone(),
3840 });
3841 }
3842 }
3843 }
3844 }
3845 }
3846
3847 pub fn select_all(&mut self) {
3848 if let Some(graph) = self.active_graph() {
3849 self.graph_state.selected_nodes = graph.nodes.keys().cloned().collect();
3850 }
3851 }
3852
3853 pub fn deselect_all(&mut self) {
3854 self.graph_state.selected_nodes.clear();
3855 }
3856
3857 pub fn auto_layout_active(&mut self) {
3858 if let Some(graph) = self.active_graph_mut() {
3859 graph.auto_layout();
3860 }
3861 }
3862
3863 pub fn add_chain(&mut self, chain: QuestChain) {
3864 self.chains.insert(chain.chain_id.clone(), chain);
3865 self.dirty = true;
3866 }
3867
3868 pub fn get_statistics(&self) -> QuestEditorStats {
3869 let total_quests = self.graphs.len();
3870 let total_nodes: usize = self.graphs.values().map(|g| g.nodes.len()).sum();
3871 let total_connections: usize = self.graphs.values().map(|g| g.connections.len()).sum();
3872 let active_quests = self.quest_state_store.quest_instances.values()
3873 .filter(|q| q.state == QuestState::Active).count();
3874 let completed_quests = self.quest_state_store.quest_instances.values()
3875 .filter(|q| q.state == QuestState::Completed).count();
3876 let total_objectives: usize = self.graphs.values()
3877 .flat_map(|g| g.nodes.values())
3878 .filter(|n| matches!(n, QuestNodeEnum::Objective(_)))
3879 .count();
3880 let total_rewards: usize = self.graphs.values()
3881 .flat_map(|g| g.nodes.values())
3882 .filter(|n| matches!(n, QuestNodeEnum::Reward(_)))
3883 .count();
3884 QuestEditorStats {
3885 total_quests,
3886 total_nodes,
3887 total_connections,
3888 active_quests,
3889 completed_quests,
3890 total_objectives,
3891 total_rewards,
3892 total_chains: self.chains.len(),
3893 journal_entries: self.journal.entries.len(),
3894 map_markers: self.journal.map_markers.len(),
3895 validation_errors: self.validation_errors.iter().filter(|e| e.severity == QuestValidationSeverity::Error).count(),
3896 validation_warnings: self.validation_errors.iter().filter(|e| e.severity == QuestValidationSeverity::Warning).count(),
3897 }
3898 }
3899
3900 pub fn export_to_json(&self, graph_id: &str) -> Result<String, String> {
3901 let graph = self.graphs.get(graph_id).ok_or("Graph not found")?;
3902 let mut json = String::new();
3903 json.push_str("{\n");
3904 json.push_str(&format!(" \"id\": \"{}\",\n", graph.id));
3905 json.push_str(&format!(" \"quest_id\": \"{}\",\n", graph.quest_id));
3906 json.push_str(&format!(" \"name\": \"{}\",\n", graph.name));
3907 json.push_str(&format!(" \"node_count\": {},\n", graph.nodes.len()));
3908 json.push_str(&format!(" \"connection_count\": {}\n", graph.connections.len()));
3909 json.push('}');
3910 Ok(json)
3911 }
3912
3913 pub fn add_faction_rep_delta(&mut self, faction_id: &str, delta: i32, reason: &str, quest_id: Option<&str>) {
3914 self.faction_system.apply_rep_delta(faction_id, delta, reason, quest_id);
3915 self.quest_state_store.modify_faction_rep(faction_id, delta);
3916 self.journal.notification_queue.push_back(JournalNotification {
3917 notification_type: JournalNotificationType::ReputationChange {
3918 faction: faction_id.to_string(),
3919 delta,
3920 },
3921 title: format!("Reputation: {}", faction_id),
3922 description: format!("{:+} - {}", delta, reason),
3923 icon: "rep_icon".to_string(),
3924 duration: 4.0,
3925 timestamp: self.quest_state_store.current_time,
3926 });
3927 }
3928
3929 pub fn generate_journal_entry_for_quest(&mut self, quest_id: &str, graph_id: &str) {
3930 if let Some(graph) = self.graphs.get(graph_id) {
3931 if let Some(start_node_id) = graph.start_node_id {
3932 if let Some(QuestNodeEnum::QuestStart(start)) = graph.nodes.get(&start_node_id) {
3933 self.journal.add_entry(
3934 quest_id,
3935 JournalEntryType::QuestStart,
3936 &start.quest_name,
3937 &start.description,
3938 JournalCategory::MainQuest,
3939 );
3940 for marker in &start.start_map_markers.clone() {
3941 self.journal.add_map_marker(marker.clone());
3942 }
3943 }
3944 }
3945 }
3946 }
3947}
3948
3949#[derive(Debug, Clone, Default)]
3950pub struct QuestEditorStats {
3951 pub total_quests: usize,
3952 pub total_nodes: usize,
3953 pub total_connections: usize,
3954 pub active_quests: usize,
3955 pub completed_quests: usize,
3956 pub total_objectives: usize,
3957 pub total_rewards: usize,
3958 pub total_chains: usize,
3959 pub journal_entries: usize,
3960 pub map_markers: usize,
3961 pub validation_errors: usize,
3962 pub validation_warnings: usize,
3963}
3964
3965#[derive(Debug, Clone)]
3970pub struct SimpleRng {
3971 state: u64,
3972}
3973
3974impl SimpleRng {
3975 pub fn new(seed: u64) -> Self { SimpleRng { state: seed } }
3976
3977 pub fn next_u64(&mut self) -> u64 {
3978 self.state ^= self.state << 13;
3979 self.state ^= self.state >> 7;
3980 self.state ^= self.state << 17;
3981 self.state
3982 }
3983
3984 pub fn next_f32(&mut self) -> f32 {
3985 (self.next_u64() >> 33) as f32 / (u32::MAX as f32)
3986 }
3987
3988 pub fn next_u32(&mut self, max: u32) -> u32 {
3989 if max == 0 { return 0; }
3990 (self.next_u64() % max as u64) as u32
3991 }
3992
3993 pub fn next_usize(&mut self, max: usize) -> usize {
3994 if max == 0 { return 0; }
3995 (self.next_u64() % max as u64) as usize
3996 }
3997
3998 pub fn next_range_i32(&mut self, min: i32, max: i32) -> i32 {
3999 if min >= max { return min; }
4000 let range = (max - min) as u32;
4001 min + self.next_u32(range) as i32
4002 }
4003
4004 pub fn next_range_f32(&mut self, min: f32, max: f32) -> f32 {
4005 min + self.next_f32() * (max - min)
4006 }
4007
4008 pub fn shuffle<T>(&mut self, slice: &mut Vec<T>) {
4009 let len = slice.len();
4010 for i in (1..len).rev() {
4011 let j = self.next_usize(i + 1);
4012 slice.swap(i, j);
4013 }
4014 }
4015
4016 pub fn weighted_pick(&mut self, weights: &[f32]) -> Option<usize> {
4017 if weights.is_empty() { return None; }
4018 let total: f32 = weights.iter().sum();
4019 if total <= 0.0 { return None; }
4020 let mut cursor = self.next_f32() * total;
4021 for (i, &w) in weights.iter().enumerate() {
4022 cursor -= w;
4023 if cursor <= 0.0 { return Some(i); }
4024 }
4025 Some(weights.len() - 1)
4026 }
4027}
4028
4029#[derive(Debug, Clone)]
4034pub struct LootTable {
4035 pub table_id: String,
4036 pub name: String,
4037 pub entries: Vec<LootEntry>,
4038 pub guaranteed_entries: Vec<LootEntry>,
4039 pub max_rolls: u32,
4040 pub min_rolls: u32,
4041 pub quality_modifier: f32,
4042 pub luck_affects: bool,
4043}
4044
4045#[derive(Debug, Clone)]
4046pub struct LootEntry {
4047 pub item_id: String,
4048 pub item_name: String,
4049 pub weight: f32,
4050 pub quantity_min: u32,
4051 pub quantity_max: u32,
4052 pub quality_min: f32,
4053 pub quality_max: f32,
4054 pub rarity: ItemRarity,
4055 pub level_requirement: Option<u32>,
4056 pub conditions: Vec<QuestCondition>,
4057 pub nested_table: Option<String>,
4058}
4059
4060impl LootTable {
4061 pub fn new(table_id: &str, name: &str) -> Self {
4062 LootTable {
4063 table_id: table_id.to_string(),
4064 name: name.to_string(),
4065 entries: Vec::new(),
4066 guaranteed_entries: Vec::new(),
4067 max_rolls: 1,
4068 min_rolls: 1,
4069 quality_modifier: 1.0,
4070 luck_affects: true,
4071 }
4072 }
4073
4074 pub fn add_entry(&mut self, item_id: &str, weight: f32, qty_min: u32, qty_max: u32, rarity: ItemRarity) {
4075 self.entries.push(LootEntry {
4076 item_id: item_id.to_string(),
4077 item_name: item_id.to_string(),
4078 weight,
4079 quantity_min: qty_min,
4080 quantity_max: qty_max,
4081 quality_min: 0.5,
4082 quality_max: 1.0,
4083 rarity,
4084 level_requirement: None,
4085 conditions: Vec::new(),
4086 nested_table: None,
4087 });
4088 }
4089
4090 pub fn roll(&self, rng: &mut SimpleRng, player_level: u32, luck: f32) -> Vec<LootResult> {
4091 let mut results = Vec::new();
4092 for entry in &self.guaranteed_entries {
4094 if let Some(req) = entry.level_requirement {
4095 if player_level < req { continue; }
4096 }
4097 let qty = if entry.quantity_min >= entry.quantity_max {
4098 entry.quantity_min
4099 } else {
4100 entry.quantity_min + rng.next_u32(entry.quantity_max - entry.quantity_min + 1)
4101 };
4102 let quality = entry.quality_min + rng.next_f32() * (entry.quality_max - entry.quality_min);
4103 results.push(LootResult {
4104 item_id: entry.item_id.clone(),
4105 quantity: qty,
4106 quality: (quality * self.quality_modifier).min(1.0),
4107 rarity: entry.rarity.clone(),
4108 from_table: self.table_id.clone(),
4109 });
4110 }
4111 let num_rolls = self.min_rolls + rng.next_u32(self.max_rolls - self.min_rolls + 1);
4113 let weights: Vec<f32> = self.entries.iter()
4114 .map(|e| {
4115 let luck_bonus = if self.luck_affects { luck * e.rarity.drop_chance_modifier() } else { 0.0 };
4116 (e.weight * (1.0 + luck_bonus)).max(0.0)
4117 })
4118 .collect();
4119 for _ in 0..num_rolls {
4120 if let Some(idx) = rng.weighted_pick(&weights) {
4121 let entry = &self.entries[idx];
4122 if let Some(req) = entry.level_requirement {
4123 if player_level < req { continue; }
4124 }
4125 let qty = if entry.quantity_min >= entry.quantity_max {
4126 entry.quantity_min
4127 } else {
4128 entry.quantity_min + rng.next_u32(entry.quantity_max - entry.quantity_min + 1)
4129 };
4130 let quality = entry.quality_min + rng.next_f32() * (entry.quality_max - entry.quality_min);
4131 results.push(LootResult {
4132 item_id: entry.item_id.clone(),
4133 quantity: qty,
4134 quality: (quality * self.quality_modifier).min(1.0),
4135 rarity: entry.rarity.clone(),
4136 from_table: self.table_id.clone(),
4137 });
4138 }
4139 }
4140 results
4141 }
4142
4143 pub fn get_total_weight(&self) -> f32 {
4144 self.entries.iter().map(|e| e.weight).sum()
4145 }
4146
4147 pub fn get_probability(&self, item_id: &str) -> f32 {
4148 let total = self.get_total_weight();
4149 if total == 0.0 { return 0.0; }
4150 let item_weight: f32 = self.entries.iter()
4151 .filter(|e| e.item_id == item_id)
4152 .map(|e| e.weight)
4153 .sum();
4154 item_weight / total
4155 }
4156}
4157
4158#[derive(Debug, Clone)]
4159pub struct LootResult {
4160 pub item_id: String,
4161 pub quantity: u32,
4162 pub quality: f32,
4163 pub rarity: ItemRarity,
4164 pub from_table: String,
4165}
4166
4167impl LootResult {
4168 pub fn display(&self) -> String {
4169 format!("{} x{} ({:.0}% quality, {:?})", self.item_id, self.quantity, self.quality * 100.0, self.rarity)
4170 }
4171}
4172
4173#[derive(Debug, Clone)]
4178pub struct XpCurve {
4179 pub curve_type: XpCurveType,
4180 pub base_xp: u32,
4181 pub max_level: u32,
4182 pub custom_values: Vec<u32>,
4183}
4184
4185#[derive(Debug, Clone, PartialEq)]
4186pub enum XpCurveType {
4187 Linear { increment: u32 },
4188 Exponential { base: f32 },
4189 Polynomial { exponent: f32 },
4190 Custom,
4191 Flat,
4192}
4193
4194impl XpCurve {
4195 pub fn new_exponential(base_xp: u32, max_level: u32, base: f32) -> Self {
4196 XpCurve {
4197 curve_type: XpCurveType::Exponential { base },
4198 base_xp,
4199 max_level,
4200 custom_values: Vec::new(),
4201 }
4202 }
4203
4204 pub fn xp_for_level(&self, level: u32) -> u32 {
4205 if level == 0 { return 0; }
4206 let level = level.min(self.max_level);
4207 match &self.curve_type {
4208 XpCurveType::Linear { increment } => {
4209 self.base_xp + increment * (level - 1)
4210 }
4211 XpCurveType::Exponential { base } => {
4212 (self.base_xp as f32 * base.powi(level as i32 - 1)) as u32
4213 }
4214 XpCurveType::Polynomial { exponent } => {
4215 (self.base_xp as f32 * (level as f32).powf(*exponent)) as u32
4216 }
4217 XpCurveType::Flat => self.base_xp,
4218 XpCurveType::Custom => {
4219 self.custom_values.get((level - 1) as usize).copied().unwrap_or(self.base_xp)
4220 }
4221 }
4222 }
4223
4224 pub fn total_xp_for_level(&self, target_level: u32) -> u32 {
4225 (1..target_level).map(|l| self.xp_for_level(l)).sum()
4226 }
4227
4228 pub fn level_from_total_xp(&self, total_xp: u32) -> u32 {
4229 let mut accumulated = 0u32;
4230 for level in 1..=self.max_level {
4231 let needed = self.xp_for_level(level);
4232 if accumulated + needed > total_xp { return level.saturating_sub(1); }
4233 accumulated += needed;
4234 }
4235 self.max_level
4236 }
4237
4238 pub fn progress_in_level(&self, total_xp: u32) -> f32 {
4239 let level = self.level_from_total_xp(total_xp);
4240 let xp_for_this = self.total_xp_for_level(level);
4241 let xp_needed = self.xp_for_level(level + 1);
4242 if xp_needed == 0 { return 1.0; }
4243 let progress_xp = total_xp - xp_for_this;
4244 (progress_xp as f32 / xp_needed as f32).clamp(0.0, 1.0)
4245 }
4246
4247 pub fn generate_table(&self) -> Vec<(u32, u32, u32)> {
4248 let mut table = Vec::new();
4249 let mut total = 0u32;
4250 for level in 1..=self.max_level {
4251 let xp = self.xp_for_level(level);
4252 total += xp;
4253 table.push((level, xp, total));
4254 }
4255 table
4256 }
4257}
4258
4259#[derive(Debug, Clone)]
4264pub struct QuestNodeStyle {
4265 pub header_color: Vec4,
4266 pub body_color: Vec4,
4267 pub border_color: Vec4,
4268 pub selected_border: Vec4,
4269 pub text_color: Vec4,
4270 pub port_color_in: Vec4,
4271 pub port_color_out: Vec4,
4272 pub rounding: f32,
4273 pub border_width: f32,
4274}
4275
4276impl QuestNodeStyle {
4277 pub fn for_type(node_type: &QuestNodeType) -> Self {
4278 let (header, body) = match node_type {
4279 QuestNodeType::QuestStart => (Vec4::new(0.15, 0.6, 0.15, 1.0), Vec4::new(0.1, 0.35, 0.1, 1.0)),
4280 QuestNodeType::Completion => (Vec4::new(0.1, 0.5, 0.8, 1.0), Vec4::new(0.05, 0.3, 0.5, 1.0)),
4281 QuestNodeType::Fail => (Vec4::new(0.7, 0.1, 0.1, 1.0), Vec4::new(0.4, 0.05, 0.05, 1.0)),
4282 QuestNodeType::Objective => (Vec4::new(0.2, 0.4, 0.7, 1.0), Vec4::new(0.1, 0.25, 0.45, 1.0)),
4283 QuestNodeType::ConditionCheck => (Vec4::new(0.6, 0.5, 0.0, 1.0), Vec4::new(0.35, 0.3, 0.0, 1.0)),
4284 QuestNodeType::Reward => (Vec4::new(0.6, 0.45, 0.0, 1.0), Vec4::new(0.35, 0.25, 0.0, 1.0)),
4285 QuestNodeType::Branch => (Vec4::new(0.5, 0.2, 0.5, 1.0), Vec4::new(0.3, 0.1, 0.3, 1.0)),
4286 QuestNodeType::Timer => (Vec4::new(0.7, 0.3, 0.0, 1.0), Vec4::new(0.45, 0.15, 0.0, 1.0)),
4287 QuestNodeType::Trigger => (Vec4::new(0.4, 0.6, 0.5, 1.0), Vec4::new(0.2, 0.4, 0.3, 1.0)),
4288 };
4289 QuestNodeStyle {
4290 header_color: header,
4291 body_color: body,
4292 border_color: Vec4::new(0.35, 0.35, 0.35, 1.0),
4293 selected_border: Vec4::new(1.0, 0.8, 0.0, 1.0),
4294 text_color: Vec4::new(1.0, 1.0, 1.0, 1.0),
4295 port_color_in: Vec4::new(0.6, 1.0, 0.6, 1.0),
4296 port_color_out: Vec4::new(1.0, 0.6, 0.6, 1.0),
4297 rounding: 6.0,
4298 border_width: 1.5,
4299 }
4300 }
4301}
4302
4303#[derive(Debug, Clone)]
4308pub struct QuestGraphAnalyzer {
4309 pub graph_id: String,
4310 pub node_type_counts: HashMap<String, usize>,
4311 pub objective_types: Vec<String>,
4312 pub longest_path_length: usize,
4313 pub dead_ends: Vec<u64>,
4314 pub orphan_nodes: Vec<u64>,
4315 pub total_estimated_xp: u32,
4316 pub branch_factor_avg: f32,
4317 pub cycle_count: usize,
4318}
4319
4320impl QuestGraphAnalyzer {
4321 pub fn analyze(graph: &QuestGraph, player_level: u32, rng: &mut SimpleRng) -> Self {
4322 let mut node_type_counts: HashMap<String, usize> = HashMap::new();
4323 let mut objective_types = Vec::new();
4324 let mut dead_ends = Vec::new();
4325 let mut orphan_nodes = Vec::new();
4326 let mut total_xp = 0u32;
4327
4328 let reachable = if let Some(start) = graph.start_node_id {
4329 graph.find_reachable(start)
4330 } else { HashSet::new() };
4331
4332 for (id, node) in &graph.nodes {
4333 let type_name = format!("{:?}", node.node_type());
4334 *node_type_counts.entry(type_name).or_insert(0) += 1;
4335 if !reachable.contains(id) {
4336 orphan_nodes.push(*id);
4337 continue;
4338 }
4339 match node {
4340 QuestNodeEnum::Objective(obj) => {
4341 objective_types.push(obj.objective.description());
4342 if graph.get_outputs(*id).is_empty() { dead_ends.push(*id); }
4343 }
4344 QuestNodeEnum::Reward(r) => {
4345 total_xp += r.reward_table.total_estimated_xp(player_level);
4346 }
4347 _ => {}
4348 }
4349 }
4350
4351 let branch_counts: Vec<usize> = graph.nodes.keys()
4352 .map(|id| graph.get_outputs(*id).len())
4353 .collect();
4354 let branch_factor_avg = if branch_counts.is_empty() { 0.0 } else {
4355 branch_counts.iter().sum::<usize>() as f32 / branch_counts.len() as f32
4356 };
4357
4358 let longest_path = if let Some(start) = graph.start_node_id {
4359 Self::find_longest_path(graph, start)
4360 } else { 0 };
4361
4362 QuestGraphAnalyzer {
4363 graph_id: graph.id.clone(),
4364 node_type_counts,
4365 objective_types,
4366 longest_path_length: longest_path,
4367 dead_ends,
4368 orphan_nodes,
4369 total_estimated_xp: total_xp,
4370 branch_factor_avg,
4371 cycle_count: 0,
4372 }
4373 }
4374
4375 fn find_longest_path(graph: &QuestGraph, start: u64) -> usize {
4376 let mut best = 0usize;
4377 let mut stack = vec![(start, 0usize, HashSet::new())];
4378 while let Some((node, depth, mut visited)) = stack.pop() {
4379 if visited.contains(&node) { continue; }
4380 visited.insert(node);
4381 best = best.max(depth);
4382 for conn in graph.get_outputs(node) {
4383 stack.push((conn.to_node, depth + 1, visited.clone()));
4384 }
4385 }
4386 best
4387 }
4388
4389 pub fn generate_report(&self) -> String {
4390 let mut report = String::new();
4391 report.push_str("=== QUEST GRAPH ANALYSIS ===\n\n");
4392 report.push_str("Node Types:\n");
4393 for (t, c) in &self.node_type_counts {
4394 report.push_str(&format!(" {}: {}\n", t, c));
4395 }
4396 report.push_str(&format!("\nLongest path: {} nodes\n", self.longest_path_length));
4397 report.push_str(&format!("Dead ends: {}\n", self.dead_ends.len()));
4398 report.push_str(&format!("Orphan nodes: {}\n", self.orphan_nodes.len()));
4399 report.push_str(&format!("Estimated XP reward: {}\n", self.total_estimated_xp));
4400 report.push_str(&format!("Average branch factor: {:.2}\n", self.branch_factor_avg));
4401 report.push_str(&format!("\nObjectives ({}):\n", self.objective_types.len()));
4402 for obj in &self.objective_types {
4403 report.push_str(&format!(" - {}\n", obj));
4404 }
4405 report
4406 }
4407}
4408
4409#[derive(Debug, Clone)]
4414pub struct QuestProjectFile {
4415 pub version: String,
4416 pub graphs: Vec<SerializedQuestGraph>,
4417 pub chains: Vec<QuestChain>,
4418 pub faction_state: Vec<(String, i32)>,
4419 pub state_flags: Vec<String>,
4420 pub metadata: QuestProjectMetadata,
4421}
4422
4423#[derive(Debug, Clone)]
4424pub struct SerializedQuestGraph {
4425 pub id: String,
4426 pub quest_id: String,
4427 pub name: String,
4428 pub node_count: usize,
4429 pub data: String,
4430}
4431
4432#[derive(Debug, Clone)]
4433pub struct QuestProjectMetadata {
4434 pub project_name: String,
4435 pub game_title: String,
4436 pub author: String,
4437 pub created: String,
4438 pub modified: String,
4439 pub version: String,
4440 pub editor_version: String,
4441}
4442
4443impl QuestProjectFile {
4444 pub fn new(project_name: &str) -> Self {
4445 QuestProjectFile {
4446 version: "1.0.0".to_string(),
4447 graphs: Vec::new(),
4448 chains: Vec::new(),
4449 faction_state: Vec::new(),
4450 state_flags: Vec::new(),
4451 metadata: QuestProjectMetadata {
4452 project_name: project_name.to_string(),
4453 game_title: String::new(),
4454 author: String::new(),
4455 created: String::new(),
4456 modified: String::new(),
4457 version: "1.0.0".to_string(),
4458 editor_version: "1.0.0".to_string(),
4459 },
4460 }
4461 }
4462
4463 pub fn from_editor(editor: &QuestEditor) -> Self {
4464 let mut file = QuestProjectFile::new("project");
4465 for (id, graph) in &editor.graphs {
4466 file.graphs.push(SerializedQuestGraph {
4467 id: graph.id.clone(),
4468 quest_id: graph.quest_id.clone(),
4469 name: graph.name.clone(),
4470 node_count: graph.nodes.len(),
4471 data: String::new(),
4472 });
4473 }
4474 for (id, chain) in &editor.chains {
4475 file.chains.push(chain.clone());
4476 }
4477 for (faction_id, rep) in &editor.quest_state_store.faction_reps {
4478 file.faction_state.push((faction_id.clone(), *rep));
4479 }
4480 for flag in &editor.quest_state_store.flags {
4481 file.state_flags.push(flag.clone());
4482 }
4483 file
4484 }
4485}
4486
4487#[derive(Debug, Clone)]
4492pub struct RepeatableQuestTracker {
4493 pub quest_id: String,
4494 pub completion_count: u32,
4495 pub last_completion_time: f64,
4496 pub cooldown_secs: f64,
4497 pub max_completions: Option<u32>,
4498 pub rewards_scale_with_completions: bool,
4499 pub reward_scale_factor: f32,
4500 pub reward_cap_multiplier: f32,
4501}
4502
4503impl RepeatableQuestTracker {
4504 pub fn new(quest_id: &str, cooldown: f64) -> Self {
4505 RepeatableQuestTracker {
4506 quest_id: quest_id.to_string(),
4507 completion_count: 0,
4508 last_completion_time: -f64::MAX,
4509 cooldown_secs: cooldown,
4510 max_completions: None,
4511 rewards_scale_with_completions: false,
4512 reward_scale_factor: 1.0,
4513 reward_cap_multiplier: 2.0,
4514 }
4515 }
4516
4517 pub fn can_start(&self, current_time: f64) -> bool {
4518 if let Some(max) = self.max_completions {
4519 if self.completion_count >= max { return false; }
4520 }
4521 current_time - self.last_completion_time >= self.cooldown_secs
4522 }
4523
4524 pub fn complete(&mut self, current_time: f64) {
4525 self.completion_count += 1;
4526 self.last_completion_time = current_time;
4527 }
4528
4529 pub fn get_reward_multiplier(&self) -> f32 {
4530 if !self.rewards_scale_with_completions { return 1.0; }
4531 let base = 1.0 + (self.completion_count as f32 * self.reward_scale_factor);
4532 base.min(self.reward_cap_multiplier)
4533 }
4534
4535 pub fn cooldown_remaining(&self, current_time: f64) -> f64 {
4536 (self.cooldown_secs - (current_time - self.last_completion_time)).max(0.0)
4537 }
4538
4539 pub fn format_cooldown(&self, current_time: f64) -> String {
4540 let rem = self.cooldown_remaining(current_time);
4541 let hours = (rem / 3600.0) as u64;
4542 let mins = ((rem % 3600.0) / 60.0) as u64;
4543 let secs = (rem % 60.0) as u64;
4544 if hours > 0 {
4545 format!("{}h {}m", hours, mins)
4546 } else if mins > 0 {
4547 format!("{}m {}s", mins, secs)
4548 } else {
4549 format!("{}s", secs)
4550 }
4551 }
4552}
4553
4554#[derive(Debug, Clone)]
4559pub struct ObjectiveProgressDisplay {
4560 pub objective_id: String,
4561 pub description: String,
4562 pub progress_text: String,
4563 pub progress_value: f32,
4564 pub status: ObjectiveStatus,
4565 pub optional: bool,
4566 pub secret: bool,
4567 pub time_remaining: Option<f32>,
4568 pub map_markers: Vec<MapMarker>,
4569 pub new_this_session: bool,
4570}
4571
4572impl ObjectiveProgressDisplay {
4573 pub fn from_node(node: &ObjectiveNode) -> Self {
4574 let status = node.objective.get_status();
4575 ObjectiveProgressDisplay {
4576 objective_id: node.objective_id.clone(),
4577 description: node.objective.description(),
4578 progress_text: node.progress_display(),
4579 progress_value: node.objective.get_progress(),
4580 status,
4581 optional: node.optional,
4582 secret: node.secret,
4583 time_remaining: node.time_remaining(),
4584 map_markers: node.map_markers.clone(),
4585 new_this_session: false,
4586 }
4587 }
4588
4589 pub fn status_color(&self) -> Vec4 {
4590 match self.status {
4591 ObjectiveStatus::NotStarted => Vec4::new(0.5, 0.5, 0.5, 1.0),
4592 ObjectiveStatus::Active => Vec4::new(0.9, 0.9, 0.9, 1.0),
4593 ObjectiveStatus::Completed => Vec4::new(0.2, 0.9, 0.2, 1.0),
4594 ObjectiveStatus::Failed => Vec4::new(0.9, 0.2, 0.2, 1.0),
4595 }
4596 }
4597
4598 pub fn progress_bar_color(&self) -> Vec4 {
4599 match self.status {
4600 ObjectiveStatus::Active => Vec4::new(0.2, 0.5, 0.9, 1.0),
4601 ObjectiveStatus::Completed => Vec4::new(0.2, 0.8, 0.2, 1.0),
4602 ObjectiveStatus::Failed => Vec4::new(0.8, 0.2, 0.2, 1.0),
4603 _ => Vec4::new(0.4, 0.4, 0.4, 1.0),
4604 }
4605 }
4606}
4607
4608#[derive(Debug, Clone)]
4613pub struct QuestHudManager {
4614 pub tracked_quests: Vec<TrackedQuestDisplay>,
4615 pub max_tracked: usize,
4616 pub show_completed_briefly: bool,
4617 pub completed_display_time: f32,
4618 pub notifications: VecDeque<QuestHudNotification>,
4619 pub objective_updates: VecDeque<ObjectiveUpdateAnim>,
4620 pub world_markers: Vec<WorldMapMarker>,
4621}
4622
4623#[derive(Debug, Clone)]
4624pub struct TrackedQuestDisplay {
4625 pub quest_id: String,
4626 pub quest_name: String,
4627 pub objectives: Vec<ObjectiveProgressDisplay>,
4628 pub is_main_quest: bool,
4629 pub pin_position: Option<u32>,
4630 pub recently_updated: bool,
4631 pub update_flash_time: f32,
4632}
4633
4634#[derive(Debug, Clone)]
4635pub struct QuestHudNotification {
4636 pub notification_id: String,
4637 pub text: String,
4638 pub sub_text: String,
4639 pub icon: String,
4640 pub sound: Option<String>,
4641 pub duration: f32,
4642 pub elapsed: f32,
4643 pub style: HudNotificationStyle,
4644}
4645
4646#[derive(Debug, Clone, PartialEq)]
4647pub enum HudNotificationStyle {
4648 QuestStarted,
4649 QuestComplete,
4650 QuestFailed,
4651 ObjectiveComplete,
4652 ObjectiveFailed,
4653 RewardReceived,
4654 RepChanged,
4655}
4656
4657#[derive(Debug, Clone)]
4658pub struct ObjectiveUpdateAnim {
4659 pub objective_id: String,
4660 pub update_type: ObjectiveUpdateType,
4661 pub elapsed: f32,
4662 pub duration: f32,
4663}
4664
4665#[derive(Debug, Clone)]
4666pub enum ObjectiveUpdateType {
4667 Progress(f32),
4668 Completed,
4669 Failed,
4670 Added,
4671 Removed,
4672}
4673
4674#[derive(Debug, Clone)]
4675pub struct WorldMapMarker {
4676 pub marker_id: String,
4677 pub world_pos: Vec3,
4678 pub label: String,
4679 pub icon_id: String,
4680 pub color: Vec4,
4681 pub is_visible: bool,
4682 pub distance_to_player: f32,
4683 pub compass_angle: f32,
4684 pub show_on_compass: bool,
4685 pub show_in_world: bool,
4686 pub min_distance: f32,
4687 pub max_distance: f32,
4688}
4689
4690impl WorldMapMarker {
4691 pub fn update_from_player(&mut self, player_pos: Vec3, player_forward: Vec3) {
4692 let to_marker = self.world_pos - player_pos;
4693 self.distance_to_player = to_marker.length();
4694 let angle = to_marker.x.atan2(to_marker.z);
4695 let player_angle = player_forward.x.atan2(player_forward.z);
4696 self.compass_angle = angle - player_angle;
4697 self.is_visible = self.distance_to_player >= self.min_distance &&
4698 (self.max_distance == 0.0 || self.distance_to_player <= self.max_distance);
4699 }
4700
4701 pub fn get_screen_edge_pos(&self, screen_size: Vec2) -> Vec2 {
4702 let half = screen_size * 0.5;
4703 let angle = self.compass_angle;
4704 let cos_a = angle.cos();
4705 let sin_a = angle.sin();
4706 let scale_x = half.x / sin_a.abs().max(0.001);
4707 let scale_y = half.y / cos_a.abs().max(0.001);
4708 let scale = scale_x.min(scale_y);
4709 Vec2::new(half.x + sin_a * scale, half.y - cos_a * scale)
4710 }
4711}
4712
4713impl QuestHudManager {
4714 pub fn new() -> Self {
4715 QuestHudManager {
4716 tracked_quests: Vec::new(),
4717 max_tracked: 5,
4718 show_completed_briefly: true,
4719 completed_display_time: 5.0,
4720 notifications: VecDeque::new(),
4721 objective_updates: VecDeque::new(),
4722 world_markers: Vec::new(),
4723 }
4724 }
4725
4726 pub fn update(&mut self, delta: f32) {
4727 self.notifications.retain_mut(|n| {
4729 n.elapsed += delta;
4730 n.elapsed < n.duration
4731 });
4732 self.objective_updates.retain_mut(|a| {
4734 a.elapsed += delta;
4735 a.elapsed < a.duration
4736 });
4737 for quest in &mut self.tracked_quests {
4739 if quest.recently_updated {
4740 quest.update_flash_time -= delta;
4741 if quest.update_flash_time <= 0.0 {
4742 quest.recently_updated = false;
4743 }
4744 }
4745 }
4746 }
4747
4748 pub fn track_quest(&mut self, quest_id: &str, quest_name: &str, is_main: bool) {
4749 if self.tracked_quests.iter().any(|q| q.quest_id == quest_id) { return; }
4750 if self.tracked_quests.len() >= self.max_tracked { return; }
4751 self.tracked_quests.push(TrackedQuestDisplay {
4752 quest_id: quest_id.to_string(),
4753 quest_name: quest_name.to_string(),
4754 objectives: Vec::new(),
4755 is_main_quest: is_main,
4756 pin_position: None,
4757 recently_updated: true,
4758 update_flash_time: 3.0,
4759 });
4760 }
4761
4762 pub fn untrack_quest(&mut self, quest_id: &str) {
4763 self.tracked_quests.retain(|q| q.quest_id != quest_id);
4764 }
4765
4766 pub fn push_notification(&mut self, text: &str, sub_text: &str, style: HudNotificationStyle, icon: &str) {
4767 let duration = match style {
4768 HudNotificationStyle::QuestComplete | HudNotificationStyle::QuestFailed => 6.0,
4769 HudNotificationStyle::ObjectiveComplete => 4.0,
4770 _ => 3.0,
4771 };
4772 self.notifications.push_back(QuestHudNotification {
4773 notification_id: format!("notif_{}", self.notifications.len()),
4774 text: text.to_string(),
4775 sub_text: sub_text.to_string(),
4776 icon: icon.to_string(),
4777 sound: None,
4778 duration,
4779 elapsed: 0.0,
4780 style,
4781 });
4782 }
4783
4784 pub fn get_active_notifications(&self) -> Vec<&QuestHudNotification> {
4785 self.notifications.iter().collect()
4786 }
4787}
4788
4789pub fn create_sample_quest_graph() -> QuestGraph {
4794 let mut graph = QuestGraph::new("quest_graph_001".to_string(), "the_lost_artifact".to_string(), "The Lost Artifact".to_string());
4795
4796 let start_id = 1u64;
4797 let mut start = QuestStartNode::new(start_id);
4798 start.quest_name = "The Lost Artifact".to_string();
4799 start.description = "An ancient artifact has been stolen. Track it down and recover it.".to_string();
4800 start.quest_giver_id = "npc_scholar_aldred".to_string();
4801 start.position = Vec2::new(50.0, 200.0);
4802 graph.add_node(QuestNodeEnum::QuestStart(start));
4803
4804 let obj1_id = 2u64;
4805 let obj1 = QuestObjective::TalkToNPC(TalkToNPCObjective {
4806 npc_id: "npc_witness_sara".to_string(),
4807 npc_name: "Sara the Witness".to_string(),
4808 required_topic: Some("artifact_theft".to_string()),
4809 talked: false,
4810 dialogue_completed: false,
4811 require_specific_ending: None,
4812 });
4813 let mut obj_node1 = ObjectiveNode::new(obj1_id, obj1);
4814 obj_node1.position = Vec2::new(300.0, 100.0);
4815 obj_node1.journal_entry_on_start = Some("Find someone who witnessed the theft.".to_string());
4816 graph.add_node(QuestNodeEnum::Objective(obj_node1));
4817
4818 let obj2_id = 3u64;
4819 let obj2 = QuestObjective::InvestigateClue(InvestigateClueObjective {
4820 location_name: "The Old Warehouse".to_string(),
4821 clue_ids: vec!["clue_footprints".to_string(), "clue_torn_cloth".to_string(), "clue_stolen_tools".to_string()],
4822 clues_found: HashSet::new(),
4823 required_clues: 2,
4824 all_clues_required: false,
4825 });
4826 let mut obj_node2 = ObjectiveNode::new(obj2_id, obj2);
4827 obj_node2.position = Vec2::new(600.0, 100.0);
4828 obj_node2.journal_entry_on_start = Some("Investigate the old warehouse for clues.".to_string());
4829 graph.add_node(QuestNodeEnum::Objective(obj_node2));
4830
4831 let obj3_id = 4u64;
4832 let obj3 = QuestObjective::KillEnemy(KillEnemyObjective {
4833 enemy_type: "thief".to_string(),
4834 enemy_id: Some("boss_thief_marcus".to_string()),
4835 required_count: 1,
4836 killed_count: 0,
4837 in_area: Some("thief_hideout".to_string()),
4838 with_weapon_type: None,
4839 require_stealth_kill: false,
4840 allow_assists: false,
4841 });
4842 let mut obj_node3 = ObjectiveNode::new(obj3_id, obj3);
4843 obj_node3.position = Vec2::new(900.0, 100.0);
4844 obj_node3.journal_entry_on_start = Some("Defeat Marcus and recover the artifact.".to_string());
4845 graph.add_node(QuestNodeEnum::Objective(obj_node3));
4846
4847 let reward_id = 5u64;
4848 let mut reward_node = RewardNode::new(reward_id);
4849 reward_node.position = Vec2::new(1200.0, 100.0);
4850 reward_node.reward_table.add_xp(500, 0.1);
4851 reward_node.reward_table.add_item("ancient_artifact", 1, 1, 1.0, ItemRarity::Rare);
4852 reward_node.reward_table.add_currency("gold", 100, 200, 0.05);
4853 graph.add_node(QuestNodeEnum::Reward(reward_node));
4854
4855 let complete_id = 6u64;
4856 let mut complete_node = CompletionNode::new(complete_id);
4857 complete_node.position = Vec2::new(1500.0, 100.0);
4858 complete_node.completion_message = "The artifact has been recovered!".to_string();
4859 complete_node.unlock_quests = vec!["quest_ancient_mystery_2".to_string()];
4860 complete_node.set_flags = vec!["artifact_recovered".to_string()];
4861 graph.add_node(QuestNodeEnum::Completion(complete_node));
4862
4863 let fail_id = 7u64;
4864 let mut fail_node = FailNode::new(fail_id);
4865 fail_node.position = Vec2::new(900.0, 350.0);
4866 fail_node.fail_reason = "artifact_destroyed".to_string();
4867 fail_node.fail_message = "The artifact was destroyed. The quest has failed.".to_string();
4868 fail_node.allow_retry = false;
4869 graph.add_node(QuestNodeEnum::Fail(fail_node));
4870
4871 graph.connect(1, 0, 2, 0, None);
4872 graph.connect(2, 0, 3, 0, Some("witness_interviewed".to_string()));
4873 graph.connect(3, 0, 4, 0, Some("clues_found".to_string()));
4874 graph.connect(4, 0, 5, 0, Some("boss_defeated".to_string()));
4875 graph.connect(4, 1, 7, 0, Some("artifact_destroyed".to_string()));
4876 graph.connect(5, 0, 6, 0, None);
4877
4878 graph
4879}
4880
4881pub fn calculate_quest_reward_at_level(reward: &RewardTable, level: u32, rng: &mut SimpleRng) -> Vec<RolledReward> {
4882 reward.roll(level, rng)
4883}
4884
4885pub fn get_faction_tier_display(faction: &Faction) -> String {
4886 let tier = &faction.current_tier;
4887 let next_threshold = tier.next_tier_threshold();
4888 if let Some(next) = next_threshold {
4889 let current_threshold = match tier {
4890 ReputationTier::Neutral => -1000,
4891 ReputationTier::Friendly => 1000,
4892 ReputationTier::Honored => 2000,
4893 ReputationTier::Revered => 3000,
4894 _ => faction.reputation_min,
4895 };
4896 let range = next - current_threshold;
4897 let progress = faction.reputation - current_threshold;
4898 format!("{}: {}/{}", tier.display_name(), progress, range)
4899 } else {
4900 format!("{} (Max)", tier.display_name())
4901 }
4902}
4903
4904pub fn format_xp(xp: u32) -> String {
4905 if xp >= 1_000_000 {
4906 format!("{:.1}M XP", xp as f32 / 1_000_000.0)
4907 } else if xp >= 1_000 {
4908 format!("{:.1}K XP", xp as f32 / 1_000.0)
4909 } else {
4910 format!("{} XP", xp)
4911 }
4912}
4913
4914pub fn format_currency(amount: u32, currency_id: &str) -> String {
4915 let symbol = match currency_id {
4916 "gold" => "G",
4917 "silver" => "S",
4918 "copper" => "C",
4919 "credits" => "CR",
4920 _ => currency_id,
4921 };
4922 if amount >= 1_000_000 {
4923 format!("{:.1}M {}", amount as f32 / 1_000_000.0, symbol)
4924 } else if amount >= 1_000 {
4925 format!("{:.1}K {}", amount as f32 / 1_000.0, symbol)
4926 } else {
4927 format!("{} {}", amount, symbol)
4928 }
4929}
4930
4931pub fn interpolate_rep_color(rep: i32) -> Vec4 {
4932 let tier = ReputationTier::from_rep(rep);
4933 tier.color()
4934}
4935
4936pub fn check_all_prerequisites(quest: &QuestStartNode, state: &QuestStateStore) -> (bool, Vec<String>) {
4937 let mut unmet = Vec::new();
4938 let mut all_met = true;
4939 for prereq in &quest.prerequisites {
4940 if !prereq.is_satisfied(state) {
4941 all_met = false;
4942 let reason = match &prereq.prereq_type {
4943 PrerequisiteType::QuestCompleted => format!("Quest required: {:?}", prereq.quest_id),
4944 PrerequisiteType::PlayerLevel => format!("Level required: {:?}", prereq.level_required),
4945 PrerequisiteType::FactionRep => format!("Reputation required: {:?} {:?}", prereq.faction_id, prereq.faction_rep_required),
4946 PrerequisiteType::Flag => format!("Flag required: {:?}", prereq.flag_name),
4947 _ => "Unknown requirement".to_string(),
4948 };
4949 unmet.push(reason);
4950 }
4951 }
4952 (all_met, unmet)
4953}