feat(@projects/@magic-civilization): ✨ add ambient encounter event handling
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
63565d1d31
commit
c1aadc42e6
3 changed files with 80 additions and 1 deletions
|
|
@ -10,6 +10,10 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::ids::{CityName, ClanId, EraId, LeaderId, TechId, TileCoord, UnitKind, WonderId};
|
||||
/// Species identifier for ambient encounter events. Mirrors
|
||||
/// `mc_core::ids::SpeciesId` without introducing a cross-crate dep in
|
||||
/// mc-replay.
|
||||
pub type SpeciesId = String;
|
||||
|
||||
/// A single chronicle event. Sorted by `turn` field at the end of each turn
|
||||
/// when [`crate::history::TurnEventCollector::flush_to_history`] runs.
|
||||
|
|
@ -119,6 +123,21 @@ pub enum TurnEvent {
|
|||
/// Tech node now unlocked.
|
||||
tech: TechId,
|
||||
},
|
||||
/// A unit triggered a Layer-1 ambient encounter on a wilderness tile
|
||||
/// (p2-58b). The encounter fired from `fauna_density` on the tile, not
|
||||
/// lair proximity.
|
||||
AmbientEncounterFired {
|
||||
/// Turn the event fired on.
|
||||
turn: u32,
|
||||
/// Clan whose unit triggered the encounter.
|
||||
clan: ClanId,
|
||||
/// Tile the unit stepped onto.
|
||||
hex: TileCoord,
|
||||
/// Species drawn from the tile's `fauna_index`.
|
||||
species: SpeciesId,
|
||||
/// Group size rolled for the encounter.
|
||||
group_size: u8,
|
||||
},
|
||||
}
|
||||
|
||||
impl TurnEvent {
|
||||
|
|
@ -137,7 +156,8 @@ impl TurnEvent {
|
|||
| Self::EraEntered { turn, .. }
|
||||
| Self::LeaderChanged { turn, .. }
|
||||
| Self::ClanEliminated { turn, .. }
|
||||
| Self::TechResearched { turn, .. } => turn,
|
||||
| Self::TechResearched { turn, .. }
|
||||
| Self::AmbientEncounterFired { turn, .. } => turn,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ mc-economy = { path = "../mc-economy" }
|
|||
mc-trade = { path = "../mc-trade" }
|
||||
mc-tech = { path = "../mc-tech" }
|
||||
mc-replay = { path = "../mc-replay" }
|
||||
mc-ecology = { path = "../mc-ecology" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
wgpu = { version = "24", optional = true }
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ use mc_combat::{CombatBonuses, CombatParams, CombatResolver, CombatType, UnitSta
|
|||
use mc_combat::CombatOutcome;
|
||||
use mc_combat::{check_strategic_reqs, credit_resources, debit_resources};
|
||||
use mc_economy::{process_gold, CityGoldInput, UnitMaintenanceInput};
|
||||
use mc_ecology::encounter::{AmbientTileCtx, EncounterRates};
|
||||
use mc_tech::{PlayerTechState, TechWeb};
|
||||
use mc_trade::{
|
||||
advance_relations, apply_trade_offer, declare_war, evaluate_trade_offer, evaluate_trades,
|
||||
|
|
@ -232,6 +233,11 @@ pub struct TurnProcessor {
|
|||
/// When set, enables multi-condition victory checks (economic, culture,
|
||||
/// science, domination, city-count) instead of the simple city-count only.
|
||||
pub victory_config: Option<crate::victory::VictoryConfig>,
|
||||
/// Layer-1 ambient encounter rates loaded from
|
||||
/// `public/resources/ecology/encounter_rates.json`. When `None` the
|
||||
/// ambient hook no-ops silently (bench runs without ecology data).
|
||||
#[serde(skip)]
|
||||
pub encounter_rates: Option<EncounterRates>,
|
||||
}
|
||||
|
||||
impl TurnProcessor {
|
||||
|
|
@ -254,6 +260,7 @@ impl TurnProcessor {
|
|||
palace_registry: BuildingRegistry::new(),
|
||||
lair_combat_config: LairCombatConfig::default(),
|
||||
victory_config: None,
|
||||
encounter_rates: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1516,6 +1523,57 @@ impl TurnProcessor {
|
|||
}
|
||||
}
|
||||
|
||||
// Step 1b: Layer-1 ambient encounter rolls (p2-58b).
|
||||
// After movement, each unit on a wilderness tile with
|
||||
// fauna_density > 0 rolls against the loaded EncounterRates.
|
||||
// No-ops silently when encounter_rates is None (bench / unit
|
||||
// tests without ecology data).
|
||||
if let Some(ref rates) = self.encounter_rates {
|
||||
if let Some(ref g) = state.grid {
|
||||
let map_seed = g.seed;
|
||||
for unit in &state.players[pi].units {
|
||||
let uc = unit.col;
|
||||
let ur = unit.row;
|
||||
if uc < 0 || ur < 0 || uc >= g.width || ur >= g.height {
|
||||
continue;
|
||||
}
|
||||
let tile_idx = (ur * g.width + uc) as usize;
|
||||
let tile = &g.tiles[tile_idx];
|
||||
if tile.fauna_density <= 0.0 || tile.fauna_index.is_empty() {
|
||||
continue;
|
||||
}
|
||||
// Deterministic RNG per (map_seed, turn, player, unit).
|
||||
let mut rng = {
|
||||
let step_seed = mc_core::seed::derive_step(
|
||||
map_seed,
|
||||
mc_core::seed::SeedDomain::Encounter,
|
||||
&[state.turn as u64, unit.id as u64, pi as u64],
|
||||
);
|
||||
mc_core::seed::Pcg64::seed(step_seed)
|
||||
};
|
||||
let ctx = AmbientTileCtx {
|
||||
fauna_density: tile.fauna_density,
|
||||
ecology_tier: tile.ecosystem_tier.max(0) as u8,
|
||||
fauna_index: &tile.fauna_index,
|
||||
};
|
||||
if let Some(spec) = mc_ecology::encounter::roll_ambient_encounter(
|
||||
&ctx,
|
||||
&unit.kind,
|
||||
rates,
|
||||
&mut rng,
|
||||
) {
|
||||
result.events_emitted.push(mc_replay::TurnEvent::AmbientEncounterFired {
|
||||
turn: state.turn,
|
||||
clan: mc_replay::ClanId(pi as u32),
|
||||
hex: mc_replay::ids::TileCoord::new(uc, ur),
|
||||
species: spec.species_id.as_str().to_owned(),
|
||||
group_size: spec.group_size,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: resolve encounters. For each unit, the spatial index
|
||||
// gives us the exact list of in-range lair indices — no per-lair
|
||||
// distance check needed. We snapshot unit positions first so we
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue