- 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.
616 lines
30 KiB
C++
616 lines
30 KiB
C++
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;
|
|
}
|
|
}
|