"""Manages the happiness level of the villagers."""
from ..utils.constants import *
import math
[docs]
class VillagerHappinessSystem:
"""Calculates and tracks the average happiness of island villagers.
Happiness is composed of three parts:
* **Food happiness** — rises with pig count up to an ideal range, then
falls off as the population grows too large.
* **Environment happiness** — average of ecosystem health (inverse of
overall degradation) and current forest coverage.
* **Feast bonus** — a temporary boost granted when an emergency feast
reduces an over-populated pig herd; decays smoothly over time.
The two base components are recalculated once per second to avoid
per-frame overhead. The feast bonus is updated every frame for smooth
visual decay.
"""
def __init__(self, isometric_map, entity_manager, degradation_system):
"""Initialise the happiness system.
Args:
isometric_map (IsometricMap): Used to query forest tile coverage.
entity_manager (EntityManager): Provides the current pig count.
degradation_system (DegradationSystem): Provides overall
degradation for the environment happiness calculation.
"""
[docs]
self.isometric_map = isometric_map
[docs]
self.entity_manager = entity_manager
[docs]
self.degradation_system = degradation_system
#: Food-based happiness component, in [0, 1].
[docs]
self.food_happiness = 0.75
#: Environment-based happiness component, in [0, 1].
[docs]
self.environment_happiness = 0.75
#: Temporary bonus from a recent feast, in [0, 0.5]; decays each frame.
[docs]
self.feast_happiness_bonus = 0.0
#: Rate at which :attr:`feast_happiness_bonus` falls, in units per second.
[docs]
self.feast_bonus_decay_rate = 0.1
self._update_timer = 0
self._update_interval = 1.0
[docs]
def register_feast(self, pigs_slaughtered):
"""Register that an emergency feast has taken place.
If the pig count *before* the feast exceeded ``HAPPINESS_IDEAL_PIG_MAX``
(i.e. the feast actually relieved overpopulation pressure), a one-off
:attr:`feast_happiness_bonus` of **0.5** is granted. The bonus then
decays each frame via :meth:`update`.
Args:
pigs_slaughtered (int): Number of pigs removed by the feast.
Added back to the current count to reconstruct the pre-feast
population for the threshold check.
"""
pig_count_before_feast = self.entity_manager.get_pig_count() + pigs_slaughtered
if pig_count_before_feast > HAPPINESS_IDEAL_PIG_MAX:
self.feast_happiness_bonus = 0.5
[docs]
def update(self, dt):
"""Periodically recalculates base happiness and decays any active bonus."""
# Decay the feast bonus over time, this happens every frame for smoothness
if self.feast_happiness_bonus > 0:
self.feast_happiness_bonus -= self.feast_bonus_decay_rate * dt
self.feast_happiness_bonus = max(0, self.feast_happiness_bonus)
# Recalculate the slower-moving base happiness components periodically
self._update_timer += dt
if self._update_timer >= self._update_interval:
self._update_timer -= self._update_interval
self._calculate_food_happiness()
self._calculate_environment_happiness()
def _calculate_food_happiness(self):
"""Recalculate :attr:`food_happiness` from the current pig population.
The curve has three regions:
* **Below** ``HAPPINESS_IDEAL_PIG_MIN``: happiness scales linearly
from 0 up to 1.
* **Within** ``[HAPPINESS_IDEAL_PIG_MIN, HAPPINESS_IDEAL_PIG_MAX]``:
happiness is 1.0 (ideal range).
* **Above** ``HAPPINESS_IDEAL_PIG_MAX``: happiness falls linearly to 0
as the count approaches ``HAPPINESS_MAX_PIG_COUNT``.
"""
pig_count = self.entity_manager.get_pig_count()
if pig_count < HAPPINESS_IDEAL_PIG_MIN:
# Happiness ramps up from 0 to 1 as pig count approaches the ideal minimum
if HAPPINESS_IDEAL_PIG_MIN > 0:
self.food_happiness = max(0, pig_count / HAPPINESS_IDEAL_PIG_MIN)
else:
self.food_happiness = 1.0 # If min is 0, any pigs are good
elif pig_count <= HAPPINESS_IDEAL_PIG_MAX:
# Peak happiness in the ideal range
self.food_happiness = 1.0
else:
# Happiness drops off as pigs become too numerous
over_population = pig_count - HAPPINESS_IDEAL_PIG_MAX
max_over = HAPPINESS_MAX_PIG_COUNT - HAPPINESS_IDEAL_PIG_MAX
if max_over > 0:
self.food_happiness = max(0, 1.0 - (over_population / max_over))
else:
self.food_happiness = 0.0
def _calculate_environment_happiness(self):
"""Recalculate :attr:`environment_happiness` from ecosystem state.
The value is the mean of two sub-scores:
* **Health score** — ``1 - overall_degradation``.
* **Forest score** — fraction of island tiles that are forest.
"""
health_score = 1.0 - self.degradation_system.get_overall_degradation()
forest_percentage = self.isometric_map.get_forest_tile_percentage()
self.environment_happiness = (health_score + forest_percentage) / 2
[docs]
def get_average_happiness(self):
"""
Calculates and returns the current average happiness level (0.0 to 1.0).
This is calculated on-demand to ensure the value is always current.
"""
# Calculate the base happiness from its core components
base_happiness = (self.food_happiness + self.environment_happiness) / 2
# Add the decaying bonus, ensuring the total doesn't exceed 1.0
current_happiness = min(1.0, base_happiness + self.feast_happiness_bonus)
return current_happiness