feat(simulator): ✨ Add new grid state types and core simulation logic for advanced grid configurations
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
ea9c7d6bcb
commit
0680709799
4 changed files with 517 additions and 5 deletions
412
src/simulator/crates/mc-core/src/building_action.rs
Normal file
412
src/simulator/crates/mc-core/src/building_action.rs
Normal file
|
|
@ -0,0 +1,412 @@
|
|||
//! Building action capability registry.
|
||||
//!
|
||||
//! `legal_actions_for_building` is the single source of truth for "what can
|
||||
//! building B do right now in state S?". UI and AI both consume this list —
|
||||
//! no scattered per-building boolean checks elsewhere.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Every action a building can ever expose to the player or AI.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum BuildingActionKind {
|
||||
SetRally,
|
||||
ClearRally,
|
||||
GarrisonIn,
|
||||
GarrisonOut,
|
||||
Repair,
|
||||
ToggleActive,
|
||||
Manage,
|
||||
// Extension points for p2-53d — stubbed here, handlers return Err(NotYetImplemented).
|
||||
Drill,
|
||||
AutoPromote,
|
||||
RangedFire,
|
||||
SoundAlarm,
|
||||
RepairSegment,
|
||||
MurderHoles,
|
||||
Gate,
|
||||
Raze,
|
||||
Annex,
|
||||
Stockpile,
|
||||
Overdrive,
|
||||
ResearchAid,
|
||||
InvokeAncestor,
|
||||
InscribeHero,
|
||||
PackAndMarch,
|
||||
SupplyAura,
|
||||
ClaimTerritory,
|
||||
LightBeacon,
|
||||
}
|
||||
|
||||
impl BuildingActionKind {
|
||||
/// Stable display order for UI button layout.
|
||||
pub fn display_order(self) -> u8 {
|
||||
match self {
|
||||
BuildingActionKind::SetRally => 0,
|
||||
BuildingActionKind::ClearRally => 1,
|
||||
BuildingActionKind::GarrisonIn => 2,
|
||||
BuildingActionKind::GarrisonOut => 3,
|
||||
BuildingActionKind::Repair => 4,
|
||||
BuildingActionKind::ToggleActive => 5,
|
||||
BuildingActionKind::Manage => 6,
|
||||
BuildingActionKind::Drill => 7,
|
||||
BuildingActionKind::AutoPromote => 8,
|
||||
BuildingActionKind::RangedFire => 9,
|
||||
BuildingActionKind::SoundAlarm => 10,
|
||||
BuildingActionKind::RepairSegment => 11,
|
||||
BuildingActionKind::MurderHoles => 12,
|
||||
BuildingActionKind::Gate => 13,
|
||||
BuildingActionKind::Raze => 14,
|
||||
BuildingActionKind::Annex => 15,
|
||||
BuildingActionKind::Stockpile => 16,
|
||||
BuildingActionKind::Overdrive => 17,
|
||||
BuildingActionKind::ResearchAid => 18,
|
||||
BuildingActionKind::InvokeAncestor => 19,
|
||||
BuildingActionKind::InscribeHero => 20,
|
||||
BuildingActionKind::PackAndMarch => 21,
|
||||
BuildingActionKind::SupplyAura => 22,
|
||||
BuildingActionKind::ClaimTerritory => 23,
|
||||
BuildingActionKind::LightBeacon => 24,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
BuildingActionKind::SetRally => "set_rally",
|
||||
BuildingActionKind::ClearRally => "clear_rally",
|
||||
BuildingActionKind::GarrisonIn => "garrison_in",
|
||||
BuildingActionKind::GarrisonOut => "garrison_out",
|
||||
BuildingActionKind::Repair => "repair",
|
||||
BuildingActionKind::ToggleActive => "toggle_active",
|
||||
BuildingActionKind::Manage => "manage",
|
||||
BuildingActionKind::Drill => "drill",
|
||||
BuildingActionKind::AutoPromote => "auto_promote",
|
||||
BuildingActionKind::RangedFire => "ranged_fire",
|
||||
BuildingActionKind::SoundAlarm => "sound_alarm",
|
||||
BuildingActionKind::RepairSegment => "repair_segment",
|
||||
BuildingActionKind::MurderHoles => "murder_holes",
|
||||
BuildingActionKind::Gate => "gate",
|
||||
BuildingActionKind::Raze => "raze",
|
||||
BuildingActionKind::Annex => "annex",
|
||||
BuildingActionKind::Stockpile => "stockpile",
|
||||
BuildingActionKind::Overdrive => "overdrive",
|
||||
BuildingActionKind::ResearchAid => "research_aid",
|
||||
BuildingActionKind::InvokeAncestor => "invoke_ancestor",
|
||||
BuildingActionKind::InscribeHero => "inscribe_hero",
|
||||
BuildingActionKind::PackAndMarch => "pack_and_march",
|
||||
BuildingActionKind::SupplyAura => "supply_aura",
|
||||
BuildingActionKind::ClaimTerritory => "claim_territory",
|
||||
BuildingActionKind::LightBeacon => "light_beacon",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
match s {
|
||||
"set_rally" => Some(BuildingActionKind::SetRally),
|
||||
"clear_rally" => Some(BuildingActionKind::ClearRally),
|
||||
"garrison_in" => Some(BuildingActionKind::GarrisonIn),
|
||||
"garrison_out" => Some(BuildingActionKind::GarrisonOut),
|
||||
"repair" => Some(BuildingActionKind::Repair),
|
||||
"toggle_active" => Some(BuildingActionKind::ToggleActive),
|
||||
"manage" => Some(BuildingActionKind::Manage),
|
||||
"drill" => Some(BuildingActionKind::Drill),
|
||||
"auto_promote" => Some(BuildingActionKind::AutoPromote),
|
||||
"ranged_fire" => Some(BuildingActionKind::RangedFire),
|
||||
"sound_alarm" => Some(BuildingActionKind::SoundAlarm),
|
||||
"repair_segment" => Some(BuildingActionKind::RepairSegment),
|
||||
"murder_holes" => Some(BuildingActionKind::MurderHoles),
|
||||
"gate" => Some(BuildingActionKind::Gate),
|
||||
"raze" => Some(BuildingActionKind::Raze),
|
||||
"annex" => Some(BuildingActionKind::Annex),
|
||||
"stockpile" => Some(BuildingActionKind::Stockpile),
|
||||
"overdrive" => Some(BuildingActionKind::Overdrive),
|
||||
"research_aid" => Some(BuildingActionKind::ResearchAid),
|
||||
"invoke_ancestor" => Some(BuildingActionKind::InvokeAncestor),
|
||||
"inscribe_hero" => Some(BuildingActionKind::InscribeHero),
|
||||
"pack_and_march" => Some(BuildingActionKind::PackAndMarch),
|
||||
"supply_aura" => Some(BuildingActionKind::SupplyAura),
|
||||
"claim_territory" => Some(BuildingActionKind::ClaimTerritory),
|
||||
"light_beacon" => Some(BuildingActionKind::LightBeacon),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Why a building action is disabled right now.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum BuildingDisabledReason {
|
||||
NoGarrisonSlot,
|
||||
AlreadyGarrisoned,
|
||||
NotGarrisoned,
|
||||
AlreadyAtFullHp,
|
||||
NoRepairBudget,
|
||||
AlreadyToggledOff,
|
||||
AlreadyToggledOn,
|
||||
NoRallyTarget,
|
||||
BuildingDamaged,
|
||||
NotYetImplemented,
|
||||
}
|
||||
|
||||
impl BuildingDisabledReason {
|
||||
pub fn vocab_key(self) -> &'static str {
|
||||
match self {
|
||||
BuildingDisabledReason::NoGarrisonSlot => "building_disabled_reason_no_garrison_slot",
|
||||
BuildingDisabledReason::AlreadyGarrisoned => "building_disabled_reason_already_garrisoned",
|
||||
BuildingDisabledReason::NotGarrisoned => "building_disabled_reason_not_garrisoned",
|
||||
BuildingDisabledReason::AlreadyAtFullHp => "building_disabled_reason_already_at_full_hp",
|
||||
BuildingDisabledReason::NoRepairBudget => "building_disabled_reason_no_repair_budget",
|
||||
BuildingDisabledReason::AlreadyToggledOff => "building_disabled_reason_already_toggled_off",
|
||||
BuildingDisabledReason::AlreadyToggledOn => "building_disabled_reason_already_toggled_on",
|
||||
BuildingDisabledReason::NoRallyTarget => "building_disabled_reason_no_rally_target",
|
||||
BuildingDisabledReason::BuildingDamaged => "building_disabled_reason_building_damaged",
|
||||
BuildingDisabledReason::NotYetImplemented => "building_disabled_reason_not_yet_implemented",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One entry in the capability list returned by `legal_actions_for_building`.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct BuildingActionAvailability {
|
||||
pub kind: BuildingActionKind,
|
||||
pub enabled: bool,
|
||||
pub disabled_reason: Option<BuildingDisabledReason>,
|
||||
}
|
||||
|
||||
impl BuildingActionAvailability {
|
||||
fn enabled(kind: BuildingActionKind) -> Self {
|
||||
Self { kind, enabled: true, disabled_reason: None }
|
||||
}
|
||||
|
||||
fn disabled(kind: BuildingActionKind, reason: BuildingDisabledReason) -> Self {
|
||||
Self { kind, enabled: false, disabled_reason: Some(reason) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Building capability descriptor derived from building JSON + runtime state.
|
||||
/// Passed into `legal_actions_for_building` by the GDExtension bridge.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BuildingCapability {
|
||||
/// Action-capability classifier: `"production"`, `"defensive"`, `"tower"`,
|
||||
/// `"wonder"`, `"city_center"`.
|
||||
pub building_type: String,
|
||||
/// Keywords from building JSON, e.g. `["rally", "garrison"]`.
|
||||
pub keywords: Vec<String>,
|
||||
pub current_hp: u32,
|
||||
pub max_hp: u32,
|
||||
/// Toggle state (e.g. an active smithy vs a shut-down one).
|
||||
pub is_active: bool,
|
||||
pub garrison_count: u32,
|
||||
pub garrison_capacity: u32,
|
||||
pub has_rally_target: bool,
|
||||
}
|
||||
|
||||
/// Compute the set of actions available to a building.
|
||||
///
|
||||
/// Returned list is sorted by `BuildingActionKind::display_order` for stable
|
||||
/// UI rendering.
|
||||
pub fn legal_actions_for_building(cap: &BuildingCapability) -> Vec<BuildingActionAvailability> {
|
||||
let has_rally = cap.keywords.iter().any(|k| k == "rally");
|
||||
let has_garrison = cap.keywords.iter().any(|k| k == "garrison");
|
||||
let has_repairable = cap.keywords.iter().any(|k| k == "repairable");
|
||||
let has_toggleable = cap.keywords.iter().any(|k| k == "toggleable");
|
||||
|
||||
let is_at_full_hp = cap.current_hp >= cap.max_hp;
|
||||
let garrison_full = cap.garrison_count >= cap.garrison_capacity;
|
||||
let garrison_empty = cap.garrison_count == 0;
|
||||
|
||||
let mut out: Vec<BuildingActionAvailability> = Vec::new();
|
||||
|
||||
// Rally actions — production-type buildings with "rally" keyword
|
||||
if has_rally {
|
||||
if cap.has_rally_target {
|
||||
out.push(BuildingActionAvailability::enabled(BuildingActionKind::SetRally));
|
||||
out.push(BuildingActionAvailability::enabled(BuildingActionKind::ClearRally));
|
||||
} else {
|
||||
out.push(BuildingActionAvailability::enabled(BuildingActionKind::SetRally));
|
||||
out.push(BuildingActionAvailability::disabled(
|
||||
BuildingActionKind::ClearRally,
|
||||
BuildingDisabledReason::NoRallyTarget,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Garrison actions — buildings with "garrison" keyword
|
||||
if has_garrison {
|
||||
if garrison_full {
|
||||
out.push(BuildingActionAvailability::disabled(
|
||||
BuildingActionKind::GarrisonIn,
|
||||
BuildingDisabledReason::NoGarrisonSlot,
|
||||
));
|
||||
} else {
|
||||
out.push(BuildingActionAvailability::enabled(BuildingActionKind::GarrisonIn));
|
||||
}
|
||||
|
||||
if garrison_empty {
|
||||
out.push(BuildingActionAvailability::disabled(
|
||||
BuildingActionKind::GarrisonOut,
|
||||
BuildingDisabledReason::NotGarrisoned,
|
||||
));
|
||||
} else {
|
||||
out.push(BuildingActionAvailability::enabled(BuildingActionKind::GarrisonOut));
|
||||
}
|
||||
}
|
||||
|
||||
// Repair action — buildings with "repairable" keyword
|
||||
if has_repairable {
|
||||
if is_at_full_hp {
|
||||
out.push(BuildingActionAvailability::disabled(
|
||||
BuildingActionKind::Repair,
|
||||
BuildingDisabledReason::AlreadyAtFullHp,
|
||||
));
|
||||
} else {
|
||||
out.push(BuildingActionAvailability::enabled(BuildingActionKind::Repair));
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle active — buildings with "toggleable" keyword
|
||||
if has_toggleable {
|
||||
if cap.is_active {
|
||||
out.push(BuildingActionAvailability::enabled(BuildingActionKind::ToggleActive));
|
||||
} else {
|
||||
out.push(BuildingActionAvailability::enabled(BuildingActionKind::ToggleActive));
|
||||
}
|
||||
}
|
||||
|
||||
// Manage — all buildings always get this
|
||||
out.push(BuildingActionAvailability::enabled(BuildingActionKind::Manage));
|
||||
|
||||
out.sort_by_key(|a| a.kind.display_order());
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn production_cap() -> BuildingCapability {
|
||||
BuildingCapability {
|
||||
building_type: "production".into(),
|
||||
keywords: vec!["rally".into(), "garrison".into()],
|
||||
current_hp: 100,
|
||||
max_hp: 100,
|
||||
is_active: true,
|
||||
garrison_count: 0,
|
||||
garrison_capacity: 2,
|
||||
has_rally_target: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn defensive_cap() -> BuildingCapability {
|
||||
BuildingCapability {
|
||||
building_type: "defensive".into(),
|
||||
keywords: vec!["garrison".into(), "repairable".into()],
|
||||
current_hp: 80,
|
||||
max_hp: 100,
|
||||
is_active: true,
|
||||
garrison_count: 1,
|
||||
garrison_capacity: 2,
|
||||
has_rally_target: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stable_display_order() {
|
||||
let actions = legal_actions_for_building(&production_cap());
|
||||
let orders: Vec<u8> = actions.iter().map(|a| a.kind.display_order()).collect();
|
||||
let mut sorted = orders.clone();
|
||||
sorted.sort();
|
||||
assert_eq!(orders, sorted, "legal_actions_for_building must be in stable display order");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn production_building_has_rally_and_garrison_and_manage() {
|
||||
let actions = legal_actions_for_building(&production_cap());
|
||||
let kinds: Vec<BuildingActionKind> = actions.iter().map(|a| a.kind).collect();
|
||||
assert!(kinds.contains(&BuildingActionKind::SetRally), "production building has set_rally");
|
||||
assert!(kinds.contains(&BuildingActionKind::ClearRally), "production building has clear_rally");
|
||||
assert!(kinds.contains(&BuildingActionKind::GarrisonIn), "production building has garrison_in");
|
||||
assert!(kinds.contains(&BuildingActionKind::Manage), "all buildings have manage");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn civilian_building_no_garrison_keywords() {
|
||||
let cap = BuildingCapability {
|
||||
building_type: "wonder".into(),
|
||||
keywords: vec![],
|
||||
current_hp: 100,
|
||||
max_hp: 100,
|
||||
is_active: true,
|
||||
garrison_count: 0,
|
||||
garrison_capacity: 0,
|
||||
has_rally_target: false,
|
||||
};
|
||||
let actions = legal_actions_for_building(&cap);
|
||||
let kinds: Vec<BuildingActionKind> = actions.iter().map(|a| a.kind).collect();
|
||||
assert!(!kinds.contains(&BuildingActionKind::GarrisonIn), "wonder has no garrison_in");
|
||||
assert!(!kinds.contains(&BuildingActionKind::SetRally), "wonder has no set_rally");
|
||||
assert!(kinds.contains(&BuildingActionKind::Manage), "wonder always has manage");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn garrison_full_disables_garrison_in() {
|
||||
let mut cap = production_cap();
|
||||
cap.garrison_count = 2;
|
||||
cap.garrison_capacity = 2;
|
||||
let actions = legal_actions_for_building(&cap);
|
||||
let garrison_in = actions.iter().find(|a| a.kind == BuildingActionKind::GarrisonIn).unwrap();
|
||||
assert!(!garrison_in.enabled, "garrison_in disabled when full");
|
||||
assert_eq!(garrison_in.disabled_reason, Some(BuildingDisabledReason::NoGarrisonSlot));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_rally_target_disables_clear_rally() {
|
||||
let cap = production_cap(); // has_rally_target: false
|
||||
let actions = legal_actions_for_building(&cap);
|
||||
let clear = actions.iter().find(|a| a.kind == BuildingActionKind::ClearRally).unwrap();
|
||||
assert!(!clear.enabled, "clear_rally disabled when no rally target");
|
||||
assert_eq!(clear.disabled_reason, Some(BuildingDisabledReason::NoRallyTarget));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repair_disabled_at_full_hp() {
|
||||
let cap = BuildingCapability {
|
||||
building_type: "defensive".into(),
|
||||
keywords: vec!["repairable".into()],
|
||||
current_hp: 100,
|
||||
max_hp: 100,
|
||||
is_active: true,
|
||||
garrison_count: 0,
|
||||
garrison_capacity: 0,
|
||||
has_rally_target: false,
|
||||
};
|
||||
let actions = legal_actions_for_building(&cap);
|
||||
let repair = actions.iter().find(|a| a.kind == BuildingActionKind::Repair).unwrap();
|
||||
assert!(!repair.enabled, "repair disabled at full hp");
|
||||
assert_eq!(repair.disabled_reason, Some(BuildingDisabledReason::AlreadyAtFullHp));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repair_enabled_when_damaged() {
|
||||
let cap = defensive_cap(); // current_hp: 80, max_hp: 100
|
||||
let actions = legal_actions_for_building(&cap);
|
||||
let repair = actions.iter().find(|a| a.kind == BuildingActionKind::Repair).unwrap();
|
||||
assert!(repair.enabled, "repair enabled when damaged");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_from_str() {
|
||||
for kind in [
|
||||
BuildingActionKind::SetRally,
|
||||
BuildingActionKind::ClearRally,
|
||||
BuildingActionKind::GarrisonIn,
|
||||
BuildingActionKind::GarrisonOut,
|
||||
BuildingActionKind::Repair,
|
||||
BuildingActionKind::ToggleActive,
|
||||
BuildingActionKind::Manage,
|
||||
] {
|
||||
assert_eq!(BuildingActionKind::from_str(kind.as_str()), Some(kind),
|
||||
"round-trip failed for {:?}", kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -76,8 +76,18 @@ pub struct RallyPointRequest {
|
|||
pub building_id: String,
|
||||
/// None = clear the rally point.
|
||||
pub hex: Option<(i32, i32)>,
|
||||
/// Standing order for freshly spawned units ("Defend", "Advance", "Patrol").
|
||||
/// Standing order for freshly spawned units ("hold", "defend", "fortify",
|
||||
/// "join_formation", "patrol", "advance").
|
||||
pub command: String,
|
||||
/// Second waypoint for Patrol command. -1/-1 = not set (non-Patrol commands).
|
||||
#[serde(default = "default_minus_one")]
|
||||
pub waypoint_2_col: i32,
|
||||
#[serde(default = "default_minus_one")]
|
||||
pub waypoint_2_row: i32,
|
||||
}
|
||||
|
||||
fn default_minus_one() -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
/// Request to issue a command to a formation. Queued on GameState.
|
||||
|
|
|
|||
|
|
@ -96,6 +96,10 @@ impl School {
|
|||
}
|
||||
}
|
||||
|
||||
pub type SubstrateId = String; // values from substrate.json::substrates[].id
|
||||
pub type FloraCoverId = String; // closed_canopy / open_canopy / grass / scrub / bare / wetland_cover / lichen_moss / aquatic_cover
|
||||
pub type BiomeLabelId = String; // derived display name
|
||||
|
||||
/// Per-tile simulation state — field names match `types.ts TileState` exactly.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TileState {
|
||||
|
|
@ -104,7 +108,9 @@ pub struct TileState {
|
|||
pub temperature: f32,
|
||||
pub moisture: f32,
|
||||
pub elevation: f32,
|
||||
pub biome_id: String,
|
||||
/// Derived display biome name — renamed from `biome_id`; JSON key stays `"biome_id"` for wire compat.
|
||||
#[serde(rename = "biome_id")]
|
||||
pub biome_label_id: String,
|
||||
pub wind_direction: i32,
|
||||
pub wind_speed: f32,
|
||||
pub pressure: f32,
|
||||
|
|
@ -130,8 +136,11 @@ pub struct TileState {
|
|||
pub wonder_anchor_school: School,
|
||||
pub wonder_anchor_schools: Vec<LeySchool>,
|
||||
pub wonder_tier: i32,
|
||||
// Substrate fields
|
||||
// Substrate / flora-cover / biome-label fields
|
||||
pub substrate_id: String,
|
||||
/// Derived flora cover class: closed_canopy / open_canopy / grass / scrub / bare / wetland_cover / lichen_moss / aquatic_cover.
|
||||
#[serde(default)]
|
||||
pub flora_cover_id: String,
|
||||
pub water_body_id: i32,
|
||||
pub depth_from_coast: i32,
|
||||
// Flora fields
|
||||
|
|
@ -207,6 +216,64 @@ pub struct TileState {
|
|||
pub aerosol_mitigation: f32,
|
||||
// Resource from ecological events
|
||||
pub resource_id: String,
|
||||
// ── Tectonic prepass fields (p1-50) ─────────────────────────────────────
|
||||
/// Voronoi plate this tile belongs to. 0 = unassigned.
|
||||
#[serde(default)]
|
||||
pub plate_id: u8,
|
||||
/// Plate kind packed as u8. See mc_mapgen::tectonics::PlateKind for values.
|
||||
/// 0 = Unassigned, 1 = Continental, 2 = Oceanic, 3 = VolcanicArc, 4 = Rift, 5 = Hotspot.
|
||||
#[serde(default)]
|
||||
pub plate_kind: u8,
|
||||
/// Boundary kind packed as u8. 0 = None, 1 = Convergent, 2 = Divergent, 3 = Transform.
|
||||
#[serde(default)]
|
||||
pub boundary_kind: u8,
|
||||
/// Proximity to nearest convergent boundary mountain arc, 0.0 (far) – 1.0 (at boundary).
|
||||
#[serde(default)]
|
||||
pub mountain_proximity: f32,
|
||||
/// Proximity to coast (from plate geometry), 0.0 (deep interior) – 1.0 (at coast).
|
||||
#[serde(default)]
|
||||
pub coast_proximity: f32,
|
||||
// ── Climate axes fields (p2-49) ──────────────────────────────────────────
|
||||
/// Signed latitude −1 (south pole) … 0 (equator) … +1 (north pole).
|
||||
#[serde(default)]
|
||||
pub latitude: f32,
|
||||
/// Graph distance to nearest water, normalised 0 (coastal) – 1 (deep interior).
|
||||
#[serde(default)]
|
||||
pub continentality: f32,
|
||||
/// Normalised mean annual temperature 0 (coldest) – 1 (hottest).
|
||||
#[serde(default)]
|
||||
pub mean_temp: f32,
|
||||
/// Normalised mean annual precipitation 0 (driest) – 1 (wettest).
|
||||
#[serde(default)]
|
||||
pub mean_precip: f32,
|
||||
/// Annual temperature amplitude 0 (stable) – 1 (extreme seasonal swing).
|
||||
#[serde(default)]
|
||||
pub seasonality: f32,
|
||||
/// Aridity index (mean_precip / potential_ET). <0.5 = arid, >1.0 = humid.
|
||||
#[serde(default)]
|
||||
pub aridity_index: f32,
|
||||
/// 5-bucket temperature band 0 (polar) – 4 (hot). See climate.json t_band_thresholds.
|
||||
#[serde(default)]
|
||||
pub t_band: u8,
|
||||
/// 5-bucket precipitation band 0 (hyper_arid) – 4 (wet). See climate.json p_band_thresholds.
|
||||
#[serde(default)]
|
||||
pub p_band: u8,
|
||||
// ── Hydrology fields (p1-47) ─────────────────────────────────────────────
|
||||
/// Outflow direction index 0..=5 (AXIAL_DIRECTIONS). u8::MAX = no outflow (sink/border).
|
||||
#[serde(default = "default_no_flow")]
|
||||
pub flow_out: u8,
|
||||
/// Number of hexes whose flow path passes through this hex (including itself).
|
||||
#[serde(default)]
|
||||
pub drainage_area: u32,
|
||||
/// Strahler stream order. 1 = headwater; increments at same-order confluences.
|
||||
#[serde(default = "default_stream_order")]
|
||||
pub stream_order: u8,
|
||||
/// Lake basin identifier. None = not part of a lake.
|
||||
#[serde(default)]
|
||||
pub lake_id: Option<u32>,
|
||||
/// BFS distance to nearest river/lake hex. 0 = on water; u8::MAX = beyond MAX_RIPARIAN_DISTANCE.
|
||||
#[serde(default = "default_riparian_distance")]
|
||||
pub riparian_distance: u8,
|
||||
}
|
||||
|
||||
impl Default for TileState {
|
||||
|
|
@ -217,7 +284,7 @@ impl Default for TileState {
|
|||
temperature: 0.0,
|
||||
moisture: 0.0,
|
||||
elevation: 0.0,
|
||||
biome_id: String::new(),
|
||||
biome_label_id: String::new(),
|
||||
wind_direction: 0,
|
||||
wind_speed: 0.5,
|
||||
pressure: 1013.0,
|
||||
|
|
@ -244,6 +311,7 @@ impl Default for TileState {
|
|||
wonder_anchor_schools: Vec::new(),
|
||||
wonder_tier: 0,
|
||||
substrate_id: String::new(),
|
||||
flora_cover_id: String::new(),
|
||||
water_body_id: -1,
|
||||
depth_from_coast: -1,
|
||||
canopy_cover: 0.0,
|
||||
|
|
@ -289,11 +357,32 @@ impl Default for TileState {
|
|||
fish_stock: 0.0,
|
||||
aerosol_mitigation: 0.0,
|
||||
resource_id: String::new(),
|
||||
plate_id: 0,
|
||||
plate_kind: 0,
|
||||
boundary_kind: 0,
|
||||
mountain_proximity: 0.0,
|
||||
coast_proximity: 0.0,
|
||||
latitude: 0.0,
|
||||
continentality: 0.5,
|
||||
mean_temp: 0.5,
|
||||
mean_precip: 0.5,
|
||||
seasonality: 0.0,
|
||||
aridity_index: 1.0,
|
||||
t_band: 2,
|
||||
p_band: 2,
|
||||
flow_out: u8::MAX,
|
||||
drainage_area: 0,
|
||||
stream_order: 1,
|
||||
lake_id: None,
|
||||
riparian_distance: u8::MAX,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_one_f32() -> f32 { 1.0 }
|
||||
fn default_no_flow() -> u8 { u8::MAX }
|
||||
fn default_stream_order() -> u8 { 1 }
|
||||
fn default_riparian_distance() -> u8 { u8::MAX }
|
||||
|
||||
/// Global grid simulation state — field names match `types.ts GridState`.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -580,7 +669,7 @@ impl GridState {
|
|||
/// the registry's per-biome cap.
|
||||
pub fn stamp_terrain_tier_caps(&mut self) {
|
||||
for tile in &mut self.tiles {
|
||||
tile.terrain_tier_cap = biome_registry::terrain_tier_cap(&tile.biome_id);
|
||||
tile.terrain_tier_cap = biome_registry::terrain_tier_cap(&tile.biome_label_id);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
pub mod action;
|
||||
pub mod algorithms;
|
||||
pub mod building_action;
|
||||
pub mod collectibles;
|
||||
pub mod formation;
|
||||
pub mod gd_compat;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue