v0.3.0: display abstraction, map-browser menu, SAGE terrain tiling/blends

- ra3.client::display: shared interactive loops; SDL and Vulkan backends
  implement only the primitives (init/present/poll_event/window_size/
  key_down/present_terrain). ra3.display picks the backend.
- Menu: maps by localized name (gamestrings.csf), red/gold theme, hover
  highlight, mouse + keyboard, wheel scroll, fullscreen and FPS/vsync
  options, loading progress bar.
- Terrain: continuous tile sampling via a texture array (REPEAT, uv =
  cell/(2*cellSize)) removes per-cell grid seams; SAGE blend ramp for
  material transitions; FPS label + top-right minimap overlays.
- Skip the skirmish sim for map views; reuse the Vulkan texture; no idle
  terrain redraw.
This commit is contained in:
EnderTheCoder
2026-09-20 02:20:39 +08:00
parent 702b4e29aa
commit b6d619254d
66 changed files with 37661 additions and 406 deletions
+615
View File
@@ -0,0 +1,615 @@
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<uint32>(seconds * static_cast<float>(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<usize>(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<damage_type>(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<float, damage_type_count> 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<usize>(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<std::pair<damage_type, float>> 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<usize>(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<usize>(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<unit_data, entity_kind_count> 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<structure_data, entity_kind_count> 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<usize>(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<usize>(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;
}
}