"""Main game state management."""
import pygame
import time
from .isometric_map import IsometricMap
from .entities.entity_manager import EntityManager
from .environment.degradation import DegradationSystem
from .environment.villager_happiness import VillagerHappinessSystem
from .ui.hud import HUD
from .ui.info_popup import InfoPopup
from .ui.combined_graph import CombinedGraph
from .ui.main_menu import MainMenu
from .ui.tutorial_popup import TutorialPopup # Import the new class
from .utils.localization import localization_manager
from .utils import high_score_manager
from .utils.asset_loader import AssetLoader
from .utils import constants as constants_module
from .utils.constants import PACKAGE_DIR
from .utils.constants import *
[docs]
class GameManager:
"""Manages the overall game state and coordinates all systems."""
def __init__(self, screen):
[docs]
self.game_state = "MAIN_MENU"
[docs]
self.should_quit = False
[docs]
self.space_key_down = False # Add this flag to track spacebar state
# Initialize font for game over messages
pygame.font.init()
[docs]
self.title_font = pygame.font.Font(None, 48)
[docs]
self.asset_loader = AssetLoader()
try:
background_original = self.asset_loader.load_image("Background.png")
self.background_image = pygame.transform.scale(background_original, self.screen.get_size())
except FileNotFoundError:
print("Warning: Background.png not found. Using solid color.")
self.background_image = None
[docs]
self.main_menu = MainMenu(self.background_image)
# Parallax background layers
BG_HEIGHT = 200
[docs]
self.BG_HEIGHT = BG_HEIGHT
bg_far_raw = self.asset_loader.load_image("bg_far.png")
bg_near_raw = self.asset_loader.load_image("bg_near.png")
[docs]
self.bg_far = pygame.transform.scale(bg_far_raw, (SCREEN_WIDTH, BG_HEIGHT)) if bg_far_raw else None
[docs]
self.bg_near = pygame.transform.scale(bg_near_raw, (SCREEN_WIDTH, BG_HEIGHT)) if bg_near_raw else None
[docs]
self.session_score_file = high_score_manager.get_session_filename()
[docs]
self.final_elapsed_time = 0
# Initialize HUD here so it's always available for all game states
[docs]
def handle_event(self, event):
"""Handles all user input and events."""
# If the tutorial is active, it captures all input
if self.tutorial_popup.is_active:
if event.type == pygame.MOUSEBUTTONDOWN:
if self.tutorial_popup.handle_click(event.pos):
# Unpause the game timers
self._adjust_timers_for_pause(self.tutorial_pause_start_time)
self.tutorial_pause_start_time = 0
# When the button is clicked, set a timer to show the next message
self.time_since_last_tutorial_step = time.time()
return # Block all other game input while tutorial is active
if event.type == pygame.QUIT:
self.quit_game()
if self.game_state == "MAIN_MENU":
self.main_menu.handle_event(event, self)
if event.type == pygame.MOUSEBUTTONDOWN:
self.hud.handle_click(event.pos, self, self.game_state)
elif self.game_state in ["PLAYING", "PAUSED", "GAME_OVER"]:
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_e:
self.toggle_pause()
elif event.key == pygame.K_SPACE and self.game_state == "PLAYING":
self.emergency_feast()
elif event.key == pygame.K_r and self.game_state in ["GAME_OVER", "PAUSED"]:
self.restart_game()
elif event.key == pygame.K_s and self.game_state == "PLAYING":
self.send_pig_away()
elif event.key == pygame.K_a and self.game_state == "PLAYING":
self.add_pigs(5)
elif event.type == pygame.MOUSEBUTTONDOWN:
self.hud.handle_click(event.pos, self, self.game_state)
if self.game_state == "PLAYING":
if event.button == 1: # Left Mouse Button
self.send_pig_away()
elif event.button == 3: # Right Mouse Button
self.add_pigs(5)
# Add support for Control + Left Click as Right Click
elif event.button == 1 and (pygame.key.get_mods() & pygame.KMOD_CTRL):
self.add_pigs(5) # Treat Control + Left Click as Right Click
[docs]
def quit_game(self):
"""Signals the main loop to exit."""
self.should_quit = True
[docs]
def start_game(self, difficulty='medium'):
"""Initializes all game systems and starts the game."""
if difficulty == 'easy':
constants_module.PIG_BIRTH_RATE = 5
elif difficulty == 'medium':
constants_module.PIG_BIRTH_RATE = 7
elif difficulty == 'hard':
constants_module.PIG_BIRTH_RATE = 9
self._initialize_game_state()
self.game_state = "PLAYING"
def _initialize_game_state(self):
"""Sets or resets the game to its initial state."""
self.score_saved = False
self.is_win_condition = False # Add this flag
self.pigs_sent_count = 0 # Track how many pigs have been sent
self.controls_explained = False # This now tracks the whole sequence
self.final_happiness = None
self.final_degradation = None
self.pause_start_time = 0
self.tutorial_pause_start_time = 0 # Add this line
# Game timing
self.game_start_time = time.time()
self.last_feast_time = 0
self.score = 0
# Initialize systems
self.isometric_map = IsometricMap(str(PACKAGE_DIR / "maps" / "iso_map3.tmj"))
self.isometric_map.randomize_land_tiles()
self.entity_manager = EntityManager(self.isometric_map)
self.degradation_system = DegradationSystem(self.isometric_map)
self.villager_happiness_system = VillagerHappinessSystem(self.isometric_map, self.entity_manager, self.degradation_system)
# self.hud = HUD() <- REMOVE THIS LINE
self.info_popup = InfoPopup()
self.combined_graph = CombinedGraph()
# Add state for the tutorial sequence
self.tutorial_steps = ['tutorial_step_1', 'tutorial_step_2', 'tutorial_step_3', 'tutorial_step_4']
self.current_tutorial_step = 0
self.tutorial_start_time = 2 # seconds into the game
self.time_since_last_tutorial_step = 0
self.tutorial_delay = 1.5 # seconds between popups
self.history_update_interval = 0.25 # seconds
self.time_since_last_history_update = 0
# Degradation ramp-up: stays near zero during tutorial, then ramps to 1.0
self.degradation_scale = 0.0
self.grace_period_elapsed = 0.0
self.GRACE_PERIOD = 15.0 # seconds to ramp from 0 to full after tutorial ends
# Initialize starting entities
self._spawn_initial_entities()
# Show welcome message using localization
self.info_popup.show_message(localization_manager.get('welcome_message'), 5)
[docs]
def restart_game(self):
"""Resets the game to the main menu without clearing session high scores."""
self.game_state = "MAIN_MENU"
# Re-create the main menu to ensure it's fresh, passing the background
self.main_menu = MainMenu(self.background_image)
def _initialize_systems(self):
"""Initializes all game systems."""
self.map = Map('maps/iso_map3.tmj')
self.entity_manager = EntityManager(self.map)
self.degradation_system = DegradationSystem(self.map)
self.hud = HUD()
self.info_popup = InfoPopup()
self.combined_graph = CombinedGraph()
def _spawn_initial_entities(self):
"""Spawn initial pigs and villagers."""
# Spawn 5 initial pigs
for _ in range(5):
self.entity_manager.spawn_pig()
# Spawn 10 villagers
for _ in range(10):
self.entity_manager.spawn_villager()
def _adjust_timers_for_pause(self, pause_start_time):
if pause_start_time > 0:
paused_duration = time.time() - pause_start_time
self.game_start_time += paused_duration
if self.last_feast_time > 0:
self.last_feast_time += paused_duration
def _trigger_game_over(self, is_win, elapsed_time, message_key):
if self.game_state == "GAME_OVER":
return
self.is_win_condition = is_win
self.game_state = "GAME_OVER"
self.final_happiness = self.villager_happiness_system.get_average_happiness()
self.final_degradation = self.degradation_system.get_overall_degradation()
self.final_elapsed_time = elapsed_time
self.final_score = int(self.score)
self.info_popup.show_message(localization_manager.get(message_key, score=self.final_score), 10000)
[docs]
def toggle_pause(self):
"""Toggle game pause state."""
if self.game_state == "PLAYING":
self.game_state = "PAUSED"
self.pause_start_time = time.time()
self.info_popup.clear_messages()
elif self.game_state == "PAUSED":
self.game_state = "PLAYING"
self._adjust_timers_for_pause(self.pause_start_time)
[docs]
def send_pig_away(self):
"""Send one pig to another island."""
if self.entity_manager.remove_pig():
self.score += 50
self.pigs_sent_count += 1
if self.pigs_sent_count <= 10:
# For the first 10 pigs, show the full, prominent message
self.info_popup.show_message(localization_manager.get('pig_sent_message'), 2)
else:
# After 10, show a less prominent, shorter score update
self.info_popup.show_message("+50", 0.75)
[docs]
def add_pigs(self, count):
"""Adds a specified number of new pigs to the island."""
for _ in range(count):
self.entity_manager.spawn_pig()
self.info_popup.show_message(localization_manager.get('add_pigs_message', count=count), 2)
[docs]
def emergency_feast(self):
"""Slaughter half the pigs for a feast."""
current_time = time.time()
if current_time - self.last_feast_time >= FEAST_COOLDOWN:
removed_pigs = self.entity_manager.emergency_feast()
if removed_pigs > 0:
self.last_feast_time = current_time
points = removed_pigs * 25
self.score += points
self.info_popup.show_message(localization_manager.get('feast_message', pigs=removed_pigs, points=points), 3)
else:
self.info_popup.show_message(localization_manager.get('no_pigs_for_feast'), 2)
else:
remaining_cooldown = FEAST_COOLDOWN - (current_time - self.last_feast_time)
self.info_popup.show_message(localization_manager.get('feast_cooldown_message', cooldown=int(remaining_cooldown)), 2)
[docs]
def update(self, dt):
"""Update game state."""
# --- Tutorial Sequence Management ---
# Check game_state first to prevent error when not in PLAYING state
if self.game_state == "PLAYING" and not self.controls_explained:
elapsed_time = time.time() - self.game_start_time
if elapsed_time > self.tutorial_start_time and not self.tutorial_popup.is_active:
if self.current_tutorial_step < len(self.tutorial_steps):
# Check if enough time has passed since the last popup was closed
if self.time_since_last_tutorial_step == 0 or \
time.time() - self.time_since_last_tutorial_step > self.tutorial_delay:
message_key = self.tutorial_steps[self.current_tutorial_step]
is_last = self.current_tutorial_step == len(self.tutorial_steps) - 1
self.tutorial_popup.show(message_key, is_last)
self.tutorial_pause_start_time = time.time() # Start the pause timer
self.current_tutorial_step += 1
self.time_since_last_tutorial_step = 0 # Reset timer until it's closed
else:
self.controls_explained = True # Tutorial is finished
# If tutorial popup is active, pause all game logic
if self.tutorial_popup.is_active:
self.info_popup.update(dt) # Allow existing popups to fade, but nothing else
return
if self.game_state != "PLAYING":
# If game is over and score hasn't been saved, save it only on a win.
if self.game_state == "GAME_OVER" and not self.score_saved:
if self.is_win_condition:
score_entry = dict()
current_scorer = {'name': self.username, 'score': int(self.score)}
score_entry.update(current_scorer)
high_score_manager.add_score_to_session(score_entry, self.session_score_file)
# Always load scores to display them, even on a loss
self.high_scores = high_score_manager.load_scores(self.session_score_file)
self.score_saved = True
return
current_time = time.time()
elapsed_time = current_time - self.game_start_time
# Check for game over by time (WIN condition)
if elapsed_time >= GAME_DURATION:
self._trigger_game_over(True, elapsed_time, 'time_up_message')
return
# Check for game over by degradation (LOSE condition)
degradation_level = self.degradation_system.get_overall_degradation()
if degradation_level >= ECOSYSTEM_COLLAPSE_THRESHOLD:
self._trigger_game_over(False, elapsed_time, 'ecosystem_collapsed_message')
return
# Advance degradation scale: zero during tutorial, ramp up afterwards
if not self.controls_explained:
self.degradation_scale = 0.0
elif self.grace_period_elapsed < self.GRACE_PERIOD:
self.grace_period_elapsed += dt
self.degradation_scale = self.grace_period_elapsed / self.GRACE_PERIOD
else:
self.degradation_scale = 1.0
self.entity_manager.update(dt)
self.degradation_system.update(dt * self.degradation_scale, self.entity_manager)
self.villager_happiness_system.update(dt)
self.info_popup.update(dt)
# Update combined graph
self.time_since_last_history_update += dt
if self.time_since_last_history_update >= self.history_update_interval:
self.time_since_last_history_update -= self.history_update_interval
self.combined_graph.add_data_point(
self.degradation_system.get_overall_degradation(),
self.villager_happiness_system.get_average_happiness(),
self.entity_manager.get_pig_count(),
)
# Update score
self.score += SCORE_PER_SECOND * dt
# REMOVE the old single message display logic
# Check for environmental warnings
degradation_level = self.degradation_system.get_overall_degradation()
if degradation_level > 0.7:
self.info_popup.show_message(localization_manager.get('critical_warning'), 4)
elif degradation_level > 0.5:
self.info_popup.show_message(localization_manager.get('heavy_warning'), 3)
# Spawn new pigs (scaled so population ramps up alongside degradation)
if self.entity_manager.should_spawn_pig(dt * self.degradation_scale):
self.entity_manager.spawn_pig()
[docs]
def render(self, screen):
"""Render all game elements."""
if self.game_state == "MAIN_MENU":
self.main_menu.render(screen)
# Also render the HUD to show the exit button
self.hud.render(screen, {
'main_menu': True,
'paused': False,
'game_over': False
})
return
screen.fill((117, 148, 62))
if self.bg_far:
screen.blit(self.bg_far, (0, 0))
if self.bg_near:
screen.blit(self.bg_near, (0, 0))
screen.set_clip(pygame.Rect(0, self.BG_HEIGHT, SCREEN_WIDTH, SCREEN_HEIGHT - self.BG_HEIGHT))
self.isometric_map.render(screen, bg_height=self.BG_HEIGHT)
self.entity_manager.render(screen)
screen.set_clip(None)
# Determine the correct time to use for rendering, accounting for all pause types
is_paused_by_tutorial = self.tutorial_popup.is_active and self.tutorial_pause_start_time > 0
if self.game_state == "GAME_OVER":
elapsed_time = self.final_elapsed_time
score = self.final_score
# The win/loss message is now rendered by the HUD, so we remove the logic from here.
else:
# Determine the 'current' time for calculation, freezing it if paused
if self.game_state == "PAUSED":
current_time = self.pause_start_time
elif is_paused_by_tutorial:
current_time = self.tutorial_pause_start_time
else:
current_time = time.time()
elapsed_time = current_time - self.game_start_time
score = int(self.score)
remaining_time = max(0, GAME_DURATION - elapsed_time)
# The feast cooldown is handled by adjusting its start time, so it can always use the real current time.
feast_cooldown = max(0, FEAST_COOLDOWN - (time.time() - self.last_feast_time))
self.hud.render(screen, {
'score': score,
'time_remaining': int(remaining_time),
'pig_count': len(self.entity_manager.pigs),
'feast_cooldown': int(feast_cooldown),
'degradation': self.degradation_system.get_overall_degradation(),
'happiness': self.villager_happiness_system.get_average_happiness(),
'paused': self.game_state == "PAUSED",
'game_over': self.game_state == "GAME_OVER",
'final_happiness': self.final_happiness,
'final_degradation': self.final_degradation,
'high_scores': self.high_scores,
'is_win': self.is_win_condition # Add this line
})
# Render info popup
self.info_popup.render(screen)
self.combined_graph.render(screen)
# Render the tutorial popup on top of everything
self.tutorial_popup.render(screen)