You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
833 lines
32 KiB
833 lines
32 KiB
import os |
|
import random |
|
import pygame |
|
from pygame import mixer |
|
|
|
|
|
class GameWindow: |
|
""" |
|
Pygame-based game window implementation. |
|
Provides a complete interface equivalent to sdl2_layer.GameWindow |
|
""" |
|
|
|
def __init__(self, width, height, cell_size, title="Default", key_callback=None): |
|
# Display configuration |
|
self.cell_size = cell_size |
|
self.width = width * cell_size |
|
self.height = height * cell_size |
|
|
|
# Screen resolution handling |
|
actual_screen_size = os.environ.get("RESOLUTION", "640x480").split("x") |
|
actual_screen_size = tuple(map(int, actual_screen_size)) |
|
self.target_size = actual_screen_size if self.width > actual_screen_size[0] or self.height > actual_screen_size[1] else (self.width, self.height) |
|
|
|
# View offset calculations |
|
self.w_start_offset = (self.target_size[0] - self.width) // 2 |
|
self.h_start_offset = (self.target_size[1] - self.height) // 2 |
|
self.w_offset = self.w_start_offset |
|
self.h_offset = self.h_start_offset |
|
self.max_w_offset = self.target_size[0] - self.width |
|
self.max_h_offset = self.target_size[1] - self.height |
|
self.scale = self.target_size[1] // self.cell_size |
|
|
|
print(f"Screen size: {self.width}x{self.height}") |
|
|
|
# Pygame initialization |
|
pygame.init() |
|
mixer.init(frequency=22050, size=-16, channels=1, buffer=2048) |
|
|
|
# Window and screen setup |
|
self.window = pygame.display.set_mode(self.target_size) |
|
pygame.display.set_caption(title) |
|
self.screen = self.window |
|
|
|
# Font system |
|
self.fonts = self.generate_fonts("assets/decterm.ttf") |
|
|
|
# Game state |
|
self.running = True |
|
self.delay = 30 |
|
self.performance = 0 |
|
self.last_status_text = "" |
|
self.stats_sprite = None |
|
self.mean_fps = 0 |
|
self.fpss = [] |
|
self.text_width = 0 |
|
self.text_height = 0 |
|
self.ammo_text = "" |
|
self.stats_background = None |
|
self.ammo_background = None |
|
self.ammo_sprite = None |
|
|
|
# White flash effect state |
|
self.white_flash_active = False |
|
self.white_flash_start_time = 0 |
|
self.white_flash_opacity = 255 |
|
|
|
# Input handling |
|
self.trigger = key_callback |
|
self.button_cursor = [0, 0] |
|
self.buttons = {} |
|
|
|
# Audio system initialization |
|
self._init_audio_system() |
|
self.audio = True |
|
|
|
# Clock for frame rate control |
|
self.clock = pygame.time.Clock() |
|
|
|
# Input devices |
|
self.load_joystick() |
|
|
|
def show(self): |
|
"""Show the window (for compatibility with SDL2 interface)""" |
|
pygame.display.set_mode(self.target_size) |
|
|
|
def _init_audio_system(self): |
|
"""Initialize audio channels for different audio types""" |
|
mixer.set_num_channels(8) # Ensure enough channels |
|
self.audio_channels = { |
|
"base": mixer.Channel(0), |
|
"effects": mixer.Channel(1), |
|
"music": mixer.Channel(2) |
|
} |
|
self.current_sounds = {} |
|
|
|
# ====================== |
|
# TEXTURE & IMAGE METHODS |
|
# ====================== |
|
|
|
def create_texture(self, tiles: list): |
|
"""Create a texture from a list of tiles""" |
|
bg_surface = pygame.Surface((self.width, self.height)) |
|
for tile in tiles: |
|
bg_surface.blit(tile[0], (tile[1], tile[2])) |
|
return bg_surface |
|
|
|
# Helpers to support incremental background generation |
|
def create_empty_background_surface(self): |
|
"""Create and return an empty background surface to incrementally blit onto.""" |
|
return pygame.Surface((self.width, self.height)) |
|
|
|
def blit_tiles_batch(self, bg_surface, tiles_batch: list): |
|
"""Blit a small batch of tiles onto the provided background surface. |
|
|
|
tiles_batch: list of (surface, x, y) |
|
Returns None. Designed to be called repeatedly with small batches to avoid long blocking operations. |
|
""" |
|
for tile, x, y in tiles_batch: |
|
try: |
|
bg_surface.blit(tile, (x, y)) |
|
except Exception: |
|
# If tile is a SpriteWrapper, extract surface |
|
try: |
|
bg_surface.blit(tile.surface, (x, y)) |
|
except Exception: |
|
pass |
|
|
|
def load_image(self, path, transparent_color=None, surface=False): |
|
"""Load and process an image with optional transparency and scaling""" |
|
image_path = os.path.join("assets", path) |
|
|
|
# First try to use pygame's native loader which avoids a Pillow dependency. |
|
try: |
|
py_image = pygame.image.load(image_path) |
|
# Ensure alpha if needed |
|
try: |
|
py_image = py_image.convert_alpha() |
|
except Exception: |
|
try: |
|
py_image = py_image.convert() |
|
except Exception: |
|
pass |
|
|
|
# Handle transparent color via colorkey if provided |
|
if transparent_color: |
|
# pygame expects a tuple of ints |
|
try: |
|
py_image.set_colorkey(transparent_color) |
|
except Exception: |
|
pass |
|
|
|
# Scale image using pygame transforms |
|
scale = max(1, self.cell_size // 20) |
|
new_size = (py_image.get_width() * scale, py_image.get_height() * scale) |
|
try: |
|
py_image = pygame.transform.scale(py_image, new_size) |
|
except Exception: |
|
# If scaling fails, continue with original |
|
pass |
|
|
|
if not surface: |
|
return SpriteWrapper(py_image) |
|
return py_image |
|
except Exception: |
|
# Fallback to PIL-based loading if pygame can't handle the file or Pillow is present |
|
try: |
|
# Import Pillow lazily to avoid hard dependency at module import time |
|
try: |
|
from PIL import Image |
|
except Exception: |
|
Image = None |
|
|
|
if Image is None: |
|
raise |
|
|
|
image = Image.open(image_path) |
|
|
|
# Handle transparency |
|
if transparent_color: |
|
image = image.convert("RGBA") |
|
datas = image.getdata() |
|
new_data = [] |
|
for item in datas: |
|
if item[:3] == transparent_color: |
|
new_data.append((255, 255, 255, 0)) |
|
else: |
|
new_data.append(item) |
|
image.putdata(new_data) |
|
|
|
# Scale image |
|
scale = max(1, self.cell_size // 20) |
|
image = image.resize((image.width * scale, image.height * scale), Image.NEAREST) |
|
|
|
# Convert PIL image to pygame surface |
|
mode = image.mode |
|
size = image.size |
|
data = image.tobytes() |
|
|
|
if mode == "RGBA": |
|
py_image = pygame.image.fromstring(data, size, mode) |
|
elif mode == "RGB": |
|
py_image = pygame.image.fromstring(data, size, mode) |
|
else: |
|
image = image.convert("RGBA") |
|
data = image.tobytes() |
|
py_image = pygame.image.fromstring(data, size, "RGBA") |
|
|
|
if not surface: |
|
return SpriteWrapper(py_image) |
|
return py_image |
|
except Exception: |
|
# If both loaders fail, raise to notify caller |
|
raise |
|
|
|
def get_image_size(self, image): |
|
"""Get the size of an image sprite""" |
|
if isinstance(image, SpriteWrapper): |
|
return image.size |
|
return image.get_size() |
|
|
|
# ====================== |
|
# FONT MANAGEMENT |
|
# ====================== |
|
|
|
def generate_fonts(self, font_file): |
|
"""Generate font objects for different sizes""" |
|
fonts = {} |
|
for i in range(10, 70, 1): |
|
try: |
|
fonts[i] = pygame.font.Font(font_file, i) |
|
except: |
|
fonts[i] = pygame.font.Font(None, i) |
|
return fonts |
|
|
|
# ====================== |
|
# DRAWING METHODS |
|
# ====================== |
|
|
|
def draw_text(self, text, font, position, color): |
|
"""Draw text at specified position with given font and color""" |
|
if isinstance(color, tuple): |
|
# Pygame color format |
|
pass |
|
else: |
|
# Convert from any other format to RGB tuple |
|
color = (color.r, color.g, color.b) if hasattr(color, 'r') else (0, 0, 0) |
|
|
|
text_surface = font.render(text, True, color) |
|
text_rect = text_surface.get_rect() |
|
|
|
# Handle center positioning |
|
if position == "center": |
|
position = ("center", "center") |
|
if isinstance(position, tuple): |
|
if position[0] == "center": |
|
text_rect.centerx = self.target_size[0] // 2 |
|
text_rect.y = position[1] |
|
elif position[1] == "center": |
|
text_rect.x = position[0] |
|
text_rect.centery = self.target_size[1] // 2 |
|
else: |
|
text_rect.topleft = position |
|
|
|
self.screen.blit(text_surface, text_rect) |
|
|
|
def draw_background(self, bg_texture): |
|
"""Draw background texture with current view offset""" |
|
self.screen.blit(bg_texture, (self.w_offset, self.h_offset)) |
|
|
|
def draw_image(self, x, y, sprite, tag=None, anchor="nw"): |
|
"""Draw an image sprite at specified coordinates""" |
|
if not self.is_in_visible_area(x, y): |
|
return |
|
|
|
if isinstance(sprite, SpriteWrapper): |
|
surface = sprite.surface |
|
else: |
|
surface = sprite |
|
|
|
self.screen.blit(surface, (x + self.w_offset, y + self.h_offset)) |
|
|
|
def draw_rectangle(self, x, y, width, height, tag, outline="red", filling=None): |
|
"""Draw a rectangle with optional fill and outline""" |
|
rect = pygame.Rect(x, y, width, height) |
|
|
|
if filling: |
|
pygame.draw.rect(self.screen, filling, rect) |
|
else: |
|
# Handle outline color |
|
if isinstance(outline, str): |
|
color_map = { |
|
"red": (255, 0, 0), |
|
"blue": (0, 0, 255), |
|
"green": (0, 255, 0), |
|
"black": (0, 0, 0), |
|
"white": (255, 255, 255) |
|
} |
|
outline = color_map.get(outline, (255, 0, 0)) |
|
pygame.draw.rect(self.screen, outline, rect, 2) |
|
|
|
def draw_pointer(self, x, y): |
|
"""Draw a red pointer rectangle at specified coordinates""" |
|
x = x + self.w_offset |
|
y = y + self.h_offset |
|
for i in range(3): |
|
rect = pygame.Rect(x + i, y + i, self.cell_size - 2*i, self.cell_size - 2*i) |
|
pygame.draw.rect(self.screen, (255, 0, 0), rect, 1) |
|
|
|
def delete_tag(self, tag): |
|
"""Placeholder for tag deletion (not needed in pygame implementation)""" |
|
pass |
|
|
|
# ====================== |
|
# UI METHODS |
|
# ====================== |
|
|
|
def dialog(self, text, **kwargs): |
|
"""Display a dialog box with text and optional extras""" |
|
# Draw dialog background |
|
dialog_rect = pygame.Rect(50, 50, self.target_size[0] - 100, self.target_size[1] - 100) |
|
pygame.draw.rect(self.screen, (255, 255, 255), dialog_rect) |
|
|
|
# Calculate layout positions to avoid overlaps |
|
title_y = self.target_size[1] // 4 # Title at 1/4 of screen height |
|
|
|
# Draw main text (title) |
|
self.draw_text(text, self.fonts[self.target_size[1]//20], |
|
("center", title_y), (0, 0, 0)) |
|
|
|
# Draw image if provided - position it below title |
|
image_bottom_y = title_y + 60 # Default position if no image |
|
if image := kwargs.get("image"): |
|
image_size = self.get_image_size(image) |
|
image_y = title_y + 50 |
|
self.draw_image(self.target_size[0] // 2 - image_size[0] // 2 - self.w_offset, |
|
image_y - self.h_offset, |
|
image, "win") |
|
image_bottom_y = image_y + image_size[1] + 20 |
|
|
|
# Draw subtitle if provided - handle multi-line text, position below image |
|
if subtitle := kwargs.get("subtitle"): |
|
subtitle_lines = subtitle.split('\n') |
|
base_y = image_bottom_y + 20 |
|
line_height = 25 # Fixed line height for consistent spacing |
|
|
|
for i, line in enumerate(subtitle_lines): |
|
if line.strip(): # Only draw non-empty lines |
|
self.draw_text(line.strip(), self.fonts[self.target_size[1]//35], |
|
("center", base_y + i * line_height), (0, 0, 0)) |
|
|
|
# Draw scores if provided - position at bottom |
|
if scores := kwargs.get("scores"): |
|
scores_start_y = self.target_size[1] * 3 // 4 # Bottom quarter of screen |
|
title_surface = self.fonts[self.target_size[1]//25].render("High Scores:", True, (0, 0, 0)) |
|
title_rect = title_surface.get_rect(center=(self.target_size[0] // 2, scores_start_y)) |
|
self.screen.blit(title_surface, title_rect) |
|
|
|
for i, score in enumerate(scores[:5]): |
|
if len(score) >= 4: # New format: date, score, name, device |
|
score_text = f"{score[2]}: {score[1]} pts ({score[3]})" |
|
elif len(score) >= 3: # Medium format: date, score, name |
|
score_text = f"{score[2]}: {score[1]} pts" |
|
else: # Old format: date, score |
|
score_text = f"Guest: {score[1]} pts" |
|
|
|
self.draw_text(score_text, self.fonts[self.target_size[1]//45], |
|
("center", scores_start_y + 30 + 25 * (i + 1)), |
|
(0, 0, 0)) |
|
|
|
def start_dialog(self, **kwargs): |
|
"""Display the welcome dialog""" |
|
self.dialog("Welcome to the Mice!", subtitle="A game by Matteo because was bored", **kwargs) |
|
|
|
def draw_button(self, x, y, text, width, height, coords): |
|
"""Draw a button with text""" |
|
color = (0, 0, 255) if self.button_cursor == list(coords) else (0, 0, 0) |
|
self.draw_rectangle(x, y, width, height, "button", outline=color) |
|
|
|
def update_status(self, text): |
|
"""Update and display the status bar with FPS information""" |
|
fps = int(self.clock.get_fps()) if self.clock.get_fps() > 0 else 0 |
|
|
|
if len(self.fpss) > 20: |
|
self.mean_fps = round(sum(self.fpss) / len(self.fpss)) if self.fpss else fps |
|
self.fpss.clear() |
|
else: |
|
self.fpss.append(fps) |
|
|
|
status_text = f"FPS: {self.mean_fps} - {text}" |
|
if status_text != self.last_status_text: |
|
self.last_status_text = status_text |
|
font = self.fonts[20] |
|
self.stats_sprite = font.render(status_text, True, (0, 0, 0)) |
|
if self.text_width != self.stats_sprite.get_width() or self.text_height != self.stats_sprite.get_height(): |
|
self.text_width, self.text_height = self.stats_sprite.get_size() |
|
self.stats_background = pygame.Surface((self.text_width + 10, self.text_height + 4)) |
|
self.stats_background.fill((255, 255, 255)) |
|
|
|
self.screen.blit(self.stats_background, (3, 3)) |
|
self.screen.blit(self.stats_sprite, (8, 5)) |
|
|
|
def update_ammo(self, ammo, assets): |
|
"""Update and display the ammo count""" |
|
ammo_text = f"{ammo['bomb']['count']}/{ammo['bomb']['max']} {ammo['mine']['count']}/{ammo['mine']['max']} {ammo['gas']['count']}/{ammo['gas']['max']} " |
|
if self.ammo_text != ammo_text: |
|
self.ammo_text = ammo_text |
|
font = self.fonts[20] |
|
self.ammo_sprite = font.render(ammo_text, True, (0, 0, 0)) |
|
text_width, text_height = self.ammo_sprite.get_size() |
|
self.ammo_background = pygame.Surface((text_width + 10, text_height + 4)) |
|
self.ammo_background.fill((255, 255, 255)) |
|
|
|
text_width, text_height = self.ammo_sprite.get_size() |
|
position = (self.target_size[0] - text_width - 10, self.target_size[1] - text_height - 5) |
|
|
|
self.screen.blit(self.ammo_background, (position[0] - 5, position[1] - 2)) |
|
self.screen.blit(self.ammo_sprite, position) |
|
|
|
# Draw ammo icons |
|
bomb_sprite = assets["BMP_BOMB0"] |
|
poison_sprite = assets["BMP_POISON"] |
|
gas_sprite = assets["BMP_GAS"] |
|
|
|
if isinstance(bomb_sprite, SpriteWrapper): |
|
self.screen.blit(bomb_sprite.surface, (position[0]+25, position[1])) |
|
else: |
|
# Scale to 20x20 if needed |
|
bomb_scaled = pygame.transform.scale(bomb_sprite, (20, 20)) |
|
self.screen.blit(bomb_scaled, (position[0]+25, position[1])) |
|
|
|
if isinstance(poison_sprite, SpriteWrapper): |
|
self.screen.blit(poison_sprite.surface, (position[0]+85, position[1])) |
|
else: |
|
poison_scaled = pygame.transform.scale(poison_sprite, (20, 20)) |
|
self.screen.blit(poison_scaled, (position[0]+85, position[1])) |
|
|
|
if isinstance(gas_sprite, SpriteWrapper): |
|
self.screen.blit(gas_sprite.surface, (position[0]+140, position[1])) |
|
else: |
|
gas_scaled = pygame.transform.scale(gas_sprite, (20, 20)) |
|
self.screen.blit(gas_scaled, (position[0]+140, position[1])) |
|
|
|
# ====================== |
|
# VIEW & NAVIGATION |
|
# ====================== |
|
|
|
def scroll_view(self, pointer): |
|
"""Adjust the view offset based on pointer coordinates""" |
|
x, y = pointer |
|
|
|
# Scale down and invert coordinates |
|
x = -(x // 2) * self.cell_size |
|
y = -(y // 2) * self.cell_size |
|
|
|
# Clamp horizontal offset to valid range |
|
if x <= self.max_w_offset + self.cell_size: |
|
x = self.max_w_offset |
|
|
|
# Clamp vertical offset to valid range |
|
if y < self.max_h_offset: |
|
y = self.max_h_offset |
|
|
|
self.w_offset = x |
|
self.h_offset = y |
|
|
|
def is_in_visible_area(self, x, y): |
|
"""Check if coordinates are within the visible area""" |
|
return (-self.w_offset - self.cell_size <= x <= self.width - self.w_offset and |
|
-self.h_offset - self.cell_size <= y <= self.height - self.h_offset) |
|
|
|
def get_view_center(self): |
|
"""Get the center coordinates of the current view""" |
|
return self.w_offset + self.width // 2, self.h_offset + self.height // 2 |
|
|
|
# ====================== |
|
# AUDIO METHODS |
|
# ====================== |
|
|
|
def play_sound(self, sound_file, tag="base"): |
|
"""Play a sound file on the specified audio channel""" |
|
if not self.audio: |
|
return |
|
|
|
try: |
|
sound_path = os.path.join("sound", sound_file) |
|
sound = mixer.Sound(sound_path) |
|
|
|
# Get the appropriate channel |
|
channel = self.audio_channels.get(tag, self.audio_channels["base"]) |
|
|
|
# Stop any currently playing sound on this channel |
|
channel.stop() |
|
|
|
# Play the new sound |
|
channel.play(sound) |
|
|
|
# Store reference to prevent garbage collection |
|
self.current_sounds[tag] = sound |
|
except Exception as e: |
|
print(f"Error playing sound {sound_file}: {e}") |
|
|
|
def stop_sound(self): |
|
"""Stop all audio playback""" |
|
for channel in self.audio_channels.values(): |
|
channel.stop() |
|
|
|
# ====================== |
|
# INPUT METHODS |
|
# ====================== |
|
|
|
def load_joystick(self): |
|
"""Initialize joystick support""" |
|
pygame.joystick.init() |
|
joystick_count = pygame.joystick.get_count() |
|
if joystick_count > 0: |
|
self.joystick = pygame.joystick.Joystick(0) |
|
self.joystick.init() |
|
print(f"Joystick initialized: {self.joystick.get_name()}") |
|
else: |
|
self.joystick = None |
|
|
|
# ====================== |
|
# MAIN GAME LOOP |
|
# ====================== |
|
|
|
def _normalize_key_name(self, key): |
|
"""Normalize pygame key names to match SDL2 key names""" |
|
# Pygame returns lowercase, SDL2 returns with proper case |
|
key_map = { |
|
"return": "Return", |
|
"escape": "Escape", |
|
"space": "Space", |
|
"tab": "Tab", |
|
"left shift": "Left_Shift", |
|
"right shift": "Right_Shift", |
|
"left ctrl": "Left_Ctrl", |
|
"right ctrl": "Right_Ctrl", |
|
"left alt": "Left_Alt", |
|
"right alt": "Right_Alt", |
|
"up": "Up", |
|
"down": "Down", |
|
"left": "Left", |
|
"right": "Right", |
|
"delete": "Delete", |
|
"backspace": "Backspace", |
|
"insert": "Insert", |
|
"home": "Home", |
|
"end": "End", |
|
"pageup": "Page_Up", |
|
"pagedown": "Page_Down", |
|
"f1": "F1", |
|
"f2": "F2", |
|
"f3": "F3", |
|
"f4": "F4", |
|
"f5": "F5", |
|
"f6": "F6", |
|
"f7": "F7", |
|
"f8": "F8", |
|
"f9": "F9", |
|
"f10": "F10", |
|
"f11": "F11", |
|
"f12": "F12", |
|
} |
|
# Return mapped value or capitalize first letter of original |
|
normalized = key_map.get(key.lower(), key) |
|
# Handle single letters (make uppercase) |
|
if len(normalized) == 1: |
|
normalized = normalized.upper() |
|
return normalized |
|
|
|
def mainloop(self, **kwargs): |
|
"""Main game loop handling events and rendering""" |
|
while self.running: |
|
performance_start = pygame.time.get_ticks() |
|
|
|
# Clear screen |
|
self.screen.fill((0, 0, 0)) |
|
|
|
# Execute background update if provided |
|
if "bg_update" in kwargs: |
|
kwargs["bg_update"]() |
|
|
|
# Execute main update |
|
kwargs["update"]() |
|
|
|
# Update and draw white flash effect |
|
if self.update_white_flash(): |
|
self.draw_white_flash() |
|
|
|
# Handle Pygame events |
|
for event in pygame.event.get(): |
|
if event.type == pygame.QUIT: |
|
self.running = False |
|
elif event.type == pygame.KEYDOWN: |
|
key = pygame.key.name(event.key) |
|
key = self._normalize_key_name(key) |
|
key = key.replace(" ", "_") |
|
self.trigger(f"keydown_{key}") |
|
elif event.type == pygame.KEYUP: |
|
key = pygame.key.name(event.key) |
|
key = self._normalize_key_name(key) |
|
key = key.replace(" ", "_") |
|
self.trigger(f"keyup_{key}") |
|
elif event.type == pygame.MOUSEMOTION: |
|
self.trigger(f"mousemove_{event.pos[0]}, {event.pos[1]}") |
|
elif event.type == pygame.JOYBUTTONDOWN: |
|
self.trigger(f"joybuttondown_{event.button}") |
|
elif event.type == pygame.JOYBUTTONUP: |
|
self.trigger(f"joybuttonup_{event.button}") |
|
elif event.type == pygame.JOYHATMOTION: |
|
self.trigger(f"joyhatmotion_{event.hat}_{event.value}") |
|
|
|
# Update display |
|
pygame.display.flip() |
|
|
|
# Control frame rate |
|
self.clock.tick(60) # Target 60 FPS |
|
|
|
# Calculate performance |
|
self.performance = pygame.time.get_ticks() - performance_start |
|
|
|
def step(self, update=None, bg_update=None): |
|
"""Execute a single frame iteration. This is non-blocking and useful when |
|
the caller (JS) schedules frames via requestAnimationFrame in the browser. |
|
""" |
|
performance_start = pygame.time.get_ticks() |
|
|
|
# Clear screen |
|
self.screen.fill((0, 0, 0)) |
|
|
|
# Background update |
|
if bg_update: |
|
try: |
|
bg_update() |
|
except Exception: |
|
pass |
|
|
|
# Main update |
|
if update: |
|
try: |
|
update() |
|
except Exception: |
|
pass |
|
|
|
# Update and draw white flash effect |
|
if self.update_white_flash(): |
|
self.draw_white_flash() |
|
|
|
# Handle Pygame events (single-frame processing) |
|
for event in pygame.event.get(): |
|
if event.type == pygame.QUIT: |
|
self.running = False |
|
elif event.type == pygame.KEYDOWN: |
|
key = pygame.key.name(event.key) |
|
key = self._normalize_key_name(key) |
|
key = key.replace(" ", "_") |
|
self.trigger(f"keydown_{key}") |
|
elif event.type == pygame.KEYUP: |
|
key = pygame.key.name(event.key) |
|
key = self._normalize_key_name(key) |
|
key = key.replace(" ", "_") |
|
self.trigger(f"keyup_{key}") |
|
elif event.type == pygame.MOUSEMOTION: |
|
self.trigger(f"mousemove_{event.pos[0]}, {event.pos[1]}") |
|
elif event.type == pygame.JOYBUTTONDOWN: |
|
self.trigger(f"joybuttondown_{event.button}") |
|
elif event.type == pygame.JOYBUTTONUP: |
|
self.trigger(f"joybuttonup_{event.button}") |
|
elif event.type == pygame.JOYHATMOTION: |
|
self.trigger(f"joyhatmotion_{event.hat}_{event.value}") |
|
|
|
# Update display once per frame |
|
pygame.display.flip() |
|
|
|
# Control frame rate |
|
self.clock.tick(60) |
|
|
|
# Calculate performance |
|
self.performance = pygame.time.get_ticks() - performance_start |
|
|
|
# ====================== |
|
# SPECIAL EFFECTS |
|
# ====================== |
|
|
|
def trigger_white_flash(self): |
|
"""Trigger the white flash effect""" |
|
self.white_flash_active = True |
|
self.white_flash_start_time = pygame.time.get_ticks() |
|
self.white_flash_opacity = 255 |
|
|
|
def update_white_flash(self): |
|
"""Update the white flash effect and return True if it should be drawn""" |
|
if not self.white_flash_active: |
|
return False |
|
|
|
current_time = pygame.time.get_ticks() |
|
elapsed_time = current_time - self.white_flash_start_time |
|
|
|
if elapsed_time < 500: # First 500ms: full white |
|
self.white_flash_opacity = 255 |
|
return True |
|
elif elapsed_time < 2000: # Next 1500ms: fade out |
|
fade_progress = (elapsed_time - 500) / 1500.0 |
|
self.white_flash_opacity = int(255 * (1.0 - fade_progress)) |
|
return True |
|
else: # Effect is complete |
|
self.white_flash_active = False |
|
self.white_flash_opacity = 0 |
|
return False |
|
|
|
def draw_white_flash(self): |
|
"""Draw the white flash overlay""" |
|
if self.white_flash_opacity > 0: |
|
white_surface = pygame.Surface(self.target_size) |
|
white_surface.fill((255, 255, 255)) |
|
white_surface.set_alpha(self.white_flash_opacity) |
|
self.screen.blit(white_surface, (0, 0)) |
|
|
|
# ====================== |
|
# UTILITY METHODS |
|
# ====================== |
|
|
|
def new_cycle(self, delay, callback): |
|
"""Placeholder for cycle management (not needed in pygame implementation)""" |
|
pass |
|
|
|
def full_screen(self, flag): |
|
"""Toggle fullscreen mode""" |
|
if flag: |
|
self.window = pygame.display.set_mode(self.target_size, pygame.FULLSCREEN) |
|
else: |
|
self.window = pygame.display.set_mode(self.target_size) |
|
self.screen = self.window |
|
|
|
def get_perf_counter(self): |
|
"""Get performance counter for timing""" |
|
return pygame.time.get_ticks() |
|
|
|
def close(self): |
|
"""Close the game window and cleanup""" |
|
self.running = False |
|
pygame.quit() |
|
|
|
# ====================== |
|
# BLOOD EFFECT METHODS |
|
# ====================== |
|
|
|
def generate_blood_surface(self): |
|
"""Generate a dynamic blood splatter surface using Pygame""" |
|
size = self.cell_size |
|
|
|
# Create RGBA surface for blood splatter |
|
blood_surface = pygame.Surface((size, size), pygame.SRCALPHA) |
|
|
|
# Blood color variations |
|
blood_colors = [ |
|
(139, 0, 0, 255), # Dark red |
|
(34, 34, 34, 255), # Very dark gray |
|
(20, 60, 60, 255), # Dark teal |
|
(255, 0, 0, 255), # Pure red |
|
(128, 0, 0, 255), # Reddish brown |
|
] |
|
|
|
# Generate splatter with diffusion algorithm |
|
center_x, center_y = size // 2, size // 2 |
|
max_radius = size // 3 + random.randint(-3, 5) |
|
|
|
for y in range(size): |
|
for x in range(size): |
|
# Calculate distance from center |
|
distance = ((x - center_x) ** 2 + (y - center_y) ** 2) ** 0.5 |
|
|
|
# Calculate blood probability based on distance |
|
if distance <= max_radius: |
|
probability = max(0, 1 - (distance / max_radius)) |
|
noise = random.random() * 0.7 |
|
|
|
if random.random() < probability * noise: |
|
color = random.choice(blood_colors) |
|
alpha = int(255 * probability * random.uniform(0.6, 1.0)) |
|
blood_surface.set_at((x, y), (*color[:3], alpha)) |
|
|
|
# Add scattered droplets around main splatter |
|
for _ in range(random.randint(3, 8)): |
|
drop_x = center_x + random.randint(-max_radius - 5, max_radius + 5) |
|
drop_y = center_y + random.randint(-max_radius - 5, max_radius + 5) |
|
|
|
if 0 <= drop_x < size and 0 <= drop_y < size: |
|
drop_size = random.randint(1, 3) |
|
for dy in range(-drop_size, drop_size + 1): |
|
for dx in range(-drop_size, drop_size + 1): |
|
nx, ny = drop_x + dx, drop_y + dy |
|
if 0 <= nx < size and 0 <= ny < size: |
|
if random.random() < 0.6: |
|
color = random.choice(blood_colors[:3]) |
|
alpha = random.randint(100, 200) |
|
blood_surface.set_at((nx, ny), (*color[:3], alpha)) |
|
|
|
return blood_surface |
|
|
|
def draw_blood_surface(self, blood_surface, position): |
|
"""Convert blood surface to texture and return it""" |
|
# In pygame, we can return the surface directly |
|
return blood_surface |
|
|
|
def combine_blood_surfaces(self, existing_surface, new_surface): |
|
"""Combine two blood surfaces by blending them together""" |
|
combined_surface = pygame.Surface((self.cell_size, self.cell_size), pygame.SRCALPHA) |
|
|
|
# Blit existing blood first |
|
combined_surface.blit(existing_surface, (0, 0)) |
|
|
|
# Blit new blood on top with alpha blending |
|
combined_surface.blit(new_surface, (0, 0)) |
|
|
|
return combined_surface |
|
|
|
def free_surface(self, surface): |
|
"""Safely free a pygame surface (not needed in pygame, handled by GC)""" |
|
pass |
|
|
|
|
|
class SpriteWrapper: |
|
""" |
|
Wrapper class to make pygame surfaces compatible with SDL2 sprite interface |
|
""" |
|
def __init__(self, surface): |
|
self.surface = surface |
|
self.size = surface.get_size() |
|
self.position = (0, 0) |
|
|
|
def get_size(self): |
|
return self.size
|
|
|