1pub mod recipes;
44pub mod workbench;
45pub mod economy;
46pub mod market;
47
48pub use recipes::{
54 Recipe,
55 RecipeBook,
56 RecipeCategory,
57 Ingredient,
58 CraftResult,
59 CraftingCalculator,
60 MasterySystem,
61 CategoryMastery,
62 RecipeDiscovery,
63 QualityTier,
64};
65
66pub use workbench::{
68 Workbench,
69 WorkbenchType,
70 WorkbenchTier,
71 WorkbenchState,
72 CraftingQueue,
73 CraftingJob,
74 WorkbenchEvent,
75 FuelType,
76 FuelSystem,
77 CraftingStation,
78 AutoCrafter,
79 AutoCraftConfig,
80};
81
82pub use economy::{
84 Economy,
85 Currency,
86 PriceModifier,
87 PlayerReputation,
88 ShopItem,
89 ShopInventory,
90 TradeOffer,
91 TaxSystem,
92};
93
94pub use market::{
96 AuctionHouse,
97 Listing,
98 Bid,
99 MarketHistory,
100 MarketBoard,
101 TradeWindow,
102 MailMessage,
103};
104
105#[derive(Debug, Clone)]
111pub struct CraftingContext {
112 pub recipe_book: recipes::RecipeBook,
113 pub mastery: recipes::MasterySystem,
114 pub discovery: recipes::RecipeDiscovery,
115 pub economy: economy::Economy,
116 pub market: market::MarketBoard,
117}
118
119impl CraftingContext {
120 pub fn new() -> Self {
122 Self {
123 recipe_book: recipes::RecipeBook::default_recipes(),
124 mastery: recipes::MasterySystem::new(),
125 discovery: recipes::RecipeDiscovery::new(),
126 economy: economy::Economy::default(),
127 market: market::MarketBoard::new(),
128 }
129 }
130
131 pub fn tick(&mut self, dt: f32, current_time: f32) {
135 self.economy.update_prices(dt);
136 self.market.tick(current_time);
137 }
138
139 pub fn award_crafting_xp(
141 &mut self,
142 category: &recipes::RecipeCategory,
143 xp: u32,
144 ) -> Vec<String> {
145 self.mastery.award_xp(category, xp)
146 }
147
148 pub fn attempt_discovery(
152 &mut self,
153 ingredient_ids: &[String],
154 rng: f32,
155 ) -> Option<String> {
156 self.discovery.attempt_discovery(ingredient_ids, &self.recipe_book, rng)
157 }
158
159 pub fn get_recipe(&self, id: &str) -> Option<&recipes::Recipe> {
161 self.recipe_book.get(id)
162 }
163
164 pub fn market_price(&self, item_id: &str) -> Option<u64> {
166 self.economy.current_price(item_id)
167 }
168
169 pub fn market_currency(&self, item_id: &str) -> Option<currency::CurrencyAlias> {
171 self.economy.current_price_currency(item_id)
172 }
173}
174
175mod currency {
177 pub type CurrencyAlias = crate::crafting::economy::Currency;
178}
179
180impl Default for CraftingContext {
181 fn default() -> Self {
182 Self::new()
183 }
184}
185
186#[derive(Debug, Clone)]
192pub struct CraftOutcome {
193 pub recipe_id: String,
194 pub items_produced: Vec<(String, u32, u8)>,
196 pub experience_gained: u32,
198 pub legendary_proc: bool,
200 pub actual_duration: f32,
202}
203
204impl CraftOutcome {
205 pub fn new(recipe_id: impl Into<String>) -> Self {
206 Self {
207 recipe_id: recipe_id.into(),
208 items_produced: Vec::new(),
209 experience_gained: 0,
210 legendary_proc: false,
211 actual_duration: 0.0,
212 }
213 }
214
215 pub fn with_item(mut self, item_id: impl Into<String>, quantity: u32, quality: u8) -> Self {
216 if quality >= recipes::QualityTier::Legendary.threshold() {
217 self.legendary_proc = true;
218 }
219 self.items_produced.push((item_id.into(), quantity, quality));
220 self
221 }
222
223 pub fn with_xp(mut self, xp: u32) -> Self {
224 self.experience_gained = xp;
225 self
226 }
227
228 pub fn with_duration(mut self, secs: f32) -> Self {
229 self.actual_duration = secs;
230 self
231 }
232
233 pub fn total_items(&self) -> u32 {
235 self.items_produced.iter().map(|(_, qty, _)| qty).sum()
236 }
237
238 pub fn best_quality_tier(&self) -> Option<recipes::QualityTier> {
240 self.items_produced
241 .iter()
242 .map(|(_, _, q)| recipes::QualityTier::from_value(*q))
243 .max()
244 }
245}
246
247#[derive(Debug, Clone)]
253pub enum CraftingError {
254 RecipeNotFound { recipe_id: String },
255 InsufficientSkill { required: u32, actual: u32 },
256 MissingIngredient { item_id: String, required: u32, available: u32 },
257 MissingTool { tool_id: String },
258 WorkbenchWrongType { expected: WorkbenchType, actual: WorkbenchType },
259 WorkbenchBroken { repair_cost: u64 },
260 QueueFull,
261 InsufficientFuel,
262}
263
264impl std::fmt::Display for CraftingError {
265 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
266 match self {
267 CraftingError::RecipeNotFound { recipe_id } =>
268 write!(f, "Recipe not found: {}", recipe_id),
269 CraftingError::InsufficientSkill { required, actual } =>
270 write!(f, "Skill too low: need {}, have {}", required, actual),
271 CraftingError::MissingIngredient { item_id, required, available } =>
272 write!(f, "Missing {}: need {}, have {}", item_id, required, available),
273 CraftingError::MissingTool { tool_id } =>
274 write!(f, "Missing required tool: {}", tool_id),
275 CraftingError::WorkbenchWrongType { expected, actual } =>
276 write!(f, "Wrong workbench: need {:?}, have {:?}", expected, actual),
277 CraftingError::WorkbenchBroken { repair_cost } =>
278 write!(f, "Workbench broken (repair cost: {} copper)", repair_cost),
279 CraftingError::QueueFull =>
280 write!(f, "Crafting queue is full"),
281 CraftingError::InsufficientFuel =>
282 write!(f, "Workbench has no fuel"),
283 }
284 }
285}
286
287pub fn validate_craft(
296 recipe_id: &str,
297 player_skill: u32,
298 player_inventory: &std::collections::HashMap<String, u32>,
299 player_tools: &[String],
300 bench_type: &WorkbenchType,
301 recipe_book: &recipes::RecipeBook,
302) -> Result<(), CraftingError> {
303 let recipe = recipe_book.get(recipe_id).ok_or_else(|| {
304 CraftingError::RecipeNotFound { recipe_id: recipe_id.to_string() }
305 })?;
306
307 if player_skill < recipe.required_level {
309 return Err(CraftingError::InsufficientSkill {
310 required: recipe.required_level,
311 actual: player_skill,
312 });
313 }
314
315 for ing in &recipe.ingredients {
317 let available = player_inventory.get(&ing.item_id).copied().unwrap_or(0);
318 if available < ing.quantity {
319 return Err(CraftingError::MissingIngredient {
320 item_id: ing.item_id.clone(),
321 required: ing.quantity,
322 available,
323 });
324 }
325 }
326
327 for tool in &recipe.required_tools {
329 if !player_tools.contains(tool) {
330 return Err(CraftingError::MissingTool { tool_id: tool.clone() });
331 }
332 }
333
334 let expected_bench = category_to_bench_type(&recipe.category);
336 if let Some(expected) = expected_bench {
337 if bench_type != &expected {
338 return Err(CraftingError::WorkbenchWrongType {
339 expected,
340 actual: bench_type.clone(),
341 });
342 }
343 }
344
345 Ok(())
346}
347
348pub fn category_to_bench_type(category: &recipes::RecipeCategory) -> Option<WorkbenchType> {
350 match category {
351 recipes::RecipeCategory::Smithing => Some(WorkbenchType::Forge),
352 recipes::RecipeCategory::Alchemy => Some(WorkbenchType::AlchemyTable),
353 recipes::RecipeCategory::Cooking => Some(WorkbenchType::CookingPot),
354 recipes::RecipeCategory::Enchanting => Some(WorkbenchType::EnchantingTable),
355 recipes::RecipeCategory::Engineering => Some(WorkbenchType::Workbench),
356 recipes::RecipeCategory::Tailoring => Some(WorkbenchType::Loom),
357 recipes::RecipeCategory::Woodworking => Some(WorkbenchType::Workbench),
358 recipes::RecipeCategory::Jeweling => Some(WorkbenchType::Jeweler),
359 }
360}