Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552use super::format::sp_per_sec;
const ATTR_MAX: u32 = 27;const ATTR_MIN: u32 = 17;const BASE_TOTAL: u32 = 99;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]pub enum Attribute { Charisma, Intelligence, Memory, Perception, Willpower,}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]pub struct Attributes { pub charisma: u32, pub intelligence: u32, pub memory: u32, pub perception: u32, pub willpower: u32,}
impl Attributes { /// The baseline before any neural remap has ever been applied: every attribute at the EVE floor (`ATTR_MIN`). pub fn unmapped() -> Attributes { Attributes { charisma: ATTR_MIN, intelligence: ATTR_MIN, memory: ATTR_MIN, perception: ATTR_MIN, willpower: ATTR_MIN, } }
fn get(self, attribute: Attribute) -> u32 { match attribute { Attribute::Charisma => self.charisma, Attribute::Intelligence => self.intelligence, Attribute::Memory => self.memory, Attribute::Perception => self.perception, Attribute::Willpower => self.willpower, } }
fn is_in_spec(self) -> bool { let attrs = [ self.charisma, self.intelligence, self.memory, self.perception, self.willpower, ]; attrs.iter().all(|&value| (ATTR_MIN..=ATTR_MAX).contains(&value)) && attrs.iter().sum::<u32>() == BASE_TOTAL }
fn plus(self, implants: Attributes) -> Attributes { Attributes { charisma: self.charisma + implants.charisma, intelligence: self.intelligence + implants.intelligence, memory: self.memory + implants.memory, perception: self.perception + implants.perception, willpower: self.willpower + implants.willpower, } }}
#[derive(Clone, Copy, Debug, PartialEq)]pub struct PairWeight { pub primary: Attribute, pub secondary: Attribute, pub sp: u64,}
#[derive(Clone, Copy, Debug, PartialEq)]pub struct Recommendation { pub base: Attributes, pub current_out_of_spec: bool, pub is_current: bool, pub total_sec: f64,}
pub fn optimize_remap(weights: &[PairWeight], current_base: Attributes, implants: Attributes) -> Recommendation { let current_out_of_spec = !current_base.is_in_spec(); let current_time = plan_time(weights, current_base.plus(implants));
let (best_base, best_time) = best_in_spec_allocation(weights, implants); let current_wins = current_time <= best_time;
if !current_out_of_spec && current_wins { Recommendation { base: current_base, total_sec: current_time, is_current: true, current_out_of_spec, } } else { Recommendation { base: best_base, total_sec: best_time, is_current: current_wins, current_out_of_spec, } }}
fn best_in_spec_allocation(weights: &[PairWeight], implants: Attributes) -> (Attributes, f64) { let mut best_base = None::<Attributes>; let mut best_time = f64::INFINITY;
for base in in_spec_bases() { let time = plan_time(weights, base.plus(implants)); if best_base.is_none() || time < best_time { best_base = Some(base); best_time = time; } }
let best_base = best_base.expect("the [17,27] cube always contains an in-spec allocation summing to 99"); (best_base, best_time)}
fn in_spec_bases() -> impl Iterator<Item = Attributes> { (ATTR_MIN..=ATTR_MAX).flat_map(|perception| { (ATTR_MIN..=ATTR_MAX).flat_map(move |memory| { (ATTR_MIN..=ATTR_MAX).flat_map(move |willpower| { (ATTR_MIN..=ATTR_MAX).filter_map(move |intelligence| base_with(perception, memory, willpower, intelligence)) }) }) })}
fn base_with(perception: u32, memory: u32, willpower: u32, intelligence: u32) -> Option<Attributes> { let fixed = perception + memory + willpower + intelligence; if fixed + ATTR_MIN > BASE_TOTAL { return None; } let charisma = BASE_TOTAL - fixed; if !(ATTR_MIN..=ATTR_MAX).contains(&charisma) { return None; } Some(Attributes { charisma, intelligence, memory, perception, willpower, })}
fn plan_time(weights: &[PairWeight], effective: Attributes) -> f64 { let mut total = 0.0; for weight in weights { let rate = sp_per_sec(effective.get(weight.primary), effective.get(weight.secondary)); if rate <= 0.0 { return f64::INFINITY; } total += weight.sp as f64 / rate; } total}
#[cfg(test)]mod tests { use super::*;
fn no_implants() -> Attributes { Attributes::default() }
fn per_wil_plan(sp: u64) -> Vec<PairWeight> { vec![PairWeight { primary: Attribute::Perception, secondary: Attribute::Willpower, sp, }] }
mod unmapped { use pretty_assertions::assert_eq;
use super::*;
#[test] fn it_puts_every_attribute_at_the_eve_minimum() { let attrs = Attributes::unmapped();
assert_eq!(attrs.charisma, ATTR_MIN); assert_eq!(attrs.intelligence, ATTR_MIN); assert_eq!(attrs.memory, ATTR_MIN); assert_eq!(attrs.perception, ATTR_MIN); assert_eq!(attrs.willpower, ATTR_MIN); } }
mod fixture_gate { use pretty_assertions::assert_eq;
use super::{ super::super::attributes::{self, Role}, *, }; use crate::clients::esi::models::{character, universe};
const ATTRIBUTES_FIXTURE: &str = include_str!("../../../tests/fixtures/esi/character_attributes.json");
const IMPLANT_FIXTURE: &str = include_str!("../../../tests/fixtures/esi/universe_types_9899.json");
const CHARISMA_BONUS_ID: i32 = 175;
const INTELLIGENCE_BONUS_ID: i32 = 176;
const MEMORY_BONUS_ID: i32 = 177;
const PERCEPTION_BONUS_ID: i32 = 178;
const WILLPOWER_BONUS_ID: i32 = 179;
fn base_from_esi(attributes: &character::Attributes) -> Attributes { Attributes { charisma: attributes.charisma as u32, intelligence: attributes.intelligence as u32, memory: attributes.memory as u32, perception: attributes.perception as u32, willpower: attributes.willpower as u32, } }
fn implants_from_esi(items: &[universe::ItemType]) -> Attributes { let mut implants = Attributes::default(); for item in items { for dogma in &item.dogma_attributes { let bonus = dogma.value.round().max(0.0) as u32; match dogma.attribute_id { CHARISMA_BONUS_ID => implants.charisma += bonus, INTELLIGENCE_BONUS_ID => implants.intelligence += bonus, MEMORY_BONUS_ID => implants.memory += bonus, PERCEPTION_BONUS_ID => implants.perception += bonus, WILLPOWER_BONUS_ID => implants.willpower += bonus, _ => {} } } } implants }
fn fixtures() -> (Attributes, Attributes) { let attributes: character::Attributes = serde_json::from_str(ATTRIBUTES_FIXTURE).expect("parse attributes fixture"); let implant: universe::ItemType = serde_json::from_str(IMPLANT_FIXTURE).expect("parse implant fixture"); (base_from_esi(&attributes), implants_from_esi(&[implant])) }
const ACTIVE_PAIR: (Attribute, Attribute) = (Attribute::Intelligence, Attribute::Memory);
fn plan() -> Vec<PairWeight> { vec![ PairWeight { primary: ACTIVE_PAIR.0, secondary: ACTIVE_PAIR.1, sp: 4_000_000, }, PairWeight { primary: Attribute::Perception, secondary: Attribute::Willpower, sp: 1_000_000, }, ] }
fn effective_of(attribute: Attribute, base: Attributes, implants: Attributes) -> u32 { base.get(attribute) + implants.get(attribute) }
#[test] fn it_computes_effective_as_base_plus_implant_without_clamping_in_both_paths() { let (base, implants) = fixtures();
let rec = optimize_remap(&plan(), base, implants); let effective = rec.base.plus(implants); assert_eq!(effective.memory, rec.base.memory + 3); let recomputed = plan_time(&plan(), effective); assert!( (rec.total_sec - recomputed).abs() < 1e-6, "optimizer time {} does not match unclamped effective recompute {}", rec.total_sec, recomputed );
let rows = attributes::project_rows(base, implants, 0, None); for row in rows { assert_eq!( row.effective, row.base + row.implant, "projected effective for {:?} is not base + implant", row.attribute ); assert_eq!( row.effective, effective_of(row.attribute, base, implants), "projection and base+implant disagree for {:?}", row.attribute ); }
let memory_row = rows.iter().find(|row| row.attribute == Attribute::Memory).unwrap(); assert_eq!(memory_row.base, 21); assert_eq!(memory_row.implant, 3); assert_eq!(memory_row.effective, 24);
let matrix = attributes::sp_per_hr_matrix(effective, Some(ACTIVE_PAIR)); let int_mem = matrix .iter() .find(|cell| (cell.primary, cell.secondary) == ACTIVE_PAIR) .unwrap(); let expected = (sp_per_sec(effective.intelligence, effective.memory) * 3_600.0).round() as u64; assert_eq!(int_mem.sp_per_hr, expected); assert!(int_mem.active); }
#[test] fn it_flags_the_out_of_spec_fixture_base_and_still_emits_an_in_spec_recommendation() { let (base, implants) = fixtures();
assert_eq!( base.charisma + base.intelligence + base.memory + base.perception + base.willpower, 103 ); assert!( !base.is_in_spec(), "fixture base {base:?} is expected to be out of spec" );
let rec = optimize_remap(&plan(), base, implants);
assert!(rec.current_out_of_spec);
let emitted = [ rec.base.charisma, rec.base.intelligence, rec.base.memory, rec.base.perception, rec.base.willpower, ]; assert_eq!(emitted.iter().sum::<u32>(), 99); assert!( emitted.iter().all(|&value| (17..=27).contains(&value)), "emitted base {:?} has an attribute outside [17, 27]", rec.base ); assert!(rec.base.is_in_spec());
let baseline_time = plan_time(&plan(), base.plus(implants)); assert!( rec.total_sec <= baseline_time, "recommendation {} must not be slower than the real baseline {}", rec.total_sec, baseline_time ); }
#[test] fn it_marks_the_active_pair_in_the_projection_for_the_emitted_recommendation() { let (base, implants) = fixtures(); let rec = optimize_remap(&plan(), base, implants);
let rows = attributes::project_rows(rec.base, implants, 0, Some(ACTIVE_PAIR)); let intelligence_row = rows .iter() .find(|row| row.attribute == Attribute::Intelligence) .unwrap(); let memory_row = rows.iter().find(|row| row.attribute == Attribute::Memory).unwrap();
assert_eq!(intelligence_row.role, Role::Primary); assert_eq!(memory_row.role, Role::Secondary); assert_eq!(memory_row.effective, rec.base.memory + 3); }
#[test] fn it_resolves_the_real_implant_as_a_plus_three_memory_bonus() { let (_, implants) = fixtures();
assert_eq!(implants.memory, 3); assert_eq!(implants.charisma, 0); assert_eq!(implants.intelligence, 0); assert_eq!(implants.perception, 0); assert_eq!(implants.willpower, 0); } }
mod formulas { use pretty_assertions::assert_eq;
use super::{super::super::format::sp_cost, *};
#[test] fn sp_cost_is_256000_at_level_five_rank_one() { assert_eq!(sp_cost(1.0, 5), 256_000); }
#[test] fn sp_per_sec_matches_the_pod_rate_formula() { assert_eq!(sp_per_sec(27, 21), (27.0 + 10.5) / 60.0); } }
mod optimize_remap { use pretty_assertions::assert_eq;
use super::*;
fn base(perception: u32, memory: u32, willpower: u32, intelligence: u32, charisma: u32) -> Attributes { Attributes { charisma, intelligence, memory, perception, willpower, } }
fn best_per_wil_base() -> Attributes { base(27, 17, 21, 17, 17) }
#[test] fn it_does_not_flag_an_in_spec_current() { let rec = optimize_remap(&per_wil_plan(1_000_000), base(20, 20, 20, 20, 19), no_implants());
assert!(!rec.current_out_of_spec); }
#[test] fn it_finds_the_known_optimum_for_a_single_pair() { let rec = optimize_remap(&per_wil_plan(1_000_000), base(20, 20, 20, 20, 19), no_implants());
assert_eq!(rec.base, best_per_wil_base()); assert!(!rec.is_current); }
#[test] fn it_flags_an_out_of_spec_current_and_still_emits_an_in_spec_base() { let current = base(17, 17, 17, 17, 31); let weights = vec![PairWeight { primary: Attribute::Charisma, secondary: Attribute::Willpower, sp: 1_000_000, }]; let current_effective = current.plus(no_implants()); let current_time = 1_000_000.0 / sp_per_sec(current_effective.charisma, current_effective.willpower);
let rec = optimize_remap(&weights, current, no_implants());
assert!(rec.current_out_of_spec); assert!(rec.base.is_in_spec()); assert!(rec.is_current); assert!(rec.total_sec >= current_time); }
#[test] fn it_never_clamps_effective_attributes_above_the_max() { let implants = Attributes { charisma: 5, intelligence: 5, memory: 5, perception: 5, willpower: 5, }; let weights = per_wil_plan(1_000_000);
let rec = optimize_remap(&weights, base(20, 20, 20, 20, 19), implants);
assert_eq!(rec.base.perception, 27); let effective = rec.base.plus(implants); let clamped_rate = sp_per_sec(27, effective.willpower.min(27)); let real_rate = sp_per_sec(effective.perception, effective.willpower); assert!(real_rate > clamped_rate, "effective {effective:?} appears clamped"); let expected = 1_000_000.0 / real_rate; assert!((rec.total_sec - expected).abs() < 1e-6); }
#[test] fn it_never_recommends_a_config_slower_than_current() { let weights = per_wil_plan(2_000_000); let current = base(20, 20, 20, 20, 19); let current_time = { let effective = current.plus(no_implants()); 2_000_000.0 / sp_per_sec(effective.perception, effective.willpower) };
let rec = optimize_remap(&weights, current, no_implants());
assert!(rec.total_sec <= current_time); }
#[test] fn it_only_ever_emits_an_in_spec_base() { let plans = [ per_wil_plan(1_000_000), vec![ PairWeight { primary: Attribute::Intelligence, secondary: Attribute::Memory, sp: 5_000_000, }, PairWeight { primary: Attribute::Charisma, secondary: Attribute::Willpower, sp: 200_000, }, ], ]; let currents = [ base(20, 20, 20, 20, 19), base(99, 99, 99, 99, 99), base(17, 17, 17, 17, 31), ];
for plan in &plans { for ¤t in ¤ts { let rec = optimize_remap(plan, current, base(5, 5, 5, 5, 5));
assert!(rec.base.is_in_spec(), "emitted base {:?} is out of spec", rec.base); } } }
#[test] fn it_reports_is_current_when_the_current_base_is_already_optimal() { let weights = per_wil_plan(1_000_000); let optimal = best_per_wil_base();
let rec = optimize_remap(&weights, optimal, no_implants());
assert!(rec.is_current); assert_eq!(rec.base, optimal); }
#[test] fn it_returns_the_current_baseline_for_an_empty_plan() { let current = base(20, 20, 20, 20, 19);
let rec = optimize_remap(&[], current, no_implants());
assert!(rec.is_current); assert_eq!(rec.base, current); assert_eq!(rec.total_sec, 0.0); } }}