Source code for game.entities.pig

"""Pig entity class."""
from .base_entity import BaseEntity
import pygame
import random
import math
from ..utils.constants import *
from ..utils.asset_loader import AssetLoader

[docs] class Pig(BaseEntity): """Represents a pig entity in the game."""
[docs] ANIM_FRAME_DURATION = 0.15 # seconds per frame
def __init__(self, x, y):
[docs] self.x = x
[docs] self.y = y
[docs] self.target_x = x
[docs] self.target_y = y
[docs] self.move_timer = 0
[docs] self.eating_timer = 0
[docs] self.size = random.uniform(0.8, 1.2) # Size variation
[docs] self.asset_loader = AssetLoader()
[docs] self.direction = 'left'
[docs] self.anim_timer = 0.0
[docs] self.anim_frame = 0
[docs] self.sprites = self._load_sprites()
def _load_sprites(self): """Load all directional walk frames; fall back to a generated sprite.""" sprite_size = int(32 * self.size) sprites = {} for direction in ('left', 'right', 'up', 'down'): frames = [] for i in range(1, 4): try: img = self.asset_loader.load_image(f"pig/pig-{direction}{i}.png") frames.append(pygame.transform.scale(img, (sprite_size, sprite_size))) except (FileNotFoundError, Exception): break if not frames: fallback = pygame.transform.scale(self._create_pig_sprite(), (sprite_size, sprite_size)) frames = [fallback] sprites[direction] = frames return sprites def _create_pig_sprite(self): """Create a simple pig sprite as fallback.""" # This is kept as fallback if no sprite file is found size = int(24 * self.size) surface = pygame.Surface((size, size), pygame.SRCALPHA) # Draw simple pig body pygame.draw.ellipse(surface, (255, 192, 203), (2, 4, size-4, size-8)) pygame.draw.ellipse(surface, (255, 182, 193), (2, 4, size-4, size-8), 2) # Draw snout pygame.draw.circle(surface, (255, 182, 193), (size//2, size//2), size//4) # Draw eyes pygame.draw.circle(surface, (0, 0, 0), (size//2 - 3, size//2 - 2), 2) pygame.draw.circle(surface, (0, 0, 0), (size//2 + 3, size//2 - 2), 2) return surface
[docs] def update(self, dt, isometric_map): """Update pig behavior.""" # Movement behavior self.move_timer -= dt if self.move_timer <= 0: # Choose new random target self._choose_new_target(isometric_map) self.move_timer = random.uniform(2, 5) # Move towards target dx, dy, distance = self._step_towards_target(dt, PIG_MOVE_SPEED) if distance > 0.1: # Map world movement to screen-space direction using the 2:1 tile ratio: # screen_x ∝ (dx - dy), screen_y ∝ (dx + dy) * 0.5 # so left/right dominates when 2*|dx-dy| >= |dx+dy| sdx = dx - dy sdy = dx + dy new_direction = self.direction if abs(sdx) * 2 >= abs(sdy): new_direction = 'right' if sdx > 0 else 'left' else: new_direction = 'down' if sdy > 0 else 'up' if new_direction != self.direction: self.direction = new_direction self.anim_frame = 0 self.anim_timer = 0.0 # Advance walk animation self.anim_timer += dt frames = self.sprites[self.direction] self.anim_frame = int(self.anim_timer / self.ANIM_FRAME_DURATION) % len(frames) else: self.anim_frame = 0 self.anim_timer = 0.0 # Eating behavior self.eating_timer -= dt if self.eating_timer <= 0: self._eat_vegetation(isometric_map) self.eating_timer = random.uniform(1, 3)
def _choose_new_target(self, isometric_map): """Choose a target tile within a local radius, preferring less degraded (healthier) land.""" candidates = [] cx, cy = int(round(self.x)), int(round(self.y)) radius = 5 world_min_x = isometric_map.map_offset_x world_max_x = isometric_map.map_offset_x + isometric_map.width - 1 world_min_y = isometric_map.map_offset_y world_max_y = isometric_map.map_offset_y + isometric_map.height - 1 lo_x = max(world_min_x, cx - radius) hi_x = min(world_max_x, cx + radius) lo_y = max(world_min_y, cy - radius) hi_y = min(world_max_y, cy + radius) if lo_x > hi_x or lo_y > hi_y: return for _ in range(20): new_x = random.randint(lo_x, hi_x) new_y = random.randint(lo_y, hi_y) if (isometric_map.is_valid_position(new_x, new_y) and isometric_map.get_tile_type(new_x, new_y) in WALKABLE_TILES): degradation = isometric_map.get_degradation(new_x, new_y) candidates.append((degradation, new_x, new_y)) if candidates: _, self.target_x, self.target_y = min(candidates) else: self.target_x = int(round(self.x)) self.target_y = int(round(self.y)) def _eat_vegetation(self, isometric_map): """Eat vegetation and cause degradation.""" tile_x, tile_y = int(round(self.x)), int(round(self.y)) # Eat in radius around pig for dx in range(-PIG_EATING_RADIUS, PIG_EATING_RADIUS + 1): for dy in range(-PIG_EATING_RADIUS, PIG_EATING_RADIUS + 1): eat_x, eat_y = tile_x + dx, tile_y + dy if isometric_map.is_valid_position(eat_x, eat_y): # Add degradation based on distance distance = math.sqrt(dx * dx + dy * dy) if distance <= PIG_EATING_RADIUS: degradation_amount = 0.01 * (1 - distance / PIG_EATING_RADIUS) isometric_map.add_degradation(eat_x, eat_y, degradation_amount)
[docs] def render(self, screen, isometric_map): """Render the pig.""" screen_x, screen_y = self.get_screen_position(isometric_map) # Render current animation frame frame = self.sprites[self.direction][self.anim_frame] sprite_rect = frame.get_rect() sprite_rect.center = (screen_x, screen_y - 10) screen.blit(frame, sprite_rect) # Render health/status indicator if self.eating_timer > 2: # Show eating indicator pygame.draw.circle(screen, (0, 255, 0), (screen_x, screen_y - 25), 3)