"""Environmental degradation system."""
from ..utils.constants import *
[docs]
class DegradationSystem:
"""Manages per-tile environmental degradation and island-wide recovery.
Each game tick this system:
1. Applies degradation from every pig and villager to the tile they occupy.
2. Recalculates :attr:`overall_degradation` as the mean degradation across
all island (grass / forest) tiles.
3. Applies natural recovery to every island tile.
4. Spreads cascading degradation from heavily degraded tiles (> 0.8) to
their 8 neighbours.
"""
def __init__(self, isometric_map):
"""Initialise the degradation system.
Args:
isometric_map (IsometricMap): The map whose
:attr:`~game.isometric_map.IsometricMap.degradation_levels`
array will be read and written each tick.
"""
[docs]
self.isometric_map = isometric_map
#: Mean degradation across all island tiles, in [0, 1].
#: Checked by :meth:`is_ecosystem_collapsed`.
[docs]
self.overall_degradation = 0.0
#: Maps warning thresholds (0.3, 0.5, 0.7, 0.9) to a bool tracking
#: whether the warning has already fired this game.
[docs]
self.degradation_threshold_warnings = {
0.3: False,
0.5: False,
0.7: False,
0.9: False
}
[docs]
def update(self, dt, entity_manager):
"""Advance the degradation simulation by one time step.
Applies entity-driven degradation, recalculates
:attr:`overall_degradation`, runs natural recovery, and triggers
cascading spread.
Args:
dt (float): Elapsed time in seconds since the last frame.
entity_manager (EntityManager): Provides access to the current
lists of pigs and villagers whose positions are used to
locate degradation sources.
"""
# --- NEW: Apply degradation from pigs and villagers using new constants ---
for pig in entity_manager.pigs:
if self.isometric_map.is_valid_position(pig.x, pig.y):
self.isometric_map.add_degradation(pig.x, pig.y, PIG_DEGRADATION_RATE * dt)
for villager in entity_manager.villagers:
if self.isometric_map.is_valid_position(villager.x, villager.y):
self.isometric_map.add_degradation(villager.x, villager.y, VILLAGER_DEGRADATION_RATE * dt)
# --- Calculate overall degradation based only on 'grass' and 'forest' tiles ---
total_degradation = 0
island_tiles = 0
for y in range(self.isometric_map.height):
for x in range(self.isometric_map.width):
gid = self.isometric_map.map_data_gids[y][x]
tile_type = self.isometric_map.tile_id_mapping.get(gid)
if tile_type in ISLAND_TILES:
total_degradation += self.isometric_map.degradation_levels[y][x]
island_tiles += 1
if island_tiles > 0:
self.overall_degradation = total_degradation / island_tiles
else:
self.overall_degradation = 0.0
# --- Natural recovery using direct array access ---
for y in range(self.isometric_map.height):
for x in range(self.isometric_map.width):
gid = self.isometric_map.map_data_gids[y][x]
tile_type = self.isometric_map.tile_id_mapping.get(gid)
if tile_type in ISLAND_TILES:
current_degradation = self.isometric_map.degradation_levels[y][x]
if current_degradation > 0:
recovered_amount = NATURAL_RECOVERY_RATE * dt
new_degradation = max(0, current_degradation - recovered_amount)
self.isometric_map.degradation_levels[y][x] = new_degradation
# Cascading degradation effect
self._apply_cascading_degradation(dt)
def _apply_cascading_degradation(self, dt):
"""Spread degradation from heavily degraded tiles to their neighbours.
Any non-water tile whose degradation level exceeds **0.8** acts as a
source and adds ``CASCADE_DEGRADATION_RATE * dt * current_degradation``
to each of its 8 neighbouring non-water tiles. All changes are
accumulated in a temporary buffer and applied atomically so that
sources and sinks within the same frame do not influence each other.
Args:
dt (float): Elapsed time in seconds since the last frame.
"""
degradation_changes = [[0.0 for _ in range(self.isometric_map.width)] for _ in range(self.isometric_map.height)]
for y in range(self.isometric_map.height):
for x in range(self.isometric_map.width):
gid = self.isometric_map.map_data_gids[y][x]
tile_type = self.isometric_map.tile_id_mapping.get(gid)
if tile_type == 'water' or tile_type is None:
continue
current_degradation = self.isometric_map.degradation_levels[y][x]
if current_degradation > 0.8:
for dy in [-1, 0, 1]:
for dx in [-1, 0, 1]:
if dx == 0 and dy == 0:
continue
nx, ny = x + dx, y + dy
if 0 <= ny < self.isometric_map.height and 0 <= nx < self.isometric_map.width:
neighbor_gid = self.isometric_map.map_data_gids[ny][nx]
neighbor_type = self.isometric_map.tile_id_mapping.get(neighbor_gid)
if neighbor_type != 'water' and neighbor_type is not None:
# --- USE CONSTANT FOR CASCADE RATE ---
cascade_amount = CASCADE_DEGRADATION_RATE * dt * current_degradation
degradation_changes[ny][nx] += cascade_amount
# Apply the calculated changes
for y in range(self.isometric_map.height):
for x in range(self.isometric_map.width):
if degradation_changes[y][x] > 0:
current_level = self.isometric_map.degradation_levels[y][x]
self.isometric_map.degradation_levels[y][x] = min(1.0, current_level + degradation_changes[y][x])
[docs]
def get_overall_degradation(self):
"""Return the current island-wide degradation level.
Returns:
float: Mean degradation across all island tiles, in [0, 1].
"""
return self.overall_degradation
[docs]
def is_ecosystem_collapsed(self):
"""Return whether the ecosystem has passed the collapse threshold.
Collapse is defined as :attr:`overall_degradation` exceeding
``ECOSYSTEM_COLLAPSE_THRESHOLD`` (see
:mod:`game.utils.constants`). When this returns ``True`` the game
triggers a loss condition.
Returns:
bool: ``True`` if the ecosystem has collapsed.
"""
return self.overall_degradation > ECOSYSTEM_COLLAPSE_THRESHOLD
[docs]
def get_degradation_status(self):
"""Return a human-readable label for the current degradation level.
Returns:
str: One of ``"Healthy"``, ``"Slight Damage"``,
``"Moderate Damage"``, ``"Heavy Damage"``, or
``"Critical Damage"``.
"""
if self.overall_degradation < 0.2:
return "Healthy"
elif self.overall_degradation < 0.4:
return "Slight Damage"
elif self.overall_degradation < 0.6:
return "Moderate Damage"
elif self.overall_degradation < 0.8:
return "Heavy Damage"
else:
return "Critical Damage"