use crate::game_params::ttx::components::ShipTtxComponents;
use crate::game_params::ttx::model::ShipStats;
use crate::game_params::ttx::modifiers::ModifierBundle;
use crate::game_params::ttx::orchestration::ship_stats;
use crate::game_params::ttx::selection::ShipUpgradeSelection;
use crate::game_params::types::GameParamProvider;
use crate::game_params::types::Param;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ModuleSlot {
Hull,
Engine,
Artillery,
Torpedoes,
FireControl,
}
#[derive(Clone, Debug)]
pub struct ModuleOption {
pub upgrade_name: String,
pub is_baseline: bool,
pub stats: ShipStats,
}
#[derive(Clone, Debug)]
pub struct SlotOptions {
pub slot: ModuleSlot,
pub baseline: Option<String>,
pub options: Vec<ModuleOption>,
}
#[derive(Clone, Debug, Default)]
pub struct ModuleOptions {
pub slots: Vec<SlotOptions>,
}
const SLOTS: [ModuleSlot; 5] =
[ModuleSlot::Hull, ModuleSlot::Engine, ModuleSlot::Artillery, ModuleSlot::Torpedoes, ModuleSlot::FireControl];
fn slot_upgrade_names(components: &ShipTtxComponents, slot: ModuleSlot) -> Vec<String> {
let mut names: Vec<String> = match slot {
ModuleSlot::Hull => components.hulls.keys().cloned().collect(),
ModuleSlot::Engine => components.engines.keys().cloned().collect(),
ModuleSlot::Artillery => components.artillery.keys().cloned().collect(),
ModuleSlot::Torpedoes => components.torpedoes.keys().cloned().collect(),
ModuleSlot::FireControl => components.fire_controls.keys().cloned().collect(),
};
names.sort();
names
}
fn baseline_name(baseline: &ShipUpgradeSelection, slot: ModuleSlot) -> Option<String> {
match slot {
ModuleSlot::Hull => baseline.hull.clone(),
ModuleSlot::Engine => baseline.engine.clone(),
ModuleSlot::Artillery => baseline.artillery.clone(),
ModuleSlot::Torpedoes => baseline.torpedoes.clone(),
ModuleSlot::FireControl => baseline.fire_control.clone(),
}
}
fn with_slot(baseline: &ShipUpgradeSelection, slot: ModuleSlot, name: &str) -> ShipUpgradeSelection {
let mut selection = baseline.clone();
let name = Some(name.to_string());
match slot {
ModuleSlot::Hull => selection.hull = name,
ModuleSlot::Engine => selection.engine = name,
ModuleSlot::Artillery => selection.artillery = name,
ModuleSlot::Torpedoes => selection.torpedoes = name,
ModuleSlot::FireControl => selection.fire_control = name,
}
selection
}
pub fn module_options(
ship: &Param,
baseline: &ShipUpgradeSelection,
modifiers: &ModifierBundle,
level: u32,
provider: &dyn GameParamProvider,
) -> ModuleOptions {
let Some(components) = ship.vehicle().and_then(|v| v.ttx_components()) else {
return ModuleOptions::default();
};
let mut slots = Vec::new();
for slot in SLOTS {
let names = slot_upgrade_names(components, slot);
if names.is_empty() {
continue;
}
let baseline_for_slot = baseline_name(baseline, slot);
let options = names
.into_iter()
.map(|name| {
let selection = with_slot(baseline, slot, &name);
let stats = ship_stats(ship, &selection, modifiers, level, provider);
let is_baseline = baseline_for_slot.as_deref() == Some(name.as_str());
ModuleOption { upgrade_name: name, is_baseline, stats }
})
.collect();
slots.push(SlotOptions { slot, baseline: baseline_for_slot, options });
}
ModuleOptions { slots }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Rc;
use crate::data::ResourceLoader;
use crate::game_params::ttx::components::EngineComponentStats;
use crate::game_params::ttx::components::HullComponentStats;
use crate::game_params::ttx::components::ShipTtxComponents;
use crate::game_params::ttx::labels::TtxStat;
use crate::game_params::ttx::model::Hp;
use crate::game_params::types::ParamData;
use crate::game_params::types::Species;
use crate::game_params::types::Vehicle;
use crate::game_types::GameParamId;
use crate::recognized::Recognized;
struct EmptyProvider;
impl GameParamProvider for EmptyProvider {
fn game_param_by_id(&self, _id: GameParamId) -> Option<Rc<Param>> {
None
}
fn game_param_by_index(&self, _index: &str) -> Option<Rc<Param>> {
None
}
fn game_param_by_name(&self, _name: &str) -> Option<Rc<Param>> {
None
}
fn params(&self) -> &[Rc<Param>] {
&[]
}
}
struct EchoLoader;
impl ResourceLoader for EchoLoader {
fn localized_name_from_param(&self, _p: &Param) -> Option<String> {
None
}
fn localized_name_from_id(&self, id: &crate::data::TranslationKey) -> Option<String> {
Some(id.as_str().to_string())
}
fn game_param_by_id(&self, _id: GameParamId) -> Option<Rc<Param>> {
None
}
fn entity_specs(&self) -> &[crate::rpc::entitydefs::EntitySpec] {
&[]
}
}
fn hull(health: f32) -> HullComponentStats {
HullComponentStats { health: Some(Hp::from(health)), ..Default::default() }
}
fn engine(speed_coef: f32) -> EngineComponentStats {
EngineComponentStats { speed_coef: Some(speed_coef) }
}
fn ship_from(components: ShipTtxComponents) -> Param {
let vehicle = Vehicle::builder()
.level(10)
.group("g".to_string())
.maybe_abilities(None)
.upgrades(Vec::new())
.maybe_config_data(None)
.maybe_model_path(None)
.maybe_armor(None)
.maybe_hit_locations(None)
.permoflages(Vec::new())
.camera_trajectories(Vec::new())
.ttx_components(components)
.innate_skills(Vec::new())
.build();
Param::builder()
.id(GameParamId::from(900u32))
.index("IDX".to_string())
.name("Fixture".to_string())
.nation("USA".to_string())
.species(Recognized::Known(Species::Cruiser))
.data(ParamData::Vehicle(vehicle))
.build()
}
fn two_hull_ship() -> Param {
let mut components = ShipTtxComponents::default();
components.hulls.insert("HULL_A".to_string(), hull(19400.0));
components.hulls.insert("HULL_B".to_string(), hull(21000.0));
components.stock_selection = ShipUpgradeSelection::new(Some("HULL_A".to_string()), None, None, None, None);
ship_from(components)
}
fn hull_and_engine_ship() -> Param {
let mut components = ShipTtxComponents::default();
components.hulls.insert("HULL_A".to_string(), hull(19400.0));
components.hulls.insert("HULL_B".to_string(), hull(21000.0));
components.engines.insert("ENG_A".to_string(), engine(0.0));
components.stock_selection =
ShipUpgradeSelection::new(Some("HULL_A".to_string()), Some("ENG_A".to_string()), None, None, None);
ship_from(components)
}
#[test]
fn enumerates_hull_options_sorted_with_baseline_flag() {
let ship = two_hull_ship();
let baseline = ShipUpgradeSelection::stock(&ship);
let provider = EmptyProvider;
let opts = module_options(&ship, &baseline, &ModifierBundle::empty(Species::Cruiser), 10, &provider);
let hull_slot = opts.slots.iter().find(|s| s.slot == ModuleSlot::Hull).expect("hull slot");
assert_eq!(hull_slot.baseline.as_deref(), Some("HULL_A"));
let names: Vec<&str> = hull_slot.options.iter().map(|o| o.upgrade_name.as_str()).collect();
assert_eq!(names, vec!["HULL_A", "HULL_B"]);
let a = &hull_slot.options[0];
let b = &hull_slot.options[1];
assert!(a.is_baseline);
assert!(!b.is_baseline);
assert_eq!(a.stats.durability.as_ref().unwrap().health.unwrap().value(), 19400.0);
assert_eq!(b.stats.durability.as_ref().unwrap().health.unwrap().value(), 21000.0);
assert_eq!(opts.slots.len(), 1);
}
#[test]
fn non_vehicle_yields_empty() {
let proj = Param::builder()
.id(GameParamId::from(1u32))
.index("S0001".to_string())
.name("X".to_string())
.nation("USA".to_string())
.data(ParamData::Unit(crate::game_params::types::Unit::new(None)))
.build();
let provider = EmptyProvider;
let opts = module_options(
&proj,
&ShipUpgradeSelection::default(),
&ModifierBundle::empty(Species::Cruiser),
10,
&provider,
);
assert!(opts.slots.is_empty());
}
#[test]
fn enumerates_multiple_slots_in_order() {
let ship = hull_and_engine_ship();
let baseline = ShipUpgradeSelection::stock(&ship);
let provider = EmptyProvider;
let opts = module_options(&ship, &baseline, &ModifierBundle::empty(Species::Cruiser), 10, &provider);
let slots: Vec<ModuleSlot> = opts.slots.iter().map(|s| s.slot).collect();
assert_eq!(slots, vec![ModuleSlot::Hull, ModuleSlot::Engine]);
assert_eq!(opts.slots[0].options.len(), 2);
let eng = &opts.slots[1];
assert_eq!(eng.options.len(), 1);
assert_eq!(eng.baseline.as_deref(), Some("ENG_A"));
assert!(eng.options[0].is_baseline);
}
#[test]
fn module_diff_renders_hull_change() {
use crate::game_params::ttx::render::diff_stat_rows;
let ship = two_hull_ship();
let baseline = ShipUpgradeSelection::stock(&ship);
let provider = EmptyProvider;
let opts = module_options(&ship, &baseline, &ModifierBundle::empty(Species::Cruiser), 10, &provider);
let hull = opts.slots.iter().find(|s| s.slot == ModuleSlot::Hull).expect("hull slot");
let base_opt = hull.options.iter().find(|o| o.is_baseline).expect("baseline (HULL_A)");
let cand_opt = hull.options.iter().find(|o| !o.is_baseline).expect("non-baseline (HULL_B)");
let loader = EchoLoader;
let deltas = diff_stat_rows(&base_opt.stats.rows(), &cand_opt.stats.rows(), &loader);
assert_eq!(deltas.len(), 1);
assert_eq!(deltas[0].stat, TtxStat::Health);
assert!(deltas[0].from.is_some() && deltas[0].to.is_some() && deltas[0].from != deltas[0].to);
}
}