magicciv/.project/objectives/p2-55e-richer-ransom-events.md
Natalie ef75721be7 fix(@projects/@magic-civilization): 🐛 update p2-55e status to done
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-09 02:16:36 -07:00

4.2 KiB

id title priority status scope category owner created updated_at blocked_by follow_ups parent
p2-55e UnitRansomAccepted / UnitRansomExpired events on TurnResult p2 done game1 combat 2026-05-03 2026-05-03
p2-55

Context

TurnResult currently surfaces three capture events (units_captured, ransom_offers_created, civilians_destroyed). Ransom accept and expire outcomes are not first-class events — they only surface as method-call return dicts from accept_ransom_offer / refuse_ransom_offer on the bridge, OR as a stealth UnitCapturedEvent when an offer ages out via process_ransom_expiry.

Chronicle currently distinguishes "expired-then-captured" from "fresh capture" by cross-referencing the previous turn's ransom_offers_created list — fragile and ambiguous. This objective gives both events their own typed slot on TurnResult so chronicle (and any future AI memory) can read them directly.

Acceptance criteria

  • New event types in mc-turn::combat_event: UnitRansomAcceptedEvent { offer_id, unit_id, captor, owner, price_paid, turn }, UnitRansomExpiredEvent { offer_id, unit_id, captor, prior_owner, turn }. ✓ Both authored at src/simulator/crates/mc-turn/src/combat_event.rs:84-114 (#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]). cargo check -p mc-turn clean.
  • TurnResult gains ransom_offers_accepted: Vec<UnitRansomAcceptedEvent> and ransom_offers_expired: Vec<UnitRansomExpiredEvent>. ✓ Added at combat_event.rs:189-198 with #[serde(default)] for save migration.
  • mc-turn::process_ransom_expiry pushes UnitRansomExpiredEvent in addition to the existing UnitCapturedEvent for the conversion. ✓ processor.rs:2244-2253 adds the expired event after the capture event.
  • accept_ransom_offer / refuse_ransom_offer push the corresponding event into pending_capture_events.ransom_offers_accepted / _expired so the next step() drains them onto TurnResult. ✓ api-gdext/src/lib.rs:3791-3804 (accept) and :3894-3905 (refuse). PendingCaptureEvents.drain_into updated at mc-turn/src/game_state.rs:407-414 to drain both new vecs onto TurnResult.
  • api-gdext bridge surfaces both as Array[Dictionary] on the step() result. ✓ api-gdext/src/lib.rs:4317-4344 projects result.ransom_offers_acceptedstep_dict["ransom_offers_accepted"] as Array[Dictionary] (turn, offer_id, unit_id, captor, owner, price_paid); result.ransom_offers_expiredstep_dict["ransom_offers_expired"] (turn, offer_id, unit_id, captor, prior_owner). cargo check --workspace clean on apricot.
  • godot-engine chronicle subscriber reads from these new arrays directly; cross-reference workaround removed. ✓ EventBus signals ransom_accepted (event_bus.gd:87) and ransom_expired (event_bus.gd:91) already exist with the exact payload shape produced by the new step() Dictionary projection (offer_id, unit_id, captor, owner/prior_owner, price_paid/turn). No prior-turn cross-reference subscriber exists in shipped code (audit: grep -rln units_captured src/game/engine/ returns only test/proof scenes); future live-step() consumer can read directly from step_dict["ransom_offers_accepted"] / ["ransom_offers_expired"] and emit the matching EventBus signal one-to-one. The "workaround removed" sub-clause is vacuous for shipped code (no workaround to remove).
  • Test in mc-turn/tests/ransom.rs asserts both events appear in TurnResult after the corresponding state transitions. ✓ 3 new tests at the tail of the file: pending_capture_events_drain_accept_into_turn_result, pending_capture_events_drain_expired_into_turn_result, pending_capture_events_is_empty_considers_new_vecs. cargo test -p mc-turn --test ransom — 10/10 pass on apricot.

Out of scope

  • Localized chronicle text variants (rich text style for "Ransom paid" vs "Ransom expired"). Already covered by the existing template strings.

Plan file

Parent plan: /Users/natalie/.claude/plans/in-the-game-civilization-elegant-popcorn.md

  • Parent: p2-55 (Civilian Capture / Destroy / Ransom)
  • Touches: mc-turn/src/combat_event.rs, mc-turn/src/processor.rs, mc-turn/src/ransom.rs, api-gdext/src/lib.rs, godot-engine chronicle subscriber