use std::collections::BTreeMap;
use std::collections::HashMap;
use std::collections::HashSet;
use bon::Builder;
use crate::Rc;
use crate::data::ResourceLoader;
use crate::data::Version;
use crate::game_types::GameParamId;
use crate::game_types::GunId;
use crate::game_types::Vec2;
use crate::game_types::Vec3;
pub const CAPTAIN_SKILL_REWORK_VERSION: (u32, u32, u32) = (0, 10, 0);
use super::provider::GameMetadataProvider;
pub use wows_core::units::BigWorldDistance;
pub use wows_core::units::Km;
pub use wows_core::units::Meters;
pub use wows_core::units::Millimeters;
pub use wows_core::units::ShipModelDistance;
pub type ArmorMap = HashMap<u32, BTreeMap<u32, f32>>;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct CrewSkillType(u32);
impl CrewSkillType {
pub fn new(value: u32) -> Self {
Self(value)
}
pub fn raw(self) -> u32 {
self.0
}
}
impl From<u8> for CrewSkillType {
fn from(v: u8) -> Self {
Self(v as u32)
}
}
impl From<u32> for CrewSkillType {
fn from(v: u32) -> Self {
Self(v)
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct CrewSkillName(String);
impl CrewSkillName {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<String> for CrewSkillName {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for CrewSkillName {
fn from(s: &str) -> Self {
Self(s.to_owned())
}
}
impl std::fmt::Display for CrewSkillName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct SkillPointCost(u8);
impl SkillPointCost {
pub fn new(cost: u8) -> Self {
Self(cost)
}
pub fn from_grid_row(row: u8) -> Self {
Self(row + 1)
}
pub fn get(self) -> u8 {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum KnownCrewSkill {
IncomingFireAlert,
Dazzle,
InertiaFuse,
MainBatteryAndAaSpecialist,
BasicsOfSurvivability,
GreaseTheGears,
FillTheTubes,
EmergencyRepairSpecialist,
ConsumableEnhancements,
Vigilance,
DemolitionExpert,
MainBatteryAndAaExpert,
AircraftArmor,
ImprovedEngineBoost,
ConcealmentExpert,
ConsumableSpecialist,
FirePreventionExpert,
SightStabilization,
ImprovedEngines,
Superintendent,
PreventiveMaintenance,
LastStand,
GunFeeder,
Interceptor,
AdrenalineRush,
SwiftFish,
SurvivabilityExpert,
ManualSecondaryBatteryAiming,
FocusFireTraining,
PriorityTarget,
AirSupremacy,
PackAPunch,
DirectionCenterForFighters,
EngineTechie,
RadioLocation,
AaDefenseAndAswExpert,
SecondaryArmamentExpert,
SuperHeavyApShells,
HeavyApShells,
ExtraHeavyAmmunition,
LongRangeSecondaryBatteryShells,
DefensiveFireExpert,
EmergencyRepairExpert,
EyeInTheSky,
ImprovedRepairPartyReadiness,
HiddenMenace,
Pyrotechnician,
HeavyHeAndSapShells,
EnhancedArmorPiercingAmmunition,
PatrolGroupLeader,
EnhancedReactions,
SearchAndDestroy,
RepairSpecialist,
EnhancedAircraftArmor,
BomberFlightControl,
LastGasp,
SurvivabilityExpertAircraft,
TorpedoBomber,
SwiftFlyingFish,
ProximityFuze,
Liquidator,
Brisk,
CloseQuartersCombat,
TopGradeGunner,
FearlessBrawler,
SwiftInSilence,
Outnumbered,
EnhancedSonar,
EnhancedImpulseGenerator,
Sonarman,
SonarmanExpert,
TorpedoCrewTraining,
TorpedoAimingMaster,
Helmsman,
ImprovedBatteryCapacity,
Watchful,
ImprovedBatteryEfficiency,
EnlargedPropellerShaft,
SubmarineConsumableSpecialist,
SubmarineConsumableEnhancements,
Furious,
SubmarineAdrenalineRush,
PlanesActiveManeuvering,
}
impl KnownCrewSkill {
pub fn recognize(
name: &CrewSkillName,
raw: CrewSkillType,
) -> crate::recognized::Recognized<KnownCrewSkill, CrewSkillType> {
use crate::recognized::Recognized::Known;
match name.as_str() {
"GmReloadAaDamageConstant" => Known(KnownCrewSkill::MainBatteryAndAaSpecialist),
"DefenceCritFireFlooding" => Known(KnownCrewSkill::BasicsOfSurvivability),
"GmTurn" => Known(KnownCrewSkill::GreaseTheGears),
"TorpedoReload" => Known(KnownCrewSkill::FillTheTubes),
"ConsumablesCrashcrewRegencrewReload" => Known(KnownCrewSkill::EmergencyRepairSpecialist),
"ConsumablesDuration" => Known(KnownCrewSkill::ConsumableEnhancements),
"DetectionTorpedoRange" => Known(KnownCrewSkill::Vigilance),
"HeFireProbability" => Known(KnownCrewSkill::DemolitionExpert),
"GmRangeAaDamageBubbles" => Known(KnownCrewSkill::MainBatteryAndAaExpert),
"PlanesDefenseDamageConstant" => Known(KnownCrewSkill::AircraftArmor),
"PlanesForsageDuration" => Known(KnownCrewSkill::ImprovedEngineBoost),
"DetectionVisibilityRange" => Known(KnownCrewSkill::ConcealmentExpert),
"ConsumablesReload" => Known(KnownCrewSkill::ConsumableSpecialist),
"DefenceFireProbability" => Known(KnownCrewSkill::FirePreventionExpert),
"PlanesAimingBoost" => Known(KnownCrewSkill::SightStabilization),
"PlanesSpeed" => Known(KnownCrewSkill::ImprovedEngines),
"ConsumablesAdditional" => Known(KnownCrewSkill::Superintendent),
"DefenseCritProbability" => Known(KnownCrewSkill::PreventiveMaintenance),
"DetectionAlert" => Known(KnownCrewSkill::IncomingFireAlert),
"Maneuverability" => Known(KnownCrewSkill::LastStand),
"GmShellReload" => Known(KnownCrewSkill::GunFeeder),
"PlanesConsumablesCallfightersUpgrade" => Known(KnownCrewSkill::Interceptor),
"ArmamentReloadAaDamage" => Known(KnownCrewSkill::AdrenalineRush),
"TorpedoSpeed" => Known(KnownCrewSkill::SwiftFish),
"DefenseHp" => Known(KnownCrewSkill::SurvivabilityExpert),
"AtbaAccuracy" => Known(KnownCrewSkill::ManualSecondaryBatteryAiming),
"AaPrioritysectorDamageConstant" => Known(KnownCrewSkill::FocusFireTraining),
"DetectionAiming" => Known(KnownCrewSkill::PriorityTarget),
"PlanesReload" => Known(KnownCrewSkill::AirSupremacy),
"TorpedoDamage" => Known(KnownCrewSkill::PackAPunch),
"ConsumablesFighterAdditional" => Known(KnownCrewSkill::DirectionCenterForFighters),
"PlanesConsumablesSpeedboosterReload" => Known(KnownCrewSkill::EngineTechie),
"HePenetration" => Known(KnownCrewSkill::InertiaFuse),
"DetectionDirection" => Known(KnownCrewSkill::RadioLocation),
"AaDamageConstantBubbles" => Known(KnownCrewSkill::AaDefenseAndAswExpert),
"AaDamageConstantBubblesCv" => Known(KnownCrewSkill::SecondaryArmamentExpert),
"ApDamageBb" => Known(KnownCrewSkill::SuperHeavyApShells),
"ApDamageCa" => Known(KnownCrewSkill::HeavyApShells),
"ApDamageDd" => Known(KnownCrewSkill::ExtraHeavyAmmunition),
"AtbaRange" => Known(KnownCrewSkill::LongRangeSecondaryBatteryShells),
"AtbaUpgrade" => Known(KnownCrewSkill::DefensiveFireExpert),
"ConsumablesCrashcrewRegencrewUpgrade" => Known(KnownCrewSkill::EmergencyRepairExpert),
"ConsumablesSpotterUpgrade" => Known(KnownCrewSkill::EyeInTheSky),
"DefenceUw" => Known(KnownCrewSkill::ImprovedRepairPartyReadiness),
"DetectionVisibilityCrashcrew" => Known(KnownCrewSkill::HiddenMenace),
"HeFireProbabilityCv" => Known(KnownCrewSkill::Pyrotechnician),
"HeSapDamage" => Known(KnownCrewSkill::HeavyHeAndSapShells),
"PlanesApDamage" => Known(KnownCrewSkill::EnhancedArmorPiercingAmmunition),
"PlanesConsumablesCallfightersAdditional" => Known(KnownCrewSkill::PatrolGroupLeader),
"PlanesConsumablesCallfightersPreparationtime" => Known(KnownCrewSkill::EnhancedReactions),
"PlanesConsumablesCallfightersRange" => Known(KnownCrewSkill::SearchAndDestroy),
"PlanesConsumablesRegeneratehealthUpgrade" => Known(KnownCrewSkill::RepairSpecialist),
"PlanesDefenseDamageBubbles" => Known(KnownCrewSkill::EnhancedAircraftArmor),
"PlanesDivebomberSpeed" => Known(KnownCrewSkill::BomberFlightControl),
"PlanesForsageRenewal" => Known(KnownCrewSkill::LastGasp),
"PlanesHp" => Known(KnownCrewSkill::SurvivabilityExpertAircraft),
"PlanesTorpedoArmingrange" => Known(KnownCrewSkill::TorpedoBomber),
"PlanesTorpedoSpeed" => Known(KnownCrewSkill::SwiftFlyingFish),
"PlanesTorpedoUwReduced" => Known(KnownCrewSkill::ProximityFuze),
"TorpedoFloodingProbability" => Known(KnownCrewSkill::Liquidator),
"TriggerSpeedBb" => Known(KnownCrewSkill::Brisk),
"TriggerGmAtbaReloadBb" => Known(KnownCrewSkill::CloseQuartersCombat),
"TriggerGmAtbaReloadCa" => Known(KnownCrewSkill::TopGradeGunner),
"TriggerGmReload" => Known(KnownCrewSkill::FearlessBrawler),
"TriggerSpeed" => Known(KnownCrewSkill::SwiftInSilence),
"TriggerSpeedAccuracy" => Known(KnownCrewSkill::Outnumbered),
"TriggerSpreading" => Known(KnownCrewSkill::Dazzle),
"TriggerPingerReloadBuff" => Known(KnownCrewSkill::EnhancedSonar),
"TriggerPingerSpeedBuff" => Known(KnownCrewSkill::EnhancedImpulseGenerator),
"SubmarineHoldSectors" => Known(KnownCrewSkill::Sonarman),
"TriggerConsSonarTimeCoeff" => Known(KnownCrewSkill::SonarmanExpert),
"TriggerSeenTorpedoReload" => Known(KnownCrewSkill::TorpedoCrewTraining),
"SubmarineTorpedoPingDamage" => Known(KnownCrewSkill::TorpedoAimingMaster),
"TriggerConsRudderTimeCoeff" => Known(KnownCrewSkill::Helmsman),
"SubmarineBatteryCapacity" => Known(KnownCrewSkill::ImprovedBatteryCapacity),
"SubmarineDangerAlert" => Known(KnownCrewSkill::Watchful),
"SubmarineBatteryBurnDown" => Known(KnownCrewSkill::ImprovedBatteryEfficiency),
"SubmarineSpeed" => Known(KnownCrewSkill::EnlargedPropellerShaft),
"SubmarineConsumablesReload" => Known(KnownCrewSkill::SubmarineConsumableSpecialist),
"SubmarineConsumablesDuration" => Known(KnownCrewSkill::SubmarineConsumableEnhancements),
"TriggerBurnGmReload" => Known(KnownCrewSkill::Furious),
"ArmamentReloadSubmarine" => Known(KnownCrewSkill::SubmarineAdrenalineRush),
"PlanesConsumablesActiveManeuveringUpgrade" => Known(KnownCrewSkill::PlanesActiveManeuvering),
_ => crate::recognized::Recognized::Unknown(raw),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub enum Species {
AAircraft,
AbilitiesUnit,
AirBase,
AirCarrier,
Airship,
AntiAircraft,
Artillery,
ArtilleryUnit,
Auxiliary,
Battleship,
Bomb,
Bomber,
BuildingType,
Camoboost,
Camouflage,
Campaign,
CoastalArtillery,
CollectionAlbum,
CollectionCard,
Complex,
Cruiser,
DCharge,
DeathSettings,
DepthCharge,
Destroyer,
Dive,
DiveBomberTypeUnit,
DogTagDoll,
DogTagItem,
DogTagSlotsScheme,
DogTagUnique,
Drop,
DropVisual,
EngineUnit,
Ensign,
Event,
Fake,
Fighter,
FighterTypeUnit,
FireControl,
Flags,
FlightControlUnit,
Generator,
GlobalWeather,
Globalboost,
Hull,
HullUnit,
IndividualTask,
Laser,
LocalWeather,
MSkin,
Main,
MapBorder,
Military,
Mine,
Mission,
Modifier,
Multiboost,
NewbieQuest,
Operation,
Permoflage,
PlaneTracer,
PrimaryWeaponsUnit,
RayTower,
Rocket,
Scout,
Search,
Secondary,
SecondaryWeaponsUnit,
SensorTower,
Sinking,
Skin,
Skip,
SkipBomb,
SkipBomberTypeUnit,
SonarUnit,
SpaceStation,
Submarine,
SuoUnit,
Task,
Torpedo,
TorpedoBomberTypeUnit,
TorpedoesUnit,
Upgrade,
Wave,
Null,
}
impl Species {
pub fn from_name(name: &str) -> crate::recognized::Recognized<Self> {
use crate::recognized::Recognized;
match name {
"AAircraft" => Recognized::Known(Self::AAircraft),
"AbilitiesUnit" => Recognized::Known(Self::AbilitiesUnit),
"AirBase" => Recognized::Known(Self::AirBase),
"AirCarrier" => Recognized::Known(Self::AirCarrier),
"Airship" => Recognized::Known(Self::Airship),
"AntiAircraft" => Recognized::Known(Self::AntiAircraft),
"Artillery" => Recognized::Known(Self::Artillery),
"ArtilleryUnit" => Recognized::Known(Self::ArtilleryUnit),
"Auxiliary" => Recognized::Known(Self::Auxiliary),
"Battleship" => Recognized::Known(Self::Battleship),
"Bomb" => Recognized::Known(Self::Bomb),
"Bomber" => Recognized::Known(Self::Bomber),
"BuildingType" => Recognized::Known(Self::BuildingType),
"Camoboost" => Recognized::Known(Self::Camoboost),
"Camouflage" => Recognized::Known(Self::Camouflage),
"Campaign" => Recognized::Known(Self::Campaign),
"CoastalArtillery" => Recognized::Known(Self::CoastalArtillery),
"CollectionAlbum" => Recognized::Known(Self::CollectionAlbum),
"CollectionCard" => Recognized::Known(Self::CollectionCard),
"Complex" => Recognized::Known(Self::Complex),
"Cruiser" => Recognized::Known(Self::Cruiser),
"DCharge" => Recognized::Known(Self::DCharge),
"DeathSettings" => Recognized::Known(Self::DeathSettings),
"DepthCharge" => Recognized::Known(Self::DepthCharge),
"Destroyer" => Recognized::Known(Self::Destroyer),
"Dive" => Recognized::Known(Self::Dive),
"DiveBomberTypeUnit" => Recognized::Known(Self::DiveBomberTypeUnit),
"DogTagDoll" => Recognized::Known(Self::DogTagDoll),
"DogTagItem" => Recognized::Known(Self::DogTagItem),
"DogTagSlotsScheme" => Recognized::Known(Self::DogTagSlotsScheme),
"DogTagUnique" => Recognized::Known(Self::DogTagUnique),
"Drop" => Recognized::Known(Self::Drop),
"DropVisual" => Recognized::Known(Self::DropVisual),
"EngineUnit" => Recognized::Known(Self::EngineUnit),
"Ensign" => Recognized::Known(Self::Ensign),
"Event" => Recognized::Known(Self::Event),
"Fake" => Recognized::Known(Self::Fake),
"Fighter" => Recognized::Known(Self::Fighter),
"FighterTypeUnit" => Recognized::Known(Self::FighterTypeUnit),
"Fire control" | "FireControl" => Recognized::Known(Self::FireControl),
"Flags" => Recognized::Known(Self::Flags),
"FlightControlUnit" => Recognized::Known(Self::FlightControlUnit),
"Generator" => Recognized::Known(Self::Generator),
"GlobalWeather" => Recognized::Known(Self::GlobalWeather),
"Globalboost" => Recognized::Known(Self::Globalboost),
"Hull" => Recognized::Known(Self::Hull),
"HullUnit" => Recognized::Known(Self::HullUnit),
"IndividualTask" => Recognized::Known(Self::IndividualTask),
"Laser" => Recognized::Known(Self::Laser),
"LocalWeather" => Recognized::Known(Self::LocalWeather),
"MSkin" => Recognized::Known(Self::MSkin),
"Main" => Recognized::Known(Self::Main),
"MapBorder" => Recognized::Known(Self::MapBorder),
"Military" => Recognized::Known(Self::Military),
"Mine" => Recognized::Known(Self::Mine),
"Mission" => Recognized::Known(Self::Mission),
"Modifier" => Recognized::Known(Self::Modifier),
"Multiboost" => Recognized::Known(Self::Multiboost),
"NewbieQuest" => Recognized::Known(Self::NewbieQuest),
"Operation" => Recognized::Known(Self::Operation),
"Permoflage" => Recognized::Known(Self::Permoflage),
"PlaneTracer" => Recognized::Known(Self::PlaneTracer),
"PrimaryWeaponsUnit" => Recognized::Known(Self::PrimaryWeaponsUnit),
"RayTower" => Recognized::Known(Self::RayTower),
"Rocket" => Recognized::Known(Self::Rocket),
"Scout" => Recognized::Known(Self::Scout),
"Search" => Recognized::Known(Self::Search),
"Secondary" => Recognized::Known(Self::Secondary),
"SecondaryWeaponsUnit" => Recognized::Known(Self::SecondaryWeaponsUnit),
"SensorTower" => Recognized::Known(Self::SensorTower),
"Sinking" => Recognized::Known(Self::Sinking),
"Skin" => Recognized::Known(Self::Skin),
"Skip" => Recognized::Known(Self::Skip),
"SkipBomb" => Recognized::Known(Self::SkipBomb),
"SkipBomberTypeUnit" => Recognized::Known(Self::SkipBomberTypeUnit),
"SonarUnit" => Recognized::Known(Self::SonarUnit),
"SpaceStation" => Recognized::Known(Self::SpaceStation),
"Submarine" => Recognized::Known(Self::Submarine),
"SuoUnit" => Recognized::Known(Self::SuoUnit),
"Task" => Recognized::Known(Self::Task),
"Torpedo" => Recognized::Known(Self::Torpedo),
"TorpedoBomberTypeUnit" => Recognized::Known(Self::TorpedoBomberTypeUnit),
"TorpedoesUnit" => Recognized::Known(Self::TorpedoesUnit),
"Upgrade" => Recognized::Known(Self::Upgrade),
"Wave" => Recognized::Known(Self::Wave),
"null" => Recognized::Known(Self::Null),
other => Recognized::Unknown(other.to_string()),
}
}
pub const fn name(&self) -> &'static str {
match self {
Self::AAircraft => "AAircraft",
Self::AbilitiesUnit => "AbilitiesUnit",
Self::AirBase => "AirBase",
Self::AirCarrier => "AirCarrier",
Self::Airship => "Airship",
Self::AntiAircraft => "AntiAircraft",
Self::Artillery => "Artillery",
Self::ArtilleryUnit => "ArtilleryUnit",
Self::Auxiliary => "Auxiliary",
Self::Battleship => "Battleship",
Self::Bomb => "Bomb",
Self::Bomber => "Bomber",
Self::BuildingType => "BuildingType",
Self::Camoboost => "Camoboost",
Self::Camouflage => "Camouflage",
Self::Campaign => "Campaign",
Self::CoastalArtillery => "CoastalArtillery",
Self::CollectionAlbum => "CollectionAlbum",
Self::CollectionCard => "CollectionCard",
Self::Complex => "Complex",
Self::Cruiser => "Cruiser",
Self::DCharge => "DCharge",
Self::DeathSettings => "DeathSettings",
Self::DepthCharge => "DepthCharge",
Self::Destroyer => "Destroyer",
Self::Dive => "Dive",
Self::DiveBomberTypeUnit => "DiveBomberTypeUnit",
Self::DogTagDoll => "DogTagDoll",
Self::DogTagItem => "DogTagItem",
Self::DogTagSlotsScheme => "DogTagSlotsScheme",
Self::DogTagUnique => "DogTagUnique",
Self::Drop => "Drop",
Self::DropVisual => "DropVisual",
Self::EngineUnit => "EngineUnit",
Self::Ensign => "Ensign",
Self::Event => "Event",
Self::Fake => "Fake",
Self::Fighter => "Fighter",
Self::FighterTypeUnit => "FighterTypeUnit",
Self::FireControl => "Fire control",
Self::Flags => "Flags",
Self::FlightControlUnit => "FlightControlUnit",
Self::Generator => "Generator",
Self::GlobalWeather => "GlobalWeather",
Self::Globalboost => "Globalboost",
Self::Hull => "Hull",
Self::HullUnit => "HullUnit",
Self::IndividualTask => "IndividualTask",
Self::Laser => "Laser",
Self::LocalWeather => "LocalWeather",
Self::MSkin => "MSkin",
Self::Main => "Main",
Self::MapBorder => "MapBorder",
Self::Military => "Military",
Self::Mine => "Mine",
Self::Mission => "Mission",
Self::Modifier => "Modifier",
Self::Multiboost => "Multiboost",
Self::NewbieQuest => "NewbieQuest",
Self::Operation => "Operation",
Self::Permoflage => "Permoflage",
Self::PlaneTracer => "PlaneTracer",
Self::PrimaryWeaponsUnit => "PrimaryWeaponsUnit",
Self::RayTower => "RayTower",
Self::Rocket => "Rocket",
Self::Scout => "Scout",
Self::Search => "Search",
Self::Secondary => "Secondary",
Self::SecondaryWeaponsUnit => "SecondaryWeaponsUnit",
Self::SensorTower => "SensorTower",
Self::Sinking => "Sinking",
Self::Skin => "Skin",
Self::Skip => "Skip",
Self::SkipBomb => "SkipBomb",
Self::SkipBomberTypeUnit => "SkipBomberTypeUnit",
Self::SonarUnit => "SonarUnit",
Self::SpaceStation => "SpaceStation",
Self::Submarine => "Submarine",
Self::SuoUnit => "SuoUnit",
Self::Task => "Task",
Self::Torpedo => "Torpedo",
Self::TorpedoBomberTypeUnit => "TorpedoBomberTypeUnit",
Self::TorpedoesUnit => "TorpedoesUnit",
Self::Upgrade => "Upgrade",
Self::Wave => "Wave",
Self::Null => "null",
}
}
pub fn translation_id(&self) -> String {
format!("IDS_{}", self.name())
}
}
#[derive(Builder, Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct Param {
id: GameParamId,
index: String,
name: String,
species: Option<crate::recognized::Recognized<Species>>,
nation: String,
data: ParamData,
}
impl Param {
pub fn id(&self) -> GameParamId {
self.id
}
pub fn index(&self) -> &str {
self.index.as_ref()
}
pub fn name(&self) -> &str {
self.name.as_ref()
}
pub fn species(&self) -> Option<&crate::recognized::Recognized<Species>> {
self.species.as_ref()
}
pub fn nation(&self) -> &str {
self.nation.as_ref()
}
pub fn data(&self) -> &ParamData {
&self.data
}
pub fn aircraft(&self) -> Option<&Aircraft> {
match &self.data {
ParamData::Aircraft(a) => Some(a),
_ => None,
}
}
pub fn vehicle(&self) -> Option<&Vehicle> {
match &self.data {
ParamData::Vehicle(v) => Some(v),
_ => None,
}
}
pub fn ability(&self) -> Option<&Ability> {
match &self.data {
ParamData::Ability(a) => Some(a),
_ => None,
}
}
pub fn projectile(&self) -> Option<&Projectile> {
match &self.data {
ParamData::Projectile(p) => Some(p),
_ => None,
}
}
pub fn crew(&self) -> Option<&Crew> {
match &self.data {
ParamData::Crew(c) => Some(c),
_ => None,
}
}
pub fn modernization(&self) -> Option<&Modernization> {
match &self.data {
ParamData::Modernization(m) => Some(m),
_ => None,
}
}
pub fn drop_data(&self) -> Option<&BuffDrop> {
match &self.data {
ParamData::Drop(d) => Some(d),
_ => None,
}
}
pub fn exterior(&self) -> Option<&Exterior> {
match &self.data {
ParamData::Exterior(e) => Some(e),
_ => None,
}
}
pub fn building(&self) -> Option<&Building> {
match &self.data {
ParamData::Building(b) => Some(b),
_ => None,
}
}
}
#[derive(PartialEq, Eq, Hash, Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub enum ParamType {
Ability,
Achievement,
AdjustmentShotActivator,
Aircraft,
BattleScript,
Building,
Campaign,
Catapult,
ClanSupply,
Collection,
Component,
Crew,
Director,
DogTag,
Drop,
EventTrigger,
Exterior,
Finder,
Gun,
Modernization,
Other,
Projectile,
Radar,
RageModeProgressAction,
Reward,
RibbonActivator,
Sfx,
Ship,
SwitchTrigger,
SwitchVehicleVisualStateAction,
TimerActivator,
ToggleTriggerAction,
Unit,
VisibilityChangedActivator,
}
impl ParamType {
pub fn from_name(name: &str) -> Option<Self> {
match name {
"Ability" => Some(Self::Ability),
"Achievement" => Some(Self::Achievement),
"AdjustmentShotActivator" => Some(Self::AdjustmentShotActivator),
"Aircraft" => Some(Self::Aircraft),
"BattleScript" => Some(Self::BattleScript),
"Building" => Some(Self::Building),
"Campaign" => Some(Self::Campaign),
"Catapult" => Some(Self::Catapult),
"ClanSupply" => Some(Self::ClanSupply),
"Collection" => Some(Self::Collection),
"Component" => Some(Self::Component),
"Crew" => Some(Self::Crew),
"Director" => Some(Self::Director),
"DogTag" => Some(Self::DogTag),
"Drop" => Some(Self::Drop),
"EventTrigger" => Some(Self::EventTrigger),
"Exterior" => Some(Self::Exterior),
"Finder" => Some(Self::Finder),
"Gun" => Some(Self::Gun),
"Modernization" => Some(Self::Modernization),
"Other" => Some(Self::Other),
"Projectile" => Some(Self::Projectile),
"Radar" => Some(Self::Radar),
"RageModeProgressAction" => Some(Self::RageModeProgressAction),
"Reward" => Some(Self::Reward),
"RibbonActivator" => Some(Self::RibbonActivator),
"Sfx" => Some(Self::Sfx),
"Ship" => Some(Self::Ship),
"SwitchTrigger" => Some(Self::SwitchTrigger),
"SwitchVehicleVisualStateAction" => Some(Self::SwitchVehicleVisualStateAction),
"TimerActivator" => Some(Self::TimerActivator),
"ToggleTriggerAction" => Some(Self::ToggleTriggerAction),
"Unit" => Some(Self::Unit),
"VisibilityChangedActivator" => Some(Self::VisibilityChangedActivator),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub enum MountSpecies {
Main,
Secondary,
AAircraft,
Torpedo,
DCharge,
FireControl,
Search,
MissileGun,
Decoration,
}
impl MountSpecies {
pub fn from_gp_str(s: &str) -> Option<Self> {
match s {
"Main" => Some(Self::Main),
"Secondary" => Some(Self::Secondary),
"AAircraft" => Some(Self::AAircraft),
"Torpedo" => Some(Self::Torpedo),
"DCharge" => Some(Self::DCharge),
"Fire control" => Some(Self::FireControl),
"Search" => Some(Self::Search),
"MissileGun" => Some(Self::MissileGun),
"Decoration" => Some(Self::Decoration),
_ => None,
}
}
pub fn display_group(&self) -> &'static str {
match self {
Self::Main => "Main Battery",
Self::Secondary => "Secondary Battery",
Self::AAircraft => "AA Guns",
Self::Torpedo => "Torpedoes",
Self::DCharge => "Depth Charges",
Self::FireControl => "Fire Control",
Self::Search => "Radar",
Self::MissileGun => "Missiles",
Self::Decoration => "Decorations",
}
}
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct MountPoint {
hp_name: String,
model_path: String,
#[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))]
mount_armor: Option<ArmorMap>,
#[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))]
species: Option<MountSpecies>,
#[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Vec::is_empty"))]
pitch_dead_zones: Vec<[f32; 4]>,
}
impl MountPoint {
pub fn new(hp_name: String, model_path: String) -> Self {
Self { hp_name, model_path, mount_armor: None, species: None, pitch_dead_zones: Vec::new() }
}
pub fn with_armor(
hp_name: String,
model_path: String,
mount_armor: Option<ArmorMap>,
species: Option<MountSpecies>,
pitch_dead_zones: Vec<[f32; 4]>,
) -> Self {
Self { hp_name, model_path, mount_armor, species, pitch_dead_zones }
}
pub fn hp_name(&self) -> &str {
&self.hp_name
}
pub fn species(&self) -> Option<MountSpecies> {
self.species
}
pub fn model_path(&self) -> &str {
&self.model_path
}
pub fn mount_armor(&self) -> Option<&ArmorMap> {
self.mount_armor.as_ref()
}
pub fn pitch_dead_zones(&self) -> &[[f32; 4]] {
&self.pitch_dead_zones
}
pub fn min_pitch_at_yaw(&self, yaw_deg: f32) -> f32 {
for dz in &self.pitch_dead_zones {
let [yaw_min, yaw_max, pitch_min, _pitch_max] = *dz;
if yaw_deg >= yaw_min && yaw_deg <= yaw_max {
return pitch_min;
}
}
0.0
}
}
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct ComponentMounts {
component_name: String,
mounts: Vec<MountPoint>,
}
impl ComponentMounts {
pub fn new(component_name: String, mounts: Vec<MountPoint>) -> Self {
Self { component_name, mounts }
}
pub fn component_name(&self) -> &str {
&self.component_name
}
pub fn mounts(&self) -> &[MountPoint] {
&self.mounts
}
}
#[derive(Clone, Builder, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct Exterior {
#[cfg_attr(feature = "serde", serde(default))]
camouflage: Option<String>,
#[cfg_attr(feature = "serde", serde(default))]
title: Option<String>,
#[cfg_attr(feature = "serde", serde(default))]
modifiers: Vec<CrewSkillModifier>,
}
impl Exterior {
pub fn camouflage(&self) -> Option<&str> {
self.camouflage.as_deref()
}
pub fn title(&self) -> Option<&str> {
self.title.as_deref()
}
pub fn modifiers(&self) -> &[CrewSkillModifier] {
&self.modifiers
}
}
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct HullUpgradeConfig {
pub detection_km: Km,
pub air_detection_km: Km,
#[cfg_attr(feature = "serde", serde(default))]
pub component_names: HashMap<super::keys::ComponentType, String>,
#[cfg_attr(feature = "serde", serde(default))]
pub hull_model_path: Option<String>,
#[cfg_attr(feature = "serde", serde(default))]
pub draft: Option<Meters>,
#[cfg_attr(feature = "serde", serde(default))]
pub dock_y_offset: Option<f32>,
#[cfg_attr(feature = "serde", serde(default))]
pub mounts_by_type: HashMap<super::keys::ComponentType, ComponentMounts>,
#[cfg_attr(feature = "serde", serde(default))]
pub component_alternatives: HashMap<super::keys::ComponentType, Vec<String>>,
#[cfg_attr(feature = "serde", serde(default))]
pub alternative_mounts: HashMap<String, ComponentMounts>,
}
impl HullUpgradeConfig {
pub fn component_name(&self, ct: super::keys::ComponentType) -> Option<&str> {
self.component_names.get(&ct).map(|s| s.as_str())
}
pub fn hull_model_path(&self) -> Option<&str> {
self.hull_model_path.as_deref()
}
pub fn draft(&self) -> Option<Meters> {
self.draft
}
pub fn dock_y_offset(&self) -> Option<f32> {
self.dock_y_offset
}
pub fn mounts(&self, ct: super::keys::ComponentType) -> Option<&[MountPoint]> {
self.mounts_by_type.get(&ct).map(|cm| cm.mounts())
}
pub fn all_mount_points(&self) -> impl Iterator<Item = &MountPoint> {
self.mounts_by_type.values().flat_map(|cm| cm.mounts.iter())
}
pub fn alternatives(&self, ct: super::keys::ComponentType) -> &[String] {
self.component_alternatives.get(&ct).map(|v| v.as_slice()).unwrap_or(&[])
}
pub fn mount_points_with_overrides<'a>(
&'a self,
overrides: &'a HashMap<super::keys::ComponentType, String>,
) -> impl Iterator<Item = &'a MountPoint> {
self.mounts_by_type.iter().flat_map(move |(ct_key, default_cm)| {
if let Some(override_name) = overrides.get(ct_key) {
if override_name != &default_cm.component_name
&& let Some(alt_cm) = self.alternative_mounts.get(override_name)
{
return alt_cm.mounts.iter();
}
}
default_cm.mounts.iter()
})
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct CameraTrajectory {
pub pos_center: [Vec3; 2],
pub semi_axes: [Vec2; 2],
pub tags: String,
pub ignore_height_multiplier: bool,
#[cfg_attr(feature = "serde", serde(default))]
pub outer: Option<TrajectoryGeometry>,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct TrajectoryGeometry {
pub pos_center: [Vec3; 2],
pub semi_axes: [Vec2; 2],
pub ignore_height_multiplier: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct CameraRing {
pub pos_center: Vec3,
pub semi_axes: Vec2,
}
const HEIGHT_TAGS: &[u8] = b"AMTOPBb";
const INNER_ONLY_TAGS: &[u8] = b"BbP";
const HEIGHT_COEF: f32 = 2.0;
impl CameraTrajectory {
fn height_applies(&self, ignore_height_multiplier: bool) -> bool {
!ignore_height_multiplier
&& self.tags.bytes().any(|c| HEIGHT_TAGS.contains(&c))
&& !self.tags.bytes().any(|c| INNER_ONLY_TAGS.contains(&c))
}
fn blend_ring(pos_center: &[Vec3; 2], semi_axes: &[Vec2; 2], fov: f32) -> CameraRing {
let f = fov.clamp(0.0, 1.0);
CameraRing { pos_center: pos_center[0].lerp(pos_center[1], f), semi_axes: semi_axes[0].lerp(semi_axes[1], f) }
}
pub fn ring(&self, fov: f32) -> CameraRing {
Self::blend_ring(&self.pos_center, &self.semi_axes, fov)
}
pub fn resolve(&self, fov: f32, height: f32) -> CameraRing {
let mut r = self.ring(fov);
if self.height_applies(self.ignore_height_multiplier) {
r.pos_center.y += HEIGHT_COEF * height;
}
r
}
pub fn outer_ring(&self, fov: f32) -> Option<CameraRing> {
let o = self.outer.as_ref()?;
Some(Self::blend_ring(&o.pos_center, &o.semi_axes, fov))
}
pub fn resolve_outer(&self, fov: f32, height: f32) -> Option<CameraRing> {
let o = self.outer.as_ref()?;
let mut r = self.outer_ring(fov)?;
if self.height_applies(o.ignore_height_multiplier) {
r.pos_center.y += HEIGHT_COEF * height;
}
Some(r)
}
}
#[cfg(test)]
mod camera_ring_tests {
use super::*;
fn traj(tags: &str, ignore: bool) -> CameraTrajectory {
CameraTrajectory {
pos_center: [Vec3::new(0.0, 1.958, 0.0), Vec3::new(0.0, 2.008, 0.0)],
semi_axes: [Vec2::new(6.552, 9.6), Vec2::new(5.981, 9.6)],
tags: tags.to_string(),
ignore_height_multiplier: ignore,
outer: None,
}
}
#[test]
fn fov0_is_inner() {
let r = traj("AT", false).resolve(0.0, 0.0);
assert!((r.semi_axes.x - 6.552).abs() < 1e-4 && (r.pos_center.y - 1.958).abs() < 1e-4);
}
#[test]
fn fov1_is_outer() {
let r = traj("AT", false).resolve(1.0, 0.0);
assert!((r.semi_axes.x - 5.981).abs() < 1e-4 && (r.pos_center.y - 2.008).abs() < 1e-4);
}
#[test]
fn height_applies_for_artillery() {
let r = traj("AT", false).resolve(0.0, 1.0);
assert!((r.pos_center.y - (1.958 + 2.0)).abs() < 1e-4);
}
#[test]
fn height_skipped_for_periscope() {
let r = traj("B", false).resolve(0.0, 1.0);
assert!((r.pos_center.y - 1.958).abs() < 1e-4);
}
#[test]
fn height_skipped_when_ignored() {
let r = traj("AT", true).resolve(0.0, 1.0);
assert!((r.pos_center.y - 1.958).abs() < 1e-4);
}
fn traj_with_outer() -> CameraTrajectory {
let mut t = traj("AT", false);
t.outer = Some(TrajectoryGeometry {
pos_center: [Vec3::new(0.0, 4.199, 0.0), Vec3::new(0.0, 6.023, 0.0)],
semi_axes: [Vec2::new(18.785, 18.785), Vec2::new(17.315, 17.315)],
ignore_height_multiplier: false,
});
t
}
#[test]
fn resolve_outer_absent_returns_none() {
assert!(traj("AT", false).resolve_outer(0.0, 0.0).is_none());
}
#[test]
fn resolve_outer_fov0_uses_first_endpoint() {
let r = traj_with_outer().resolve_outer(0.0, 0.0).expect("outer present");
assert!((r.semi_axes.x - 18.785).abs() < 1e-4);
assert!((r.pos_center.y - 4.199).abs() < 1e-4);
}
#[test]
fn resolve_outer_fov1_uses_second_endpoint() {
let r = traj_with_outer().resolve_outer(1.0, 0.0).expect("outer present");
assert!((r.semi_axes.y - 17.315).abs() < 1e-4);
assert!((r.pos_center.y - 6.023).abs() < 1e-4);
}
#[test]
fn resolve_outer_height_applies_for_artillery() {
let r = traj_with_outer().resolve_outer(0.0, 1.0).expect("outer present");
assert!((r.pos_center.y - (4.199 + 2.0)).abs() < 1e-4);
}
}
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct ShipConfigData {
pub hull_upgrades: HashMap<String, HullUpgradeConfig>,
pub main_battery_m: Option<Meters>,
pub secondary_battery_m: Option<Meters>,
pub torpedo_ammo: HashSet<String>,
pub main_battery_ammo: HashSet<String>,
pub secondary_battery_ammo: HashSet<String>,
pub secondary_guns: Vec<String>,
}
#[derive(Clone, Debug, Default)]
pub struct ShipRanges {
pub detection_km: Option<Km>,
pub air_detection_km: Option<Km>,
pub main_battery_m: Option<Meters>,
pub secondary_battery_m: Option<Meters>,
pub torpedo_range_m: Option<Meters>,
pub radar_m: Option<Meters>,
pub hydro_m: Option<Meters>,
}
#[derive(Clone, Builder, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct HitLocation {
max_hp: f32,
hl_type: String,
regenerated_hp_part: f32,
#[cfg_attr(feature = "serde", serde(default))]
thickness: f32,
#[cfg_attr(feature = "serde", serde(default))]
splash_boxes: Vec<String>,
}
impl HitLocation {
pub fn max_hp(&self) -> f32 {
self.max_hp
}
pub fn hl_type(&self) -> &str {
&self.hl_type
}
pub fn regenerated_hp_part(&self) -> f32 {
self.regenerated_hp_part
}
pub fn thickness(&self) -> f32 {
self.thickness
}
pub fn splash_boxes(&self) -> &[String] {
&self.splash_boxes
}
}
#[derive(Clone, Builder, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct Vehicle {
level: u32,
group: String,
abilities: Option<Vec<Vec<(String, String)>>>,
#[cfg_attr(feature = "serde", serde(default))]
upgrades: Vec<String>,
#[cfg_attr(feature = "serde", serde(default))]
config_data: Option<ShipConfigData>,
#[cfg_attr(feature = "serde", serde(default))]
model_path: Option<String>,
#[cfg_attr(feature = "serde", serde(default))]
armor: Option<ArmorMap>,
#[cfg_attr(feature = "serde", serde(default))]
hit_locations: Option<HashMap<String, HitLocation>>,
#[cfg_attr(feature = "serde", serde(default))]
permoflages: Vec<String>,
#[cfg_attr(feature = "serde", serde(default))]
camera_trajectories: Vec<(String, CameraTrajectory)>,
}
impl Vehicle {
pub fn level(&self) -> u32 {
self.level
}
pub fn group(&self) -> &str {
self.group.as_ref()
}
pub fn abilities(&self) -> Option<&[Vec<(String, String)>]> {
self.abilities.as_deref()
}
pub fn upgrades(&self) -> &[String] {
self.upgrades.as_slice()
}
pub fn config_data(&self) -> Option<&ShipConfigData> {
self.config_data.as_ref()
}
pub fn model_path(&self) -> Option<&str> {
self.model_path.as_deref()
}
pub fn armor(&self) -> Option<&ArmorMap> {
self.armor.as_ref()
}
pub fn hit_locations(&self) -> Option<&HashMap<String, HitLocation>> {
self.hit_locations.as_ref()
}
pub fn permoflages(&self) -> &[String] {
&self.permoflages
}
pub fn camera_trajectories(&self) -> &[(String, CameraTrajectory)] {
&self.camera_trajectories
}
pub fn hull_upgrade(&self, name: &str) -> Option<&HullUpgradeConfig> {
self.config_data.as_ref()?.hull_upgrades.get(name)
}
pub fn hull_upgrades(&self) -> Option<&HashMap<String, HullUpgradeConfig>> {
self.config_data.as_ref().map(|c| &c.hull_upgrades)
}
pub fn model_path_for_hull(&self, upgrade_name: &str) -> Option<&str> {
self.hull_upgrade(upgrade_name)?.hull_model_path()
}
pub fn mounts_for_hull(&self, upgrade_name: &str) -> Vec<&MountPoint> {
self.hull_upgrade(upgrade_name).map(|c| c.all_mount_points().collect()).unwrap_or_default()
}
pub fn resolve_ranges(
&self,
game_params: Option<&dyn GameParamProvider>,
hull_name: Option<&str>,
version: crate::data::Version,
) -> ShipRanges {
let mut ranges = ShipRanges::default();
if let Some(config) = &self.config_data {
let hull_config = hull_name
.and_then(|name| config.hull_upgrades.get(name))
.or_else(|| config.hull_upgrades.values().next());
if let Some(hc) = hull_config {
ranges.detection_km = Some(hc.detection_km);
ranges.air_detection_km = Some(hc.air_detection_km);
ranges.main_battery_m = config.main_battery_m;
ranges.secondary_battery_m = config.secondary_battery_m;
if let Some(game_params) = game_params {
let mut max_range: Option<Meters> = None;
for ammo_name in &config.torpedo_ammo {
if let Some(ammo_param) = game_params.game_param_by_name(ammo_name)
&& let Some(projectile) = ammo_param.projectile()
&& let Some(dist) = projectile.max_dist()
{
let m = dist.to_meters();
max_range = Some(match max_range {
Some(prev) if prev.value() >= m.value() => prev,
_ => m,
});
}
}
ranges.torpedo_range_m = max_range;
}
}
}
if let (Some(game_params), Some(abilities)) = (game_params, &self.abilities) {
for slot in abilities {
for (ability_name, variant_name) in slot {
let param = match game_params.game_param_by_name(ability_name) {
Some(p) => p,
None => continue,
};
let ability = match param.ability() {
Some(a) => a,
None => continue,
};
let cat = match ability.get_category(variant_name) {
Some(c) => c,
None => continue,
};
match cat.consumable_type(version).known() {
Some(&crate::game_types::Consumable::Radar) => {
ranges.radar_m = cat.detection_radius();
}
Some(&crate::game_types::Consumable::HydroacousticSearch) => {
ranges.hydro_m = cat.detection_radius();
}
_ => {}
}
}
}
}
ranges
}
}
#[derive(Clone, Builder, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct AbilityCategory {
special_sound_id: Option<String>,
consumable_type: String,
group: String,
icon_id: String,
num_consumables: isize,
preparation_time: f32,
reload_time: f32,
work_time: f32,
#[cfg_attr(feature = "serde", serde(default))]
dist_ship: Option<BigWorldDistance>,
#[cfg_attr(feature = "serde", serde(default))]
dist_torpedo: Option<BigWorldDistance>,
#[cfg_attr(feature = "serde", serde(default))]
hydrophone_wave_radius: Option<Meters>,
#[cfg_attr(feature = "serde", serde(default))]
patrol_radius: Option<BigWorldDistance>,
#[cfg_attr(feature = "serde", serde(default))]
regeneration_hp_speed: Option<f32>,
#[cfg_attr(feature = "serde", serde(default))]
regeneration_hp_speed_units: Option<f32>,
}
impl AbilityCategory {
pub fn consumable_type_raw(&self) -> &str {
&self.consumable_type
}
pub fn consumable_type(
&self,
version: crate::data::Version,
) -> crate::recognized::Recognized<crate::game_types::Consumable> {
crate::game_types::Consumable::from_consumable_type(&self.consumable_type, version)
}
pub fn icon_id(&self) -> &str {
&self.icon_id
}
pub fn work_time(&self) -> f32 {
self.work_time
}
pub fn reload_time(&self) -> f32 {
self.reload_time
}
pub fn preparation_time(&self) -> f32 {
self.preparation_time
}
pub fn num_consumables(&self) -> isize {
self.num_consumables
}
pub fn detection_radius(&self) -> Option<Meters> {
self.hydrophone_wave_radius.or_else(|| self.dist_ship.map(|d| d.to_meters()))
}
pub fn torpedo_detection_radius(&self) -> Option<Meters> {
self.dist_torpedo.map(|d| d.to_meters())
}
pub fn patrol_radius(&self) -> Option<Meters> {
self.patrol_radius.map(|d| d.to_meters())
}
pub fn regeneration_hp_speed(&self) -> Option<f32> {
self.regeneration_hp_speed
}
pub fn regeneration_hp_speed_units(&self) -> Option<f32> {
self.regeneration_hp_speed_units
}
}
#[derive(Clone, Builder, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct Ability {
can_buy: bool,
cost_credits: isize,
cost_gold: isize,
is_free: bool,
categories: HashMap<String, AbilityCategory>,
}
impl Ability {
pub fn categories(&self) -> &HashMap<String, AbilityCategory> {
&self.categories
}
pub fn get_category(&self, name: &str) -> Option<&AbilityCategory> {
self.categories.get(name)
}
}
#[derive(Clone, Builder, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct CrewPersonalityShips {
groups: Vec<String>,
nation: Vec<String>,
peculiarity: Vec<String>,
ships: Vec<String>,
}
#[derive(Clone, Builder, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct CrewPersonality {
can_reset_skills_for_free: bool,
cost_credits: usize,
cost_elite_xp: usize,
cost_gold: usize,
cost_xp: usize,
has_custom_background: Option<bool>,
has_overlay: Option<bool>,
has_rank: Option<bool>,
has_sample_voiceover: Option<bool>,
is_animated: Option<bool>,
is_person: Option<bool>,
is_retrainable: Option<bool>,
is_unique: Option<bool>,
peculiarity: Option<String>,
permissions: Option<u32>,
person_name: String,
ships: CrewPersonalityShips,
subnation: Option<String>,
tags: Vec<String>,
}
#[derive(Clone, Builder, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct ConsumableReloadTimeModifier {
aircraft_carrier: f32,
auxiliary: f32,
battleship: f32,
cruiser: f32,
destroyer: f32,
submarine: f32,
}
impl ConsumableReloadTimeModifier {
pub fn get_for_species(&self, species: Species) -> f32 {
match species {
Species::AirCarrier => self.aircraft_carrier,
Species::Battleship => self.battleship,
Species::Cruiser => self.cruiser,
Species::Destroyer => self.destroyer,
Species::Submarine => self.submarine,
Species::Auxiliary => self.auxiliary,
other => panic!("Unexpected species {other:?}"),
}
}
pub fn aircraft_carrier(&self) -> f32 {
self.aircraft_carrier
}
pub fn auxiliary(&self) -> f32 {
self.auxiliary
}
pub fn battleship(&self) -> f32 {
self.battleship
}
pub fn cruiser(&self) -> f32 {
self.cruiser
}
pub fn destroyer(&self) -> f32 {
self.destroyer
}
pub fn submarine(&self) -> f32 {
self.submarine
}
}
#[derive(Clone, Builder, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct CrewSkillModifier {
name: String,
aircraft_carrier: f32,
auxiliary: f32,
battleship: f32,
cruiser: f32,
destroyer: f32,
submarine: f32,
#[cfg_attr(feature = "serde", serde(default))]
excluded_consumables: Vec<String>,
}
impl CrewSkillModifier {
pub fn name(&self) -> &str {
&self.name
}
pub fn get_for_species(&self, species: &Species) -> f32 {
match species {
Species::AirCarrier => self.aircraft_carrier,
Species::Battleship => self.battleship,
Species::Cruiser => self.cruiser,
Species::Destroyer => self.destroyer,
Species::Submarine => self.submarine,
Species::Auxiliary => self.auxiliary,
_ => 1.0,
}
}
pub fn excluded_consumables(&self) -> &[String] {
&self.excluded_consumables
}
pub fn excludes(&self, consumable_type: &str) -> bool {
self.excluded_consumables.iter().any(|t| t == consumable_type)
}
}
#[derive(Clone, Builder, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct CrewSkillLogicTrigger {
burn_count: Option<usize>,
change_priority_target_penalty: Option<f32>,
consumable_type: String,
cooling_delay: f32,
cooling_interpolator: Vec<()>,
divider_type: Option<String>,
divider_value: Option<f32>,
duration: f32,
energy_coeff: f32,
flood_count: Option<usize>,
health_factor: Option<f32>,
heat_interpolator: Vec<()>,
modifiers: Option<Vec<CrewSkillModifier>>,
trigger_desc_ids: String,
trigger_type: String,
}
impl CrewSkillLogicTrigger {
pub fn modifiers(&self) -> Option<&Vec<CrewSkillModifier>> {
self.modifiers.as_ref()
}
pub fn trigger_type(&self) -> &str {
&self.trigger_type
}
}
#[derive(Clone, Builder, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct CrewSkillTiers {
aircraft_carrier: SkillPointCost,
auxiliary: SkillPointCost,
battleship: SkillPointCost,
cruiser: SkillPointCost,
destroyer: SkillPointCost,
submarine: SkillPointCost,
}
impl CrewSkillTiers {
pub fn get_for_species(&self, species: Species) -> SkillPointCost {
match species {
Species::AirCarrier => self.aircraft_carrier,
Species::Battleship => self.battleship,
Species::Cruiser => self.cruiser,
Species::Destroyer => self.destroyer,
Species::Submarine => self.submarine,
Species::Auxiliary => self.auxiliary,
other => panic!("Unexpected species {other:?}"),
}
}
pub fn aircraft_carrier(&self) -> SkillPointCost {
self.aircraft_carrier
}
pub fn auxiliary(&self) -> SkillPointCost {
self.auxiliary
}
pub fn battleship(&self) -> SkillPointCost {
self.battleship
}
pub fn cruiser(&self) -> SkillPointCost {
self.cruiser
}
pub fn destroyer(&self) -> SkillPointCost {
self.destroyer
}
pub fn submarine(&self) -> SkillPointCost {
self.submarine
}
}
#[derive(Clone, Builder, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct CrewSkill {
internal_name: CrewSkillName,
logic_trigger: Option<CrewSkillLogicTrigger>,
can_be_learned: bool,
is_epic: bool,
modifiers: Option<Vec<CrewSkillModifier>>,
skill_type: CrewSkillType,
tier: CrewSkillTiers,
ui_treat_as_trigger: bool,
}
impl CrewSkill {
pub fn internal_name(&self) -> &CrewSkillName {
&self.internal_name
}
fn skill_translation_keys(&self, prefix: &str, version: &Version) -> (String, String) {
use convert_case::Case;
use convert_case::Casing;
let snake = format!("{prefix}_{}", self.internal_name().as_str().to_case(Case::UpperSnake));
let plain = format!("{prefix}_{}", self.internal_name().as_str().to_uppercase());
let rework = Version {
major: CAPTAIN_SKILL_REWORK_VERSION.0,
minor: CAPTAIN_SKILL_REWORK_VERSION.1,
patch: CAPTAIN_SKILL_REWORK_VERSION.2,
build: 0,
};
if version.is_at_least(&rework) { (snake, plain) } else { (plain, snake) }
}
pub fn translated_name(&self, metadata_provider: &GameMetadataProvider, version: &Version) -> Option<String> {
let (primary, fallback) = self.skill_translation_keys("IDS_SKILL", version);
metadata_provider
.localized_name_from_id(&primary)
.or_else(|| metadata_provider.localized_name_from_id(&fallback))
}
pub fn translated_description(
&self,
metadata_provider: &GameMetadataProvider,
version: &Version,
) -> Option<String> {
self.translated_description_with(metadata_provider, version)
}
fn translated_description_with(
&self,
metadata: &dyn crate::data::ResourceLoader,
version: &Version,
) -> Option<String> {
let (primary, fallback) = self.skill_translation_keys("IDS_SKILL_DESC", version);
let description =
metadata.localized_name_from_id(&primary).or_else(|| metadata.localized_name_from_id(&fallback));
description.and_then(|desc| if desc.is_empty() || desc == " " { None } else { Some(desc) })
}
pub fn description(&self, species: Species, metadata: &GameMetadataProvider, version: &Version) -> Option<String> {
self.description_with(species, metadata, version)
}
fn description_with(
&self,
species: Species,
metadata: &dyn crate::data::ResourceLoader,
version: &Version,
) -> Option<String> {
if let Some(d) = self.translated_description_with(metadata, version) {
return Some(d);
}
let build = version.build;
let mut lines: Vec<String> = Vec::new();
if let Some(mods) = self.modifiers() {
lines.extend(crate::game_params::modifier_settings_data::describe_modifiers(
build,
mods.iter().map(|m| (m.name(), m.get_for_species(&species))),
species,
metadata,
));
}
if let Some(trig) = self.logic_trigger() {
let sentence = metadata
.localized_name_from_id(&format!("IDS_SKILL_TRIGGER_{}", trig.trigger_type().to_uppercase()))
.and_then(|s| if s.is_empty() || s == " " { None } else { Some(s) });
let has_sentence = sentence.is_some();
if let Some(sentence) = sentence {
lines.push(sentence);
}
if let Some(tmods) = trig.modifiers() {
for line in crate::game_params::modifier_settings_data::describe_modifiers(
build,
tmods.iter().map(|m| (m.name(), m.get_for_species(&species))),
species,
metadata,
) {
if has_sentence {
lines.push(line);
} else {
lines.push(format!("{line} (when triggered)"));
}
}
}
}
(!lines.is_empty()).then(|| lines.join("\n"))
}
pub fn logic_trigger(&self) -> Option<&CrewSkillLogicTrigger> {
self.logic_trigger.as_ref()
}
pub fn can_be_learned(&self) -> bool {
self.can_be_learned
}
pub fn is_epic(&self) -> bool {
self.is_epic
}
pub fn modifiers(&self) -> Option<&Vec<CrewSkillModifier>> {
self.modifiers.as_ref()
}
pub fn skill_type(&self) -> CrewSkillType {
self.skill_type
}
pub fn tier(&self) -> &CrewSkillTiers {
&self.tier
}
pub fn ui_treat_as_trigger(&self) -> bool {
self.ui_treat_as_trigger
}
}
#[derive(Clone, Builder, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct Crew {
money_training_level: usize,
personality: CrewPersonality,
skills: Option<Vec<CrewSkill>>,
}
impl Crew {
pub fn skill_by_type(&self, typ: CrewSkillType) -> Option<&CrewSkill> {
self.skills.as_ref().and_then(|skills| skills.iter().find(|skill| skill.skill_type == typ))
}
pub fn skills(&self) -> Option<&[CrewSkill]> {
self.skills.as_deref()
}
}
#[derive(Clone, Builder, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct Achievement {
is_group: bool,
one_per_battle: bool,
ui_type: String,
ui_name: String,
}
impl Achievement {
pub fn is_group(&self) -> bool {
self.is_group
}
pub fn one_per_battle(&self) -> bool {
self.one_per_battle
}
pub fn ui_type(&self) -> &str {
&self.ui_type
}
pub fn ui_name(&self) -> &str {
&self.ui_name
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub enum PlaneCategory {
Consumable,
Airsupport,
#[default]
Controllable,
}
#[derive(Clone, Builder, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct Aircraft {
#[cfg_attr(feature = "serde", serde(default))]
category: PlaneCategory,
#[cfg_attr(feature = "serde", serde(default))]
ammo_type: String,
}
impl Aircraft {
pub fn category(&self) -> &PlaneCategory {
&self.category
}
pub fn ammo_type(&self) -> &str {
&self.ammo_type
}
pub fn effective_category(&self, species: Option<&Species>) -> PlaneCategory {
let is_airship = matches!(species, Some(Species::Airship | Species::Auxiliary));
if is_airship {
return PlaneCategory::Controllable;
}
if matches!(self.category, PlaneCategory::Controllable) && self.ammo_type.is_empty() {
return PlaneCategory::Consumable;
}
self.category.clone()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum AmmoType {
AP,
HE,
SAP,
Unknown(String),
}
impl AmmoType {
pub fn from_game_str(s: &str) -> Self {
match s {
"AP" => Self::AP,
"HE" => Self::HE,
"CS" => Self::SAP,
_ => Self::Unknown(s.to_string()),
}
}
pub fn display_name(&self) -> &str {
match self {
Self::AP => "AP",
Self::HE => "HE",
Self::SAP => "SAP",
Self::Unknown(s) => s,
}
}
pub fn sort_order(&self) -> u8 {
match self {
Self::AP => 0,
Self::HE => 1,
Self::SAP => 2,
Self::Unknown(_) => 3,
}
}
}
impl std::fmt::Display for AmmoType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.display_name())
}
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ShellInfo {
pub name: String,
pub ammo_type: AmmoType,
pub caliber: Millimeters,
pub he_pen_mm: Option<f32>,
pub sap_pen_mm: Option<f32>,
pub alpha_damage: f32,
pub muzzle_velocity: f32,
pub mass_kg: f32,
pub krupp: f32,
pub ricochet_angle: f32,
pub always_ricochet_angle: f32,
pub fuse_time: f32,
pub fuse_threshold: Option<f32>,
pub burn_prob: f32,
pub air_drag: f32,
pub normalization: Option<f32>,
pub cap: bool,
}
#[derive(Clone, Builder, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct Projectile {
#[cfg_attr(feature = "serde", serde(default))]
ammo_type: String,
#[cfg_attr(feature = "serde", serde(default))]
max_dist: Option<BigWorldDistance>,
#[cfg_attr(feature = "serde", serde(default))]
bullet_diametr: Option<f32>,
#[cfg_attr(feature = "serde", serde(default))]
bullet_mass: Option<f32>,
#[cfg_attr(feature = "serde", serde(default))]
bullet_speed: Option<f32>,
#[cfg_attr(feature = "serde", serde(default))]
bullet_krupp: Option<f32>,
#[cfg_attr(feature = "serde", serde(default))]
bullet_cap: Option<bool>,
#[cfg_attr(feature = "serde", serde(default))]
bullet_cap_normalize_max_angle: Option<f32>,
#[cfg_attr(feature = "serde", serde(default))]
bullet_detonator: Option<f32>,
#[cfg_attr(feature = "serde", serde(default))]
bullet_detonator_threshold: Option<f32>,
#[cfg_attr(feature = "serde", serde(default))]
bullet_ricochet_at: Option<f32>,
#[cfg_attr(feature = "serde", serde(default))]
bullet_always_ricochet_at: Option<f32>,
#[cfg_attr(feature = "serde", serde(default))]
alpha_piercing_he: Option<f32>,
#[cfg_attr(feature = "serde", serde(default))]
alpha_piercing_cs: Option<f32>,
#[cfg_attr(feature = "serde", serde(default))]
alpha_damage: Option<f32>,
#[cfg_attr(feature = "serde", serde(default))]
burn_prob: Option<f32>,
#[cfg_attr(feature = "serde", serde(default))]
bullet_air_drag: Option<f32>,
}
impl Projectile {
pub fn ammo_type(&self) -> &str {
&self.ammo_type
}
pub fn max_dist(&self) -> Option<BigWorldDistance> {
self.max_dist
}
pub fn bullet_diametr(&self) -> Option<f32> {
self.bullet_diametr
}
pub fn bullet_mass(&self) -> Option<f32> {
self.bullet_mass
}
pub fn bullet_speed(&self) -> Option<f32> {
self.bullet_speed
}
pub fn bullet_krupp(&self) -> Option<f32> {
self.bullet_krupp
}
pub fn bullet_cap(&self) -> Option<bool> {
self.bullet_cap
}
pub fn bullet_cap_normalize_max_angle(&self) -> Option<f32> {
self.bullet_cap_normalize_max_angle
}
pub fn bullet_detonator(&self) -> Option<f32> {
self.bullet_detonator
}
pub fn bullet_detonator_threshold(&self) -> Option<f32> {
self.bullet_detonator_threshold
}
pub fn bullet_ricochet_at(&self) -> Option<f32> {
self.bullet_ricochet_at
}
pub fn bullet_always_ricochet_at(&self) -> Option<f32> {
self.bullet_always_ricochet_at
}
pub fn alpha_piercing_he(&self) -> Option<f32> {
self.alpha_piercing_he
}
pub fn alpha_piercing_cs(&self) -> Option<f32> {
self.alpha_piercing_cs
}
pub fn alpha_damage(&self) -> Option<f32> {
self.alpha_damage
}
pub fn burn_prob(&self) -> Option<f32> {
self.burn_prob
}
pub fn bullet_air_drag(&self) -> Option<f32> {
self.bullet_air_drag
}
pub fn to_shell_info(&self, name: String) -> ShellInfo {
let caliber_mm = Millimeters::new(self.bullet_diametr.unwrap_or(0.0) * 1000.0);
ShellInfo {
name,
ammo_type: AmmoType::from_game_str(&self.ammo_type),
caliber: caliber_mm,
he_pen_mm: self.alpha_piercing_he,
sap_pen_mm: self.alpha_piercing_cs,
alpha_damage: self.alpha_damage.unwrap_or(0.0),
muzzle_velocity: self.bullet_speed.unwrap_or(0.0),
mass_kg: self.bullet_mass.unwrap_or(0.0),
krupp: self.bullet_krupp.unwrap_or(0.0),
ricochet_angle: self.bullet_ricochet_at.unwrap_or(45.0),
always_ricochet_angle: self.bullet_always_ricochet_at.unwrap_or(60.0),
fuse_time: self.bullet_detonator.unwrap_or(0.033),
fuse_threshold: self.bullet_detonator_threshold,
burn_prob: self.burn_prob.unwrap_or(-0.5),
air_drag: self.bullet_air_drag.unwrap_or(0.0),
normalization: self.bullet_cap_normalize_max_angle,
cap: self.bullet_cap.unwrap_or(true),
}
}
}
#[derive(Clone, Debug, Builder)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct BuffDrop {
#[cfg_attr(feature = "serde", serde(default))]
marker_name_active: String,
#[cfg_attr(feature = "serde", serde(default))]
marker_name_inactive: String,
#[cfg_attr(feature = "serde", serde(default))]
sorting: i64,
}
impl BuffDrop {
pub fn marker_name_active(&self) -> &str {
&self.marker_name_active
}
pub fn marker_name_inactive(&self) -> &str {
&self.marker_name_inactive
}
pub fn sorting(&self) -> i64 {
self.sorting
}
pub fn active_icon_path(&self) -> String {
format!("gui/powerups/drops/icon_marker_{}.png", self.marker_name_active)
}
pub fn inactive_icon_path(&self) -> String {
format!("gui/powerups/drops/icon_marker_{}.png", self.marker_name_inactive)
}
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct Modernization {
modifiers: Vec<CrewSkillModifier>,
slot: Option<u8>,
ship_levels: Vec<u32>,
ship_types: Vec<String>,
nations: Vec<String>,
groups: Vec<String>,
ships: Vec<String>,
excludes: Vec<String>,
}
impl Modernization {
#[allow(clippy::too_many_arguments)]
pub fn new(
modifiers: Vec<CrewSkillModifier>,
slot: Option<u8>,
ship_levels: Vec<u32>,
ship_types: Vec<String>,
nations: Vec<String>,
groups: Vec<String>,
ships: Vec<String>,
excludes: Vec<String>,
) -> Self {
Self { modifiers, slot, ship_levels, ship_types, nations, groups, ships, excludes }
}
pub fn modifiers(&self) -> &[CrewSkillModifier] {
&self.modifiers
}
pub fn slot(&self) -> Option<u8> {
self.slot
}
pub fn applies_to(
&self,
ship_name: &str,
ship_level: u32,
ship_species: &str,
ship_nation: &str,
ship_group: &str,
) -> bool {
if self.excludes.iter().any(|s| s == ship_name) {
return false;
}
let in_ships = self.ships.iter().any(|s| s == ship_name);
if !self.groups.iter().any(|g| g == ship_group) {
return in_ships;
}
if !self.nations.iter().any(|n| n == ship_nation) {
return in_ships;
}
if !self.ship_types.iter().any(|t| t == ship_species) {
return in_ships;
}
self.ship_levels.contains(&ship_level) || in_ships
}
}
pub fn modernization_slot_count(params: &[crate::Rc<Param>], ship: &Param) -> usize {
let Some(vehicle) = ship.vehicle() else { return 0 };
let Some(species) = ship.species().and_then(|r| r.known()) else { return 0 };
let species_name = species.name();
let ship_name = ship.name();
let level = vehicle.level();
let group = vehicle.group();
let nation = ship.nation();
let mut max_slot: i32 = -1;
for p in params {
let Some(m) = p.modernization() else { continue };
let Some(slot) = m.slot() else { continue };
if m.applies_to(ship_name, level, species_name, nation, group) {
max_slot = max_slot.max(slot as i32);
}
}
(max_slot + 1) as usize
}
#[cfg(test)]
mod modernization_tests {
use super::Modernization;
fn mk(
slot: Option<u8>,
levels: &[u32],
types: &[&str],
nations: &[&str],
groups: &[&str],
ships: &[&str],
excludes: &[&str],
) -> Modernization {
Modernization::new(
Vec::new(),
slot,
levels.to_vec(),
types.iter().map(|s| s.to_string()).collect(),
nations.iter().map(|s| s.to_string()).collect(),
groups.iter().map(|s| s.to_string()).collect(),
ships.iter().map(|s| s.to_string()).collect(),
excludes.iter().map(|s| s.to_string()).collect(),
)
}
#[test]
fn applies_respects_excludes_and_chain() {
let m = mk(
Some(2),
&[8, 9, 10],
&["Battleship"],
&["USA"],
&["upgradeable"],
&["PASB013_Arkansas_1912"],
&["PASB099_Excluded"],
);
assert!(!m.applies_to("PASB099_Excluded", 10, "Battleship", "USA", "upgradeable"));
assert!(m.applies_to("PASB017_Montana_1945", 10, "Battleship", "USA", "upgradeable"));
assert!(!m.applies_to("PASC001_Foo", 10, "Cruiser", "USA", "upgradeable"));
assert!(m.applies_to("PASB013_Arkansas_1912", 3, "Cruiser", "USA", "upgradeable"));
assert!(!m.applies_to("PASB500_LowTier", 5, "Battleship", "USA", "upgradeable"));
}
#[test]
fn empty_lists_fall_through_to_ships_not_match_all() {
let m = mk(Some(0), &[], &[], &[], &[], &["PASB013_Arkansas_1912"], &[]);
assert!(m.applies_to("PASB013_Arkansas_1912", 3, "Battleship", "USA", "upgradeable"));
assert!(!m.applies_to("PASB017_Montana_1945", 10, "Battleship", "USA", "upgradeable"));
let none = mk(Some(0), &[], &[], &[], &[], &[], &[]);
assert!(!none.applies_to("PASB017_Montana_1945", 10, "Battleship", "USA", "upgradeable"));
}
}
#[derive(Builder, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct Building {
level: u32,
health: f32,
}
impl Building {
pub fn level(&self) -> u32 {
self.level
}
pub fn health(&self) -> f32 {
self.health
}
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub enum ParamData {
Vehicle(Vehicle),
Crew(Crew),
Ability(Ability),
Achievement(Achievement),
Modernization(Modernization),
Exterior(Exterior),
Unit,
Aircraft(Aircraft),
Projectile(Projectile),
Drop(BuffDrop),
Building(Building),
}
variant_accessors!(ParamData {
tuple Vehicle(Vehicle) => vehicle;
tuple Crew(Crew) => crew;
tuple Ability(Ability) => ability;
tuple Achievement(Achievement) => achievement;
tuple Modernization(Modernization) => modernization;
tuple Exterior(Exterior) => exterior;
tuple Aircraft(Aircraft) => aircraft;
tuple Projectile(Projectile) => projectile;
tuple Drop(BuffDrop) => drop;
tuple Building(Building) => building;
unit Unit => unit;
});
pub trait GameParamProvider {
fn game_param_by_id(&self, id: GameParamId) -> Option<Rc<Param>>;
fn game_param_by_index(&self, index: &str) -> Option<Rc<Param>>;
fn game_param_by_name(&self, name: &str) -> Option<Rc<Param>>;
fn params(&self) -> &[Rc<Param>];
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct GameParams {
params: Vec<Rc<Param>>,
#[cfg_attr(feature = "serde", serde(skip))]
#[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Skip))]
id_to_params: HashMap<GameParamId, Rc<Param>>,
#[cfg_attr(feature = "serde", serde(skip))]
#[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Skip))]
index_to_params: HashMap<String, Rc<Param>>,
#[cfg_attr(feature = "serde", serde(skip))]
#[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Skip))]
name_to_params: HashMap<String, Rc<Param>>,
}
impl GameParamProvider for GameParams {
fn game_param_by_id(&self, id: GameParamId) -> Option<Rc<Param>> {
self.id_to_params.get(&id).cloned()
}
fn game_param_by_index(&self, index: &str) -> Option<Rc<Param>> {
self.index_to_params.get(index).cloned()
}
fn game_param_by_name(&self, name: &str) -> Option<Rc<Param>> {
self.name_to_params.get(name).cloned()
}
fn params(&self) -> &[Rc<Param>] {
self.params.as_slice()
}
}
struct ParamLookups {
by_id: HashMap<GameParamId, Rc<Param>>,
by_index: HashMap<String, Rc<Param>>,
by_name: HashMap<String, Rc<Param>>,
}
fn build_param_lookups(params: &[Rc<Param>]) -> ParamLookups {
let mut by_id = HashMap::with_capacity(params.len());
let mut by_index = HashMap::with_capacity(params.len());
let mut by_name = HashMap::with_capacity(params.len());
for param in params {
by_id.insert(param.id, param.clone());
by_index.insert(param.index.clone(), param.clone());
by_name.insert(param.name.clone(), param.clone());
}
ParamLookups { by_id, by_index, by_name }
}
impl<I> From<I> for GameParams
where
I: IntoIterator<Item = Param>,
{
fn from(value: I) -> Self {
let params: Vec<Rc<Param>> = value.into_iter().map(Rc::new).collect();
let lookups = build_param_lookups(params.as_ref());
Self { params, id_to_params: lookups.by_id, index_to_params: lookups.by_index, name_to_params: lookups.by_name }
}
}
pub fn secondary_ammo_param<P: GameParamProvider + ?Sized>(provider: &P, ship: GameParamId) -> Option<GameParamId> {
let ship_param = provider.game_param_by_id(ship)?;
let ammo_name = ship_param.vehicle()?.config_data()?.secondary_battery_ammo.iter().min()?;
Some(provider.game_param_by_name(ammo_name)?.id())
}
pub fn is_secondary_ammo<P: GameParamProvider + ?Sized>(provider: &P, ship: GameParamId, ammo: GameParamId) -> bool {
let Some(ship_param) = provider.game_param_by_id(ship) else {
return false;
};
let Some(config) = ship_param.vehicle().and_then(|v| v.config_data()) else {
return false;
};
let Some(ammo_param) = provider.game_param_by_id(ammo) else {
return false;
};
config.secondary_battery_ammo.contains(ammo_param.name())
}
pub fn secondary_gun_ammo_param<P: GameParamProvider + ?Sized>(
provider: &P,
ship: GameParamId,
gun: GunId,
) -> Option<GameParamId> {
let ship_param = provider.game_param_by_id(ship)?;
let guns = &ship_param.vehicle()?.config_data()?.secondary_guns;
let name = guns.get(gun.index()).filter(|n| !n.is_empty());
match name.and_then(|n| provider.game_param_by_name(n)) {
Some(p) => Some(p.id()),
None => secondary_ammo_param(provider, ship),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn secondary_ammo_strings_map_to_ammo_type() {
assert_eq!(AmmoType::from_game_str("HE"), AmmoType::HE);
assert_eq!(AmmoType::from_game_str("CS"), AmmoType::SAP);
}
fn shell_param(id: u32, name: &str) -> Param {
Param {
id: GameParamId::from(id),
index: name.to_string(),
name: name.to_string(),
species: None,
nation: String::new(),
data: ParamData::Unit,
}
}
#[test]
fn is_secondary_ammo_matches_only_the_atba_set() {
let ship_id = GameParamId::from(100u32);
let secondary_id = GameParamId::from(200u32);
let main_id = GameParamId::from(300u32);
let config = ShipConfigData {
secondary_battery_ammo: HashSet::from(["SECONDARY_SHELL".to_string()]),
..Default::default()
};
let vehicle = Vehicle {
level: 0,
group: String::new(),
abilities: None,
upgrades: Vec::new(),
config_data: Some(config),
model_path: None,
armor: None,
hit_locations: None,
permoflages: Vec::new(),
camera_trajectories: Vec::new(),
};
let ship = Param {
id: ship_id,
index: "ship".to_string(),
name: "SHIP".to_string(),
species: None,
nation: String::new(),
data: ParamData::Vehicle(vehicle),
};
let params = GameParams::from(vec![ship, shell_param(200, "SECONDARY_SHELL"), shell_param(300, "MAIN_SHELL")]);
assert!(is_secondary_ammo(¶ms, ship_id, secondary_id), "ATBA shell must classify as secondary");
assert!(!is_secondary_ammo(¶ms, ship_id, main_id), "main battery shell must not classify as secondary");
assert!(!is_secondary_ammo(¶ms, GameParamId::from(999u32), secondary_id));
assert!(!is_secondary_ammo(¶ms, ship_id, GameParamId::from(999u32)));
}
}
#[cfg(test)]
mod skill_type_tests {
use super::CrewSkillType;
#[test]
fn crew_skill_type_roundtrips_raw() {
let t = CrewSkillType::new(67);
assert_eq!(t.raw(), 67);
assert_eq!(CrewSkillType::from(67u8), CrewSkillType::new(67));
}
}
#[cfg(test)]
mod point_cost_tests {
use super::SkillPointCost;
#[test]
fn from_grid_row_is_one_based() {
assert_eq!(SkillPointCost::from_grid_row(0).get(), 1);
assert_eq!(SkillPointCost::from_grid_row(3).get(), 4);
}
}
#[cfg(test)]
mod skill_name_tests {
use super::CrewSkillName;
#[test]
fn as_str_and_display_match() {
let n = CrewSkillName::from("TriggerSpreading");
assert_eq!(n.as_str(), "TriggerSpreading");
assert_eq!(n.to_string(), "TriggerSpreading");
}
}
#[cfg(test)]
mod crew_skill_description_tests {
use super::*;
use crate::data::Version;
struct EchoLoader;
impl crate::data::ResourceLoader for EchoLoader {
fn localized_name_from_param(&self, _param: &Param) -> Option<String> {
None
}
fn localized_name_from_id(&self, id: &str) -> Option<String> {
Some(id.to_string())
}
fn game_param_by_id(&self, _id: crate::game_types::GameParamId) -> Option<crate::Rc<Param>> {
None
}
fn entity_specs(&self) -> &[crate::rpc::entitydefs::EntitySpec] {
&[]
}
}
struct ModifierOnlyLoader;
impl crate::data::ResourceLoader for ModifierOnlyLoader {
fn localized_name_from_param(&self, _param: &Param) -> Option<String> {
None
}
fn localized_name_from_id(&self, id: &str) -> Option<String> {
if id.starts_with("IDS_SKILL_DESC") || id.starts_with("IDS_SKILL_TRIGGER_") {
None
} else {
Some(id.to_string())
}
}
fn game_param_by_id(&self, _id: crate::game_types::GameParamId) -> Option<crate::Rc<Param>> {
None
}
fn entity_specs(&self) -> &[crate::rpc::entitydefs::EntitySpec] {
&[]
}
}
struct TriggerSentenceLoader;
impl crate::data::ResourceLoader for TriggerSentenceLoader {
fn localized_name_from_param(&self, _param: &Param) -> Option<String> {
None
}
fn localized_name_from_id(&self, id: &str) -> Option<String> {
if id.starts_with("IDS_SKILL_DESC") { None } else { Some(id.to_string()) }
}
fn game_param_by_id(&self, _id: crate::game_types::GameParamId) -> Option<crate::Rc<Param>> {
None
}
fn entity_specs(&self) -> &[crate::rpc::entitydefs::EntitySpec] {
&[]
}
}
const BUILD: u32 = 11791718;
fn version() -> Version {
Version { major: 13, minor: 0, patch: 0, build: BUILD }
}
fn tiers() -> CrewSkillTiers {
CrewSkillTiers::builder()
.aircraft_carrier(SkillPointCost::new(1))
.auxiliary(SkillPointCost::new(1))
.battleship(SkillPointCost::new(1))
.cruiser(SkillPointCost::new(1))
.destroyer(SkillPointCost::new(1))
.submarine(SkillPointCost::new(1))
.build()
}
fn uniform_modifier(name: &str, value: f32) -> CrewSkillModifier {
CrewSkillModifier::builder()
.name(name.to_owned())
.aircraft_carrier(value)
.auxiliary(value)
.battleship(value)
.cruiser(value)
.destroyer(value)
.submarine(value)
.excluded_consumables(Vec::new())
.build()
}
fn skill_with_modifiers(modifiers: Vec<CrewSkillModifier>) -> CrewSkill {
CrewSkill::builder()
.internal_name(CrewSkillName::from("GunFeeder"))
.can_be_learned(true)
.is_epic(false)
.skill_type(CrewSkillType::new(1))
.ui_treat_as_trigger(false)
.tier(tiers())
.modifiers(modifiers)
.build()
}
#[test]
fn static_description_wins_over_generated() {
let skill = skill_with_modifiers(vec![uniform_modifier("GMRotationSpeed", 0.9)]);
let out =
skill.description_with(Species::Battleship, &EchoLoader, &version()).expect("static description present");
assert!(out.contains("IDS_SKILL_DESC"), "expected static desc id, got {out}");
assert!(!out.contains("IDS_PARAMS_MODIFIER"), "should not generate, got {out}");
}
#[test]
fn generates_from_modifiers_when_static_absent() {
let skill = skill_with_modifiers(vec![uniform_modifier("GMRotationSpeed", 0.9)]);
let out = skill
.description_with(Species::Battleship, &ModifierOnlyLoader, &version())
.expect("modifier-generated description");
assert!(out.contains("IDS_PARAMS_MODIFIER_GMROTATIONSPEED"), "got {out}");
assert!(!out.contains("(when triggered)"), "plain modifier, got {out}");
}
fn trigger_with(trigger_type: &str, modifiers: Vec<CrewSkillModifier>) -> CrewSkillLogicTrigger {
CrewSkillLogicTrigger::builder()
.consumable_type(String::new())
.cooling_delay(0.0)
.cooling_interpolator(Vec::new())
.duration(0.0)
.energy_coeff(0.0)
.heat_interpolator(Vec::new())
.modifiers(modifiers)
.trigger_desc_ids(String::new())
.trigger_type(trigger_type.to_owned())
.build()
}
fn skill_with_trigger(trigger: CrewSkillLogicTrigger) -> CrewSkill {
CrewSkill::builder()
.internal_name(CrewSkillName::from("GunFeeder"))
.can_be_learned(true)
.is_epic(false)
.skill_type(CrewSkillType::new(1))
.ui_treat_as_trigger(false)
.tier(tiers())
.logic_trigger(trigger)
.build()
}
#[test]
fn trigger_modifiers_are_labeled_when_triggered_without_sentence() {
let trigger = trigger_with("activationOnDetectTrigger", vec![uniform_modifier("GMRotationSpeed", 1.1)]);
let skill = skill_with_trigger(trigger);
let out = skill
.description_with(Species::Battleship, &ModifierOnlyLoader, &version())
.expect("trigger-generated description");
assert!(out.contains("(when triggered)"), "got {out}");
assert!(out.contains("IDS_PARAMS_MODIFIER_GMROTATIONSPEED"), "got {out}");
}
#[test]
fn trigger_sentence_precedes_unsuffixed_modifier_lines() {
let trigger = trigger_with("activationOnDetectTrigger", vec![uniform_modifier("GMRotationSpeed", 1.1)]);
let skill = skill_with_trigger(trigger);
let out = skill
.description_with(Species::Battleship, &TriggerSentenceLoader, &version())
.expect("trigger-generated description");
let mut iter = out.lines();
assert_eq!(iter.next(), Some("IDS_SKILL_TRIGGER_ACTIVATIONONDETECTTRIGGER"), "sentence should lead, got {out}");
assert!(out.contains("IDS_PARAMS_MODIFIER_GMROTATIONSPEED"), "got {out}");
assert!(!out.contains("(when triggered)"), "suffix should be dropped, got {out}");
}
#[test]
fn no_text_yields_none() {
let skill = skill_with_modifiers(Vec::new());
assert!(skill.description_with(Species::Battleship, &ModifierOnlyLoader, &version()).is_none());
}
}
#[cfg(test)]
mod known_skill_tests {
use super::CrewSkillName;
use super::CrewSkillType;
use super::KnownCrewSkill;
use crate::recognized::Recognized;
#[test]
fn recognizes_known_skills_by_name() {
let dazzle = KnownCrewSkill::recognize(&CrewSkillName::from("TriggerSpreading"), CrewSkillType::new(67));
assert_eq!(dazzle, Recognized::Known(KnownCrewSkill::Dazzle));
let ifhe = KnownCrewSkill::recognize(&CrewSkillName::from("HePenetration"), CrewSkillType::new(33));
assert_eq!(ifhe, Recognized::Known(KnownCrewSkill::InertiaFuse));
let concealment =
KnownCrewSkill::recognize(&CrewSkillName::from("DetectionVisibilityRange"), CrewSkillType::new(14));
assert_eq!(concealment, Recognized::Known(KnownCrewSkill::ConcealmentExpert));
let sub_adrenaline =
KnownCrewSkill::recognize(&CrewSkillName::from("ArmamentReloadSubmarine"), CrewSkillType::new(82));
assert_eq!(sub_adrenaline, Recognized::Known(KnownCrewSkill::SubmarineAdrenalineRush));
}
#[test]
fn detection_alert_is_incoming_fire_alert() {
let ifa = KnownCrewSkill::recognize(&CrewSkillName::from("DetectionAlert"), CrewSkillType::new(19));
assert_eq!(ifa, Recognized::Known(KnownCrewSkill::IncomingFireAlert));
}
#[test]
fn unknown_skill_preserves_raw_type() {
let other = KnownCrewSkill::recognize(&CrewSkillName::from("NotARealSkill"), CrewSkillType::new(255));
assert_eq!(other, Recognized::Unknown(CrewSkillType::new(255)));
}
}