use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use super::common::Rarity;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Item {
pub id: String,
pub slug: String,
#[serde(rename = "gameRef", default)]
pub game_ref: Option<String>,
#[serde(default)]
pub tradable: Option<bool>,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub i18n: HashMap<String, ItemTranslation>,
#[serde(default)]
pub rarity: Option<Rarity>,
#[serde(default)]
pub vaulted: Option<bool>,
#[serde(default)]
pub ducats: Option<u32>,
#[serde(rename = "tradingTax", default)]
pub trading_tax: Option<u32>,
#[serde(rename = "reqMasteryRank", default)]
pub mastery_rank: Option<u32>,
#[serde(rename = "maxRank", default)]
pub max_rank: Option<u32>,
#[serde(rename = "maxCharges", default)]
pub max_charges: Option<u32>,
#[serde(rename = "maxAmberStars", default)]
pub max_amber_stars: Option<u32>,
#[serde(rename = "maxCyanStars", default)]
pub max_cyan_stars: Option<u32>,
#[serde(rename = "baseEndo", default)]
pub base_endo: Option<u32>,
#[serde(rename = "endoMultiplier", default)]
pub endo_multiplier: Option<f32>,
#[serde(rename = "setRoot", default)]
pub set_root: Option<bool>,
#[serde(rename = "setParts", default)]
pub set_parts: Option<Vec<String>>,
#[serde(rename = "quantityInSet", default)]
pub quantity_in_set: Option<u32>,
#[serde(rename = "bulkTradable", default)]
pub bulk_tradable: Option<bool>,
#[serde(default)]
pub subtypes: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ItemTranslation {
pub name: String,
pub icon: String,
#[serde(default)]
pub thumb: Option<String>,
#[serde(rename = "subIcon", default)]
pub sub_icon: Option<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(rename = "wikiLink", default)]
pub wiki_link: Option<String>,
}
impl Item {
pub fn name(&self) -> &str {
self.i18n.get("en").map(|t| t.name.as_str()).unwrap_or("")
}
pub fn name_localized(&self, lang: &str) -> &str {
self.i18n
.get(lang)
.or_else(|| self.i18n.get("en"))
.map(|t| t.name.as_str())
.unwrap_or("")
}
pub fn translation(&self) -> Option<&ItemTranslation> {
self.i18n.get("en")
}
pub fn icon(&self) -> Option<&str> {
self.i18n.get("en").map(|t| t.icon.as_str())
}
pub fn wiki_link(&self) -> Option<&str> {
self.i18n.get("en").and_then(|t| t.wiki_link.as_deref())
}
pub fn is_mod(&self) -> bool {
self.max_rank.is_some()
}
pub fn is_sculpture(&self) -> bool {
self.base_endo.is_some() && self.endo_multiplier.is_some()
}
pub fn is_set(&self) -> bool {
self.set_root.unwrap_or(false)
}
pub fn is_vaulted(&self) -> bool {
self.vaulted.unwrap_or(false)
}
pub fn has_charges(&self) -> bool {
self.max_charges.is_some()
}
pub fn as_mod(&self) -> Option<ModView<'_>> {
if self.max_rank.is_some() {
Some(ModView(self))
} else {
None
}
}
pub fn as_sculpture(&self) -> Option<SculptureView<'_>> {
if self.base_endo.is_some() && self.endo_multiplier.is_some() {
Some(SculptureView(self))
} else {
None
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ModView<'a>(&'a Item);
impl<'a> ModView<'a> {
pub fn item(&self) -> &Item {
self.0
}
pub fn max_rank(&self) -> u32 {
self.0.max_rank.unwrap() }
pub fn max_charges(&self) -> Option<u32> {
self.0.max_charges
}
pub fn has_charges(&self) -> bool {
self.0.max_charges.is_some()
}
}
#[derive(Debug, Clone, Copy)]
pub struct SculptureView<'a>(&'a Item);
impl<'a> SculptureView<'a> {
pub fn item(&self) -> &Item {
self.0
}
pub fn base_endo(&self) -> u32 {
self.0.base_endo.unwrap() }
pub fn endo_multiplier(&self) -> f32 {
self.0.endo_multiplier.unwrap() }
pub fn max_amber_stars(&self) -> u32 {
self.0.max_amber_stars.unwrap_or(0)
}
pub fn max_cyan_stars(&self) -> u32 {
self.0.max_cyan_stars.unwrap_or(0)
}
pub fn total_slots(&self) -> u32 {
self.max_amber_stars() + self.max_cyan_stars()
}
pub fn calculate_endo(&self, cyan_stars: Option<u32>, amber_stars: Option<u32>) -> u32 {
let base = self.base_endo() as f32;
let multiplier = self.endo_multiplier();
let total_slots = self.total_slots();
if total_slots == 0 {
return base as u32;
}
let cyan = cyan_stars.unwrap_or_else(|| self.max_cyan_stars());
let amber = amber_stars.unwrap_or_else(|| self.max_amber_stars());
let filled_slots = (cyan + amber) as f32;
let base_value = base + 50.0 * (cyan as f32) + 100.0 * (amber as f32);
let socket_factor = 1.0 + multiplier * filled_slots / (total_slots as f32);
(base_value * socket_factor) as u32
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_test_item() -> Item {
Item {
id: "test-id".to_string(),
slug: "test-slug".to_string(),
game_ref: None,
tradable: Some(true),
tags: vec!["mod".to_string()],
i18n: HashMap::from([(
"en".to_string(),
ItemTranslation {
name: "Test Item".to_string(),
icon: "/icons/test.png".to_string(),
thumb: None,
sub_icon: None,
description: None,
wiki_link: None,
},
)]),
rarity: Some(Rarity::Rare),
vaulted: None,
ducats: None,
trading_tax: None,
mastery_rank: None,
max_rank: Some(10),
max_charges: None,
max_amber_stars: None,
max_cyan_stars: None,
base_endo: None,
endo_multiplier: None,
set_root: None,
set_parts: None,
quantity_in_set: None,
bulk_tradable: None,
subtypes: None,
}
}
fn make_sculpture() -> Item {
Item {
id: "sculpture-id".to_string(),
slug: "ayatan-anasa".to_string(),
game_ref: None,
tradable: Some(true),
tags: vec!["ayatan".to_string()],
i18n: HashMap::from([(
"en".to_string(),
ItemTranslation {
name: "Ayatan Anasa Sculpture".to_string(),
icon: "/icons/anasa.png".to_string(),
thumb: None,
sub_icon: None,
description: None,
wiki_link: None,
},
)]),
rarity: None,
vaulted: None,
ducats: None,
trading_tax: None,
mastery_rank: None,
max_rank: None,
max_charges: None,
max_amber_stars: Some(2),
max_cyan_stars: Some(4),
base_endo: Some(450),
endo_multiplier: Some(0.9),
set_root: None,
set_parts: None,
quantity_in_set: None,
bulk_tradable: None,
subtypes: None,
}
}
#[test]
fn test_item_name() {
let item = make_test_item();
assert_eq!(item.name(), "Test Item");
}
#[test]
fn test_item_is_mod() {
let item = make_test_item();
assert!(item.is_mod());
assert!(!item.is_sculpture());
}
#[test]
fn test_mod_view() {
let item = make_test_item();
let mod_view = item.as_mod().expect("Should be a mod");
assert_eq!(mod_view.max_rank(), 10);
}
#[test]
fn test_sculpture_view() {
let item = make_sculpture();
assert!(item.is_sculpture());
assert!(!item.is_mod());
let sculpture = item.as_sculpture().expect("Should be a sculpture");
assert_eq!(sculpture.base_endo(), 450);
assert_eq!(sculpture.max_amber_stars(), 2);
assert_eq!(sculpture.max_cyan_stars(), 4);
assert_eq!(sculpture.total_slots(), 6);
}
#[test]
fn test_sculpture_endo_calculation() {
let item = make_sculpture();
let sculpture = item.as_sculpture().unwrap();
let full = sculpture.calculate_endo(None, None);
assert!(full > sculpture.base_endo());
let empty = sculpture.calculate_endo(Some(0), Some(0));
assert_eq!(empty, sculpture.base_endo());
let partial = sculpture.calculate_endo(Some(2), Some(1));
assert!(partial > empty);
assert!(partial < full);
}
#[test]
fn test_non_mod_returns_none() {
let item = make_sculpture();
assert!(item.as_mod().is_none());
}
#[test]
fn test_non_sculpture_returns_none() {
let item = make_test_item();
assert!(item.as_sculpture().is_none());
}
}