feat(@projects/@magic-civilization): ✨ update p2-57 status to partial
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
ef75721be7
commit
2a734f16fb
6 changed files with 147 additions and 11 deletions
|
|
@ -320,7 +320,7 @@
|
|||
| [p2-56a](p2-56a-worker-category-types.md) | ✅ done | P2 | Worker category types — Sustenance / Construction / Wealth taxonomy | [unassigned](../team-leads/unassigned.md) | 🟢 |
|
||||
| [p2-56b](p2-56b-expertise-tier-progression.md) | ✅ done | P2 | Expertise tier progression — 5-tier specialist XP ladder | [simulator-infra](../team-leads/simulator-infra.md) | 🟢 |
|
||||
| [p2-56c](p2-56c-master-grandmaster-auras.md) | ✅ done | P2 | Master / Grandmaster auras — adjacent-slot yield propagation | [unassigned](../team-leads/unassigned.md) | 🟢 |
|
||||
| [p2-57](p2-57-production-chain-typed-resources.md) | 🔴 stub | P2 | Production-chain typed resources — raw → processed pipelines wired into mc-city | [unassigned](../team-leads/unassigned.md) | 🟢 |
|
||||
| [p2-57](p2-57-production-chain-typed-resources.md) | 🟡 partial | P2 | Production-chain typed resources — raw → processed pipelines wired into mc-city | [unassigned](../team-leads/unassigned.md) | 🟢 |
|
||||
| [p2-57a](p2-57a-typed-resource-stockpile.md) | ✅ done | P2 | Typed resource stockpile — raw vs processed taxonomy | [unassigned](../team-leads/unassigned.md) | 🟢 |
|
||||
| [p2-57b](p2-57b-consume-produce-edges.md) | 🔴 stub | P2 | Building consume/produce edges — stockpile coupled to unit quality | [unassigned](../team-leads/unassigned.md) | 🟢 |
|
||||
| [p2-58](p2-58-ambient-encounter-rolls.md) | ✅ done | P2 | Ambient encounter rolls per tile moved — fauna_density × ecology_tier | [unassigned](../team-leads/unassigned.md) | 🟢 |
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@
|
|||
|---|---|---|---|---|---|---|---|
|
||||
| **P0** | 0 | 0 | 0 | 0 | 0 | 44 | 44 |
|
||||
| **P1** | 1 | 14 | 1 | 5 | 1 | 55 | 77 |
|
||||
| **P2** | 0 | 8 | 11 | 0 | 6 | 70 | 95 |
|
||||
| **P2** | 0 | 9 | 10 | 0 | 6 | 70 | 95 |
|
||||
| **P3 (oos)** | 0 | 7 | 6 | 0 | 21 | 9 | 43 |
|
||||
| **total** | **1** | **29** | **18** | **5** | **28** | **178** | **259** |
|
||||
| **total** | **1** | **30** | **17** | **5** | **28** | **178** | **259** |
|
||||
|
||||
</td><td valign='top' style='padding-left:2em'>
|
||||
|
||||
|
|
@ -84,9 +84,9 @@
|
|||
| [p2-48](p2-48-end-of-game-summary-screen.md) | 🟡 partial | End-of-game summary screen — outcome banner, standings, score graph, awards, timeline, footer actions | — | [shipwright](../team-leads/shipwright.md) | 2026-05-03 | 🟢 unblocked |
|
||||
| [p2-55](p2-55-civilian-capture-system.md) | 🟡 partial | Civilian Capture / Destroy / Ransom | — | [combat-dev](../team-leads/combat-dev.md) | 2026-05-07 | 🟢 unblocked |
|
||||
| [p2-56](p2-56-worker-categories-and-expertise-tiers.md) | 🟡 partial | Worker categories (Sustenance/Construction/Wealth) + 5-tier expertise + Master/Grandmaster auras + idle decay | — | [unassigned](../team-leads/unassigned.md) | 2026-05-07 | 🟢 unblocked |
|
||||
| [p2-57](p2-57-production-chain-typed-resources.md) | 🟡 partial | Production-chain typed resources — raw → processed pipelines wired into mc-city | — | [unassigned](../team-leads/unassigned.md) | 2026-05-03 | 🟢 unblocked |
|
||||
| [p2-48a](p2-48a-end-game-summary-gut-and-proof.md) | 🔴 stub | End-of-game summary — GUT tests + headless proof scene | — | [shipwright](../team-leads/shipwright.md) | 2026-05-08 | 🟢 unblocked |
|
||||
| [p2-55d](p2-55d-ai-ransom-decision-hook.md) | 🔴 stub | AI ransom accept/refuse hook in mc-turn start-of-turn | — | — | 2026-05-03 | 🟢 unblocked |
|
||||
| [p2-57](p2-57-production-chain-typed-resources.md) | 🔴 stub | Production-chain typed resources — raw → processed pipelines wired into mc-city | — | [unassigned](../team-leads/unassigned.md) | 2026-05-03 | 🟢 unblocked |
|
||||
| [p2-57b](p2-57b-consume-produce-edges.md) | 🔴 stub | Building consume/produce edges — stockpile coupled to unit quality | — | [unassigned](../team-leads/unassigned.md) | 2026-05-03 | 🟢 unblocked |
|
||||
| [p2-59](p2-59-pioneer-escort-mechanic.md) | 🔴 stub | Pioneer escort mechanic — protection rules vs ambient encounters | — | [unassigned](../team-leads/unassigned.md) | 2026-05-03 | 🟢 unblocked |
|
||||
| [p2-60](p2-60-weather-lens-godot-ui.md) | 🔴 stub | Weather / observation lens switcher in the Godot HUD | — | [unassigned](../team-leads/unassigned.md) | 2026-05-03 | 🟢 unblocked |
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"generated_at": "2026-05-09T09:11:16Z",
|
||||
"generated_at": "2026-05-09T16:25:09Z",
|
||||
"totals": {
|
||||
"done": 178,
|
||||
"in_progress": 1,
|
||||
"partial": 29,
|
||||
"stub": 18,
|
||||
"partial": 30,
|
||||
"stub": 17,
|
||||
"missing": 5,
|
||||
"oos": 28,
|
||||
"total": 259
|
||||
|
|
@ -2246,7 +2246,7 @@
|
|||
"id": "p2-57",
|
||||
"title": "Production-chain typed resources — raw → processed pipelines wired into mc-city",
|
||||
"priority": "p2",
|
||||
"status": "stub",
|
||||
"status": "partial",
|
||||
"scope": "game1",
|
||||
"owner": "unassigned",
|
||||
"updated_at": "2026-05-03",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
id: p2-57
|
||||
title: "Production-chain typed resources — raw → processed pipelines wired into mc-city"
|
||||
priority: p2
|
||||
status: stub
|
||||
status: partial
|
||||
scope: game1
|
||||
category: cities
|
||||
owner: unassigned
|
||||
|
|
@ -20,8 +20,8 @@ This objective wires the production graph as consume/produce edges keyed on type
|
|||
|
||||
## Acceptance
|
||||
|
||||
- ❌ `mc-core::ResourceId` enum (or strong newtype) covers every raw + processed resource referenced in PRODUCTION_CHAIN.md. Stringly-typed `String` keys removed from production code paths.
|
||||
- ❌ `mc-city::production` exposes `consume(ResourceId, qty) -> bool` and `produce(ResourceId, qty)` against a per-city `Stockpile<ResourceId, i32>`.
|
||||
- ✓ `mc-core::ResourceId` enum (or strong newtype) covers every raw + processed resource referenced in PRODUCTION_CHAIN.md. Stringly-typed `String` keys removed from production code paths. **Done via p2-57a (cycle 53):** `mc-core::ids::ResourceId` newtype + `mc-core::resources::ResourceKind` enum (Raw / Processed) at `src/simulator/crates/mc-core/src/ids.rs:112-115` and `resources.rs:46-63`. Re-exported from mc-core lib.
|
||||
- ✓ `mc-city::production` exposes `consume(ResourceId, qty)` and `add(ResourceId, qty)` (the spec's "produce") against a per-city `ResourceStockpile`. **Done via p2-57a:** `ResourceStockpile::add/remove/consume/available/has/entries` at `mc-core/src/resources.rs:103-167`. Note: `consume` returns `Result<(), StockpileError::Insufficient { resource, requested, available }>` instead of the spec's `bool` — typed error is the stronger shape and matches Rail-1.
|
||||
- ❌ Building JSONs under `public/resources/buildings/` carry `consumes: [{resource, qty_per_turn}]` and `produces: [{resource, qty_per_turn}]` arrays for processing buildings (e.g., `forge.json::consumes=[{iron_ore,1}], produces=[{steel,1}]`).
|
||||
- ❌ Turn-end production pass: each building runs only if its `consumes` are fully satisfied from city stockpile; deficit buildings idle (logged) without crashing.
|
||||
- ❌ Unit production at a city queries the producer building's *output stockpile depth* and stamps the resulting unit with a `quality_tier` field (Bronze/Iron/Steel/Mithril) per PRODUCTION_CHAIN.md tier table.
|
||||
|
|
|
|||
135
src/simulator/crates/mc-turn/src/combat_balance.rs
Normal file
135
src/simulator/crates/mc-turn/src/combat_balance.rs
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
//! p2-55f — Data-driven combat balance config.
|
||||
//!
|
||||
//! Replaces the hardcoded `RANSOM_OFFER_DURATION_TURNS` const + the mc-ai
|
||||
//! constants `default_ransom_multiplier` and `denial_value_factor` with a
|
||||
//! single typed struct loaded from
|
||||
//! `public/games/age-of-dwarves/data/combat_balance.json`.
|
||||
//!
|
||||
//! The JSON file is the canonical source per Rail-2 (JSON game packs are
|
||||
//! canonical content). Defaults match the prior hardcoded constants exactly,
|
||||
//! so the deserialise-from-empty path produces identical behaviour to a
|
||||
//! pre-p2-55f run.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Combat balance constants loaded from `combat_balance.json`.
|
||||
///
|
||||
/// All fields default to the prior hardcoded values, so games saved before
|
||||
/// p2-55f land round-trip cleanly through `serde(default)`.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct CombatBalance {
|
||||
/// How many turns a `RansomOffer` survives in the queue before
|
||||
/// auto-converting to a permanent capture. Was the
|
||||
/// `RANSOM_OFFER_DURATION_TURNS` const (= 3).
|
||||
#[serde(default = "default_ransom_offer_duration_turns")]
|
||||
pub ransom_offer_duration_turns: u32,
|
||||
/// Default ransom price multiplier applied to a unit's gold value when
|
||||
/// the resolver creates a fresh ransom offer. Was an mc-ai const (= 2.0).
|
||||
#[serde(default = "default_default_ransom_multiplier")]
|
||||
pub default_ransom_multiplier: f32,
|
||||
/// "Value of denying the unit to the enemy" coefficient used by
|
||||
/// `decide_ransom_response` to scale the refusal threshold. Was an
|
||||
/// mc-ai const (= 0.5).
|
||||
#[serde(default = "default_denial_value_factor")]
|
||||
pub denial_value_factor: f32,
|
||||
/// XP awarded to the captor unit on a successful civilian capture
|
||||
/// (Capture posture). Was a mc-combat const (= 25).
|
||||
#[serde(default = "default_capture_civilian_xp_award")]
|
||||
pub capture_civilian_xp_award: u32,
|
||||
/// Multiplier applied to `capture_civilian_xp_award` when the captor
|
||||
/// chose Destroy posture instead. Was a mc-combat const (= 0.5).
|
||||
#[serde(default = "default_destroy_civilian_xp_award_multiplier")]
|
||||
pub destroy_civilian_xp_award_multiplier: f32,
|
||||
}
|
||||
|
||||
fn default_ransom_offer_duration_turns() -> u32 {
|
||||
3
|
||||
}
|
||||
fn default_default_ransom_multiplier() -> f32 {
|
||||
2.0
|
||||
}
|
||||
fn default_denial_value_factor() -> f32 {
|
||||
0.5
|
||||
}
|
||||
fn default_capture_civilian_xp_award() -> u32 {
|
||||
25
|
||||
}
|
||||
fn default_destroy_civilian_xp_award_multiplier() -> f32 {
|
||||
0.5
|
||||
}
|
||||
|
||||
impl Default for CombatBalance {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ransom_offer_duration_turns: default_ransom_offer_duration_turns(),
|
||||
default_ransom_multiplier: default_default_ransom_multiplier(),
|
||||
denial_value_factor: default_denial_value_factor(),
|
||||
capture_civilian_xp_award: default_capture_civilian_xp_award(),
|
||||
destroy_civilian_xp_award_multiplier: default_destroy_civilian_xp_award_multiplier(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserialise a `CombatBalance` from a JSON string. Unknown keys are
|
||||
/// silently ignored. Missing keys fall through to the default values.
|
||||
pub fn load_combat_balance(json: &str) -> Result<CombatBalance, crate::end_conditions::ConfigError> {
|
||||
serde_json::from_str(json).map_err(crate::end_conditions::ConfigError::from)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn defaults_match_legacy_constants() {
|
||||
let cb = CombatBalance::default();
|
||||
assert_eq!(cb.ransom_offer_duration_turns, 3);
|
||||
assert_eq!(cb.default_ransom_multiplier, 2.0);
|
||||
assert_eq!(cb.denial_value_factor, 0.5);
|
||||
assert_eq!(cb.capture_civilian_xp_award, 25);
|
||||
assert_eq!(cb.destroy_civilian_xp_award_multiplier, 0.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loads_full_json() {
|
||||
let json = r#"{
|
||||
"ransom_offer_duration_turns": 5,
|
||||
"default_ransom_multiplier": 1.75,
|
||||
"denial_value_factor": 0.6,
|
||||
"capture_civilian_xp_award": 30,
|
||||
"destroy_civilian_xp_award_multiplier": 0.4
|
||||
}"#;
|
||||
let cb = load_combat_balance(json).unwrap();
|
||||
assert_eq!(cb.ransom_offer_duration_turns, 5);
|
||||
assert_eq!(cb.default_ransom_multiplier, 1.75);
|
||||
assert_eq!(cb.denial_value_factor, 0.6);
|
||||
assert_eq!(cb.capture_civilian_xp_award, 30);
|
||||
assert_eq!(cb.destroy_civilian_xp_award_multiplier, 0.4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_keys_fall_through_to_defaults() {
|
||||
let json = r#"{"ransom_offer_duration_turns": 7}"#;
|
||||
let cb = load_combat_balance(json).unwrap();
|
||||
assert_eq!(cb.ransom_offer_duration_turns, 7);
|
||||
// unspecified keys take defaults
|
||||
assert_eq!(cb.default_ransom_multiplier, 2.0);
|
||||
assert_eq!(cb.denial_value_factor, 0.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_keys_silently_ignored() {
|
||||
let json = r#"{"ransom_offer_duration_turns": 4, "_doc": "future field"}"#;
|
||||
let cb = load_combat_balance(json).unwrap();
|
||||
assert_eq!(cb.ransom_offer_duration_turns, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_json_returns_typed_error() {
|
||||
let json = r#"{"ransom_offer_duration_turns": "three"}"#;
|
||||
let result = load_combat_balance(json);
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err();
|
||||
assert!(err.detail.contains("expected") || err.detail.contains("invalid"));
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ pub mod abstract_projection;
|
|||
pub mod action;
|
||||
pub mod action_handlers;
|
||||
pub mod capture;
|
||||
pub mod combat_balance;
|
||||
pub mod ransom;
|
||||
pub mod building_action_handlers;
|
||||
pub mod chronicle;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue