use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::models::skyblock::Bazaar;
#[derive(Debug, Clone, Default)]
pub struct PriceBook {
prices: HashMap<String, f64>,
}
impl PriceBook {
pub fn new() -> Self {
Self::default()
}
pub fn with_bazaar(mut self, bazaar: &Bazaar) -> Self {
for (id, product) in &bazaar.products {
if let Some(status) = &product.quick_status {
self.prices.insert(id.to_uppercase(), status.buy_price);
}
}
self
}
pub fn with_lowest_bin(mut self, lowest_bin: &HashMap<String, i64>) -> Self {
for (id, price) in lowest_bin {
self.prices
.entry(id.to_uppercase())
.or_insert(*price as f64);
}
self
}
pub fn insert(&mut self, item_id: impl Into<String>, price: f64) {
self.prices.insert(item_id.into().to_uppercase(), price);
}
pub fn price(&self, item_id: &str) -> Option<f64> {
self.prices.get(&item_id.to_uppercase()).copied()
}
pub fn prices(&self) -> &HashMap<String, f64> {
&self.prices
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ItemStack {
pub item_id: String,
pub count: i64,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct ItemValuation {
pub item_id: String,
pub count: i64,
pub unit_price: f64,
pub total: f64,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct Networth {
pub total: f64,
pub items: Vec<ItemValuation>,
pub unpriced: Vec<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct ItemModifiers {
#[serde(default)]
pub recombobulated: bool,
#[serde(default)]
pub hot_potato_books: i64,
#[serde(default)]
pub art_of_war: i64,
#[serde(default)]
pub enchantments: HashMap<String, i64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DetailedItemStack {
pub item_id: String,
pub count: i64,
#[serde(default)]
pub modifiers: ItemModifiers,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct DetailedItemValuation {
pub item_id: String,
pub count: i64,
pub base_value: f64,
pub modifiers_value: f64,
pub total: f64,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct DetailedNetworth {
pub total: f64,
pub items: Vec<DetailedItemValuation>,
pub unpriced: Vec<String>,
}
pub fn value_items_detailed(stacks: &[DetailedItemStack], prices: &PriceBook) -> DetailedNetworth {
let mut networth = DetailedNetworth::default();
for stack in stacks {
let Some(unit_price) = prices.price(&stack.item_id) else {
networth.unpriced.push(stack.item_id.clone());
continue;
};
let m = &stack.modifiers;
let mut modifiers_value = 0.0;
if m.recombobulated {
modifiers_value += prices.price("RECOMBOBULATOR_3000").unwrap_or(0.0);
}
if m.hot_potato_books > 0 {
let plain = m.hot_potato_books.min(10);
let fuming = (m.hot_potato_books - 10).max(0);
modifiers_value += plain as f64 * prices.price("HOT_POTATO_BOOK").unwrap_or(0.0);
modifiers_value += fuming as f64 * prices.price("FUMING_POTATO_BOOK").unwrap_or(0.0);
}
if m.art_of_war > 0 {
modifiers_value += m.art_of_war as f64 * prices.price("THE_ART_OF_WAR").unwrap_or(0.0);
}
for (enchant, level) in &m.enchantments {
let id = format!("ENCHANTMENT_{}_{}", enchant.to_uppercase(), level);
modifiers_value += prices.price(&id).unwrap_or(0.0);
}
let base_value = unit_price * stack.count as f64;
let total = base_value + modifiers_value;
networth.total += total;
networth.items.push(DetailedItemValuation {
item_id: stack.item_id.clone(),
count: stack.count,
base_value,
modifiers_value,
total,
});
}
networth
}
pub fn value_items(stacks: &[ItemStack], prices: &PriceBook) -> Networth {
let mut networth = Networth::default();
for stack in stacks {
match prices.price(&stack.item_id) {
Some(unit_price) => {
let total = unit_price * stack.count as f64;
networth.total += total;
networth.items.push(ItemValuation {
item_id: stack.item_id.clone(),
count: stack.count,
unit_price,
total,
});
}
None => networth.unpriced.push(stack.item_id.clone()),
}
}
networth
}
#[cfg(feature = "nbt")]
pub fn extract_item_stacks(nbt: &fastnbt::Value) -> Vec<ItemStack> {
extract_item_stacks_detailed(nbt)
.into_iter()
.map(|stack| ItemStack {
item_id: stack.item_id,
count: stack.count,
})
.collect()
}
#[cfg(feature = "nbt")]
pub fn extract_item_stacks_detailed(nbt: &fastnbt::Value) -> Vec<DetailedItemStack> {
use fastnbt::Value;
fn as_compound(v: &Value) -> Option<&std::collections::HashMap<String, Value>> {
match v {
Value::Compound(map) => Some(map),
_ => None,
}
}
fn as_int(v: &Value) -> Option<i64> {
match v {
Value::Byte(n) => Some(*n as i64),
Value::Short(n) => Some(*n as i64),
Value::Int(n) => Some(*n as i64),
Value::Long(n) => Some(*n),
_ => None,
}
}
let items = match nbt {
Value::Compound(root) => match root.get("i") {
Some(Value::List(list)) => list,
_ => return Vec::new(),
},
Value::List(list) => list,
_ => return Vec::new(),
};
let mut stacks = Vec::new();
for entry in items {
let Some(item) = as_compound(entry) else {
continue;
};
let count = item.get("Count").and_then(as_int).unwrap_or(1);
let Some(attrs) = as_compound(item.get("tag").unwrap_or(&Value::Byte(0)))
.and_then(|tag| as_compound(tag.get("ExtraAttributes").unwrap_or(&Value::Byte(0))))
else {
continue;
};
let Some(Value::String(item_id)) = attrs.get("id") else {
continue;
};
let mut modifiers = ItemModifiers {
recombobulated: attrs.get("rarity_upgrades").and_then(as_int).unwrap_or(0) > 0,
hot_potato_books: attrs.get("hot_potato_count").and_then(as_int).unwrap_or(0),
art_of_war: attrs.get("art_of_war_count").and_then(as_int).unwrap_or(0),
..ItemModifiers::default()
};
if let Some(enchants) = attrs.get("enchantments").and_then(as_compound) {
for (name, level) in enchants {
if let Some(level) = as_int(level) {
modifiers.enchantments.insert(name.clone(), level);
}
}
}
stacks.push(DetailedItemStack {
item_id: item_id.clone(),
count,
modifiers,
});
}
stacks
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detailed_valuation_prices_modifiers() {
let mut book = PriceBook::new();
book.insert("ASPECT_OF_THE_END", 100_000.0);
book.insert("RECOMBOBULATOR_3000", 8_000_000.0);
book.insert("HOT_POTATO_BOOK", 80_000.0);
book.insert("FUMING_POTATO_BOOK", 1_200_000.0);
book.insert("ENCHANTMENT_SHARPNESS_6", 500_000.0);
let stack = DetailedItemStack {
item_id: "ASPECT_OF_THE_END".into(),
count: 1,
modifiers: ItemModifiers {
recombobulated: true,
hot_potato_books: 12,
art_of_war: 0,
enchantments: [("sharpness".to_string(), 6i64)].into_iter().collect(),
},
};
let nw = value_items_detailed(&[stack], &book);
assert_eq!(nw.items.len(), 1);
let expected_modifiers = 8_000_000.0 + 10.0 * 80_000.0 + 2.0 * 1_200_000.0 + 500_000.0;
assert!((nw.items[0].modifiers_value - expected_modifiers).abs() < 1e-6);
assert!((nw.total - (100_000.0 + expected_modifiers)).abs() < 1e-6);
}
#[test]
fn values_known_items_and_tracks_unpriced() {
let mut book = PriceBook::new();
book.insert("HYPERION", 1_000_000.0);
let stacks = vec![
ItemStack {
item_id: "hyperion".into(),
count: 2,
},
ItemStack {
item_id: "MYSTERY".into(),
count: 1,
},
];
let nw = value_items(&stacks, &book);
assert_eq!(nw.total, 2_000_000.0);
assert_eq!(nw.items.len(), 1);
assert_eq!(nw.unpriced, vec!["MYSTERY".to_string()]);
}
}