Skip to main content

proof_engine/crafting/
mod.rs

1// crafting/mod.rs — Crafting and economy system module
2
3//! # Crafting & Economy System
4//!
5//! A full crafting, workbench, economy, and market module for the Proof Engine.
6//!
7//! ## Sub-modules
8//!
9//! - [`recipes`] — Recipe definitions, ingredient/result types, quality tiers,
10//!   crafting calculator, mastery system, and recipe discovery.
11//! - [`workbench`] — Crafting stations, job queues, fuel systems, and auto-crafters.
12//! - [`economy`] — Currency, shops, supply/demand simulation, trade offers, tax.
13//! - [`market`] — Auction house, bidding, buyouts, market history, and peer trade windows.
14//!
15//! ## Quick Start
16//!
17//! ```rust,no_run
18//! use proof_engine::crafting::{
19//!     RecipeBook, Economy, AuctionHouse, Workbench, WorkbenchType, WorkbenchTier,
20//!     Currency,
21//! };
22//! use glam::Vec3;
23//!
24//! // Load default recipes
25//! let book = RecipeBook::default_recipes();
26//!
27//! // Query smithing recipes available at skill level 10
28//! let available = book.available_for_level_and_category(
29//!     10,
30//!     &proof_engine::crafting::recipes::RecipeCategory::Smithing,
31//! );
32//!
33//! // Create a forge workbench
34//! let mut bench = Workbench::new(1, Vec3::ZERO, WorkbenchType::Forge, WorkbenchTier::Basic);
35//!
36//! // Initialise the global economy
37//! let mut economy = Economy::default();
38//!
39//! // Set up an auction house
40//! let mut ah = AuctionHouse::new();
41//! ```
42
43pub mod recipes;
44pub mod workbench;
45pub mod economy;
46pub mod market;
47
48// ---------------------------------------------------------------------------
49// Flat re-exports — bring the most commonly used types to crafting::*
50// ---------------------------------------------------------------------------
51
52// recipes
53pub use recipes::{
54    Recipe,
55    RecipeBook,
56    RecipeCategory,
57    Ingredient,
58    CraftResult,
59    CraftingCalculator,
60    MasterySystem,
61    CategoryMastery,
62    RecipeDiscovery,
63    QualityTier,
64};
65
66// workbench
67pub 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
82// economy
83pub use economy::{
84    Economy,
85    Currency,
86    PriceModifier,
87    PlayerReputation,
88    ShopItem,
89    ShopInventory,
90    TradeOffer,
91    TaxSystem,
92};
93
94// market
95pub use market::{
96    AuctionHouse,
97    Listing,
98    Bid,
99    MarketHistory,
100    MarketBoard,
101    TradeWindow,
102    MailMessage,
103};
104
105// ---------------------------------------------------------------------------
106// System-level helpers
107// ---------------------------------------------------------------------------
108
109/// A single crafting session context bundling all subsystems.
110#[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    /// Create a new context with default recipes and a populated economy.
121    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    /// Advance all time-dependent systems by `dt` seconds.
132    ///
133    /// `current_time` is the absolute game time in seconds.
134    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    /// Award crafting XP in a category and return any newly unlocked recipe IDs.
140    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    /// Attempt to discover a recipe from a set of ingredient ids.
149    ///
150    /// `rng` should be a value in [0.0, 1.0).
151    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    /// Look up a recipe by id.
160    pub fn get_recipe(&self, id: &str) -> Option<&recipes::Recipe> {
161        self.recipe_book.get(id)
162    }
163
164    /// Get the current market price for an item (in copper), or None if not tracked.
165    pub fn market_price(&self, item_id: &str) -> Option<u64> {
166        self.economy.current_price(item_id)
167    }
168
169    /// Get the current market price as a `Currency` value.
170    pub fn market_currency(&self, item_id: &str) -> Option<currency::CurrencyAlias> {
171        self.economy.current_price_currency(item_id)
172    }
173}
174
175// Alias to avoid the "currency" sub-module naming clash in CraftingContext::market_currency.
176mod 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// ---------------------------------------------------------------------------
187// Crafting result summary (returned to callers after a craft completes)
188// ---------------------------------------------------------------------------
189
190/// A resolved crafting outcome with final computed values.
191#[derive(Debug, Clone)]
192pub struct CraftOutcome {
193    pub recipe_id: String,
194    /// Produced items: (item_id, quantity, quality_byte).
195    pub items_produced: Vec<(String, u32, u8)>,
196    /// XP awarded.
197    pub experience_gained: u32,
198    /// Whether any Legendary quality item was produced.
199    pub legendary_proc: bool,
200    /// Duration the craft actually took (seconds).
201    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    /// Total items produced across all result slots.
234    pub fn total_items(&self) -> u32 {
235        self.items_produced.iter().map(|(_, qty, _)| qty).sum()
236    }
237
238    /// Best quality tier produced.
239    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// ---------------------------------------------------------------------------
248// Crafting error types
249// ---------------------------------------------------------------------------
250
251/// Reasons a crafting attempt can fail.
252#[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
287// ---------------------------------------------------------------------------
288// Convenience validation helper
289// ---------------------------------------------------------------------------
290
291/// Validate whether a crafting attempt can begin given current game state.
292///
293/// Returns `Ok(())` if all preconditions are met, or a `CraftingError` explaining
294/// the first failure.
295pub 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    // Skill check
308    if player_skill < recipe.required_level {
309        return Err(CraftingError::InsufficientSkill {
310            required: recipe.required_level,
311            actual: player_skill,
312        });
313    }
314
315    // Ingredient check
316    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    // Tool check
328    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    // Bench type check: derive expected type from category
335    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
348/// Map a RecipeCategory to the WorkbenchType it requires (if any).
349pub 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}