Add semi-transparent tunnel cover overlay for internal passages

- Add sdl2.create_overlay_texture() and draw_overlay_texture(alpha) for
  transparent full-map overlays built from sub-tile surface blits.
- Add Graphics.regenerate_tunnel_cover() which builds an overlay of
  4 random grass sub-tiles (20x20) for every internal tunnel cell,
  defined as a tunnel cell surrounded by occupied cells (wall or
  tunnel) on all four sides. Cells with at least one open side are
  handled by cave_foreground and skipped here.
- Draw the tunnel cover in the game loop after top-layer units/effects
  but before cave_foreground and points, at 95% opacity (alpha=242)
  so the unit and effect passing underneath is just barely visible.
This commit is contained in:
2026-06-16 22:41:11 +02:00
parent 62a65f599d
commit 2eccf504f5
3 changed files with 104 additions and 1 deletions
+42
View File
@@ -26,6 +26,8 @@ class Graphics:
self.blood_layer_sprites = []
if not hasattr(self, "cave_foreground_tiles"):
self.cave_foreground_tiles = []
if not hasattr(self, "tunnel_cover_texture"):
self.tunnel_cover_texture = None
if not getattr(self, "common_assets_loaded", False):
print("Loading graphics assets...")
@@ -194,6 +196,45 @@ class Graphics:
# Draw blood layer as sprites (optimized - no background regeneration)
self.draw_blood_layer()
def regenerate_tunnel_cover(self):
"""Generate an overlay texture that covers internal tunnel passages with grass sub-tiles.
A tunnel cell is considered an internal passage only when it is surrounded
by occupied cells (walls or tunnels) on all four sides. In that case there is
no cave entrance drawn by cave_foreground, so we cover it here.
"""
tunnel_tiles = []
half_cell = self.game.cell_size // 2
def occupied(x, y):
return self.game.map.in_bounds(x, y) and self.game.map.get_cell(x, y) != maze.MAP_EMPTY
for y, row in enumerate(self.game.map.tiles):
for x, cell in enumerate(row):
if cell != maze.MAP_TUNNEL:
continue
above = occupied(x, y - 1)
below = occupied(x, y + 1)
left = occupied(x - 1, y)
right = occupied(x + 1, y)
if not (above and below and left and right):
continue
px = x * self.game.cell_size
py = y * self.game.cell_size
for qx, qy, sx, sy in [
(0, 0, 0, 0),
(half_cell, 0, half_cell, 0),
(0, half_cell, 0, half_cell),
(half_cell, half_cell, half_cell, half_cell),
]:
grass = random.choice(self.grasses)
tunnel_tiles.append((grass, sx, sy, half_cell, half_cell, px + qx, py + qy))
self.tunnel_cover_texture = self.game.render_engine.create_overlay_texture(tunnel_tiles)
def draw_cave_foreground(self):
active_cave_explosions = {}
for unit in self.game.units.values():
@@ -218,6 +259,7 @@ class Graphics:
"""Generate or regenerate the background texture (static - no blood stains)"""
texture_tiles = []
self.cave_foreground_tiles = []
self.regenerate_tunnel_cover()
half_cell = self.game.cell_size // 2
def draw(surface, x, y):
+55
View File
@@ -236,6 +236,61 @@ class GameWindow:
sdl2.SDL_FreeSurface(bg_surface)
return bg_texture
def create_overlay_texture(self, tiles: list):
"""Create a transparent RGBA texture from sub-tile surface blits.
Each tile is a tuple (surface, src_x, src_y, width, height, dst_x, dst_y).
Source surfaces are converted to RGBA so the resulting texture has an
alpha channel and supports global alpha modulation.
"""
bg_surface = sdl2.SDL_CreateRGBSurface(
0, self.width, self.height, 32,
0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000
)
sdl2.SDL_FillRect(bg_surface, None, 0)
sdl2.SDL_SetSurfaceBlendMode(bg_surface, sdl2.SDL_BLENDMODE_BLEND)
rgba_format = sdl2.SDL_PIXELFORMAT_RGBA8888
for surface, src_x, src_y, width, height, dst_x, dst_y in tiles:
src_fmt = sdl2.SDL_PIXELFORMAT_UNKNOWN
try:
src_fmt = surface.format.format
except AttributeError:
src_fmt = sdl2.SDL_PIXELFORMAT_UNKNOWN
srcrect = sdl2.SDL_Rect(int(src_x), int(src_y), int(width), int(height))
dstrect = sdl2.SDL_Rect(int(dst_x), int(dst_y), int(width), int(height))
if src_fmt == rgba_format:
sdl2.SDL_BlitSurface(surface, srcrect, bg_surface, dstrect)
else:
converted = sdl2.SDL_ConvertSurfaceFormat(surface, rgba_format, 0)
if converted:
sdl2.SDL_BlitSurface(converted, srcrect, bg_surface, dstrect)
sdl2.SDL_FreeSurface(converted)
else:
sdl2.SDL_BlitSurface(surface, srcrect, bg_surface, dstrect)
bg_texture = self.factory.from_surface(bg_surface)
try:
sdl2.SDL_SetTextureBlendMode(bg_texture.texture, sdl2.SDL_BLENDMODE_BLEND)
except Exception:
pass
sdl2.SDL_FreeSurface(bg_surface)
return bg_texture
def draw_overlay_texture(self, texture, alpha=255):
"""Draw a full-map transparent overlay texture with current view offset."""
if texture is None:
return
raw_texture = getattr(texture, "texture", texture)
if alpha < 255:
try:
sdl2.SDL_SetTextureAlphaMod(raw_texture, int(alpha))
except Exception:
pass
try:
sdl2.SDL_SetTextureBlendMode(raw_texture, sdl2.SDL_BLENDMODE_BLEND)
except Exception:
pass
self.renderer.copy(texture, dstrect=sdl2.SDL_Rect(self.w_offset, self.h_offset, self.width, self.height))
def create_color_surface(self, color, width=None, height=None):
"""Create a solid color surface matching the current cell size by default."""
width = width or self.cell_size
+7 -1
View File
@@ -62,6 +62,7 @@ class MiceMaze:
self.scrolling = False
self.sounds = {}
self.background_texture = None
self.tunnel_cover_texture = None
self.combined_scores = None
def _setup_engine(self):
@@ -527,12 +528,17 @@ class MiceMaze:
if getattr(unit, "draw_on_top", False) and not getattr(unit, "draw_last", False):
unit.draw()
# Draw tunnel cover overlay above units/effects but below cave foreground and points
self.render_engine.draw_overlay_texture(self.graphics.tunnel_cover_texture, alpha=191)
# Draw cave foreground (tunnel entrances) above the tunnel cover
self.graphics.draw_cave_foreground()
# Draw foreground/last-layer units (points) on top of everything
for unit in sorted_units:
if getattr(unit, "draw_last", False):
unit.draw()
self.graphics.draw_cave_foreground()
self.render_engine.draw_pointer(self.pointer[0] * self.cell_size, self.pointer[1] * self.cell_size)
self.render_engine.update_status(f"Mice: {self.unit_manager.count_rats()} - Points: {self.points}")