feat(simulator): ✨ Add CourierRoute and OpenBordersAgreement structs/enums with trade logic to simulator API and trade crate
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
0658101581
commit
1cbf3fcd4e
2 changed files with 576 additions and 63 deletions
|
|
@ -2828,6 +2828,126 @@ impl GdTrade {
|
|||
}
|
||||
}
|
||||
|
||||
/// Create a `CourierRoute` resource for the given sender/receiver capitals.
|
||||
///
|
||||
/// Returns a `GdCourierRoute` RefCounted resource.
|
||||
#[func]
|
||||
fn courier_route_new(
|
||||
&self,
|
||||
sender_capital_idx: i64,
|
||||
receiver_capital_idx: i64,
|
||||
) -> Gd<GdCourierRoute> {
|
||||
use mc_trade::CourierRoute;
|
||||
let route = CourierRoute {
|
||||
sender: sender_capital_idx as u8,
|
||||
receiver: receiver_capital_idx as u8,
|
||||
courier_era_tier: 2,
|
||||
dispatched_turn: 0,
|
||||
position: (0, 0),
|
||||
eta_turn: None,
|
||||
delivered: false,
|
||||
intercepted: false,
|
||||
planned_path: Vec::new(),
|
||||
path_step: 0,
|
||||
};
|
||||
Gd::from_init_fn(|base| GdCourierRoute { inner: route, base })
|
||||
}
|
||||
|
||||
/// Create an `OpenBordersAgreement` resource.
|
||||
///
|
||||
/// Returns a `GdOpenBordersAgreement` RefCounted resource.
|
||||
#[func]
|
||||
fn open_borders_agreement_new(
|
||||
&self,
|
||||
player_a: i64,
|
||||
player_b: i64,
|
||||
gold: i64,
|
||||
turns: i64,
|
||||
) -> Gd<GdOpenBordersAgreement> {
|
||||
use mc_trade::{OpenBordersAgreement, TradeLedger};
|
||||
let mut tmp_ledger = TradeLedger::default();
|
||||
let id = tmp_ledger.alloc_agreement_id();
|
||||
let (a, b) = (player_a as u8, player_b as u8);
|
||||
let (pa, pb) = if a <= b { (a, b) } else { (b, a) };
|
||||
let ag = OpenBordersAgreement {
|
||||
agreement_id: id,
|
||||
partners: (pa, pb),
|
||||
turn_started: 0,
|
||||
turns_remaining: turns as u32,
|
||||
payment_gold: gold as u32,
|
||||
payment_luxury: None,
|
||||
};
|
||||
Gd::from_init_fn(|base| GdOpenBordersAgreement { inner: ag, base })
|
||||
}
|
||||
|
||||
/// Create a `SharedMapAgreement` resource.
|
||||
///
|
||||
/// Returns a `GdSharedMapAgreement` RefCounted resource.
|
||||
#[func]
|
||||
fn shared_map_agreement_new(
|
||||
&self,
|
||||
player_a: i64,
|
||||
player_b: i64,
|
||||
gold: i64,
|
||||
turns: i64,
|
||||
) -> Gd<GdSharedMapAgreement> {
|
||||
use mc_trade::{SharedMapAgreement, TradeLedger};
|
||||
let mut tmp_ledger = TradeLedger::default();
|
||||
let id = tmp_ledger.alloc_agreement_id();
|
||||
let (a, b) = (player_a as u8, player_b as u8);
|
||||
let (pa, pb) = if a <= b { (a, b) } else { (b, a) };
|
||||
let ag = SharedMapAgreement {
|
||||
agreement_id: id,
|
||||
partners: (pa, pb),
|
||||
turn_started: 0,
|
||||
duration: turns as u32,
|
||||
share_turns_remaining: 0,
|
||||
payment_gold: gold as u32,
|
||||
payment_luxury: None,
|
||||
courier_route: None,
|
||||
};
|
||||
Gd::from_init_fn(|base| GdSharedMapAgreement { inner: ag, base })
|
||||
}
|
||||
|
||||
/// Advance all OpenBorders and SharedMap agreements in `ledger` by one turn.
|
||||
///
|
||||
/// `ledger` — `GdTradeLedger` resource (mutated in place).
|
||||
/// `map_view` — `GdCourierMapView` resource providing the spatial view.
|
||||
/// `current_turn` — used as RNG seed for deterministic intercept rolls.
|
||||
///
|
||||
/// Returns an `Array[Dictionary]` of diplomacy events emitted this step.
|
||||
#[func]
|
||||
fn step_shared_map_agreements(
|
||||
&self,
|
||||
mut ledger: Gd<GdTradeLedger>,
|
||||
map_view: Gd<GdCourierMapView>,
|
||||
current_turn: i64,
|
||||
) -> Array<Dictionary> {
|
||||
use mc_trade::step_shared_map_agreements;
|
||||
use rand::SeedableRng;
|
||||
use rand::rngs::SmallRng;
|
||||
use mc_turn::courier_resolver::GameStateMapView;
|
||||
|
||||
let mv_bind = map_view.bind();
|
||||
let Some(ref gs_gd) = mv_bind.game_state else {
|
||||
godot_error!("GdTrade::step_shared_map_agreements: map_view has no GameState");
|
||||
return Array::new();
|
||||
};
|
||||
let gs_bind = gs_gd.bind();
|
||||
let map_impl = GameStateMapView { state: &gs_bind.inner };
|
||||
let mut rng = SmallRng::seed_from_u64(current_turn as u64);
|
||||
let events = step_shared_map_agreements(
|
||||
&mut ledger.bind_mut().inner,
|
||||
&map_impl,
|
||||
&mut rng,
|
||||
);
|
||||
|
||||
events
|
||||
.into_iter()
|
||||
.map(diplomacy_event_to_dict)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Break all trades involving a war pair and advance relation states.
|
||||
///
|
||||
/// `ledger_json` — current `TradeLedger` JSON.
|
||||
|
|
@ -2871,6 +2991,398 @@ impl GdTrade {
|
|||
}
|
||||
}
|
||||
|
||||
// ── GdTrade courier diplomacy types ─────────────────────────────────────
|
||||
|
||||
/// Convert a `DiplomacyEvent` to a `Dictionary` for GDScript consumption.
|
||||
fn diplomacy_event_to_dict(ev: mc_trade::DiplomacyEvent) -> Dictionary {
|
||||
use mc_trade::DiplomacyEvent;
|
||||
let mut d = Dictionary::new();
|
||||
match ev {
|
||||
DiplomacyEvent::CourierDispatched(e) => {
|
||||
d.set("type", GString::from("courier_dispatched"));
|
||||
d.set("agreement_id", e.agreement_id as i64);
|
||||
d.set("from_player", e.from_player as i64);
|
||||
d.set("to_player", e.to_player as i64);
|
||||
d.set("courier_unit", GString::from(e.courier_unit));
|
||||
d.set("eta_turns", e.eta_turns as i64);
|
||||
}
|
||||
DiplomacyEvent::CourierIntercepted(e) => {
|
||||
d.set("type", GString::from("courier_intercepted"));
|
||||
d.set("agreement_id", e.agreement_id as i64);
|
||||
d.set("at_col", e.at_position.0 as i64);
|
||||
d.set("at_row", e.at_position.1 as i64);
|
||||
d.set("by_player", e.by_player as i64);
|
||||
}
|
||||
DiplomacyEvent::SharedMapDelivered(e) => {
|
||||
d.set("type", GString::from("shared_map_delivered"));
|
||||
d.set("agreement_id", e.agreement_id as i64);
|
||||
d.set("from_player", e.from_player as i64);
|
||||
d.set("to_player", e.to_player as i64);
|
||||
d.set("turns_remaining", e.turns_remaining as i64);
|
||||
}
|
||||
DiplomacyEvent::SharedMapExpired(e) => {
|
||||
d.set("type", GString::from("shared_map_expired"));
|
||||
d.set("agreement_id", e.agreement_id as i64);
|
||||
}
|
||||
DiplomacyEvent::OpenBordersSigned(e) => {
|
||||
d.set("type", GString::from("open_borders_signed"));
|
||||
d.set("agreement_id", e.agreement_id as i64);
|
||||
d.set("player_a", e.player_a as i64);
|
||||
d.set("player_b", e.player_b as i64);
|
||||
d.set("turns_remaining", e.turns_remaining as i64);
|
||||
}
|
||||
DiplomacyEvent::OpenBordersExpired(e) => {
|
||||
d.set("type", GString::from("open_borders_expired"));
|
||||
d.set("agreement_id", e.agreement_id as i64);
|
||||
}
|
||||
other => {
|
||||
if let Ok(s) = serde_json::to_string(&other) {
|
||||
d.set("type", GString::from("other"));
|
||||
d.set("json", GString::from(s));
|
||||
}
|
||||
}
|
||||
}
|
||||
d
|
||||
}
|
||||
|
||||
// ── GdCourierRoute ───────────────────────────────────────────────────────
|
||||
|
||||
/// Godot-visible wrapper for `CourierRoute`. Holds the in-transit state
|
||||
/// of a courier moving from one capital to another.
|
||||
#[derive(GodotClass)]
|
||||
#[class(base=RefCounted)]
|
||||
pub struct GdCourierRoute {
|
||||
pub(crate) inner: mc_trade::CourierRoute,
|
||||
base: Base<RefCounted>,
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl IRefCounted for GdCourierRoute {
|
||||
fn init(base: Base<RefCounted>) -> Self {
|
||||
use mc_trade::CourierRoute;
|
||||
Self {
|
||||
inner: CourierRoute {
|
||||
sender: 0,
|
||||
receiver: 0,
|
||||
courier_era_tier: 2,
|
||||
dispatched_turn: 0,
|
||||
position: (0, 0),
|
||||
eta_turn: None,
|
||||
delivered: false,
|
||||
intercepted: false,
|
||||
planned_path: Vec::new(),
|
||||
path_step: 0,
|
||||
},
|
||||
base,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl GdCourierRoute {
|
||||
#[func]
|
||||
fn get_sender(&self) -> i64 { self.inner.sender as i64 }
|
||||
#[func]
|
||||
fn get_receiver(&self) -> i64 { self.inner.receiver as i64 }
|
||||
#[func]
|
||||
fn get_courier_era_tier(&self) -> i64 { self.inner.courier_era_tier as i64 }
|
||||
#[func]
|
||||
fn is_delivered(&self) -> bool { self.inner.delivered }
|
||||
#[func]
|
||||
fn is_intercepted(&self) -> bool { self.inner.intercepted }
|
||||
#[func]
|
||||
fn get_position_col(&self) -> i64 { self.inner.position.0 as i64 }
|
||||
#[func]
|
||||
fn get_position_row(&self) -> i64 { self.inner.position.1 as i64 }
|
||||
#[func]
|
||||
fn get_eta_turn(&self) -> i64 {
|
||||
self.inner.eta_turn.map(|t| t as i64).unwrap_or(-1)
|
||||
}
|
||||
|
||||
#[func]
|
||||
fn to_dict(&self) -> Dictionary {
|
||||
let mut d = Dictionary::new();
|
||||
d.set("sender", self.inner.sender as i64);
|
||||
d.set("receiver", self.inner.receiver as i64);
|
||||
d.set("courier_era_tier", self.inner.courier_era_tier as i64);
|
||||
d.set("delivered", self.inner.delivered);
|
||||
d.set("intercepted", self.inner.intercepted);
|
||||
d.set("position_col", self.inner.position.0 as i64);
|
||||
d.set("position_row", self.inner.position.1 as i64);
|
||||
d.set("eta_turn", self.inner.eta_turn.map(|t| t as i64).unwrap_or(-1));
|
||||
d
|
||||
}
|
||||
}
|
||||
|
||||
// ── GdOpenBordersAgreement ───────────────────────────────────────────────
|
||||
|
||||
/// Godot-visible wrapper for `OpenBordersAgreement`.
|
||||
#[derive(GodotClass)]
|
||||
#[class(base=RefCounted)]
|
||||
pub struct GdOpenBordersAgreement {
|
||||
pub(crate) inner: mc_trade::OpenBordersAgreement,
|
||||
base: Base<RefCounted>,
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl IRefCounted for GdOpenBordersAgreement {
|
||||
fn init(base: Base<RefCounted>) -> Self {
|
||||
use mc_trade::OpenBordersAgreement;
|
||||
Self {
|
||||
inner: OpenBordersAgreement {
|
||||
agreement_id: 0,
|
||||
partners: (0, 1),
|
||||
turn_started: 0,
|
||||
turns_remaining: 0,
|
||||
payment_gold: 0,
|
||||
payment_luxury: None,
|
||||
},
|
||||
base,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl GdOpenBordersAgreement {
|
||||
#[func]
|
||||
fn get_agreement_id(&self) -> i64 { self.inner.agreement_id as i64 }
|
||||
#[func]
|
||||
fn get_player_a(&self) -> i64 { self.inner.partners.0 as i64 }
|
||||
#[func]
|
||||
fn get_player_b(&self) -> i64 { self.inner.partners.1 as i64 }
|
||||
#[func]
|
||||
fn get_turns_remaining(&self) -> i64 { self.inner.turns_remaining as i64 }
|
||||
#[func]
|
||||
fn get_payment_gold(&self) -> i64 { self.inner.payment_gold as i64 }
|
||||
|
||||
#[func]
|
||||
fn to_dict(&self) -> Dictionary {
|
||||
let mut d = Dictionary::new();
|
||||
d.set("type", GString::from("open_borders"));
|
||||
d.set("agreement_id", self.inner.agreement_id as i64);
|
||||
d.set("partner_a", self.inner.partners.0 as i64);
|
||||
d.set("partner_b", self.inner.partners.1 as i64);
|
||||
d.set("turns_remaining", self.inner.turns_remaining as i64);
|
||||
d.set("payment_gold", self.inner.payment_gold as i64);
|
||||
d
|
||||
}
|
||||
}
|
||||
|
||||
// ── GdSharedMapAgreement ─────────────────────────────────────────────────
|
||||
|
||||
/// Godot-visible wrapper for `SharedMapAgreement`.
|
||||
#[derive(GodotClass)]
|
||||
#[class(base=RefCounted)]
|
||||
pub struct GdSharedMapAgreement {
|
||||
pub(crate) inner: mc_trade::SharedMapAgreement,
|
||||
base: Base<RefCounted>,
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl IRefCounted for GdSharedMapAgreement {
|
||||
fn init(base: Base<RefCounted>) -> Self {
|
||||
use mc_trade::SharedMapAgreement;
|
||||
Self {
|
||||
inner: SharedMapAgreement {
|
||||
agreement_id: 0,
|
||||
partners: (0, 1),
|
||||
turn_started: 0,
|
||||
duration: 0,
|
||||
share_turns_remaining: 0,
|
||||
payment_gold: 0,
|
||||
payment_luxury: None,
|
||||
courier_route: None,
|
||||
},
|
||||
base,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl GdSharedMapAgreement {
|
||||
#[func]
|
||||
fn get_agreement_id(&self) -> i64 { self.inner.agreement_id as i64 }
|
||||
#[func]
|
||||
fn get_player_a(&self) -> i64 { self.inner.partners.0 as i64 }
|
||||
#[func]
|
||||
fn get_player_b(&self) -> i64 { self.inner.partners.1 as i64 }
|
||||
#[func]
|
||||
fn get_share_turns_remaining(&self) -> i64 { self.inner.share_turns_remaining as i64 }
|
||||
#[func]
|
||||
fn get_payment_gold(&self) -> i64 { self.inner.payment_gold as i64 }
|
||||
#[func]
|
||||
fn is_delivered(&self) -> bool {
|
||||
self.inner.courier_route.as_ref().map(|r| r.delivered).unwrap_or(false)
|
||||
}
|
||||
|
||||
#[func]
|
||||
fn get_courier_route(&self) -> Option<Gd<GdCourierRoute>> {
|
||||
self.inner.courier_route.as_ref().map(|r| {
|
||||
Gd::from_init_fn(|base| GdCourierRoute { inner: r.clone(), base })
|
||||
})
|
||||
}
|
||||
|
||||
#[func]
|
||||
fn to_dict(&self) -> Dictionary {
|
||||
let mut d = Dictionary::new();
|
||||
d.set("type", GString::from("shared_map"));
|
||||
d.set("agreement_id", self.inner.agreement_id as i64);
|
||||
d.set("partner_a", self.inner.partners.0 as i64);
|
||||
d.set("partner_b", self.inner.partners.1 as i64);
|
||||
d.set("share_turns_remaining", self.inner.share_turns_remaining as i64);
|
||||
d.set("payment_gold", self.inner.payment_gold as i64);
|
||||
d.set("delivered", self.is_delivered());
|
||||
d
|
||||
}
|
||||
}
|
||||
|
||||
// ── GdTradeLedger ────────────────────────────────────────────────────────
|
||||
|
||||
/// Godot-visible wrapper for `TradeLedger`. Holds all active diplomatic
|
||||
/// agreements. Construct via `GdTradeLedger.from_json(json)` or
|
||||
/// `GdTradeLedger.create()` for an empty ledger.
|
||||
#[derive(GodotClass)]
|
||||
#[class(base=RefCounted)]
|
||||
pub struct GdTradeLedger {
|
||||
pub(crate) inner: mc_trade::TradeLedger,
|
||||
base: Base<RefCounted>,
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl IRefCounted for GdTradeLedger {
|
||||
fn init(base: Base<RefCounted>) -> Self {
|
||||
Self {
|
||||
inner: mc_trade::TradeLedger::default(),
|
||||
base,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl GdTradeLedger {
|
||||
/// Create an empty trade ledger.
|
||||
#[func]
|
||||
fn create() -> Gd<GdTradeLedger> {
|
||||
Gd::from_init_fn(|base| GdTradeLedger {
|
||||
inner: mc_trade::TradeLedger::default(),
|
||||
base,
|
||||
})
|
||||
}
|
||||
|
||||
/// Deserialize a `TradeLedger` from JSON.
|
||||
#[func]
|
||||
fn from_json(json: GString) -> Gd<GdTradeLedger> {
|
||||
let inner = serde_json::from_str(&json.to_string()).unwrap_or_else(|e| {
|
||||
godot_error!("GdTradeLedger::from_json error: {}", e);
|
||||
mc_trade::TradeLedger::default()
|
||||
});
|
||||
Gd::from_init_fn(|base| GdTradeLedger { inner, base })
|
||||
}
|
||||
|
||||
/// Serialize the ledger to JSON.
|
||||
#[func]
|
||||
fn to_json(&self) -> GString {
|
||||
match serde_json::to_string(&self.inner) {
|
||||
Ok(s) => GString::from(s),
|
||||
Err(e) => {
|
||||
godot_error!("GdTradeLedger::to_json error: {}", e);
|
||||
GString::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns all active OpenBorders agreements as an `Array[GdOpenBordersAgreement]`.
|
||||
#[func]
|
||||
fn iter_open_borders(&self) -> Array<Gd<GdOpenBordersAgreement>> {
|
||||
use mc_trade::DiplomaticAgreement;
|
||||
self.inner
|
||||
.agreements
|
||||
.iter()
|
||||
.filter_map(|ag| {
|
||||
if let DiplomaticAgreement::OpenBorders(ob) = ag {
|
||||
Some(Gd::from_init_fn(|base| GdOpenBordersAgreement {
|
||||
inner: ob.clone(),
|
||||
base,
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns all active SharedMap agreements as an `Array[GdSharedMapAgreement]`.
|
||||
#[func]
|
||||
fn iter_shared_map(&self) -> Array<Gd<GdSharedMapAgreement>> {
|
||||
use mc_trade::DiplomaticAgreement;
|
||||
self.inner
|
||||
.agreements
|
||||
.iter()
|
||||
.filter_map(|ag| {
|
||||
if let DiplomaticAgreement::SharedMap(sm) = ag {
|
||||
Some(Gd::from_init_fn(|base| GdSharedMapAgreement {
|
||||
inner: sm.clone(),
|
||||
base,
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Push an OpenBorders agreement into this ledger.
|
||||
#[func]
|
||||
fn add_open_borders(&mut self, agreement: Gd<GdOpenBordersAgreement>) {
|
||||
use mc_trade::DiplomaticAgreement;
|
||||
self.inner
|
||||
.agreements
|
||||
.push(DiplomaticAgreement::OpenBorders(agreement.bind().inner.clone()));
|
||||
}
|
||||
|
||||
/// Push a SharedMap agreement into this ledger.
|
||||
#[func]
|
||||
fn add_shared_map(&mut self, agreement: Gd<GdSharedMapAgreement>) {
|
||||
use mc_trade::DiplomaticAgreement;
|
||||
self.inner
|
||||
.agreements
|
||||
.push(DiplomaticAgreement::SharedMap(agreement.bind().inner.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
// ── GdCourierMapView ─────────────────────────────────────────────────────
|
||||
|
||||
/// Godot-visible `CourierMapView` backed by a `GdGameState` handle.
|
||||
/// Delegates `capital_position`, `route_intact`, and `intercept_chance_at`
|
||||
/// to `mc_turn::courier_resolver::GameStateMapView`.
|
||||
///
|
||||
/// Construct via `GdCourierMapView.from_game_state(game_state)`.
|
||||
#[derive(GodotClass)]
|
||||
#[class(base=RefCounted)]
|
||||
pub struct GdCourierMapView {
|
||||
pub(crate) game_state: Option<Gd<GdGameState>>,
|
||||
base: Base<RefCounted>,
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl IRefCounted for GdCourierMapView {
|
||||
fn init(base: Base<RefCounted>) -> Self {
|
||||
Self { game_state: None, base }
|
||||
}
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl GdCourierMapView {
|
||||
/// Construct a `GdCourierMapView` from an existing `GdGameState` handle.
|
||||
#[func]
|
||||
fn from_game_state(state: Gd<GdGameState>) -> Gd<GdCourierMapView> {
|
||||
Gd::from_init_fn(|base| GdCourierMapView {
|
||||
game_state: Some(state),
|
||||
base,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ── GdCulture ───────────────────────────────────────────────────────────
|
||||
//
|
||||
// Thin wrapper over `mc_culture::CulturePool`. GDScript calls `register_city`
|
||||
|
|
|
|||
|
|
@ -582,49 +582,54 @@ pub fn step_shared_map_agreements(
|
|||
}
|
||||
|
||||
DiplomaticAgreement::SharedMap(sm) => {
|
||||
// Copy stable fields up front to avoid split-borrow conflicts below.
|
||||
let agreement_id = sm.agreement_id;
|
||||
let duration = sm.duration;
|
||||
|
||||
// Phase A: courier already delivered — tick share window.
|
||||
if let Some(ref route) = sm.courier_route {
|
||||
if route.delivered {
|
||||
if sm.courier_route.as_ref().map(|r| r.delivered).unwrap_or(false) {
|
||||
if sm.share_turns_remaining == 0 {
|
||||
events.push(DiplomacyEvent::SharedMapExpired(SharedMapExpired {
|
||||
agreement_id,
|
||||
}));
|
||||
to_remove.push(idx);
|
||||
} else {
|
||||
sm.share_turns_remaining -= 1;
|
||||
if sm.share_turns_remaining == 0 {
|
||||
events.push(DiplomacyEvent::SharedMapExpired(SharedMapExpired {
|
||||
agreement_id: sm.agreement_id,
|
||||
agreement_id,
|
||||
}));
|
||||
to_remove.push(idx);
|
||||
} else {
|
||||
sm.share_turns_remaining -= 1;
|
||||
if sm.share_turns_remaining == 0 {
|
||||
events.push(DiplomacyEvent::SharedMapExpired(SharedMapExpired {
|
||||
agreement_id: sm.agreement_id,
|
||||
}));
|
||||
to_remove.push(idx);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Phase B: courier in transit — check if already intercepted.
|
||||
if route.intercepted {
|
||||
// Terminal state — skip until caller cleans up the agreement.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Safety check: route still structurally sound.
|
||||
if !map.route_intact(route) {
|
||||
// Infrastructure severed — courier intercepted at current position.
|
||||
route.intercepted = true;
|
||||
events.push(DiplomacyEvent::CourierIntercepted(CourierIntercepted {
|
||||
agreement_id: sm.agreement_id,
|
||||
at_position: route.position,
|
||||
by_player: route.receiver, // severed by hostile action on receiver side
|
||||
}));
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Advance courier toward destination capital.
|
||||
// Phase B: terminal state — already intercepted.
|
||||
if sm.courier_route.as_ref().map(|r| r.intercepted).unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Phase C: route_intact check (severance). Read-only first, then mut.
|
||||
let severed = sm.courier_route.as_ref().map(|r| !map.route_intact(r)).unwrap_or(false);
|
||||
if severed {
|
||||
if let Some(route) = sm.courier_route.as_mut() {
|
||||
let pos = route.position;
|
||||
let by = route.receiver;
|
||||
route.intercepted = true;
|
||||
events.push(DiplomacyEvent::CourierIntercepted(CourierIntercepted {
|
||||
agreement_id,
|
||||
at_position: pos,
|
||||
by_player: by,
|
||||
}));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Phase D: advance courier.
|
||||
let route = match sm.courier_route.as_mut() {
|
||||
Some(r) => r,
|
||||
None => continue, // no courier dispatched yet
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let dest = match map.capital_position(route.receiver) {
|
||||
|
|
@ -632,42 +637,42 @@ pub fn step_shared_map_agreements(
|
|||
None => continue,
|
||||
};
|
||||
|
||||
// Adamantine Echo: both players have the wonder — deliver instantly.
|
||||
// Adamantine Echo: deliver instantly (copy route fields before mut sm).
|
||||
if map.adamantine_echo_active(route.sender, route.receiver) {
|
||||
let (sender, receiver) = (route.sender, route.receiver);
|
||||
route.position = dest;
|
||||
route.delivered = true;
|
||||
sm.share_turns_remaining = sm.duration;
|
||||
let _ = route; // release borrow on sm.courier_route before touching sm fields
|
||||
sm.share_turns_remaining = duration;
|
||||
events.push(DiplomacyEvent::SharedMapDelivered(SharedMapDelivered {
|
||||
agreement_id: sm.agreement_id,
|
||||
from_player: route.sender,
|
||||
to_player: route.receiver,
|
||||
agreement_id,
|
||||
from_player: sender,
|
||||
to_player: receiver,
|
||||
turns_remaining: sm.share_turns_remaining,
|
||||
}));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Advance `movement_per_turn` steps along planned_path (if populated),
|
||||
// or fall back to straight-line for legacy / test paths with empty paths.
|
||||
// Advance movement_per_turn steps along planned_path (or straight-line fallback).
|
||||
let steps = map.movement_per_turn(route.courier_era_tier);
|
||||
let mut intercept_event: Option<CourierIntercepted> = None;
|
||||
for _ in 0..steps {
|
||||
if route.position == dest {
|
||||
if route.position == dest || route.intercepted {
|
||||
break;
|
||||
}
|
||||
|
||||
// Check intercept at current position before each step.
|
||||
let intercept_roll: f32 = rng.gen();
|
||||
let intercept_chance = map.intercept_chance_at(route.position, route.sender);
|
||||
if intercept_roll < intercept_chance {
|
||||
let pos = route.position;
|
||||
let by = route.receiver;
|
||||
route.intercepted = true;
|
||||
events.push(DiplomacyEvent::CourierIntercepted(CourierIntercepted {
|
||||
agreement_id: sm.agreement_id,
|
||||
at_position: route.position,
|
||||
by_player: route.receiver,
|
||||
}));
|
||||
intercept_event = Some(CourierIntercepted {
|
||||
agreement_id,
|
||||
at_position: pos,
|
||||
by_player: by,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
// Advance one step: follow planned_path if available, else straight-line.
|
||||
if !route.planned_path.is_empty() {
|
||||
let next_step = route.path_step + 1;
|
||||
if next_step < route.planned_path.len() {
|
||||
|
|
@ -679,27 +684,23 @@ pub fn step_shared_map_agreements(
|
|||
} else {
|
||||
let (cx, cy) = route.position;
|
||||
let (dx, dy) = dest;
|
||||
let step_x = (dx - cx).signum();
|
||||
let step_y = (dy - cy).signum();
|
||||
route.position = (cx + step_x, cy + step_y);
|
||||
}
|
||||
|
||||
if route.intercepted {
|
||||
break;
|
||||
route.position = (cx + (dx - cx).signum(), cy + (dy - cy).signum());
|
||||
}
|
||||
}
|
||||
|
||||
if route.intercepted {
|
||||
if let Some(ev) = intercept_event {
|
||||
events.push(DiplomacyEvent::CourierIntercepted(ev));
|
||||
continue;
|
||||
}
|
||||
|
||||
if route.position == dest {
|
||||
if route.position == dest && !route.delivered {
|
||||
let (sender, receiver) = (route.sender, route.receiver);
|
||||
route.delivered = true;
|
||||
sm.share_turns_remaining = sm.duration;
|
||||
drop(route);
|
||||
sm.share_turns_remaining = duration;
|
||||
events.push(DiplomacyEvent::SharedMapDelivered(SharedMapDelivered {
|
||||
agreement_id: sm.agreement_id,
|
||||
from_player: route.sender,
|
||||
to_player: route.receiver,
|
||||
agreement_id,
|
||||
from_player: sender,
|
||||
to_player: receiver,
|
||||
turns_remaining: sm.share_turns_remaining,
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue