use std::collections::HashSet;
use crate::abi::{ItemLine, StatModifierData};
use crate::stats::{ItemStats, Op, StatKey};
use super::defs::{AffixDef, AffixId, AffixKey, ItemRarity};
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Rolled {
pub affix: AffixKey,
pub values: Vec<f32>,
}
#[derive(Clone, Copy, Debug)]
pub struct Candidate<'a> {
pub key: AffixKey,
pub def: &'a AffixDef,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct RollRules {
pub min: u8,
pub max: u8,
pub max_rarity: ItemRarity,
}
impl RollRules {
#[must_use]
pub const fn new(min: u8, max: u8) -> Self {
Self {
min,
max,
max_rarity: ItemRarity::Legendary,
}
}
#[must_use]
pub const fn max_rarity(mut self, rarity: ItemRarity) -> Self {
self.max_rarity = rarity;
self
}
}
#[must_use]
pub fn roll(pool: &[Candidate<'_>], category: u8, rules: RollRules, seed: u64) -> Vec<Rolled> {
let mut rng = fastrand::Rng::with_seed(seed);
let mut fitting: Vec<&Candidate<'_>> = pool
.iter()
.filter(|c| {
c.def.weight > 0 && c.def.categories.holds(category) && c.def.rarity <= rules.max_rarity
})
.collect();
fitting.sort_by_key(|c| c.key);
fitting.dedup_by_key(|c| c.key);
let (low, high) = (rules.min.min(rules.max), rules.max.max(rules.min));
let count = rng.u8(low..=high);
let mut groups: HashSet<&str> = HashSet::new();
let mut out = Vec::new();
for _ in 0..count {
fitting.retain(|c| c.def.group.as_deref().is_none_or(|g| !groups.contains(g)));
let total: u64 = fitting.iter().map(|c| u64::from(c.def.weight)).sum();
if total == 0 {
break;
}
let mut pick = rng.u64(0..total);
let Some(at) = fitting.iter().position(|c| {
let weight = u64::from(c.def.weight);
if pick < weight {
true
} else {
pick -= weight;
false
}
}) else {
break;
};
let chosen = fitting.remove(at);
if let Some(group) = chosen.def.group.as_deref() {
groups.insert(group);
}
out.push(Rolled {
affix: chosen.key,
values: chosen.def.ranges.iter().map(|r| r.at(rng.f32())).collect(),
});
}
out
}
#[must_use]
pub fn reroll(
stats: &ItemStats,
pool: &[Candidate<'_>],
category: u8,
rules: RollRules,
seed: u64,
once: bool,
) -> Option<ItemStats> {
if once && stats.rolled {
return None;
}
let mut next = stats.clone();
if pool.iter().any(|c| c.def.categories.holds(category)) {
next.affixes = roll(pool, category, rules, seed);
next.rolled = true;
}
Some(next)
}
#[must_use]
pub fn lines_of(
stats: &ItemStats,
pool: &[Candidate<'_>],
stat: impl Fn(StatKey) -> Option<u32>,
) -> Vec<ItemLine> {
let line = |stat: u32, op: Op, value: f32, affix: u32| ItemLine {
modifier: StatModifierData {
stat,
op: op.to_raw(),
reserved: [0; 3],
value,
},
affix,
};
let mut out: Vec<ItemLine> = stats
.modifiers
.iter()
.filter_map(|m| Some(line(stat(m.stat)?, m.op, m.value, 0)))
.collect();
for rolled in &stats.affixes {
let Some((at, candidate)) = pool.iter().enumerate().find(|(_, c)| c.key == rolled.affix)
else {
continue;
};
let affix = u32::try_from(at + 1).unwrap_or(u32::MAX);
for (range, &value) in candidate.def.ranges.iter().zip(&rolled.values) {
out.push(line(range.stat.index(), range.op, value, affix));
}
}
out
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub struct Line {
pub stat: u32,
pub op: Op,
pub value: f32,
pub affix: Option<AffixId>,
}