diff --git a/.project/objectives/p3-23-trade-richness-gold-strategic.md b/.project/objectives/p3-23-trade-richness-gold-strategic.md index 094e6e83..fe438b72 100644 --- a/.project/objectives/p3-23-trade-richness-gold-strategic.md +++ b/.project/objectives/p3-23-trade-richness-gold-strategic.md @@ -36,8 +36,12 @@ tradeable in concept, but no exchange path exists). `#[serde(default)]`. Wiring follow-up.)* - [~] Logic in Rust (`mc-trade`): gold-for-luxury sale + strategic-for-strategic swap + strategic sale all evaluate + activate (8 cargo tests; mc-trade 66/0). - **[ ] GDScript deal-UI surfacing + in-game application not done** → status stays - `partial`. + **In-game application — part A (gold flow) DONE:** `GdTradeLedger.gold_flow_for` + + `GdEconomy` `trade_gold` param + `economy.gd` sourcing → inter-player sales now + hit the treasury (GUT `test_trade_gold_flows_into_net_gold`, seller +/buyer −). + **[ ] Remaining: part B** (strategic-resource gating — FFI sources `tile_strategics`, + `PlayerState.traded_strategics`, unit-gating reads `incoming_strategics`) **+ deal + UI** → status stays `partial`. ## Progress (2026-06-25) @@ -47,6 +51,15 @@ halves: strategic-resource **swaps** (`StrategicSwap`) and gold **sales** coexists/breaks-on-war; gold sale forms-as-fallback/no-sale-when-swap/strategic- routing/breaks-on-war. api-gdext + mc-turn compile with both new variants. +**In-game application part A — gold flow LIVE (2026-06-25).** `GdTradeLedger` +gains `gold_flow_for` + `incoming_strategics` #[func]s; `GdEconomy` gains a +`trade_gold` param added to net gold after the yield/golden-age multipliers (a +transfer isn't amplified by difficulty); `economy.gd._player_trade_gold` reads the +ledger and passes it. Inter-player gold sales now move treasury in the played game. +Verified: GUT `test_trade_gold_flows_into_net_gold` (seller +3 → net 8, buyer −2 → +net 3); dylib rebuilt + GUT 748/0. **Remaining: part B** (strategic-resource gating ++ deal UI). + **Remaining for `done` (the in-game application — overlaps p3-24's economy port):** (a) `mc-economy::process_gold` consumes `gold_flow_for` so sales hit the treasury; (b) FFI sources `tile_strategics`, new `PlayerState.traded_strategics`, unit-gating diff --git a/public/games/age-of-dwarves/data/objectives.json b/public/games/age-of-dwarves/data/objectives.json index 1e94b0c8..ba85242c 100644 --- a/public/games/age-of-dwarves/data/objectives.json +++ b/public/games/age-of-dwarves/data/objectives.json @@ -1,12 +1,12 @@ { - "generated_at": "2026-06-25T23:39:51Z", + "generated_at": "2026-06-26T00:12:40Z", "totals": { "in_progress": 0, + "done": 295, "stub": 0, "oos": 31, - "partial": 2, "missing": 0, - "done": 295, + "partial": 2, "total": 328 }, "objectives": [ diff --git a/src/game/engine/src/modules/empire/economy.gd b/src/game/engine/src/modules/empire/economy.gd index 5d89e04d..1d619a19 100644 --- a/src/game/engine/src/modules/empire/economy.gd +++ b/src/game/engine/src/modules/empire/economy.gd @@ -128,10 +128,31 @@ static func _build_params_json(player: RefCounted) -> String: "current_gold": int(player.gold), "deficit_floor": deficit_floor, "yield_mult": yield_mult, + # p3-23: net per-turn gold from this player's active inter-player trade + # sales (ResourceSale). Rust adds it to net gold after the multipliers. + "trade_gold": _player_trade_gold(player), } ) +static func _player_trade_gold(player: RefCounted) -> int: + ## Net per-turn gold this player nets from active ResourceSale trades, read + ## from the shared trade ledger via GdTradeLedger.gold_flow_for. Returns 0 when + ## there is no ledger yet, the GDExtension class is absent, or no sales involve + ## the player. Mirrors diplomacy.gd's ledger-read guard pattern. + if GameState.trade_ledger_json.is_empty(): + return 0 + if not ClassDB.class_exists("GdTradeLedger"): + return 0 + var ledger: RefCounted = ClassDB.instantiate("GdTradeLedger") as RefCounted + if ledger == null: + return 0 + ledger = ledger.from_json(GameState.trade_ledger_json) + if ledger == null: + return 0 + return int(ledger.gold_flow_for(int(player.index))) + + static func _disband_cheapest(player: RefCounted, count: int) -> void: ## All military units share the flat `UNIT_UPKEEP_FLAT` cost, so "cheapest" ## is just the first alive military unit encountered (matches the legacy diff --git a/src/game/engine/tests/unit/empire/test_economy_bridge.gd b/src/game/engine/tests/unit/empire/test_economy_bridge.gd index bc821346..861311fd 100644 --- a/src/game/engine/tests/unit/empire/test_economy_bridge.gd +++ b/src/game/engine/tests/unit/empire/test_economy_bridge.gd @@ -151,6 +151,44 @@ func test_insolvency_disbands_one_unit_when_below_deficit_floor() -> void: assert_eq(int(result.get("new_gold", -1)), 0) +func test_trade_gold_flows_into_net_gold() -> void: + ## p3-23: inter-player ResourceSale gold (params.trade_gold) is added to net + ## gold after the multipliers. Seller (+) and buyer (−) both verified. + if not ClassDB.class_exists("GdEconomy"): + pending("GdEconomy extension not loaded in this headless run") + return + var cities: Array = [ + { + "building_gold_effects": [5], + "gold_percent_effects": [], + "tile_gold": 0, + "building_upkeep": 0, + } + ] + var bridge: RefCounted = ClassDB.instantiate("GdEconomy") as RefCounted + + # Seller: +3 trade gold → income 5 + 3 = 8. + var seller_params: Dictionary = { + "golden_age_active": false, "golden_age_bonus": 0.2, + "current_gold": 0, "deficit_floor": -5, "trade_gold": 3, + } + var seller: Dictionary = bridge.process_turn( + JSON.stringify(cities), JSON.stringify([]), JSON.stringify(seller_params) + ) + assert_eq(int(seller.get("net_gold", 0)), 8, "seller nets income + trade_gold") + assert_eq(int(seller.get("trade_gold", -1)), 3) + + # Buyer: −2 trade gold → income 5 − 2 = 3. + var buyer_params: Dictionary = { + "golden_age_active": false, "golden_age_bonus": 0.2, + "current_gold": 0, "deficit_floor": -5, "trade_gold": -2, + } + var buyer: Dictionary = bridge.process_turn( + JSON.stringify(cities), JSON.stringify([]), JSON.stringify(buyer_params) + ) + assert_eq(int(buyer.get("net_gold", 0)), 3, "buyer pays trade_gold out of net") + + func test_wrapper_class_is_thin_static_and_preloads() -> void: ## Even without the extension, `Economy` itself must be a valid script ## with a static `process_turn` entry point — catches regressions where diff --git a/src/simulator/api-gdext/src/lib.rs b/src/simulator/api-gdext/src/lib.rs index 794894a4..5e5c9365 100644 --- a/src/simulator/api-gdext/src/lib.rs +++ b/src/simulator/api-gdext/src/lib.rs @@ -7531,6 +7531,26 @@ impl GdTradeLedger { } } + /// Net gold-per-turn this player nets from active ResourceSale deals (p3-23): + /// `+gold_per_turn` for each sale it is the seller of, `−gold_per_turn` for + /// each it buys. economy.gd passes this to `GdEconomy` as `trade_gold` so the + /// sale hits the treasury. 0 when the player has no active sales. + #[func] + fn gold_flow_for(&self, player: i64) -> i64 { + self.inner.gold_flow_for(player as u8) as i64 + } + + /// Strategic resources this player gains from active StrategicSwap / strategic + /// ResourceSale deals (p3-23) — unit-gating access. Returned as an `Array[String]`. + #[func] + fn incoming_strategics(&self, player: i64) -> Array { + self.inner + .incoming_strategics(player as u8) + .into_iter() + .map(GString::from) + .collect() + } + /// Returns all active OpenBorders agreements as an `Array[GdOpenBordersAgreement]`. #[func] fn iter_open_borders(&self) -> Array> { @@ -7831,6 +7851,12 @@ struct EconomyParams { /// handicap should help income, not amplify deficits. Default 1.0 = no-op. #[serde(default = "default_yield_mult")] yield_mult: f64, + /// Net per-turn gold from active inter-player ResourceSale deals (p3-23): + /// `+` for the seller, `−` for the buyer. Sourced from + /// `GdTradeLedger.gold_flow_for` and added to net gold AFTER the yield/golden + /// -age multipliers (a trade transfer isn't amplified by difficulty handicap). + #[serde(default)] + trade_gold: i32, } fn default_yield_mult() -> f64 { 1.0 } @@ -7843,6 +7869,7 @@ impl Default for EconomyParams { current_gold: 0, deficit_floor: 0, yield_mult: 1.0, + trade_gold: 0, } } } @@ -7903,7 +7930,9 @@ impl GdEconomy { // → 15 gold gross, which then nets out upkeep normally (warcouncil p1-31). let yield_mult = params.yield_mult.max(0.0); let gross_income_after_yield = (gross_income as f64 * yield_mult) as i32; - let net_gold = gross_income_after_yield - base_result.gold_expenses; + // p3-23: inter-player trade gold (ResourceSale flow) is a direct transfer — + // added after the multipliers so difficulty/golden-age don't amplify it. + let net_gold = gross_income_after_yield - base_result.gold_expenses + params.trade_gold; // Apply net gold, then clamp to 0 if the result breaches the // tech-scaled deficit floor (matches mc-turn's insolvency branch @@ -7925,6 +7954,7 @@ impl GdEconomy { d.set("treasury_cap_hit", false); d.set("gold_income", gross_income as i64); d.set("gold_expenses", base_result.gold_expenses as i64); + d.set("trade_gold", params.trade_gold as i64); d } }