diff --git a/src/simulator/crates/mc-happiness/src/pool.rs b/src/simulator/crates/mc-happiness/src/pool.rs index 3284fdef..9a2df4a4 100644 --- a/src/simulator/crates/mc-happiness/src/pool.rs +++ b/src/simulator/crates/mc-happiness/src/pool.rs @@ -1,6 +1,7 @@ //! Happiness pool calculation, racial growth tiers, and Golden Age logic. use serde::{Deserialize, Serialize}; +use std::collections::BTreeSet; // ── Constants (match GDScript Happiness class) ───────────────────────── @@ -142,8 +143,9 @@ pub struct HappinessInput { pub ascension_active: bool, /// Total happiness from buildings (already summed by GDScript from DataLoader). pub building_happiness: i32, - /// Number of unique improved luxury resources. - pub unique_luxury_count: i32, + /// IDs of improved luxury resources owned by this player. Duplicates are + /// ignored — only unique luxury types grant happiness. + pub owned_luxuries: BTreeSet, /// Racial growth tier string (e.g., "balanced", "expansionist"). pub growth_tier: String, } @@ -175,6 +177,13 @@ pub struct GoldenAgeState { // ── Core functions ───────────────────────────────────────────────────── +/// Happiness contributed by unique luxury resources. +/// Each distinct luxury ID contributes `config.luxury_happiness`; duplicates +/// are ignored because `owned_luxuries` is a `BTreeSet`. +pub fn happiness_from_luxuries(owned_luxuries: &BTreeSet, config: &HappinessConfig) -> i32 { + owned_luxuries.len() as i32 * config.luxury_happiness +} + /// 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); @@ -193,7 +202,7 @@ pub fn calculate_happiness(input: &HappinessInput, config: &HappinessConfig) -> }; let base_unhappiness = city_unhappiness + citizen_unhappiness + war_weariness + ascension_penalty; - let luxury_happiness = input.unique_luxury_count * config.luxury_happiness; + let luxury_happiness = happiness_from_luxuries(&input.owned_luxuries, config); let total = (input.building_happiness + luxury_happiness) - base_unhappiness; let status = HappinessStatus::from_total(total); @@ -277,7 +286,7 @@ mod tests { units_in_enemy_territory: 0, ascension_active: false, building_happiness: 10, - unique_luxury_count: 2, + owned_luxuries: BTreeSet::from(["silk".into(), "gold_ore".into()]), growth_tier: "balanced".to_string(), } } @@ -458,7 +467,7 @@ mod tests { units_in_enemy_territory: 0, ascension_active: false, building_happiness: 0, - unique_luxury_count: 0, + owned_luxuries: BTreeSet::new(), growth_tier: String::new(), }; @@ -542,6 +551,34 @@ mod tests { assert!((get_combat_modifier(5) - 1.0).abs() < f64::EPSILON); } + // ── Luxury happiness tests ───────────────────────────────────────────── + + #[test] + fn luxury_happiness_empty_set_yields_zero() { + let config = HappinessConfig::default(); + assert_eq!(happiness_from_luxuries(&BTreeSet::new(), &config), 0); + } + + #[test] + fn luxury_happiness_three_unique_luxuries() { + let config = HappinessConfig::default(); + let luxuries = BTreeSet::from(["silk".into(), "amber".into(), "deepstone".into()]); + // 3 unique * LUXURY_HAPPINESS(4) = 12 + assert_eq!(happiness_from_luxuries(&luxuries, &config), 12); + } + + #[test] + fn luxury_happiness_duplicate_counted_once() { + let config = HappinessConfig::default(); + // BTreeSet deduplicates — inserting "silk" twice still yields one entry. + let mut luxuries = BTreeSet::new(); + luxuries.insert("silk".to_string()); + luxuries.insert("silk".to_string()); + luxuries.insert("amber".to_string()); + // 2 unique * 4 = 8, not 3 * 4 = 12 + assert_eq!(happiness_from_luxuries(&luxuries, &config), 8); + } + /// The breakdown carries both modifiers — make sure they are wired up /// from the raw helpers rather than being hardcoded/stale. #[test] @@ -557,7 +594,7 @@ mod tests { units_in_enemy_territory: 0, ascension_active: false, building_happiness: 0, - unique_luxury_count: 0, + owned_luxuries: BTreeSet::new(), growth_tier: "balanced".to_string(), }; let result = calculate_happiness(&input, &config);