feat(@projects/@magic-civilization): 💰 p3-23 part A — inter-player gold sales hit the treasury in-game
Wires the ResourceSale gold flow into the live economy (leveraging the p3-24 phase-1 economy port). 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 trade transfer must not be amplified by difficulty handicap); economy.gd._player_trade_gold reads GameState.trade_ledger_json via GdTradeLedger and passes the player's net flow. A seller now gains gold and a buyer pays it each turn a sale is active. Verified: GUT test_trade_gold_flows_into_net_gold (seller +3 → net 8, buyer −2 → net 3, trade_gold echoed); dylib rebuilt + canonical GUT 748/0. p3-23 stays partial — gold-trade flow now live (part A); remaining part B is the strategic-resource gating (FFI sources tile_strategics, PlayerState.traded_strategics, unit-gating reads incoming_strategics) + the deal UI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e00c0477ab
commit
a8c01cb5e1
5 changed files with 108 additions and 6 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<GString> {
|
||||
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<Gd<GdOpenBordersAgreement>> {
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue