export module ra3.data; import std; export import ra3.core; /** * Faithful Red Alert 3 gameplay data. * * Every number in this module is taken from the retail game's own asset * definitions. Two sources back it, both auditable: * * 1. EA's open-sourced RA3 asset XML (`CnC_Modding_Support/Red Alert 3/Xml`, * GPLv3) - the `*.xml` that `BinaryAssetBuilder` compiles into `*.big`. * The `ra3_id` on each record is the originating `GameObject` id, so a * value can be looked up in `Allied/Structures/AlliedPowerPlant.xml`, * `Allied/Infantry/AlliedScoutInfantry.xml`, etc. * 2. The shipped `ArmorTemplate` percentages, cross-checked against the * compiled assets by the sibling `ra3-headless` extractor. * * The *semantics* of the fields (armour adjusts damage per damage type, a * weapon's `AntiMask` decides what it may target, `UNRESISTABLE` bypasses * armour) follow SAGE and are corroborated by the OpenSAGE re-implementation * (GPLv3). Where a value is not yet recovered the field is documented as such. * * Nothing here is loaded from disk yet: the compiled `global.bin` / * `static.*.bin` deserialiser is a later milestone, so the base-game balance * used by `ra3.skirmish` is pinned as constants. */ export namespace ra3::data { using ra3::core::int32; using ra3::core::uint8; using ra3::core::uint32; using ra3::core::usize; /** * World units per terrain cell. RA3's `TheTerrainLogic` grid stores * `1 / cellSize` at `grid + 0x38` and the layer reports `cellSize == 10`. */ inline constexpr float cell_size = 10.0F; inline constexpr float inv_cell_size = 1.0F / cell_size; /** * The simulation advances on a fixed 30 Hz logic tick, matching SAGE. * * Note: the sibling `ra3-sim` paces its own simulator at 15 ticks/s from * `DAT_00cdbc1c = 1000 / 15`; `ra3-headless` (which drives the real binary) * reports a fixed 30 Hz logic rate. OpenRA3 keeps the 30 Hz SAGE cadence * used elsewhere in this codebase until the discrepancy is settled. */ inline constexpr int logic_frames_per_second = ra3::core::logic_frames_per_second; /** Convert a duration in seconds to a whole number of logic frames. */ [[nodiscard]] constexpr auto seconds_to_frames(float seconds) -> uint32 { return static_cast(seconds * static_cast(logic_frames_per_second) + 0.5F); } // --------------------------------------------------------------------- // Damage types // --------------------------------------------------------------------- /** * RA3's damage types, recovered from the keys of the shipped * `ArmorTemplate` tables and the `DamageType` of every `WeaponTemplate`. * * This is *not* the Generals/ZH set (`EXPLOSION`/`SMALL_ARMS`/...): RA3 * replaced it with these. `unresistable` is special - it skips the armour * multiplier entirely. */ enum class damage_type : uint8 { gun, melee, concussive, auto_cannon, rocket, flak, cannon, prism, tesla, explosive, impact, sniper, grenade, radiation, magic, crush, healing, unresistable, count, }; inline constexpr usize damage_type_count = static_cast(damage_type::count); [[nodiscard]] constexpr auto to_string(damage_type value) -> std::string_view { switch (value) { case damage_type::gun: return "GUN"; case damage_type::melee: return "MELEE"; case damage_type::concussive: return "CONCUSSIVE"; case damage_type::auto_cannon: return "AUTO_CANNON"; case damage_type::rocket: return "ROCKET"; case damage_type::flak: return "FLAK"; case damage_type::cannon: return "CANNON"; case damage_type::prism: return "PRISM"; case damage_type::tesla: return "TESLA"; case damage_type::explosive: return "EXPLOSIVE"; case damage_type::impact: return "IMPACT"; case damage_type::sniper: return "SNIPER"; case damage_type::grenade: return "GRENADE"; case damage_type::radiation: return "RADIATION"; case damage_type::magic: return "MAGIC"; case damage_type::crush: return "CRUSH"; case damage_type::healing: return "HEALING"; case damage_type::unresistable: return "UNRESISTABLE"; case damage_type::count: break; } return "UNKNOWN"; } /** Parse an asset damage-type name (e.g. `"AUTO_CANNON"`). */ [[nodiscard]] inline auto damage_type_from_name(std::string_view name) -> damage_type { for (usize i = 0; i < damage_type_count; ++i) { const auto value = static_cast(i); if (to_string(value) == name) return value; } return damage_type::gun; } // --------------------------------------------------------------------- // Armour // --------------------------------------------------------------------- /** * A `ArmorTemplate`: a per-damage-type damage multiplier. * * Values are fractions (1.0 == 100%). Unlisted types default to 100%, * except when the template declares a `DEFAULT` (`default_fraction`), as * `InvulnerableArmor` does with 0. Matches OpenSAGE's `ArmorTemplate`. */ struct armor_set { std::string_view id; std::array multipliers{}; float default_fraction = 1.0F; /** Apply this armour. `unresistable` is never scaled. */ [[nodiscard]] constexpr auto adjust(damage_type type, float damage) const -> float { if (type == damage_type::unresistable) return damage; const auto scaled = damage * multipliers[static_cast(type)]; return scaled < 0.0F ? 0.0F : scaled; } }; /** * Build an armour set. `vs` entries are the raw percentages from the * shipped `ArmorTemplate` (`100.0` means normal damage). */ constexpr auto make_armor(std::string_view id, float default_percent, std::initializer_list> vs) -> armor_set { armor_set armor{}; armor.id = id; armor.default_fraction = default_percent / 100.0F; armor.multipliers.fill(armor.default_fraction); for (const auto &[type, percent]: vs) armor.multipliers[static_cast(type)] = percent / 100.0F; return armor; } /** * The armour templates used by the modelled units, copied verbatim from the * shipped `ArmorTemplate` tables (`ra3-headless/out/armors.json`). */ inline constexpr armor_set allied_scout_infantry_armor = make_armor( "AlliedScoutInfantryArmor", 100.0F, {{damage_type::gun, 1.0F}, {damage_type::melee, 50.0F}, {damage_type::concussive, 50.0F}, {damage_type::auto_cannon, 50.0F}, {damage_type::rocket, 50.0F}, {damage_type::flak, 50.0F}, {damage_type::cannon, 50.0F}, {damage_type::prism, 50.0F}, {damage_type::tesla, 50.0F}, {damage_type::explosive, 50.0F}, {damage_type::impact, 50.0F}}); inline constexpr armor_set allied_anti_infantry_infantry_armor = make_armor( "AlliedAntiInfantryInfantryArmor", 100.0F, {{damage_type::sniper, 100.0F}, {damage_type::cannon, 20.0F}, {damage_type::rocket, 20.0F}, {damage_type::grenade, 100.0F}, {damage_type::gun, 100.0F}, {damage_type::melee, 100.0F}, {damage_type::auto_cannon, 150.0F}, {damage_type::impact, 100.0F}, {damage_type::explosive, 100.0F}, {damage_type::flak, 10.0F}, {damage_type::prism, 100.0F}, {damage_type::tesla, 1000.0F}, {damage_type::radiation, 100.0F}}); inline constexpr armor_set soviet_anti_infantry_infantry_armor = make_armor( "SovietAntiInfantryInfantryArmor", 100.0F, {{damage_type::sniper, 100.0F}, {damage_type::cannon, 20.0F}, {damage_type::rocket, 20.0F}, {damage_type::grenade, 100.0F}, {damage_type::gun, 100.0F}, {damage_type::melee, 100.0F}, {damage_type::auto_cannon, 150.0F}, {damage_type::impact, 100.0F}, {damage_type::explosive, 100.0F}, {damage_type::flak, 10.0F}, {damage_type::prism, 100.0F}, {damage_type::tesla, 1000.0F}, {damage_type::radiation, 100.0F}}); inline constexpr armor_set soviet_scout_infantry_armor = make_armor( "SovietScoutInfantryArmor", 100.0F, {{damage_type::gun, 1.0F}, {damage_type::melee, 50.0F}, {damage_type::concussive, 50.0F}, {damage_type::auto_cannon, 50.0F}, {damage_type::rocket, 50.0F}, {damage_type::flak, 50.0F}, {damage_type::cannon, 50.0F}, {damage_type::prism, 50.0F}, {damage_type::tesla, 50.0F}, {damage_type::explosive, 50.0F}, {damage_type::impact, 50.0F}}); inline constexpr armor_set allied_miner_armor = make_armor("AlliedMinerArmor", 100.0F, {{damage_type::melee, 0.0F}, {damage_type::sniper, 0.0F}, {damage_type::gun, 50.0F}, {damage_type::auto_cannon, 100.0F}, {damage_type::impact, 100.0F}, {damage_type::flak, 100.0F}, {damage_type::rocket, 100.0F}, {damage_type::cannon, 150.0F}, {damage_type::prism, 150.0F}, {damage_type::tesla, 150.0F}, {damage_type::explosive, 100.0F}, {damage_type::concussive, 100.0F}, {damage_type::radiation, 5.0F}}); inline constexpr armor_set allied_anti_vehicle_vehicle_tech1_armor = make_armor( "AlliedAntiVehicleVehicleTech1Armor", 100.0F, {{damage_type::melee, 0.0F}, {damage_type::sniper, 0.0F}, {damage_type::gun, 25.0F}, {damage_type::auto_cannon, 100.0F}, {damage_type::impact, 75.0F}, {damage_type::rocket, 75.0F}, {damage_type::flak, 100.0F}, {damage_type::cannon, 170.0F}, {damage_type::prism, 170.0F}, {damage_type::tesla, 170.0F}, {damage_type::explosive, 100.0F}, {damage_type::radiation, 5.0F}}); inline constexpr armor_set soviet_anti_vehicle_vehicle_tech1_armor = make_armor( "SovietAntiVehicleVehicleTech1Armor", 100.0F, {{damage_type::melee, 0.0F}, {damage_type::sniper, 0.0F}, {damage_type::gun, 50.0F}, {damage_type::auto_cannon, 25.0F}, {damage_type::impact, 75.0F}, {damage_type::flak, 100.0F}, {damage_type::rocket, 100.0F}, {damage_type::cannon, 100.0F}, {damage_type::prism, 100.0F}, {damage_type::tesla, 100.0F}, {damage_type::explosive, 100.0F}, {damage_type::concussive, 100.0F}, {damage_type::radiation, 5.0F}}); /** The faction structure armour shared by con yard, power plant, barracks, refinery and war factory. */ inline constexpr armor_set allied_structure_armor = make_armor( "AlliedConYardArmor", 100.0F, {{damage_type::sniper, 0.0F}, {damage_type::cannon, 100.0F}, {damage_type::rocket, 100.0F}, {damage_type::grenade, 100.0F}, {damage_type::gun, 100.0F}, {damage_type::melee, 0.0F}, {damage_type::auto_cannon, 50.0F}, {damage_type::impact, 100.0F}, {damage_type::explosive, 125.0F}, {damage_type::flak, 100.0F}, {damage_type::prism, 100.0F}, {damage_type::tesla, 100.0F}, {damage_type::radiation, 0.0F}}); inline constexpr armor_set soviet_structure_armor = make_armor( "SovietConYardArmor", 100.0F, {{damage_type::sniper, 0.0F}, {damage_type::cannon, 100.0F}, {damage_type::rocket, 100.0F}, {damage_type::grenade, 100.0F}, {damage_type::gun, 100.0F}, {damage_type::melee, 0.0F}, {damage_type::auto_cannon, 50.0F}, {damage_type::impact, 100.0F}, {damage_type::explosive, 125.0F}, {damage_type::flak, 100.0F}, {damage_type::prism, 100.0F}, {damage_type::tesla, 100.0F}, {damage_type::radiation, 0.0F}}); /** Neutral armour: everything does normal damage. */ inline constexpr armor_set neutral_armor = make_armor("NoArmor", 100.0F, {}); // --------------------------------------------------------------------- // Target classification and weapon anti-mask // --------------------------------------------------------------------- /** Coarse target classes, derived from a `GameObject`'s `KindOf` flags. */ namespace target_class { inline constexpr uint32 none = 0U; inline constexpr uint32 infantry = 1U << 0U; inline constexpr uint32 vehicle = 1U << 1U; inline constexpr uint32 structure = 1U << 2U; inline constexpr uint32 aircraft = 1U << 3U; inline constexpr uint32 ground = infantry | vehicle | structure; inline constexpr uint32 any = 0xFFFFFFFFU; } /** * A `WeaponTemplate`'s `AntiMask`. RA3's tokens are recovered from the * shipped weapons (`ra3-headless/out/weapons.json`). */ namespace anti { inline constexpr uint32 ground = 1U << 0U; inline constexpr uint32 water = 1U << 1U; inline constexpr uint32 structure = 1U << 2U; inline constexpr uint32 infantry = 1U << 3U; inline constexpr uint32 vehicle = 1U << 4U; inline constexpr uint32 airborne_vehicle = 1U << 5U; inline constexpr uint32 airborne_infantry = 1U << 6U; inline constexpr uint32 submerged = 1U << 7U; inline constexpr uint32 lifted_ground_unit = 1U << 8U; inline constexpr uint32 mine = 1U << 9U; inline constexpr uint32 projectile = 1U << 10U; inline constexpr uint32 small_missile = 1U << 11U; inline constexpr uint32 ballistic_missile = 1U << 12U; inline constexpr uint32 parachute = 1U << 13U; } // --------------------------------------------------------------------- // Weapons // --------------------------------------------------------------------- /** Primary weapon parameters. Timings are in seconds. */ struct weapon_data { std::string_view ra3_id; float attack_range = 0.0F; float damage = 0.0F; damage_type type = damage_type::gun; float splash_radius = 0.0F; float weapon_speed = 0.0F; uint32 clip_size = 1; float reload_seconds = 1.0F; float firing_seconds = 0.5F; uint32 anti_mask = 0U; /** Classes this weapon may never target (the nugget's `SpecialObjectFilter`). */ uint32 excluded_classes = target_class::none; /** One hit kills any valid target (the maul's `InstakillNugget`). */ bool instakill = false; bool projectile = false; /** * Can this weapon target an object of `target` class? * * `ANTI_GROUND` covers all ground classes; the airborne flags cover * aircraft. A weapon with no anti-air flag cannot hit aircraft, which is * why an attack dog can never maul a plane. */ [[nodiscard]] constexpr auto can_target(uint32 target) const -> bool { if ((target & excluded_classes) != 0U) return false; if ((target & target_class::aircraft) != 0U) { return (anti_mask & (anti::airborne_vehicle | anti::airborne_infantry)) != 0U; } if ((anti_mask & anti::ground) != 0U && (target & target_class::ground) != 0U) return true; if ((anti_mask & anti::infantry) != 0U && (target & target_class::infantry) != 0U) return true; if ((anti_mask & anti::vehicle) != 0U && (target & target_class::vehicle) != 0U) return true; if ((anti_mask & anti::structure) != 0U && (target & target_class::structure) != 0U) return true; return false; } }; /** * The primary weapons of the modelled units, from the shipped * `WeaponTemplate`s. */ inline constexpr weapon_data maul_weapon{ "AlliedScoutInfantryMaul", 30.0F, 1.0F, damage_type::unresistable, 0.0F, 125.0F, 1U, 1.5F, 0.5F, anti::ground | anti::water, target_class::vehicle | target_class::structure | target_class::aircraft, true, false}; inline constexpr weapon_data shotgun_weapon{"AlliedAntiInfantryInfantryShotgun", 150.0F, 40.0F, damage_type::gun, 155.0F, 750.0F, 1U, 1.0F, 0.5F, anti::ground | anti::structure | anti::water, target_class::none, false, true}; inline constexpr weapon_data ak47_weapon{"SovietAntiInfantryInfantryAK47", 150.0F, 5.0F, damage_type::gun, 0.0F, 750.0F, 1U, 0.5F, 0.25F, anti::water | anti::ground | anti::structure, target_class::none, false, true}; inline constexpr weapon_data guardian_cannon_weapon{"AlliedAntiVehicleVehicleTech1Cannon", 150.0F, 60.0F, damage_type::cannon, 0.0F, 999999.0F, 1U, 1.8F, 0.2F, anti::water | anti::ground | anti::structure, target_class::none, false, true}; inline constexpr weapon_data rhino_cannon_weapon{"SovietAntiVehicleVehicleTech1CannonWeapon", 150.0F, 35.0F, damage_type::cannon, 0.0F, 999999.0F, 1U, 1.0F, 0.2F, anti::water | anti::ground | anti::structure, target_class::none, false, true}; // --------------------------------------------------------------------- // Entities // --------------------------------------------------------------------- /** Everything the skirmish simulation can place. */ enum class entity_kind : uint8 { none = 0, attack_dog, ///< AlliedScoutInfantry peacekeeper, ///< AlliedAntiInfantryInfantry conscript, ///< SovietAntiInfantryInfantry guardian_tank, ///< AlliedAntiVehicleVehicleTech1 rhino_tank, ///< SovietAntiVehicleVehicleTech1 miner, ///< AlliedMiner construction_yard, power_plant, barracks, war_factory, refinery, count, }; inline constexpr usize entity_kind_count = static_cast(entity_kind::count); [[nodiscard]] constexpr auto to_string(entity_kind value) -> std::string_view { switch (value) { case entity_kind::attack_dog: return "attack_dog"; case entity_kind::peacekeeper: return "peacekeeper"; case entity_kind::conscript: return "conscript"; case entity_kind::guardian_tank: return "guardian_tank"; case entity_kind::rhino_tank: return "rhino_tank"; case entity_kind::miner: return "miner"; case entity_kind::construction_yard: return "construction_yard"; case entity_kind::power_plant: return "power_plant"; case entity_kind::barracks: return "barracks"; case entity_kind::war_factory: return "war_factory"; case entity_kind::refinery: return "refinery"; case entity_kind::none: break; case entity_kind::count: break; } return "none"; } [[nodiscard]] constexpr auto is_unit(entity_kind kind) -> bool { switch (kind) { case entity_kind::attack_dog: case entity_kind::peacekeeper: case entity_kind::conscript: case entity_kind::guardian_tank: case entity_kind::rhino_tank: case entity_kind::miner: return true; default: return false; } } [[nodiscard]] constexpr auto is_structure(entity_kind kind) -> bool { switch (kind) { case entity_kind::construction_yard: case entity_kind::power_plant: case entity_kind::barracks: case entity_kind::war_factory: case entity_kind::refinery: return true; default: return false; } } /** Target class of an entity, from its `KindOf` (`INFANTRY`/`VEHICLE`/...). */ [[nodiscard]] constexpr auto class_of(entity_kind kind) -> uint32 { switch (kind) { case entity_kind::attack_dog: case entity_kind::peacekeeper: case entity_kind::conscript: return target_class::infantry; case entity_kind::guardian_tank: case entity_kind::rhino_tank: case entity_kind::miner: return target_class::vehicle; case entity_kind::construction_yard: case entity_kind::power_plant: case entity_kind::barracks: case entity_kind::war_factory: case entity_kind::refinery: return target_class::structure; default: return target_class::none; } } /** Mobile unit parameters. */ struct unit_data { entity_kind kind = entity_kind::none; std::string_view ra3_id; int32 cost = 0; float build_seconds = 0.0F; float max_health = 0.0F; const armor_set *armor = &neutral_armor; float speed = 0.0F; ///< LocomotorSet Speed, world units per second. float vision = 0.0F; ///< VisionInfo VisionRange. float shroud_clear = 0.0F; ///< VisionInfo ShroudClearingRange. float radius = 0.0F; ///< Geometry MajorRadius. const weapon_data *weapon = nullptr; entity_kind built_by = entity_kind::none; ///< Producer structure. }; /** Structure parameters. */ struct structure_data { entity_kind kind = entity_kind::none; std::string_view ra3_id; int32 cost = 0; float build_seconds = 0.0F; float max_health = 0.0F; const armor_set *armor = &neutral_armor; int32 energy = 0; ///< `EnergyProduction`: positive supplies, negative draws. float vision = 0.0F; float shroud_clear = 0.0F; float radius = 0.0F; int size_cells = 1; ///< Footprint, cells per side. entity_kind prerequisite = entity_kind::none; ///< Prerequisite structure. bool is_producer = false; ///< Can build units. bool is_refinery = false; ///< Runs an ore extraction cycle. }; /** * Mobile units. Health, cost, build time, speed and armour are the retail * `GameObject`/`LocomotorSet`/`ArmorTemplate` values. */ inline constexpr std::array units{{ {}, // none {entity_kind::attack_dog, "AlliedScoutInfantry", 200, 2.0F, 30.0F, &allied_scout_infantry_armor, 100.0F, 200.0F, 750.0F, 7.0F, &maul_weapon, entity_kind::barracks}, {entity_kind::peacekeeper, "AlliedAntiInfantryInfantry", 200, 5.0F, 150.0F, &allied_anti_infantry_infantry_armor, 50.0F, 200.0F, 500.0F, 7.0F, &shotgun_weapon, entity_kind::barracks}, {entity_kind::conscript, "SovietAntiInfantryInfantry", 100, 4.0F, 100.0F, &soviet_anti_infantry_infantry_armor, 50.0F, 200.0F, 500.0F, 5.0F, &ak47_weapon, entity_kind::barracks}, {entity_kind::guardian_tank, "AlliedAntiVehicleVehicleTech1", 950, 10.0F, 480.0F, &allied_anti_vehicle_vehicle_tech1_armor, 80.0F, 200.0F, 500.0F, 20.0F, &guardian_cannon_weapon, entity_kind::war_factory}, {entity_kind::rhino_tank, "SovietAntiVehicleVehicleTech1", 1000, 10.0F, 550.0F, &soviet_anti_vehicle_vehicle_tech1_armor, 75.0F, 200.0F, 500.0F, 20.0F, &rhino_cannon_weapon, entity_kind::war_factory}, {entity_kind::miner, "AlliedMiner", 1000, 20.0F, 500.0F, &allied_miner_armor, 50.0F, 200.0F, 500.0F, 20.0F, nullptr, entity_kind::refinery}, {}, {}, {}, {}, {}, }}; /** * Structures. `energy` is the `EnergyProduction` attribute; `build_seconds` * is `BuildTime`. Soviet `BuildTime` is not set on the base structures (the * extractor reads 0), so the Allied values are used for both sides and the * discrepancy is documented rather than invented. */ inline constexpr std::array structures{{ {}, {}, {}, {}, {}, {}, {}, // none + the six units {entity_kind::construction_yard, "AlliedConstructionYard", 5000, 25.0F, 4000.0F, &allied_structure_armor, 50, 150.0F, 1000.0F, 60.0F, 3, entity_kind::none, false, false}, {entity_kind::power_plant, "AlliedPowerPlant", 800, 10.0F, 1000.0F, &allied_structure_armor, 100, 150.0F, 300.0F, 30.0F, 2, entity_kind::none, false, false}, {entity_kind::barracks, "AlliedBarracks", 500, 10.0F, 1000.0F, &allied_structure_armor, -25, 150.0F, 300.0F, 45.0F, 2, entity_kind::none, true, false}, {entity_kind::war_factory, "AlliedWarFactory", 2000, 20.0F, 2500.0F, &allied_structure_armor, -50, 150.0F, 500.0F, 60.0F, 3, entity_kind::none, true, false}, {entity_kind::refinery, "AlliedRefinery", 2000, 20.0F, 2000.0F, &allied_structure_armor, -50, 150.0F, 500.0F, 60.0F, 3, entity_kind::power_plant, false, true}, }}; /** Look up a mobile unit. Only valid when `is_unit(kind)`. */ [[nodiscard]] inline auto unit_of(entity_kind kind) -> const unit_data & { return units.at(static_cast(kind)); } /** Look up a structure. Only valid when `is_structure(kind)`. */ [[nodiscard]] inline auto structure_of(entity_kind kind) -> const structure_data & { return structures.at(static_cast(kind)); } /** Cost in credits. */ [[nodiscard]] inline auto cost_of(entity_kind kind) -> int32 { if (is_unit(kind)) return unit_of(kind).cost; if (is_structure(kind)) return structure_of(kind).cost; return 0; } /** Build time in logic frames. */ [[nodiscard]] inline auto build_frames_of(entity_kind kind) -> uint32 { if (is_unit(kind)) return seconds_to_frames(unit_of(kind).build_seconds); if (is_structure(kind)) return seconds_to_frames(structure_of(kind).build_seconds); return 0U; } /** Maximum health. */ [[nodiscard]] inline auto max_health_of(entity_kind kind) -> float { if (is_unit(kind)) return unit_of(kind).max_health; if (is_structure(kind)) return structure_of(kind).max_health; return 0.0F; } /** Armour template. */ [[nodiscard]] inline auto armor_of(entity_kind kind) -> const armor_set & { if (is_unit(kind)) return *unit_of(kind).armor; if (is_structure(kind)) return *structure_of(kind).armor; return neutral_armor; } /** Vision range in world units. */ [[nodiscard]] inline auto vision_of(entity_kind kind) -> float { if (is_unit(kind)) return unit_of(kind).vision; if (is_structure(kind)) return structure_of(kind).vision; return 0.0F; } /** The weapon a unit fires, or `nullptr` (structures, miners). */ [[nodiscard]] inline auto weapon_of(entity_kind kind) -> const weapon_data * { return is_unit(kind) ? unit_of(kind).weapon : nullptr; } // --------------------------------------------------------------------- // Economy and world rules // --------------------------------------------------------------------- /** * Ore economy, from `OreNodeBehaviour` and `MpGameRules`. * * A refinery runs one extraction cycle: `MOVE_TO_EXTRACT + EXTRACT + * MOVE_TO_DELIVER + DELIVERY` = 11 s for 250 credits while its node has * ore. The engine actually spawns a separate miner; OpenRA3 folds the cycle * into the refinery (a documented simplification, as in `ra3-sim`). */ struct economy_data { int32 starting_credits = 10000; ///< MpGameRules SkirmishStartCash LoCash. int32 credits_step = 5000; ///< SkirmishStartCash ChoiceStepAmount. int32 credits_max = 40000; ///< SkirmishStartCash HiCash. int32 ore_per_delivery = 250; ///< OreNodeBehaviour DeliveryAmount. int32 ore_per_delivery_empty = 60; ///< OreNodeBehaviour DeliveryAmountWhenEmpty. int32 ore_node_capacity = 30000; ///< OreNodeBehaviour MaximumGatheredValue. float extraction_seconds = 11.0F; ///< 3.5 + 2 + 3.5 + 2. }; /** Miscellaneous world rules. */ struct world_data { float standard_shroud_clear = 500.0F; ///< Define STANDARD_SHROUD_CLEAR. float scout_shroud_clear = 750.0F; ///< Define SCOUT_SHROUD_CLEAR. int low_power_production_divisor = 2; ///< Low power halves production speed. int low_power_mining_divisor = 2; ///< Low power halves refinery income. }; inline constexpr economy_data economy{}; inline constexpr world_data world_rules{}; /** Ore extracted per completed refinery cycle, given the remaining node ore. */ [[nodiscard]] constexpr auto ore_per_cycle(int32 node_remaining) -> int32 { return node_remaining > 0 ? economy.ore_per_delivery : economy.ore_per_delivery_empty; } }