refactor(@projects/@magic-civilization): 🏛️ p3-24 phase 2 — port happiness aggregation GDScript→Rust (Rail-1)
happiness.gd summed building happiness effects and applied the happiness_per_city × city_count multiply IN GDScript before the GdHappiness call. Moved both into mc-happiness: - HappinessInput gains building_happiness_effects + happiness_per_city_effects (#[serde(default)]); building_happiness_total() does the sum + per-city multiply. calculate_happiness uses it. Legacy building_happiness kept as a back-compat default field. - happiness.gd passes the raw effect lists (no arithmetic); turn_processor_helpers sum_building_effects → collect_building_effects (pure per-building extraction, its only caller was happiness.gd). The luxury-map assembly stays GDScript (tile/ DataLoader extraction; mc-happiness is pure). Verified: 2 new mc-happiness cargo tests (aggregates effects+per-city; back-compat legacy field); mc-happiness 23/0; dylib rebuilt + canonical GUT 747/0 (full happiness.gd path test_happiness_turn -6/-4 unchanged). p3-24 bullet 2 done; stays partial — remaining: climate HP-loss→Rust, orchestration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a330704fe4
commit
417c8d195b
6 changed files with 96 additions and 30 deletions
|
|
@ -34,8 +34,13 @@ economy/happiness/event/turn surface.
|
|||
gold_percent composition; the `GdEconomy` FFI deserializes raw per-building
|
||||
effect lists and aggregates; `economy.gd` reduced to data extraction
|
||||
(`_collect_effect_ints/_floats`, mine count) — no gold arithmetic.
|
||||
- [ ] Happiness input assembly + computation fully in `mc-happiness`; `happiness.gd`
|
||||
reduced to a thin bridge.
|
||||
- [x] Happiness input assembly + computation fully in `mc-happiness`; `happiness.gd`
|
||||
reduced to a thin bridge. **Done (phase 2):** `HappinessInput` gains
|
||||
`building_happiness_effects` + `happiness_per_city_effects`; `building_happiness_total()`
|
||||
does the building-sum + `happiness_per_city × city_count` multiply in Rust.
|
||||
`happiness.gd` passes raw effect lists (no arithmetic); `sum_building_effects` →
|
||||
`collect_building_effects` (data extraction). The luxury-map assembly stays
|
||||
GDScript (tile/DataLoader extraction; mc-happiness is pure). 2 cargo tests; GUT 747/0.
|
||||
- [ ] Climate-effect damage application (unit HP loss) owned by Rust; GDScript
|
||||
renders/animates only.
|
||||
- [ ] (Stretch) per-turn orchestration moved behind a Rust turn driver so the
|
||||
|
|
@ -51,8 +56,16 @@ building-sum + per-pop + per-mine + percent composition into Rust:
|
|||
now deserializes the raw per-building effect shape and aggregates before
|
||||
`process_gold`; economy.gd `_build_cities_json` emits raw effect lists + population
|
||||
+ mine_count (pure extraction). Verified: 3 cargo tests, GdEconomy bridge GUT tests
|
||||
migrated to the raw shape, mc-economy + canonical GUT 747/0. **Remaining phases:**
|
||||
happiness assembly→mc-happiness, climate HP-loss→Rust, orchestration (stretch).
|
||||
migrated to the raw shape, mc-economy + canonical GUT 747/0.
|
||||
|
||||
**Phase 2 — happiness aggregation ported (bullet 2 done).** Moved happiness.gd's
|
||||
building-effect sum + `happiness_per_city × city_count` multiply into
|
||||
`mc-happiness::HappinessInput::building_happiness_total()` (new
|
||||
`building_happiness_effects` + `happiness_per_city_effects` fields). happiness.gd
|
||||
passes raw effect lists; `sum_building_effects` → `collect_building_effects` (pure
|
||||
extraction). `building_happiness` kept as a `#[serde(default)]` legacy field for
|
||||
back-compat. 2 cargo tests; dylib rebuilt + GUT 747/0. **Remaining phases:**
|
||||
climate HP-loss→Rust, per-turn orchestration (stretch).
|
||||
|
||||
## Code sites
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"generated_at": "2026-06-25T22:48:57Z",
|
||||
"generated_at": "2026-06-25T23:15:35Z",
|
||||
"totals": {
|
||||
"oos": 31,
|
||||
"missing": 0,
|
||||
"in_progress": 0,
|
||||
"done": 295,
|
||||
"stub": 0,
|
||||
"partial": 2,
|
||||
"oos": 31,
|
||||
"stub": 0,
|
||||
"missing": 0,
|
||||
"done": 295,
|
||||
"in_progress": 0,
|
||||
"total": 328
|
||||
},
|
||||
"objectives": [
|
||||
|
|
|
|||
|
|
@ -57,15 +57,15 @@ static func process_turn(player: RefCounted, _game_map: RefCounted) -> void:
|
|||
total_citizens += int(city.population)
|
||||
|
||||
var units_in_enemy_territory: int = _count_units_in_enemy_territory(player)
|
||||
var building_happiness: int = TurnProcessorHelpersScript.sum_building_effects(
|
||||
player, "happiness"
|
||||
# p3-24: pass RAW per-building effect lists; mc-happiness sums them and applies
|
||||
# the per-city multiply (happiness_per_city × city_count). No happiness
|
||||
# arithmetic lives here — Rail-1.
|
||||
var building_happiness_effects: Array[int] = (
|
||||
TurnProcessorHelpersScript.collect_building_effects(player, "happiness")
|
||||
)
|
||||
# happiness_per_city (wonder effect) — +N happiness per owned city, applied
|
||||
# once per building that carries the effect (royal_runestone +1/city etc.).
|
||||
var per_city_bonus: int = TurnProcessorHelpersScript.sum_building_effects(
|
||||
player, "happiness_per_city"
|
||||
var happiness_per_city_effects: Array[int] = (
|
||||
TurnProcessorHelpersScript.collect_building_effects(player, "happiness_per_city")
|
||||
)
|
||||
building_happiness += per_city_bonus * player.cities.size()
|
||||
var growth_tier: String = player.growth_tier if player.growth_tier != "" else "balanced"
|
||||
|
||||
# Collect distinct luxury IDs + per-deposit happiness_per_unique_copy values.
|
||||
|
|
@ -77,7 +77,8 @@ static func process_turn(player: RefCounted, _game_map: RefCounted) -> void:
|
|||
"city_count": player.cities.size(),
|
||||
"total_citizens": total_citizens,
|
||||
"units_in_enemy_territory": units_in_enemy_territory,
|
||||
"building_happiness": building_happiness,
|
||||
"building_happiness_effects": building_happiness_effects,
|
||||
"happiness_per_city_effects": happiness_per_city_effects,
|
||||
"owned_luxuries": owned_luxuries,
|
||||
"growth_tier": growth_tier,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -147,13 +147,14 @@ static func process_improvements(player: RefCounted) -> void:
|
|||
# ── Building effect summation ────────────────────────────────────
|
||||
|
||||
|
||||
static func sum_building_effects(player: RefCounted, effect_type: String) -> int:
|
||||
## Sum the numeric `value` of every building effect matching `effect_type`
|
||||
## across all of a player's cities. Building roster lives on `city.buildings`
|
||||
## (a `PackedStringArray`/`Array[String]` mirroring the Rust-side list).
|
||||
## Building data is read from DataLoader.get_building; missing entries are
|
||||
## skipped silently so saves referencing legacy ids do not crash.
|
||||
var total: int = 0
|
||||
static func collect_building_effects(player: RefCounted, effect_type: String) -> Array[int]:
|
||||
## Collect the numeric `value` of every building effect matching `effect_type`
|
||||
## across all of a player's cities — one entry per matching effect, NOT summed.
|
||||
## Aggregation (sum, per-city multiply) happens Rust-side in mc-happiness /
|
||||
## mc-economy; this is pure data extraction (Rail-1 p3-24). Building roster
|
||||
## lives on `city.buildings` (mirrors the Rust-side list); data is read from
|
||||
## DataLoader.get_building, missing entries skipped so legacy-id saves don't crash.
|
||||
var values: Array[int] = []
|
||||
for city: RefCounted in player.cities:
|
||||
if not city is CityScript:
|
||||
continue
|
||||
|
|
@ -168,8 +169,8 @@ static func sum_building_effects(player: RefCounted, effect_type: String) -> int
|
|||
continue
|
||||
var ed: Dictionary = effect as Dictionary
|
||||
if str(ed.get("type", "")) == effect_type:
|
||||
total += int(ed.get("value", 0))
|
||||
return total
|
||||
values.append(int(ed.get("value", 0)))
|
||||
return values
|
||||
|
||||
|
||||
# ── Tile yields JSON builder ─────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -145,8 +145,17 @@ pub struct HappinessInput {
|
|||
pub total_citizens: i32,
|
||||
/// Number of military units in enemy territory.
|
||||
pub units_in_enemy_territory: i32,
|
||||
/// Total happiness from buildings (already summed by GDScript from DataLoader).
|
||||
/// Pre-summed flat building happiness. Retained for backward-compat / tests;
|
||||
/// GDScript now passes 0 and the raw effect lists below instead (p3-24).
|
||||
#[serde(default)]
|
||||
pub building_happiness: i32,
|
||||
/// p3-24: per-building flat `happiness` effect VALUES (summed in Rust, not GDScript).
|
||||
#[serde(default)]
|
||||
pub building_happiness_effects: Vec<i32>,
|
||||
/// p3-24: per-building `happiness_per_city` effect VALUES — each multiplied by
|
||||
/// `city_count` and added (e.g. royal_runestone +1/city). Summed in Rust.
|
||||
#[serde(default)]
|
||||
pub happiness_per_city_effects: Vec<i32>,
|
||||
/// Map of luxury resource ID → `happiness_per_unique_copy` from deposit JSON.
|
||||
/// Each key is a unique luxury type; duplicates are excluded by the caller.
|
||||
/// Value is the per-deposit happiness value; falls back to `LUXURY_HAPPINESS`
|
||||
|
|
@ -194,6 +203,18 @@ pub fn happiness_from_luxuries(owned_luxuries: &BTreeMap<String, i32>, config: &
|
|||
.sum()
|
||||
}
|
||||
|
||||
impl HappinessInput {
|
||||
/// p3-24: total building happiness = legacy pre-summed `building_happiness`
|
||||
/// + the per-building flat `happiness` effects + per-building
|
||||
/// `happiness_per_city` × `city_count`. Moves the building-sum and per-city
|
||||
/// multiply out of GDScript (`happiness.gd`) into the Rust source of truth.
|
||||
pub fn building_happiness_total(&self) -> i32 {
|
||||
self.building_happiness
|
||||
+ self.building_happiness_effects.iter().sum::<i32>()
|
||||
+ self.happiness_per_city_effects.iter().sum::<i32>() * self.city_count
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the full happiness breakdown for a player.
|
||||
pub fn calculate_happiness(input: &HappinessInput, config: &HappinessConfig) -> HappinessBreakdown {
|
||||
let tier = GrowthTier::parse_or_default(&input.growth_tier);
|
||||
|
|
@ -212,7 +233,8 @@ pub fn calculate_happiness(input: &HappinessInput, config: &HappinessConfig) ->
|
|||
|
||||
let base_unhappiness = city_unhappiness + citizen_unhappiness + war_weariness + ascension_penalty;
|
||||
let luxury_happiness = happiness_from_luxuries(&input.owned_luxuries, config);
|
||||
let total = (input.building_happiness + luxury_happiness) - base_unhappiness;
|
||||
let building_happiness = input.building_happiness_total();
|
||||
let total = (building_happiness + luxury_happiness) - base_unhappiness;
|
||||
let status = HappinessStatus::from_total(total);
|
||||
|
||||
HappinessBreakdown {
|
||||
|
|
@ -221,7 +243,7 @@ pub fn calculate_happiness(input: &HappinessInput, config: &HappinessConfig) ->
|
|||
citizen_unhappiness,
|
||||
war_weariness,
|
||||
ascension_penalty,
|
||||
building_happiness: input.building_happiness,
|
||||
building_happiness,
|
||||
luxury_happiness,
|
||||
total,
|
||||
status: status.as_str().to_string(),
|
||||
|
|
@ -294,12 +316,33 @@ mod tests {
|
|||
total_citizens: 12,
|
||||
units_in_enemy_territory: 0,
|
||||
building_happiness: 10,
|
||||
building_happiness_effects: Vec::new(),
|
||||
happiness_per_city_effects: Vec::new(),
|
||||
// silk=4, gold_ore=4 → luxury_happiness = 8 (matches old flat rate)
|
||||
owned_luxuries: BTreeMap::from([("silk".into(), 4), ("gold_ore".into(), 4)]),
|
||||
growth_tier: "balanced".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn building_happiness_total_aggregates_effects_and_per_city() {
|
||||
// p3-24: per-building flat effects summed + per-city effects × city_count,
|
||||
// all in Rust (moved out of happiness.gd).
|
||||
let mut input = default_input();
|
||||
input.building_happiness = 0;
|
||||
input.city_count = 4;
|
||||
input.building_happiness_effects = vec![2, 3]; // flat → 5
|
||||
input.happiness_per_city_effects = vec![1, 1]; // (1+1)/city × 4 → 8
|
||||
assert_eq!(input.building_happiness_total(), 5 + 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn building_happiness_total_backward_compat_legacy_field() {
|
||||
// Legacy pre-summed building_happiness still honored when no lists given.
|
||||
let input = default_input(); // building_happiness: 10, no effect lists
|
||||
assert_eq!(input.building_happiness_total(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn balanced_tier_basic_calculation() {
|
||||
let input = default_input();
|
||||
|
|
@ -475,6 +518,8 @@ mod tests {
|
|||
total_citizens: 10,
|
||||
units_in_enemy_territory: 0,
|
||||
building_happiness: 0,
|
||||
building_happiness_effects: Vec::new(),
|
||||
happiness_per_city_effects: Vec::new(),
|
||||
owned_luxuries: BTreeMap::new(),
|
||||
growth_tier: String::new(),
|
||||
};
|
||||
|
|
@ -609,6 +654,8 @@ mod tests {
|
|||
total_citizens: 4,
|
||||
units_in_enemy_territory: 0,
|
||||
building_happiness: 0,
|
||||
building_happiness_effects: Vec::new(),
|
||||
happiness_per_city_effects: Vec::new(),
|
||||
owned_luxuries: BTreeMap::new(),
|
||||
growth_tier: "balanced".to_string(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ fn content_input() -> HappinessInput {
|
|||
total_citizens: 0,
|
||||
units_in_enemy_territory: 0,
|
||||
building_happiness: 0,
|
||||
building_happiness_effects: Vec::new(),
|
||||
happiness_per_city_effects: Vec::new(),
|
||||
owned_luxuries: BTreeMap::new(),
|
||||
growth_tier: "balanced".to_string(),
|
||||
}
|
||||
|
|
@ -34,6 +36,8 @@ fn happy_input() -> HappinessInput {
|
|||
total_citizens: 2,
|
||||
units_in_enemy_territory: 0,
|
||||
building_happiness: 10,
|
||||
building_happiness_effects: Vec::new(),
|
||||
happiness_per_city_effects: Vec::new(),
|
||||
owned_luxuries: BTreeMap::new(),
|
||||
growth_tier: "balanced".to_string(),
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue