use crate::abi::{RateId, Status};
use crate::{Host, Load, Off};
pub const MAX_FACTOR: f32 = 100.0;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ExperienceGroup {
Combat,
Mining,
Exploration,
}
impl ExperienceGroup {
#[must_use]
pub const fn of(source: crate::game_events::ExperienceSource) -> Option<Self> {
use crate::game_events::ExperienceSource as Source;
match source {
Source::Combat => Some(Self::Combat),
Source::Mining => Some(Self::Mining),
Source::Exploration | Source::Lore | Source::Quest | Source::ItemSetCompleted => {
Some(Self::Exploration)
}
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Rate {
Experience(ExperienceGroup),
WorkshopSpeed,
ResourceDrops,
Comfort,
SkillPoints,
Loot,
}
impl Rate {
pub const ALL: &[Self] = &[
Self::Experience(ExperienceGroup::Combat),
Self::Experience(ExperienceGroup::Mining),
Self::Experience(ExperienceGroup::Exploration),
Self::WorkshopSpeed,
Self::ResourceDrops,
Self::Comfort,
Self::SkillPoints,
Self::Loot,
];
#[must_use]
pub const fn id(self) -> RateId {
match self {
Self::Experience(ExperienceGroup::Combat) => RateId::EXPERIENCE_COMBAT,
Self::Experience(ExperienceGroup::Mining) => RateId::EXPERIENCE_MINING,
Self::Experience(ExperienceGroup::Exploration) => RateId::EXPERIENCE_EXPLORATION,
Self::WorkshopSpeed => RateId::WORKSHOP_SPEED,
Self::ResourceDrops => RateId::RESOURCE_DROPS,
Self::Comfort => RateId::COMFORT,
Self::SkillPoints => RateId::SKILL_POINTS,
Self::Loot => RateId::LOOT,
}
}
#[must_use]
pub fn from_id(id: RateId) -> Option<Self> {
Self::ALL.iter().copied().find(|rate| rate.id() == id)
}
#[must_use]
pub const fn seen_by_clients(self) -> bool {
matches!(self, Self::WorkshopSpeed | Self::SkillPoints)
}
}
#[must_use]
pub fn is_valid_factor(factor: f32) -> bool {
(0.0..=MAX_FACTOR).contains(&factor)
}
#[must_use]
#[expect(
clippy::cast_possible_truncation,
reason = "held to 0..=100 first, which an f32 holds"
)]
pub fn combine(factors: impl IntoIterator<Item = f32>) -> f32 {
let product = factors
.into_iter()
.fold(1.0_f64, |product, factor| product * f64::from(factor));
product.clamp(0.0, f64::from(MAX_FACTOR)) as f32
}
#[must_use]
#[expect(
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
reason = "the game's whole numbers are far below 2^53, and the result is clamped first"
)]
pub fn scale_whole(value: i64, factor: f32) -> i64 {
let scaled = (value as f64 * f64::from(factor)).round();
scaled.clamp(i64::MIN as f64, i64::MAX as f64) as i64
}
#[derive(Clone, Copy, Debug, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum RateError {
#[error("factor {0} is not between 0 and 100")]
OutOfRange(f32),
#[error("the loader refused the factor")]
Refused,
}
#[derive(Clone, Copy, Debug)]
pub struct RateHandle {
host: Host,
rate: Rate,
}
impl RateHandle {
#[must_use]
pub const fn rate(&self) -> Rate {
self.rate
}
pub fn set(&self, factor: f32) -> Result<(), RateError> {
if !is_valid_factor(factor) {
return Err(RateError::OutOfRange(factor));
}
match (self.host.api().set_rate)(self.host.handle(), self.rate.id(), factor) {
Status::OK => Ok(()),
_ => Err(RateError::Refused),
}
}
}
impl Load<'_> {
pub fn rate(&mut self, rate: Rate) -> Result<RateHandle, Off> {
if rate.seen_by_clients() && !self.joins() {
return Err(Off::new(format!(
"the rate {rate:?}: clients count it too, so only a mod with `join: true` declares it"
)));
}
let host = self.host();
match (host.api().rate)(host.handle(), rate.id()) {
Status::OK => Ok(RateHandle { host, rate }),
status => Err(Off::status(&format!("the rate {rate:?}"), status)),
}
}
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::*;
proptest! {
#[test]
fn combine_is_the_same_in_any_order_and_stays_in_range(
factors in proptest::collection::vec(0.0f32..=MAX_FACTOR, 0..6),
) {
let forward = combine(factors.iter().copied());
let backward = combine(factors.iter().rev().copied());
prop_assert_eq!(forward.to_bits(), backward.to_bits());
prop_assert!((0.0..=MAX_FACTOR).contains(&forward));
}
#[test]
fn scale_whole_never_flips_the_sign(value in any::<i64>(), factor in 0.0f32..=MAX_FACTOR) {
let scaled = scale_whole(value, factor);
prop_assert!(scaled == 0 || scaled.signum() == value.signum());
}
}
}