proof-engine 0.2.1

Real-time graphics from math: glyphs and particles moved by ODEs, strange attractors and force fields, drawn with HDR bloom on OpenGL.
Documentation

// ── Inventory Management ──────────────────────────────────────────────────────

#[derive(Clone, Debug)]
pub struct InventorySlot {
    pub slot_index: u32,
    pub item_id: Option<u32>,
    pub quantity: u32,
    pub locked: bool,
}

impl InventorySlot {
    pub fn empty(index: u32) -> Self { Self { slot_index: index, item_id: None, quantity: 0, locked: false } }
    pub fn filled(index: u32, item_id: u32, qty: u32) -> Self { Self { slot_index: index, item_id: Some(item_id), quantity: qty, locked: false } }
    pub fn is_empty(&self) -> bool { self.item_id.is_none() || self.quantity == 0 }
    pub fn clear(&mut self) { self.item_id = None; self.quantity = 0; }
    pub fn lock(&mut self) { self.locked = true; }
    pub fn unlock(&mut self) { self.locked = false; }
}

#[derive(Clone, Debug)]
pub struct Inventory {
    pub slots: Vec<InventorySlot>,
    pub capacity: u32,
    pub owner_id: u32,
    pub bag_type: String,
}

impl Inventory {
    pub fn new(owner_id: u32, capacity: u32) -> Self {
        Self { slots: (0..capacity).map(InventorySlot::empty).collect(), capacity, owner_id, bag_type: "standard".into() }
    }
    pub fn free_slots(&self) -> usize { self.slots.iter().filter(|s| s.is_empty()).count() }
    pub fn used_slots(&self) -> usize { self.slots.iter().filter(|s| !s.is_empty()).count() }
    pub fn is_full(&self) -> bool { self.free_slots() == 0 }
    pub fn find_item(&self, item_id: u32) -> Option<&InventorySlot> { self.slots.iter().find(|s| s.item_id == Some(item_id)) }
    pub fn add_item(&mut self, item_id: u32, qty: u32) -> bool {
        if let Some(slot) = self.slots.iter_mut().find(|s| s.is_empty() && !s.locked) {
            slot.item_id = Some(item_id); slot.quantity = qty; true
        } else { false }
    }
    pub fn remove_item(&mut self, item_id: u32) -> bool {
        if let Some(slot) = self.slots.iter_mut().find(|s| s.item_id == Some(item_id)) {
            slot.clear(); true
        } else { false }
    }
    pub fn item_count(&self, item_id: u32) -> u32 {
        self.slots.iter().filter(|s| s.item_id == Some(item_id)).map(|s| s.quantity).sum()
    }
    pub fn all_items(&self) -> Vec<(u32, u32)> {
        self.slots.iter().filter_map(|s| s.item_id.map(|id| (id, s.quantity))).collect()
    }
    pub fn sort_by_id(&mut self) {
        self.slots.sort_by_key(|s| s.item_id.unwrap_or(u32::MAX));
    }
    pub fn fill_percent(&self) -> f32 {
        if self.capacity == 0 { return 0.0; }
        self.used_slots() as f32 / self.capacity as f32 * 100.0
    }
}

#[derive(Clone, Debug)]
pub struct InventorySystem {
    pub bags: HashMap<u32, Inventory>,
    pub next_owner: u32,
}

impl InventorySystem {
    pub fn new() -> Self { Self { bags: HashMap::new(), next_owner: 1 } }
    pub fn create_inventory(&mut self, capacity: u32) -> u32 {
        let id = self.next_owner; self.next_owner += 1;
        self.bags.insert(id, Inventory::new(id, capacity));
        id
    }
    pub fn get(&self, owner_id: u32) -> Option<&Inventory> { self.bags.get(&owner_id) }
    pub fn get_mut(&mut self, owner_id: u32) -> Option<&mut Inventory> { self.bags.get_mut(&owner_id) }
    pub fn transfer(&mut self, from: u32, to: u32, item_id: u32) -> bool {
        let qty = self.bags.get(&from).and_then(|b| b.find_item(item_id)).map(|s| s.quantity).unwrap_or(0);
        if qty == 0 { return false; }
        if self.bags.get(&to).map(|b| b.is_full()).unwrap_or(true) { return false; }
        if let Some(b) = self.bags.get_mut(&from) { b.remove_item(item_id); }
        if let Some(b) = self.bags.get_mut(&to) { b.add_item(item_id, qty) } else { false }
    }
    pub fn total_inventories(&self) -> usize { self.bags.len() }
}

impl Default for InventorySystem {
    fn default() -> Self { Self::new() }
}

// ── Merchant NPC System ───────────────────────────────────────────────────────

#[derive(Clone, Debug)]
pub struct MerchantInventory {
    pub merchant_id: u32,
    pub merchant_name: String,
    pub items: Vec<(u32, u32, u32)>,
    pub restock_interval_secs: f32,
    pub last_restock: f32,
    pub accepts_currency: Vec<u32>,
    pub markup: f32,
    pub discount_for_faction: HashMap<String, f32>,
}

impl MerchantInventory {
    pub fn new(merchant_id: u32, name: impl Into<String>) -> Self {
        Self { merchant_id, merchant_name: name.into(), items: Vec::new(), restock_interval_secs: 3600.0, last_restock: 0.0, accepts_currency: vec![0], markup: 1.2, discount_for_faction: HashMap::new() }
    }
    pub fn add_item(&mut self, item_id: u32, price: u32, stock: u32) {
        self.items.push((item_id, price, stock));
    }
    pub fn buy_item(&mut self, item_id: u32) -> Option<u32> {
        if let Some(entry) = self.items.iter_mut().find(|(id, _, stock)| *id == item_id && *stock > 0) {
            entry.2 -= 1;
            Some(entry.1)
        } else { None }
    }
    pub fn effective_price(&self, base: u32, faction: &str) -> u32 {
        let discount = self.discount_for_faction.get(faction).copied().unwrap_or(1.0);
        (base as f32 * self.markup * discount) as u32
    }
    pub fn needs_restock(&self, time: f32) -> bool { time - self.last_restock >= self.restock_interval_secs }
    pub fn restock(&mut self, time: f32) { self.last_restock = time; }
    pub fn in_stock_count(&self) -> usize { self.items.iter().filter(|(_, _, s)| *s > 0).count() }
    pub fn add_faction_discount(&mut self, faction: impl Into<String>, discount: f32) { self.discount_for_faction.insert(faction.into(), discount); }
}

// ── Final system info ─────────────────────────────────────────────────────────

pub const LOOT_INVENTORY_DEFAULT_SLOTS: u32 = 40;
pub const LOOT_MERCHANT_MAX_ITEMS: usize = 64;
pub const LOOT_MERCHANT_MAX_MARKUP: f32 = 3.0;
pub const LOOT_MERCHANT_MIN_MARKUP: f32 = 0.5;
pub const LOOT_TRANSFER_COOLDOWN_SECS: f32 = 0.1;

pub fn loot_editor_info() -> String {
    format!("LootEditor — {} total subsystems, full drop pipeline from zone to player wallet", total_loot_modules())
}