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 atsrc/simulator/crates/mc-turn/src/combat_event.rs:84-114(#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]).cargo check -p mc-turnclean. TurnResultgainsransom_offers_accepted: Vec<UnitRansomAcceptedEvent>andransom_offers_expired: Vec<UnitRansomExpiredEvent>. ✓ Added atcombat_event.rs:189-198with#[serde(default)]for save migration.mc-turn::process_ransom_expirypushesUnitRansomExpiredEventin addition to the existingUnitCapturedEventfor the conversion. ✓processor.rs:2244-2253adds the expired event after the capture event.accept_ransom_offer/refuse_ransom_offerpush the corresponding event intopending_capture_events.ransom_offers_accepted/_expiredso the nextstep()drains them onto TurnResult. ✓api-gdext/src/lib.rs:3791-3804(accept) and:3894-3905(refuse).PendingCaptureEvents.drain_intoupdated atmc-turn/src/game_state.rs:407-414to drain both new vecs onto TurnResult.- api-gdext bridge surfaces both as
Array[Dictionary]on thestep()result. ✓api-gdext/src/lib.rs:4317-4344projectsresult.ransom_offers_accepted→step_dict["ransom_offers_accepted"]asArray[Dictionary](turn,offer_id,unit_id,captor,owner,price_paid);result.ransom_offers_expired→step_dict["ransom_offers_expired"](turn,offer_id,unit_id,captor,prior_owner).cargo check --workspaceclean on apricot. - godot-engine chronicle subscriber reads from these new arrays directly; cross-reference workaround removed. ✓ EventBus signals
ransom_accepted(event_bus.gd:87) andransom_expired(event_bus.gd:91) already exist with the exact payload shape produced by the newstep()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 fromstep_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.rsasserts both events appear inTurnResultafter 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
Related
- 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