Enhance rendering system: improve screen resolution handling, add audio system initialization, and refine blood surface generation and blending

This commit is contained in:
2025-08-14 17:29:58 +02:00
parent c35fc9f1f7
commit f4ca5bba5b
2 changed files with 508 additions and 213 deletions
+294 -184
View File
@@ -1,24 +1,28 @@
import os
import random
import ctypes
from ctypes import *
import sdl2
import sdl2.ext
from sdl2.ext.compat import byteify
from ctypes import *
import ctypes
from PIL import Image
from sdl2 import SDL_AudioSpec
from PIL import Image
class 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
@@ -26,32 +30,56 @@ class GameWindow:
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}")
# SDL2 initialization
sdl2.ext.init(joystick=True)
sdl2.SDL_Init(sdl2.SDL_INIT_AUDIO)
self.window = sdl2.ext.Window(title=title, size=self.target_size,)# flags=sdl2.SDL_WINDOW_FULLSCREEN)
self.delay = 30
self.load_joystick()
# Window and renderer setup
self.window = sdl2.ext.Window(title=title, size=self.target_size)
self.window.show()
self.renderer = sdl2.ext.Renderer(self.window, flags=sdl2.SDL_RENDERER_ACCELERATED)
self.factory = sdl2.ext.SpriteFactory(renderer=self.renderer)
# Font system
self.fonts = self.generate_fonts("assets/decterm.ttf")
# Initial loading dialog
self.dialog("Loading assets...")
self.renderer.present()
# Game state
self.running = True
self.key_down, self.key_up, self.axis_scroll = key_callback
self.delay = 30
self.performance = 0
self.audio_devs = {}
# Input handling
self.key_down, self.key_up, self.axis_scroll = key_callback
self.button_cursor = [0, 0]
self.buttons = {}
self.audio_devs["base"] = sdl2.SDL_OpenAudioDevice(None, 0, SDL_AudioSpec(freq=22050, aformat=sdl2.AUDIO_U8, channels=1, samples=2048), None, 0)
self.audio_devs["effects"] = sdl2.SDL_OpenAudioDevice(None, 0, SDL_AudioSpec(freq=22050, aformat=sdl2.AUDIO_U8, channels=1, samples=2048), None, 0)
self.audio_devs["music"] = sdl2.SDL_OpenAudioDevice(None, 0, SDL_AudioSpec(freq=22050, aformat=sdl2.AUDIO_U8, channels=1, samples=2048), None, 0)
# Audio system initialization
self._init_audio_system()
# Input devices
self.load_joystick()
def _init_audio_system(self):
"""Initialize audio devices for different audio channels"""
audio_spec = SDL_AudioSpec(freq=22050, aformat=sdl2.AUDIO_U8, channels=1, samples=2048)
self.audio_devs = {}
self.audio_devs["base"] = sdl2.SDL_OpenAudioDevice(None, 0, audio_spec, None, 0)
self.audio_devs["effects"] = sdl2.SDL_OpenAudioDevice(None, 0, audio_spec, None, 0)
self.audio_devs["music"] = sdl2.SDL_OpenAudioDevice(None, 0, audio_spec, None, 0)
# ======================
# TEXTURE & IMAGE METHODS
# ======================
def create_texture(self, tiles: list):
# Always create a fresh surface since we free it after use
"""Create a texture from a list of tiles"""
bg_surface = sdl2.SDL_CreateRGBSurface(0, self.width, self.height, 32, 0, 0, 0, 0)
for tile in tiles:
dstrect = sdl2.SDL_Rect(tile[1], tile[2], self.cell_size, self.cell_size)
@@ -60,20 +88,12 @@ class GameWindow:
sdl2.SDL_FreeSurface(bg_surface)
return bg_texture
def load_joystick(self):
sdl2.SDL_Init(sdl2.SDL_INIT_JOYSTICK)
sdl2.SDL_JoystickOpen(0)
def generate_fonts(self,font_file):
fonts = {}
for i in range(10, 70, 1):
fonts.update({i: sdl2.ext.FontManager(font_path=font_file, size=i)})
return fonts
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)
image = Image.open(image_path)
# Handle transparency
if transparent_color:
image = image.convert("RGBA")
datas = image.getdata()
@@ -84,164 +104,181 @@ class GameWindow:
else:
new_data.append(item)
image.putdata(new_data)
# Scale image
scale = self.cell_size // 20
if surface:
return sdl2.ext.pillow_to_surface(image.resize((image.width * scale, image.height * scale), Image.NEAREST))
image = image.resize((image.width * scale, image.height * scale), Image.NEAREST)
if surface:
return sdl2.ext.pillow_to_surface(image)
return self.factory.from_surface(sdl2.ext.pillow_to_surface(image))
def get_image_size(self, image):
"""Get the size of an image sprite"""
return image.size
# ======================
# FONT MANAGEMENT
# ======================
def generate_fonts(self, font_file):
"""Generate font managers for different sizes"""
fonts = {}
for i in range(10, 70, 1):
fonts.update({i: sdl2.ext.FontManager(font_path=font_file, size=i)})
return fonts
# ======================
# DRAWING METHODS
# ======================
def draw_text(self, text, font, position, color):
"""Draw text at specified position with given font and color"""
sprite = self.factory.from_text(text, color=color, fontmanager=font)
# Handle center positioning
if position == "center":
position = ("center", "center")
if position[0] == "center":
position = (self.target_size[0] // 2 - sprite.size[0] // 2, position[1])
if position[1] == "center":
position = (position[0], self.target_size[1] // 2 - sprite.size[1] // 2)
sprite.position = position
#print(sprite.position)
self.renderer.copy(sprite, dstrect=sprite.position)
sprite.position = position
self.renderer.copy(sprite, dstrect=sprite.position)
def draw_background(self, bg_texture):
"""Draw background texture with current view offset"""
self.renderer.copy(bg_texture, dstrect=sdl2.SDL_Rect(self.w_offset, self.h_offset, self.width, self.height))
def draw_image(self, x, y, sprite, tag, anchor="nw"):
"""Draw an image sprite at specified coordinates"""
if not self.is_in_visible_area(x, y):
return
sprite.position = (x+self.w_offset, y+self.h_offset)
sprite.position = (x + self.w_offset, y + self.h_offset)
self.renderer.copy(sprite, dstrect=sprite.position)
def draw_rectangle(self, x, y, width, height, tag, outline="red", filling=None):
"""Draw a rectangle with optional fill and outline"""
if filling:
self.renderer.fill((x, y, width, height), sdl2.ext.Color(*filling))
else:
self.renderer.draw_rect((x, y, width, height), sdl2.ext.Color(*outline))
def draw_pointer(self, x, y):
x=x+self.w_offset
y=y+self.h_offset
"""Draw a red pointer rectangle at specified coordinates"""
x = x + self.w_offset
y = y + self.h_offset
for i in range(3):
self.renderer.draw_rect((x + i,y+i, self.cell_size-2*i, self.cell_size-2*i), color=sdl2.ext.Color(255, 0, 0))
self.renderer.draw_rect((x + i, y + i, self.cell_size - 2*i, self.cell_size - 2*i),
color=sdl2.ext.Color(255, 0, 0))
def delete_tag(self, tag):
"""Placeholder for tag deletion (not implemented)"""
pass
# ======================
# UI METHODS
# ======================
def dialog(self, text, **kwargs):
self.draw_rectangle(50, 50,
self.target_size[0] - 100, self.target_size[1] - 100, "win", filling=(255, 255, 255))
"""Display a dialog box with text and optional extras"""
# Draw dialog background
self.draw_rectangle(50, 50,
self.target_size[0] - 100, self.target_size[1] - 100,
"win", filling=(255, 255, 255))
# Draw main text
self.draw_text(text, self.fonts[self.target_size[1]//20], "center", sdl2.ext.Color(0, 0, 0))
# Draw subtitle if provided
if subtitle := kwargs.get("subtitle"):
self.draw_text(subtitle, self.fonts[self.target_size[1]//30], ("center", self.target_size[1] // 2 + 50), sdl2.ext.Color(0, 0, 0))
self.draw_text(subtitle, self.fonts[self.target_size[1]//30],
("center", self.target_size[1] // 2 + 50), sdl2.ext.Color(0, 0, 0))
# Draw image if provided
if image := kwargs.get("image"):
image_size = self.get_image_size(image)
self.draw_image(self.target_size[0] // 2 - image_size[0] // 2 - self.w_offset,
self.target_size[1] // 2 - image_size[1] * 2 - self.h_offset,
image, "win")
self.target_size[1] // 2 - image_size[1] * 2 - self.h_offset,
image, "win")
# Draw scores if provided
if scores := kwargs.get("scores"):
#self.draw_text("Scores:", self.fonts[self.target_size[1]//20], (self.target_size[0] // 2 - 50, self.target_size[1] // 2 + 50), sdl2.ext.Color(0, 0, 0))
sprite = self.factory.from_text("Scores:", color=sdl2.ext.Color(0, 0, 0), fontmanager=self.fonts[self.target_size[1]//20])
sprite = self.factory.from_text("Scores:", color=sdl2.ext.Color(0, 0, 0),
fontmanager=self.fonts[self.target_size[1]//20])
sprite.position = (self.target_size[0] // 2 - 50, self.target_size[1] // 2 + 30)
self.renderer.copy(sprite, dstrect=sprite.position)
for i, score in enumerate(scores[:5]):
score = " - ".join(score)
self.draw_text(score, self.fonts[self.target_size[1]//40], ("center", self.target_size[1] // 2 + 50 + 30 * (i + 1)), sdl2.ext.Color(0, 0, 0))
def get_image_size(self, image):
return image.size
def update_status(self, text):
fps = int(1000 / self.performance) if self.performance != 0 else 0
text = f"FPS: {fps} - {text}"
font = self.fonts[20]
sprite = self.factory.from_text(text, color=sdl2.ext.Color(0, 0, 0), fontmanager=font)
text_width, text_height = sprite.size
self.renderer.fill((3, 3, text_width + 10, text_height + 4), sdl2.ext.Color(255, 255, 255))
self.draw_text(text, font, (8, 5), sdl2.ext.Color(0, 0, 0))
def new_cycle(self, delay, callback):
pass
def full_screen(self,flag):
sdl2.SDL_SetWindowFullscreen(self.window.window, flag)
def is_in_visible_area(self, x, y):
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_perf_counter(self):
return sdl2.SDL_GetPerformanceCounter()
def mainloop(self, **kwargs):
while self.running:
performance_start = sdl2.SDL_GetPerformanceCounter()
self.renderer.clear()
if "bg_update" in kwargs:
kwargs["bg_update"]()
kwargs["update"]()
events = sdl2.ext.get_events()
for event in events:
if event.type == sdl2.SDL_QUIT:
self.running = False
elif event.type == sdl2.SDL_KEYDOWN and self.key_down:
key = sdl2.SDL_GetKeyName(event.key.keysym.sym).decode('utf-8')
self.key_down(key)
elif event.type == sdl2.SDL_KEYUP and self.key_down:
key = sdl2.SDL_GetKeyName(event.key.keysym.sym).decode('utf-8')
self.key_up(key)
print(key)
elif event.type == sdl2.SDL_MOUSEMOTION:
self.key_down("mouse", coords=(event.motion.x, event.motion.y))
elif event.type == sdl2.SDL_JOYBUTTONDOWN:
key = event.jbutton.button
self.key_down(key)
elif event.type == sdl2.SDL_JOYBUTTONUP:
key = event.jbutton.button
self.key_up(key)
# elif event.type == sdl2.SDL_JOYAXISMOTION:
# self.axis_scroll(event.jaxis.axis, event.jaxis.value)
# Disegna qui gli sprite
#rect = sdl2.SDL_Rect(self.w_offset, self.h_offset, self.target_size[0], self.target_size[1])
#sdl2.SDL_RenderSetClipRect(self.renderer.sdlrenderer, rect)
self.renderer.present()
self.performance = (sdl2.SDL_GetPerformanceCounter() - performance_start) / sdl2.SDL_GetPerformanceFrequency() * 1000
if self.performance < self.delay:
delay = self.delay - round(self.performance)
else:
delay = 0
sdl2.SDL_Delay(delay)
def close(self):
self.running = False
sdl2.ext.quit()
for i, score in enumerate(scores[:5]):
score_text = " - ".join(score)
self.draw_text(score_text, self.fonts[self.target_size[1]//40],
("center", self.target_size[1] // 2 + 50 + 30 * (i + 1)),
sdl2.ext.Color(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"""
# TODO: Fix outline parameter usage
color = (0, 0, 255) if self.button_cursor == list(coords) else (0, 0, 0)
self.draw_rectangle(x, y, width, height, "button", outline=color)
self.draw_text(text, self.fonts[20], (x + 10, y + 10), (0, 0, 0))
def update_status(self, text):
"""Update and display the status bar with FPS information"""
fps = int(1000 / self.performance) if self.performance != 0 else 0
status_text = f"FPS: {fps} - {text}"
font = self.fonts[20]
sprite = self.factory.from_text(status_text, color=sdl2.ext.Color(0, 0, 0), fontmanager=font)
text_width, text_height = sprite.size
# Draw background for status text
self.renderer.fill((3, 3, text_width + 10, text_height + 4), sdl2.ext.Color(255, 255, 255))
self.draw_text(status_text, font, (8, 5), sdl2.ext.Color(0, 0, 0))
# ======================
# VIEW & NAVIGATION
# ======================
def scroll_view(self, pointer):
"""
Adjusts the view offset based on the given pointer coordinates.
Scales them down by half, then adjusts offsets, ensuring they don't
exceed maximum allowed values.
"""
"""Adjust the view offset based on pointer coordinates"""
x, y = pointer
# Scale down and invert
# Scale down and invert coordinates
x = -(x // 2) * self.cell_size
y = -(y // 2) * self.cell_size
# Clamp horizontal offset
# Clamp horizontal offset to valid range
if x <= self.max_w_offset + self.cell_size:
x = self.max_w_offset
# Clamp vertical 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"):
sound_file = os.path.join("sound", sound_file)
rw = sdl2.SDL_RWFromFile(byteify(sound_file, "utf-8"), b"rb")
"""Play a sound file on the specified audio channel"""
sound_path = os.path.join("sound", sound_file)
rw = sdl2.SDL_RWFromFile(byteify(sound_path, "utf-8"), b"rb")
if not rw:
raise RuntimeError("Failed to open sound file")
@@ -251,41 +288,116 @@ class GameWindow:
spec = SDL_AudioSpec(freq=22050, aformat=sdl2.AUDIO_U8, channels=1, samples=2048)
if sdl2.SDL_LoadWAV_RW(rw, 1, byref(spec), byref(_buf), byref(_length)) == None:
raise RuntimeError("Failed to load WAV")
devid = self.audio_devs[tag]
# Clear any queued audio
sdl2.SDL_ClearQueuedAudio(devid)
# Start playing audio
sdl2.SDL_QueueAudio(devid, _buf, _length)
sdl2.SDL_PauseAudioDevice(devid, 0)
def stop_sound(self):
for dev in self.audio_devs:
if not dev[0]:
sdl2.SDL_PauseAudioDevice(dev[1], 1)
sdl2.SDL_ClearQueuedAudio(dev[1])
def start_dialog(self, **kwargs):
self.dialog("Welcome to the Mice!", subtitle="A game by Matteo because was bored", **kwargs)
center = self.get_view_center()
#self.draw_button(center[0], center[1] + 10 * self.scale, "Start", 120, 50, (0, 0))
def draw_button(self, x, y, text, width, height, coords):
if self.button_cursor[0] == coords[0] and self.button_cursor[1] == coords[1]:
color = (0, 0, 255)
self.draw_rectangle(x, y, width, height, "button", outline8u=color)
self.draw_text(text, self.fonts[20], (x + 10, y + 10), (0,0,0))
def get_view_center(self):
return self.w_offset + self.width // 2, self.h_offset + self.height // 2
def stop_sound(self):
"""Stop all audio playback"""
for dev in self.audio_devs.values():
sdl2.SDL_PauseAudioDevice(dev, 1)
sdl2.SDL_ClearQueuedAudio(dev)
# ======================
# INPUT METHODS
# ======================
def load_joystick(self):
"""Initialize joystick support"""
sdl2.SDL_Init(sdl2.SDL_INIT_JOYSTICK)
sdl2.SDL_JoystickOpen(0)
# ======================
# MAIN GAME LOOP
# ======================
def mainloop(self, **kwargs):
"""Main game loop handling events and rendering"""
while self.running:
performance_start = sdl2.SDL_GetPerformanceCounter()
self.renderer.clear()
# Execute background update if provided
if "bg_update" in kwargs:
kwargs["bg_update"]()
# Execute main update
kwargs["update"]()
# Handle SDL events
events = sdl2.ext.get_events()
for event in events:
if event.type == sdl2.SDL_QUIT:
self.running = False
elif event.type == sdl2.SDL_KEYDOWN and self.key_down:
key = sdl2.SDL_GetKeyName(event.key.keysym.sym).decode('utf-8')
self.key_down(key)
elif event.type == sdl2.SDL_KEYUP and self.key_up:
key = sdl2.SDL_GetKeyName(event.key.keysym.sym).decode('utf-8')
self.key_up(key)
elif event.type == sdl2.SDL_MOUSEMOTION:
self.key_down("mouse", coords=(event.motion.x, event.motion.y))
elif event.type == sdl2.SDL_JOYBUTTONDOWN:
key = event.jbutton.button
self.key_down(key)
elif event.type == sdl2.SDL_JOYBUTTONUP:
key = event.jbutton.button
self.key_up(key)
# Present the rendered frame
self.renderer.present()
# Calculate performance and delay
self.performance = ((sdl2.SDL_GetPerformanceCounter() - performance_start) /
sdl2.SDL_GetPerformanceFrequency() * 1000)
delay = max(0, self.delay - round(self.performance))
sdl2.SDL_Delay(delay)
# ======================
# SPECIAL EFFECTS
# ======================
# ======================
# UTILITY METHODS
# ======================
def new_cycle(self, delay, callback):
"""Placeholder for cycle management (not implemented)"""
pass
def full_screen(self, flag):
"""Toggle fullscreen mode"""
sdl2.SDL_SetWindowFullscreen(self.window.window, flag)
def get_perf_counter(self):
"""Get performance counter for timing"""
return sdl2.SDL_GetPerformanceCounter()
def close(self):
"""Close the game window and cleanup"""
self.running = False
sdl2.ext.quit()
# ======================
# MAIN GAME LOOP
# ======================
# ======================
# SPECIAL EFFECTS
# ======================
def generate_blood_surface(self):
"""Genera dinamicamente una superficie di macchia di sangue usando SDL2"""
"""Generate a dynamic blood splatter surface using SDL2"""
size = self.cell_size
# Crea una superficie RGBA per la macchia di sangue
# Create RGBA surface for blood splatter
blood_surface = sdl2.SDL_CreateRGBSurface(
0, size, size, 32,
0x000000FF, # R mask
@@ -297,58 +409,56 @@ class GameWindow:
if not blood_surface:
return None
# Blocca la superficie per il disegno pixel per pixel
# Lock surface for pixel manipulation
sdl2.SDL_LockSurface(blood_surface)
# Ottieni i dati dei pixel
# Get pixel data
pixels = cast(blood_surface.contents.pixels, POINTER(c_uint32))
pitch = blood_surface.contents.pitch // 4 # pitch in pixel (32-bit)
pitch = blood_surface.contents.pitch // 4 # Convert pitch to pixels (32-bit)
# Colori del sangue (variazioni di rosso in formato ABGR)
# Blood color variations (ABGR format)
blood_colors = [
0xFF00008B, # Rosso scuro (A=FF, B=00, G=00, R=8B)
0xFF002222, # Rosso mattone (A=FF, B=00, G=22, R=22)
0xFF003C14, # Cremisi (A=FF, B=00, G=3C, R=14)
0xFF0000FF, # Rosso puro (A=FF, B=00, G=00, R=FF)
0xFF000080, # Marrone rossastro (A=FF, B=00, G=00, R=80)
0xFF00008B, # Dark red
0xFF002222, # Brick red
0xFF003C14, # Crimson
0xFF0000FF, # Pure red
0xFF000080, # Reddish brown
]
# Genera la macchia con un algoritmo di diffusione
# Generate splatter with diffusion algorithm
center_x, center_y = size // 2, size // 2
# Inizia dal centro e espandi verso l'esterno
max_radius = size // 3 + random.randint(-3, 5)
for y in range(size):
for x in range(size):
# Calcola la distanza dal centro
# Calculate distance from center
distance = ((x - center_x) ** 2 + (y - center_y) ** 2) ** 0.5
# Probabilità di avere sangue basata sulla distanza
# Calculate blood probability based on distance
if distance <= max_radius:
# Più vicino al centro, più probabile avere sangue
# Closer to center = higher probability
probability = max(0, 1 - (distance / max_radius))
# Aggiungi rumore per forma irregolare
# Add noise for irregular shape
noise = random.random() * 0.7
if random.random() < probability * noise:
# Scegli un colore di sangue casuale
# Choose random blood color
color = random.choice(blood_colors)
# Aggiungi variazione di alpha per trasparenza
# Add alpha variation for transparency
alpha = int(255 * probability * random.uniform(0.6, 1.0))
color = (color & 0x00FFFFFF) | (alpha << 24)
pixels[y * pitch + x] = color
else:
# Pixel trasparente
# Transparent pixel
pixels[y * pitch + x] = 0x00000000
else:
# Fuori dal raggio, trasparente
# Outside radius, transparent
pixels[y * pitch + x] = 0x00000000
# Aggiungi alcune gocce sparse intorno alla macchia principale
# 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)
@@ -360,36 +470,36 @@ class GameWindow:
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]) # Colori più scuri per le gocce
color = random.choice(blood_colors[:3]) # Darker colors for drops
alpha = random.randint(100, 200)
color = (color & 0x00FFFFFF) | (alpha << 24)
pixels[ny * pitch + nx] = color
# Sblocca la superficie
# Unlock surface
sdl2.SDL_UnlockSurface(blood_surface)
# Converte la superficie in una texture usando il factory del gioco
return blood_surface
def draw_blood_surface(self, blood_surface, position):
# Create a new surface for the blood texture since bg_surface may have been freed
"""Convert blood surface to texture and return it"""
# Create temporary surface for blood texture
temp_surface = sdl2.SDL_CreateRGBSurface(0, self.cell_size, self.cell_size, 32, 0, 0, 0, 0)
if temp_surface is None:
sdl2.SDL_FreeSurface(blood_surface)
return None
# Copy the blood surface to the temporary surface
# Copy blood surface to temporary surface
sdl2.SDL_BlitSurface(blood_surface, None, temp_surface, None)
sdl2.SDL_FreeSurface(blood_surface)
# Create texture from the temporary surface
# Create texture from temporary surface
texture = self.factory.from_surface(temp_surface)
sdl2.SDL_FreeSurface(temp_surface)
return texture
def combine_blood_surfaces(self, existing_surface, new_surface):
"""Combine two blood surfaces by blending them together"""
# Create a new surface for the combined result
# Create combined surface
combined_surface = sdl2.SDL_CreateRGBSurface(
0, self.cell_size, self.cell_size, 32,
0x000000FF, # R mask
@@ -401,7 +511,7 @@ class GameWindow:
if combined_surface is None:
return existing_surface
# Lock both surfaces for pixel manipulation
# Lock surfaces for pixel manipulation
sdl2.SDL_LockSurface(existing_surface)
sdl2.SDL_LockSurface(new_surface)
sdl2.SDL_LockSurface(combined_surface)
@@ -411,9 +521,9 @@ class GameWindow:
new_pixels = cast(new_surface.contents.pixels, POINTER(c_uint32))
combined_pixels = cast(combined_surface.contents.pixels, POINTER(c_uint32))
pitch = combined_surface.contents.pitch // 4 # pitch in pixels (32-bit)
pitch = combined_surface.contents.pitch // 4 # Convert pitch to pixels (32-bit)
# Combine pixels manually for better blending
# Combine pixels with additive blending
for y in range(self.cell_size):
for x in range(self.cell_size):
idx = y * pitch + x
@@ -432,7 +542,7 @@ class GameWindow:
new_g = (new_pixel >> 8) & 0xFF
new_b = new_pixel & 0xFF
# Blend the colors (additive blending for blood accumulation)
# Blend colors (additive blending for blood accumulation)
if new_a > 0: # If new pixel has color
if existing_a > 0: # If existing pixel has color
# Combine both colors, making it darker/more opaque
@@ -462,7 +572,7 @@ class GameWindow:
sdl2.SDL_UnlockSurface(combined_surface)
return combined_surface
def free_surface(self, surface):
"""Safely free an SDL surface"""
if surface is not None: