1use std::collections::HashMap;
4
5#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
11pub enum QualityTier {
12 Poor,
13 Common,
14 Fine,
15 Exceptional,
16 Masterwork,
17 Legendary,
18}
19
20impl QualityTier {
21 pub fn stat_multiplier(&self) -> f32 {
23 match self {
24 QualityTier::Poor => 0.60,
25 QualityTier::Common => 1.00,
26 QualityTier::Fine => 1.20,
27 QualityTier::Exceptional => 1.50,
28 QualityTier::Masterwork => 2.00,
29 QualityTier::Legendary => 3.00,
30 }
31 }
32
33 pub fn from_value(v: u8) -> Self {
35 match v {
36 0..=39 => QualityTier::Poor,
37 40..=99 => QualityTier::Common,
38 100..=149 => QualityTier::Fine,
39 150..=199 => QualityTier::Exceptional,
40 200..=239 => QualityTier::Masterwork,
41 240..=255 => QualityTier::Legendary,
42 }
43 }
44
45 pub fn threshold(&self) -> u8 {
47 match self {
48 QualityTier::Poor => 0,
49 QualityTier::Common => 40,
50 QualityTier::Fine => 100,
51 QualityTier::Exceptional => 150,
52 QualityTier::Masterwork => 200,
53 QualityTier::Legendary => 240,
54 }
55 }
56
57 pub fn label(&self) -> &'static str {
59 match self {
60 QualityTier::Poor => "Poor",
61 QualityTier::Common => "Common",
62 QualityTier::Fine => "Fine",
63 QualityTier::Exceptional => "Exceptional",
64 QualityTier::Masterwork => "Masterwork",
65 QualityTier::Legendary => "Legendary",
66 }
67 }
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Hash)]
75pub enum RecipeCategory {
76 Smithing,
77 Alchemy,
78 Cooking,
79 Enchanting,
80 Engineering,
81 Tailoring,
82 Woodworking,
83 Jeweling,
84}
85
86impl RecipeCategory {
87 pub fn label(&self) -> &'static str {
88 match self {
89 RecipeCategory::Smithing => "Smithing",
90 RecipeCategory::Alchemy => "Alchemy",
91 RecipeCategory::Cooking => "Cooking",
92 RecipeCategory::Enchanting => "Enchanting",
93 RecipeCategory::Engineering => "Engineering",
94 RecipeCategory::Tailoring => "Tailoring",
95 RecipeCategory::Woodworking => "Woodworking",
96 RecipeCategory::Jeweling => "Jeweling",
97 }
98 }
99}
100
101#[derive(Debug, Clone)]
107pub struct Ingredient {
108 pub item_id: String,
109 pub quantity: u32,
110 pub quality_min: u8,
112}
113
114impl Ingredient {
115 pub fn new(item_id: impl Into<String>, quantity: u32, quality_min: u8) -> Self {
116 Self { item_id: item_id.into(), quantity, quality_min }
117 }
118
119 pub fn basic(item_id: impl Into<String>, quantity: u32) -> Self {
121 Self::new(item_id, quantity, 0)
122 }
123}
124
125#[derive(Debug, Clone)]
127pub struct CraftResult {
128 pub item_id: String,
129 pub quantity: u32,
130 pub quality: u8,
132 pub chance: f32,
134}
135
136impl CraftResult {
137 pub fn new(item_id: impl Into<String>, quantity: u32, quality: u8, chance: f32) -> Self {
138 Self { item_id: item_id.into(), quantity, quality, chance: chance.clamp(0.0, 1.0) }
139 }
140
141 pub fn guaranteed(item_id: impl Into<String>, quantity: u32, quality: u8) -> Self {
143 Self::new(item_id, quantity, quality, 1.0)
144 }
145}
146
147#[derive(Debug, Clone)]
153pub struct Recipe {
154 pub id: String,
155 pub name: String,
156 pub category: RecipeCategory,
157 pub ingredients: Vec<Ingredient>,
158 pub results: Vec<CraftResult>,
159 pub required_level: u32,
161 pub required_tools: Vec<String>,
163 pub duration_secs: f32,
165 pub experience_reward: u32,
167 pub discovery_chance: f32,
169}
170
171impl Recipe {
172 pub fn new(
173 id: impl Into<String>,
174 name: impl Into<String>,
175 category: RecipeCategory,
176 ) -> Self {
177 Self {
178 id: id.into(),
179 name: name.into(),
180 category,
181 ingredients: Vec::new(),
182 results: Vec::new(),
183 required_level: 1,
184 required_tools: Vec::new(),
185 duration_secs: 5.0,
186 experience_reward: 10,
187 discovery_chance: 0.0,
188 }
189 }
190
191 pub fn with_ingredient(mut self, ing: Ingredient) -> Self {
192 self.ingredients.push(ing);
193 self
194 }
195
196 pub fn with_result(mut self, res: CraftResult) -> Self {
197 self.results.push(res);
198 self
199 }
200
201 pub fn with_level(mut self, level: u32) -> Self {
202 self.required_level = level;
203 self
204 }
205
206 pub fn with_tool(mut self, tool: impl Into<String>) -> Self {
207 self.required_tools.push(tool.into());
208 self
209 }
210
211 pub fn with_duration(mut self, secs: f32) -> Self {
212 self.duration_secs = secs;
213 self
214 }
215
216 pub fn with_experience(mut self, xp: u32) -> Self {
217 self.experience_reward = xp;
218 self
219 }
220
221 pub fn with_discovery_chance(mut self, chance: f32) -> Self {
222 self.discovery_chance = chance.clamp(0.0, 1.0);
223 self
224 }
225}
226
227#[derive(Debug, Clone)]
233pub struct RecipeBook {
234 recipes: HashMap<String, Recipe>,
235 by_category: HashMap<String, Vec<String>>,
236}
237
238impl RecipeBook {
239 pub fn new() -> Self {
240 Self {
241 recipes: HashMap::new(),
242 by_category: HashMap::new(),
243 }
244 }
245
246 pub fn register(&mut self, recipe: Recipe) {
248 let cat_key = recipe.category.label().to_string();
249 self.by_category
250 .entry(cat_key)
251 .or_insert_with(Vec::new)
252 .push(recipe.id.clone());
253 self.recipes.insert(recipe.id.clone(), recipe);
254 }
255
256 pub fn get(&self, id: &str) -> Option<&Recipe> {
258 self.recipes.get(id)
259 }
260
261 pub fn by_category(&self, category: &RecipeCategory) -> Vec<&Recipe> {
263 let key = category.label();
264 match self.by_category.get(key) {
265 None => Vec::new(),
266 Some(ids) => ids
267 .iter()
268 .filter_map(|id| self.recipes.get(id))
269 .collect(),
270 }
271 }
272
273 pub fn available_for_level(&self, skill_level: u32) -> Vec<&Recipe> {
275 self.recipes
276 .values()
277 .filter(|r| r.required_level <= skill_level)
278 .collect()
279 }
280
281 pub fn available_for_level_and_category(
283 &self,
284 skill_level: u32,
285 category: &RecipeCategory,
286 ) -> Vec<&Recipe> {
287 self.by_category(category)
288 .into_iter()
289 .filter(|r| r.required_level <= skill_level)
290 .collect()
291 }
292
293 pub fn count(&self) -> usize {
295 self.recipes.len()
296 }
297
298 pub fn ids(&self) -> Vec<&str> {
300 self.recipes.keys().map(|s| s.as_str()).collect()
301 }
302
303 pub fn default_recipes() -> Self {
305 let mut book = Self::new();
306
307 book.register(
309 Recipe::new("iron_sword", "Iron Sword", RecipeCategory::Smithing)
310 .with_ingredient(Ingredient::basic("iron_ingot", 3))
311 .with_ingredient(Ingredient::basic("leather_strip", 1))
312 .with_result(CraftResult::guaranteed("iron_sword", 1, 80))
313 .with_level(5)
314 .with_tool("forge_hammer")
315 .with_duration(12.0)
316 .with_experience(25),
317 );
318 book.register(
319 Recipe::new("iron_shield", "Iron Shield", RecipeCategory::Smithing)
320 .with_ingredient(Ingredient::basic("iron_ingot", 4))
321 .with_ingredient(Ingredient::basic("wooden_plank", 2))
322 .with_result(CraftResult::guaranteed("iron_shield", 1, 75))
323 .with_level(8)
324 .with_tool("forge_hammer")
325 .with_duration(18.0)
326 .with_experience(30),
327 );
328 book.register(
329 Recipe::new("steel_ingot", "Steel Ingot", RecipeCategory::Smithing)
330 .with_ingredient(Ingredient::basic("iron_ingot", 2))
331 .with_ingredient(Ingredient::basic("coal", 3))
332 .with_result(CraftResult::guaranteed("steel_ingot", 1, 90))
333 .with_level(15)
334 .with_duration(8.0)
335 .with_experience(15),
336 );
337 book.register(
338 Recipe::new("steel_sword", "Steel Sword", RecipeCategory::Smithing)
339 .with_ingredient(Ingredient::new("steel_ingot", 3, 80))
340 .with_ingredient(Ingredient::basic("leather_strip", 1))
341 .with_result(CraftResult::guaranteed("steel_sword", 1, 110))
342 .with_level(20)
343 .with_tool("forge_hammer")
344 .with_duration(20.0)
345 .with_experience(50),
346 );
347 book.register(
348 Recipe::new("iron_helmet", "Iron Helmet", RecipeCategory::Smithing)
349 .with_ingredient(Ingredient::basic("iron_ingot", 5))
350 .with_result(CraftResult::guaranteed("iron_helmet", 1, 75))
351 .with_level(10)
352 .with_tool("forge_hammer")
353 .with_duration(15.0)
354 .with_experience(35),
355 );
356
357 book.register(
359 Recipe::new("health_potion_minor", "Minor Health Potion", RecipeCategory::Alchemy)
360 .with_ingredient(Ingredient::basic("red_herb", 2))
361 .with_ingredient(Ingredient::basic("clean_water", 1))
362 .with_result(CraftResult::guaranteed("health_potion_minor", 1, 70))
363 .with_level(1)
364 .with_duration(5.0)
365 .with_experience(8)
366 .with_discovery_chance(0.3),
367 );
368 book.register(
369 Recipe::new("health_potion_major", "Major Health Potion", RecipeCategory::Alchemy)
370 .with_ingredient(Ingredient::new("red_herb", 4, 60))
371 .with_ingredient(Ingredient::basic("clean_water", 1))
372 .with_ingredient(Ingredient::basic("golden_root", 1))
373 .with_result(CraftResult::guaranteed("health_potion_major", 1, 100))
374 .with_level(12)
375 .with_duration(10.0)
376 .with_experience(20),
377 );
378 book.register(
379 Recipe::new("mana_potion", "Mana Potion", RecipeCategory::Alchemy)
380 .with_ingredient(Ingredient::basic("blue_herb", 2))
381 .with_ingredient(Ingredient::basic("spring_water", 1))
382 .with_result(CraftResult::guaranteed("mana_potion", 1, 70))
383 .with_level(5)
384 .with_duration(6.0)
385 .with_experience(12),
386 );
387 book.register(
388 Recipe::new("poison_vial", "Poison Vial", RecipeCategory::Alchemy)
389 .with_ingredient(Ingredient::basic("viper_fang", 1))
390 .with_ingredient(Ingredient::basic("dark_mushroom", 2))
391 .with_result(CraftResult::guaranteed("poison_vial", 2, 80))
392 .with_result(CraftResult::new("potent_poison_vial", 1, 120, 0.15))
393 .with_level(18)
394 .with_duration(14.0)
395 .with_experience(40)
396 .with_discovery_chance(0.1),
397 );
398
399 book.register(
401 Recipe::new("roasted_meat", "Roasted Meat", RecipeCategory::Cooking)
402 .with_ingredient(Ingredient::basic("raw_meat", 2))
403 .with_ingredient(Ingredient::basic("salt", 1))
404 .with_result(CraftResult::guaranteed("roasted_meat", 2, 60))
405 .with_level(1)
406 .with_duration(4.0)
407 .with_experience(5),
408 );
409 book.register(
410 Recipe::new("hearty_stew", "Hearty Stew", RecipeCategory::Cooking)
411 .with_ingredient(Ingredient::basic("raw_meat", 1))
412 .with_ingredient(Ingredient::basic("carrot", 2))
413 .with_ingredient(Ingredient::basic("potato", 2))
414 .with_ingredient(Ingredient::basic("salt", 1))
415 .with_result(CraftResult::guaranteed("hearty_stew", 1, 80))
416 .with_level(8)
417 .with_duration(10.0)
418 .with_experience(18),
419 );
420 book.register(
421 Recipe::new("energy_bread", "Energy Bread", RecipeCategory::Cooking)
422 .with_ingredient(Ingredient::basic("flour", 3))
423 .with_ingredient(Ingredient::basic("honey", 1))
424 .with_result(CraftResult::guaranteed("energy_bread", 2, 65))
425 .with_level(4)
426 .with_duration(6.0)
427 .with_experience(10),
428 );
429
430 book.register(
432 Recipe::new("enchant_sharpness_i", "Sharpness I Enchant", RecipeCategory::Enchanting)
433 .with_ingredient(Ingredient::new("iron_sword", 1, 70))
434 .with_ingredient(Ingredient::basic("magic_dust", 5))
435 .with_result(CraftResult::guaranteed("iron_sword_sharpened", 1, 90))
436 .with_level(10)
437 .with_tool("enchanting_focus")
438 .with_duration(30.0)
439 .with_experience(60),
440 );
441 book.register(
442 Recipe::new("enchant_fire_i", "Fire I Enchant", RecipeCategory::Enchanting)
443 .with_ingredient(Ingredient::new("steel_sword", 1, 90))
444 .with_ingredient(Ingredient::basic("fire_essence", 3))
445 .with_ingredient(Ingredient::basic("magic_dust", 8))
446 .with_result(CraftResult::guaranteed("fire_steel_sword", 1, 120))
447 .with_result(CraftResult::new("ember_steel_sword", 1, 140, 0.10))
448 .with_level(25)
449 .with_tool("enchanting_focus")
450 .with_duration(45.0)
451 .with_experience(100),
452 );
453 book.register(
454 Recipe::new("enchant_ward_armor", "Ward Armor Enchant", RecipeCategory::Enchanting)
455 .with_ingredient(Ingredient::basic("iron_helmet", 1))
456 .with_ingredient(Ingredient::basic("light_rune", 2))
457 .with_ingredient(Ingredient::basic("magic_dust", 6))
458 .with_result(CraftResult::guaranteed("warded_iron_helmet", 1, 95))
459 .with_level(20)
460 .with_tool("enchanting_focus")
461 .with_duration(40.0)
462 .with_experience(80),
463 );
464
465 book.register(
467 Recipe::new("gear_small", "Small Gear", RecipeCategory::Engineering)
468 .with_ingredient(Ingredient::basic("iron_ingot", 1))
469 .with_result(CraftResult::guaranteed("gear_small", 3, 70))
470 .with_level(3)
471 .with_tool("wrench")
472 .with_duration(4.0)
473 .with_experience(8),
474 );
475 book.register(
476 Recipe::new("clockwork_device", "Clockwork Device", RecipeCategory::Engineering)
477 .with_ingredient(Ingredient::basic("gear_small", 6))
478 .with_ingredient(Ingredient::basic("copper_wire", 4))
479 .with_ingredient(Ingredient::basic("spring_coil", 2))
480 .with_result(CraftResult::guaranteed("clockwork_device", 1, 85))
481 .with_level(20)
482 .with_tool("wrench")
483 .with_duration(25.0)
484 .with_experience(55),
485 );
486 book.register(
487 Recipe::new("bomb_smoke", "Smoke Bomb", RecipeCategory::Engineering)
488 .with_ingredient(Ingredient::basic("charcoal_powder", 2))
489 .with_ingredient(Ingredient::basic("sulfur", 1))
490 .with_ingredient(Ingredient::basic("cloth_scrap", 1))
491 .with_result(CraftResult::guaranteed("bomb_smoke", 2, 70))
492 .with_level(15)
493 .with_duration(8.0)
494 .with_experience(22),
495 );
496
497 book.register(
499 Recipe::new("cloth_tunic", "Cloth Tunic", RecipeCategory::Tailoring)
500 .with_ingredient(Ingredient::basic("cloth_bolt", 3))
501 .with_ingredient(Ingredient::basic("thread_spool", 2))
502 .with_result(CraftResult::guaranteed("cloth_tunic", 1, 60))
503 .with_level(1)
504 .with_tool("sewing_needle")
505 .with_duration(8.0)
506 .with_experience(10),
507 );
508 book.register(
509 Recipe::new("leather_armor", "Leather Armor", RecipeCategory::Tailoring)
510 .with_ingredient(Ingredient::basic("tanned_hide", 4))
511 .with_ingredient(Ingredient::basic("thread_spool", 2))
512 .with_ingredient(Ingredient::basic("iron_buckle", 2))
513 .with_result(CraftResult::guaranteed("leather_armor", 1, 75))
514 .with_level(10)
515 .with_tool("sewing_needle")
516 .with_duration(15.0)
517 .with_experience(30),
518 );
519 book.register(
520 Recipe::new("silk_robe", "Silk Robe", RecipeCategory::Tailoring)
521 .with_ingredient(Ingredient::new("silk_bolt", 5, 80))
522 .with_ingredient(Ingredient::basic("thread_spool", 3))
523 .with_ingredient(Ingredient::basic("gem_dust", 1))
524 .with_result(CraftResult::guaranteed("silk_robe", 1, 100))
525 .with_level(22)
526 .with_tool("sewing_needle")
527 .with_duration(22.0)
528 .with_experience(55),
529 );
530
531 book.register(
533 Recipe::new("wooden_bow", "Wooden Bow", RecipeCategory::Woodworking)
534 .with_ingredient(Ingredient::basic("flexible_branch", 2))
535 .with_ingredient(Ingredient::basic("sinew_string", 1))
536 .with_result(CraftResult::guaranteed("wooden_bow", 1, 65))
537 .with_level(5)
538 .with_tool("carving_knife")
539 .with_duration(10.0)
540 .with_experience(18),
541 );
542 book.register(
543 Recipe::new("wooden_staff", "Wooden Staff", RecipeCategory::Woodworking)
544 .with_ingredient(Ingredient::basic("hardwood_log", 1))
545 .with_result(CraftResult::guaranteed("wooden_staff", 1, 70))
546 .with_level(3)
547 .with_tool("carving_knife")
548 .with_duration(7.0)
549 .with_experience(12),
550 );
551 book.register(
552 Recipe::new("arrow_bundle", "Arrow Bundle", RecipeCategory::Woodworking)
553 .with_ingredient(Ingredient::basic("straight_stick", 10))
554 .with_ingredient(Ingredient::basic("feather", 10))
555 .with_ingredient(Ingredient::basic("iron_tip", 10))
556 .with_result(CraftResult::guaranteed("arrow", 10, 60))
557 .with_level(2)
558 .with_duration(12.0)
559 .with_experience(14),
560 );
561
562 book.register(
564 Recipe::new("silver_ring", "Silver Ring", RecipeCategory::Jeweling)
565 .with_ingredient(Ingredient::basic("silver_ingot", 1))
566 .with_result(CraftResult::guaranteed("silver_ring", 1, 70))
567 .with_level(5)
568 .with_tool("jeweler_loupe")
569 .with_duration(10.0)
570 .with_experience(20),
571 );
572 book.register(
573 Recipe::new("ruby_necklace", "Ruby Necklace", RecipeCategory::Jeweling)
574 .with_ingredient(Ingredient::basic("gold_chain", 1))
575 .with_ingredient(Ingredient::new("ruby_gem", 1, 100))
576 .with_result(CraftResult::guaranteed("ruby_necklace", 1, 120))
577 .with_result(CraftResult::new("radiant_ruby_necklace", 1, 180, 0.08))
578 .with_level(20)
579 .with_tool("jeweler_loupe")
580 .with_duration(25.0)
581 .with_experience(70),
582 );
583 book.register(
584 Recipe::new("sapphire_amulet", "Sapphire Amulet of Clarity", RecipeCategory::Jeweling)
585 .with_ingredient(Ingredient::basic("mithril_chain", 1))
586 .with_ingredient(Ingredient::new("sapphire_gem", 2, 110))
587 .with_ingredient(Ingredient::basic("magic_dust", 4))
588 .with_result(CraftResult::guaranteed("sapphire_amulet", 1, 140))
589 .with_level(30)
590 .with_tool("jeweler_loupe")
591 .with_duration(35.0)
592 .with_experience(110),
593 );
594
595 book
596 }
597
598 pub fn find_by_ingredients(&self, ingredient_ids: &[String]) -> Option<&Recipe> {
600 let mut sorted_input = ingredient_ids.to_vec();
601 sorted_input.sort();
602 for recipe in self.recipes.values() {
603 let mut sorted_recipe: Vec<String> =
604 recipe.ingredients.iter().map(|i| i.item_id.clone()).collect();
605 sorted_recipe.sort();
606 if sorted_recipe == sorted_input {
607 return Some(recipe);
608 }
609 }
610 None
611 }
612}
613
614impl Default for RecipeBook {
615 fn default() -> Self {
616 Self::new()
617 }
618}
619
620#[derive(Debug, Clone)]
626pub struct CraftingCalculator;
627
628impl CraftingCalculator {
629 pub fn calculate_quality(base: u8, skill_level: u32, tool_bonus: u32) -> u8 {
634 let skill_bonus = ((skill_level as f32 * 0.4) as u32).min(80);
635 let raw = base as u32 + skill_bonus + tool_bonus;
636 raw.min(255) as u8
637 }
638
639 pub fn calculate_quantity(base: u32, skill_level: u32, luck: f32) -> u32 {
644 let t = ((skill_level as f32 / 100.0) + luck * 0.2).clamp(0.0, 1.0);
646 let a = base as f32;
647 let b = (base * 2) as f32;
648 let result = a + t * (b - a);
650 result.round() as u32
651 }
652
653 pub fn calculate_duration(base: f32, skill_level: u32, tool_speed: f32) -> f32 {
657 let skill_reduction = (skill_level as f32 * 0.005).min(0.5);
658 let tool_reduction = tool_speed.clamp(0.0, 0.4);
659 let total_reduction = (skill_reduction + tool_reduction).min(0.9);
660 let t = 1.0 - total_reduction;
661 let a = base;
664 let b = base * 0.1;
665 let result = a + (1.0 - t) * (b - a); result.max(0.5)
667 }
668
669 pub fn evaluate_chance(result: &CraftResult, skill_level: u32, rng: f32) -> bool {
671 let bonus = (skill_level as f32 * 0.001).min(0.1);
672 let effective_chance = (result.chance + bonus).min(1.0);
673 rng <= effective_chance
674 }
675
676 pub fn calculate_experience(base_xp: u32, quality: u8) -> u32 {
678 let quality_factor = 1.0 + (quality as f32 - 80.0).max(0.0) * 0.005;
679 ((base_xp as f32) * quality_factor).round() as u32
680 }
681}
682
683#[derive(Debug, Clone)]
689pub struct CategoryMastery {
690 pub category: RecipeCategory,
691 pub current_xp: u32,
692 pub level: u32,
693 pub unlocked_recipes: Vec<String>,
695}
696
697impl CategoryMastery {
698 pub fn new(category: RecipeCategory) -> Self {
699 Self {
700 category,
701 current_xp: 0,
702 level: 1,
703 unlocked_recipes: Vec::new(),
704 }
705 }
706
707 pub fn xp_to_next_level(&self) -> u32 {
709 (self.level * self.level * 100).max(100)
710 }
711
712 pub fn add_xp(&mut self, xp: u32) -> bool {
714 self.current_xp += xp;
715 let threshold = self.xp_to_next_level();
716 if self.current_xp >= threshold {
717 self.current_xp -= threshold;
718 self.level += 1;
719 return true;
720 }
721 false
722 }
723}
724
725#[derive(Debug, Clone)]
727pub struct MasterySystem {
728 masteries: HashMap<String, CategoryMastery>,
729 bonus_recipes: Vec<(String, u32, String)>,
731}
732
733impl MasterySystem {
734 pub fn new() -> Self {
735 let mut s = Self {
736 masteries: HashMap::new(),
737 bonus_recipes: Vec::new(),
738 };
739 for cat in Self::all_categories() {
741 let label = cat.label().to_string();
742 s.masteries.insert(label, CategoryMastery::new(cat));
743 }
744 s.bonus_recipes = vec![
746 ("Smithing".into(), 10, "iron_sword".into()),
747 ("Smithing".into(), 25, "steel_sword".into()),
748 ("Alchemy".into(), 5, "health_potion_major".into()),
749 ("Alchemy".into(), 20, "poison_vial".into()),
750 ("Enchanting".into(), 15, "enchant_sharpness_i".into()),
751 ("Jeweling".into(), 10, "silver_ring".into()),
752 ];
753 s
754 }
755
756 fn all_categories() -> Vec<RecipeCategory> {
757 vec![
758 RecipeCategory::Smithing,
759 RecipeCategory::Alchemy,
760 RecipeCategory::Cooking,
761 RecipeCategory::Enchanting,
762 RecipeCategory::Engineering,
763 RecipeCategory::Tailoring,
764 RecipeCategory::Woodworking,
765 RecipeCategory::Jeweling,
766 ]
767 }
768
769 pub fn award_xp(&mut self, category: &RecipeCategory, xp: u32) -> Vec<String> {
771 let key = category.label().to_string();
772 let mut newly_unlocked = Vec::new();
773 if let Some(mastery) = self.masteries.get_mut(&key) {
774 let leveled_up = mastery.add_xp(xp);
775 if leveled_up {
776 let new_level = mastery.level;
777 let unlocks: Vec<String> = self.bonus_recipes.iter()
779 .filter(|(cat, thresh, _)| cat == &key && *thresh == new_level)
780 .map(|(_, _, recipe_id)| recipe_id.clone())
781 .collect();
782 for recipe_id in &unlocks {
783 if let Some(m) = self.masteries.get_mut(&key) {
784 if !m.unlocked_recipes.contains(recipe_id) {
785 m.unlocked_recipes.push(recipe_id.clone());
786 }
787 }
788 }
789 newly_unlocked = unlocks;
790 }
791 }
792 newly_unlocked
793 }
794
795 pub fn get_mastery(&self, category: &RecipeCategory) -> Option<&CategoryMastery> {
797 self.masteries.get(category.label())
798 }
799
800 pub fn level(&self, category: &RecipeCategory) -> u32 {
802 self.masteries
803 .get(category.label())
804 .map(|m| m.level)
805 .unwrap_or(1)
806 }
807
808 pub fn unlocked_recipes(&self, category: &RecipeCategory) -> Vec<String> {
810 self.masteries
811 .get(category.label())
812 .map(|m| m.unlocked_recipes.clone())
813 .unwrap_or_default()
814 }
815}
816
817impl Default for MasterySystem {
818 fn default() -> Self {
819 Self::new()
820 }
821}
822
823#[derive(Debug, Clone)]
829pub struct RecipeDiscovery {
830 attempted_combinations: Vec<Vec<String>>,
832 discovered_recipe_ids: Vec<String>,
834}
835
836impl RecipeDiscovery {
837 pub fn new() -> Self {
838 Self {
839 attempted_combinations: Vec::new(),
840 discovered_recipe_ids: Vec::new(),
841 }
842 }
843
844 pub fn attempt_discovery(
849 &mut self,
850 ingredient_ids: &[String],
851 recipe_book: &RecipeBook,
852 rng: f32,
853 ) -> Option<String> {
854 let mut sorted = ingredient_ids.to_vec();
855 sorted.sort();
856
857 if self.attempted_combinations.contains(&sorted) {
859 return None;
860 }
861 self.attempted_combinations.push(sorted.clone());
862
863 let recipe = recipe_book.find_by_ingredients(ingredient_ids)?;
865
866 if self.discovered_recipe_ids.contains(&recipe.id) {
868 return None;
869 }
870
871 if recipe.discovery_chance <= 0.0 {
873 return None;
874 }
875
876 if rng <= recipe.discovery_chance {
878 self.discovered_recipe_ids.push(recipe.id.clone());
879 return Some(recipe.id.clone());
880 }
881
882 None
883 }
884
885 pub fn is_discovered(&self, recipe_id: &str) -> bool {
887 self.discovered_recipe_ids.iter().any(|id| id == recipe_id)
888 }
889
890 pub fn learn_recipe(&mut self, recipe_id: String) {
892 if !self.discovered_recipe_ids.contains(&recipe_id) {
893 self.discovered_recipe_ids.push(recipe_id);
894 }
895 }
896
897 pub fn all_discovered(&self) -> &[String] {
899 &self.discovered_recipe_ids
900 }
901}
902
903impl Default for RecipeDiscovery {
904 fn default() -> Self {
905 Self::new()
906 }
907}