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

// ── Extended Loot Analytics ───────────────────────────────────────────────────

#[derive(Clone, Debug)]
pub struct LootAnalyticsEvent {
    pub event_type: String,
    pub item_id: u32,
    pub player_id: u32,
    pub zone_id: u32,
    pub timestamp: u64,
    pub value: f32,
    pub metadata: HashMap<String, String>,
}

impl LootAnalyticsEvent {
    pub fn drop_event(item_id: u32, player_id: u32, zone_id: u32, value: f32) -> Self {
        Self { event_type: "drop".into(), item_id, player_id, zone_id, timestamp: 0, value, metadata: HashMap::new() }
    }
    pub fn purchase_event(item_id: u32, player_id: u32, price: f32) -> Self {
        Self { event_type: "purchase".into(), item_id, player_id, zone_id: 0, timestamp: 0, value: price, metadata: HashMap::new() }
    }
    pub fn salvage_event(item_id: u32, player_id: u32, recovered_value: f32) -> Self {
        Self { event_type: "salvage".into(), item_id, player_id, zone_id: 0, timestamp: 0, value: recovered_value, metadata: HashMap::new() }
    }
    pub fn add_meta(&mut self, key: impl Into<String>, val: impl Into<String>) { self.metadata.insert(key.into(), val.into()); }
}

#[derive(Clone, Debug)]
pub struct LootAnalyticsPipeline {
    pub events: VecDeque<LootAnalyticsEvent>,
    pub max_events: usize,
    pub total_processed: u64,
    pub filters: Vec<String>,
}

impl LootAnalyticsPipeline {
    pub fn new(max_events: usize) -> Self { Self { events: VecDeque::new(), max_events, total_processed: 0, filters: Vec::new() } }
    pub fn push(&mut self, ev: LootAnalyticsEvent) {
        if self.events.len() >= self.max_events { self.events.pop_front(); }
        self.events.push_back(ev);
        self.total_processed += 1;
    }
    pub fn events_of_type(&self, event_type: &str) -> Vec<&LootAnalyticsEvent> {
        self.events.iter().filter(|e| e.event_type == event_type).collect()
    }
    pub fn total_value_by_type(&self, event_type: &str) -> f32 {
        self.events_of_type(event_type).iter().map(|e| e.value).sum()
    }
    pub fn unique_items_seen(&self) -> usize {
        self.events.iter().map(|e| e.item_id).collect::<HashSet<_>>().len()
    }
    pub fn events_in_zone(&self, zone_id: u32) -> Vec<&LootAnalyticsEvent> {
        self.events.iter().filter(|e| e.zone_id == zone_id).collect()
    }
    pub fn add_filter(&mut self, f: impl Into<String>) { self.filters.push(f.into()); }
    pub fn clear(&mut self) { self.events.clear(); }
    pub fn count(&self) -> usize { self.events.len() }
}

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

// ── Item Salvage System ───────────────────────────────────────────────────────

#[derive(Clone, Debug)]
pub struct SalvageResult {
    pub source_item_id: u32,
    pub materials: Vec<(u32, u32)>,
    pub gold_recovered: u32,
    pub essence_recovered: u32,
}

#[derive(Clone, Debug)]
pub struct SalvageTable {
    pub table_id: u32,
    pub rarity: ItemRarity,
    pub material_rolls: Vec<(u32, f32, u32, u32)>,
    pub gold_percent_of_value: f32,
    pub essence_per_tier: u32,
}

impl SalvageTable {
    pub fn new(table_id: u32, rarity: ItemRarity) -> Self {
        Self { table_id, rarity, material_rolls: Vec::new(), gold_percent_of_value: 0.1, essence_per_tier: 1 }
    }
    pub fn add_material_roll(&mut self, material_id: u32, chance: f32, min_qty: u32, max_qty: u32) {
        self.material_rolls.push((material_id, chance, min_qty, max_qty));
    }
    pub fn salvage(&self, item_value: f32) -> SalvageResult {
        let gold = (item_value * self.gold_percent_of_value) as u32;
        SalvageResult { source_item_id: 0, materials: Vec::new(), gold_recovered: gold, essence_recovered: self.essence_per_tier }
    }
}

#[derive(Clone, Debug)]
pub struct SalvageSystem {
    pub tables: HashMap<String, SalvageTable>,
    pub total_salvaged: u32,
    pub total_gold_recovered: u64,
}

impl SalvageSystem {
    pub fn new() -> Self { Self { tables: HashMap::new(), total_salvaged: 0, total_gold_recovered: 0 } }
    pub fn register_table(&mut self, key: impl Into<String>, table: SalvageTable) { self.tables.insert(key.into(), table); }
    pub fn salvage_item(&mut self, rarity_key: &str, item_value: f32) -> Option<SalvageResult> {
        let result = self.tables.get(rarity_key)?.salvage(item_value);
        self.total_salvaged += 1;
        self.total_gold_recovered += result.gold_recovered as u64;
        Some(result)
    }
    pub fn salvage_count(&self) -> u32 { self.total_salvaged }
}

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

pub const LOOT_ANALYTICS_MAX_EVENTS: usize = 50000;
pub const LOOT_SALVAGE_GOLD_RATE_DEFAULT: f32 = 0.1;
pub const LOOT_SALVAGE_ESSENCE_MAX: u32 = 100;
pub const LOOT_ANALYTICS_PIPELINE_DEFAULT_SIZE: usize = 10000;

pub fn loot_analytics_summary(pipeline: &LootAnalyticsPipeline) -> String {
    format!("Analytics: {} events processed, {} unique items, {:.0} total drop value",
        pipeline.total_processed,
        pipeline.unique_items_seen(),
        pipeline.total_value_by_type("drop"))
}