import os import random from engine import maze from engine.collision_system import CollisionLayer from runtime_paths import bundle_path class Graphics(): def load_assets(self): theme_index = self.get_theme_index() print(f"[gfx] load_assets requested: level={self.current_level + 1} theme={theme_index}") if not hasattr(self, "theme_assets_cache"): self.theme_assets_cache = {} if not hasattr(self, "blood_layer_sprites"): self.blood_layer_sprites = [] if not hasattr(self, "cave_foreground_tiles"): self.cave_foreground_tiles = [] if not getattr(self, "common_assets_loaded", False): print("Loading graphics assets...") self.rat_assets = {} self.rat_assets_textures = {} self.rat_image_sizes = {} self.bomb_assets = {} self.assets = {} for sex in ["MALE", "FEMALE", "BABY"]: self.rat_assets[sex] = {} self.rat_assets_textures[sex] = {} self.rat_image_sizes[sex] = {} for direction in ["UP", "DOWN", "LEFT", "RIGHT"]: self.rat_assets[sex][direction] = self.render_engine.load_image( f"Rat/BMP_{sex}_{direction}.png", transparent_color=((125, 125, 125), (128, 128, 128)), ) texture = self.render_engine.load_image( f"Rat/BMP_{sex}_{direction}.png", transparent_color=((125, 125, 125), (128, 128, 128)), surface=False, ) self.rat_assets_textures[sex][direction] = texture self.rat_image_sizes[sex][direction] = texture.size for n in range(5): self.bomb_assets[n] = self.render_engine.load_image( f"Rat/BMP_BOMB{n}.png", transparent_color=((125, 125, 125), (128, 128, 128)), ) rat_asset_dir = bundle_path("assets", "Rat") for file in os.listdir(rat_asset_dir): if file.endswith(".png"): self.assets[file[:-4]] = self.render_engine.load_image( f"Rat/{file}", transparent_color=((125, 125, 125), (128, 128, 128)), ) print("Pre-generating blood stain pool...") self.blood_stain_textures = [] for _ in range(10): blood_surface = self.render_engine.generate_blood_surface() blood_texture = self.render_engine.draw_blood_surface(blood_surface, (0, 0)) if blood_texture: self.blood_stain_textures.append(blood_texture) self.common_assets_loaded = True print("[gfx] common assets loaded") else: print("[gfx] common assets cache hit") if theme_index not in self.theme_assets_cache: print(f"Loading theme assets {theme_index}...") self.theme_assets_cache[theme_index] = { "floor_tile": self.render_engine.create_color_surface((128, 128, 128)), "tunnel": self.render_engine.load_image("Rat/BMP_TUNNEL.png"), "grasses": [ self.render_engine.load_image(f"Rat/BMP_{theme_index}_GRASS_{i+1}.png", surface=True) for i in range(4) ], "grass_textures": [ self.render_engine.load_image(f"Rat/BMP_{theme_index}_GRASS_{i+1}.png") for i in range(4) ], "flowers": [ self.render_engine.load_image(f"Rat/BMP_{theme_index}_FLOWER_{i+1}.png", surface=True) for i in range(4) ], "flower_textures": [ self.render_engine.load_image(f"Rat/BMP_{theme_index}_FLOWER_{i+1}.png") for i in range(4) ], "caves": { direction: self.render_engine.load_image( f"Rat/BMP_{theme_index}_CAVE_{direction}.png", transparent_color=((125, 125, 125), (128, 128, 128)), surface=False, ) for direction in ["UP", "DOWN", "LEFT", "RIGHT"] }, "explosions": { direction: self.render_engine.load_image( f"Rat/BMP_{theme_index}_EXPLOSION_{direction}.png", transparent_color=((125, 125, 125), (128, 128, 128)), surface=False, ) for direction in ["UP", "DOWN", "LEFT", "RIGHT"] }, "edges": { direction: self.render_engine.load_image(f"Rat/BMP_{theme_index}_{direction}.png", surface=True) for direction in ["N", "S", "E", "W"] }, "corners": { direction: self.render_engine.load_image(f"Rat/BMP_{theme_index}_{direction}.png", surface=True) for direction in ["NE", "NW", "SE", "SW"] }, "inner_corners": { direction: self.render_engine.load_image(f"Rat/BMP_{theme_index}_{direction}.png", surface=True) for direction in ["EN", "ES", "WN", "WS"] }, } print(f"[gfx] theme cache miss -> loaded theme {theme_index}") else: print(f"[gfx] theme cache hit -> reusing theme {theme_index}") self.loaded_theme_index = theme_index theme_assets = self.theme_assets_cache[theme_index] self.floor_tile = theme_assets["floor_tile"] self.tunnel = theme_assets["tunnel"] self.grasses = theme_assets["grasses"] self.grass_textures = theme_assets["grass_textures"] self.flowers = theme_assets["flowers"] self.flower_textures = theme_assets["flower_textures"] self.caves = theme_assets["caves"] self.explosions = theme_assets["explosions"] self.edges = theme_assets["edges"] self.corners = theme_assets["corners"] self.inner_corners = theme_assets["inner_corners"] def get_theme_index(self): return self.current_level % 32 // 8 + 1 # ==================== RENDERING ==================== def draw_maze(self): if self.background_texture is None: print(f"[gfx] generating background texture for level={self.current_level + 1} theme={self.loaded_theme_index}") self.regenerate_background() self.render_engine.draw_background(self.background_texture) # Draw blood layer as sprites (optimized - no background regeneration) self.draw_blood_layer() def draw_cave_foreground(self): active_cave_explosions = {} for unit in self.units.values(): if unit.collision_layer != CollisionLayer.EXPLOSION: continue if not self.map.is_tunnel(*unit.position): continue active_cave_explosions[unit.position] = getattr(unit, "cave_direction", None) for cell_x, cell_y, direction, surface, x, y in self.cave_foreground_tiles: if (cell_x, cell_y) in active_cave_explosions: explosion_direction = active_cave_explosions[(cell_x, cell_y)] or direction surface = self.explosions.get(explosion_direction, surface) self.render_engine.draw_image(x, y, surface, anchor="nw", tag="cave") def draw_blood_layer(self): """Draw all blood stains as sprites overlay (optimized)""" for blood_texture, x, y in self.blood_layer_sprites: self.render_engine.draw_image(x, y, blood_texture, tag="blood") def regenerate_background(self): """Generate or regenerate the background texture (static - no blood stains)""" texture_tiles = [] self.cave_foreground_tiles = [] half_cell = self.cell_size // 2 def draw(surface, x, y): texture_tiles.append((surface, x, y)) def draw_cave(surface, x, y, direction): self.cave_foreground_tiles.append((x // self.cell_size, y // self.cell_size, direction, surface, x, y)) def occupied(x, y): return self.map.in_bounds(x, y) and self.map.get_cell(x, y) != maze.MAP_EMPTY def is_tunnel(x, y): return self.map.in_bounds(x, y) and self.map.get_cell(x, y) == maze.MAP_TUNNEL def random_wall(): return random.choice(self.grasses) def random_wall_texture(): return random.choice(self.grass_textures) def random_flower(): return random.choice(self.flowers) def random_flower_texture(): return random.choice(self.flower_textures) for y, row in enumerate(self.map.tiles): for x, cell in enumerate(row): px = x * self.cell_size py = y * self.cell_size if cell == maze.MAP_EMPTY: continue if cell == maze.MAP_WALL: if x == 0 or y == 0 or x == self.map.width - 1 or y == self.map.height - 1: draw(random_wall(), px, py) if x > 0 and y > 0 and (not occupied(x - 1, y - 1) or not occupied(x, y - 1) or not occupied(x - 1, y)): north = occupied(x, y - 1) west = occupied(x - 1, y) if north or west: if north and west: draw(self.inner_corners["WN"], px, py) elif north and not west: draw(self.edges["W"], px, py) else: draw(self.edges["N"], px, py) else: draw(self.corners["NW"], px, py) if y < self.map.height - 1 and x < self.map.width - 1: south = occupied(x, y + 1) east = occupied(x + 1, y) southeast = occupied(x + 1, y + 1) if southeast and south and east: if ( random.randrange(10) != 0 or x == 0 or y == 0 or x == self.map.width - 2 or y == self.map.height - 2 or is_tunnel(x + 1, y) or is_tunnel(x, y + 1) or is_tunnel(x + 1, y + 1) ): draw(random_wall(), px + half_cell, py + half_cell) else: draw(random_flower(), px + half_cell, py + half_cell) elif south or east: if south and east: draw(self.inner_corners["ES"], px + half_cell, py + half_cell) elif south and not east: draw(self.edges["E"], px + half_cell, py + half_cell) else: draw(self.edges["S"], px + half_cell, py + half_cell) else: draw(self.corners["SE"], px + half_cell, py + half_cell) if y > 0 and x < self.map.width - 1 and (not occupied(x + 1, y - 1) or not occupied(x, y - 1) or not occupied(x + 1, y)): north = occupied(x, y - 1) east = occupied(x + 1, y) if north or east: if north and east: draw(self.inner_corners["EN"], px + half_cell, py) elif north and not east: draw(self.edges["E"], px + half_cell, py) else: draw(self.edges["N"], px + half_cell, py) else: draw(self.corners["NE"], px + half_cell, py) if y < self.map.height - 1 and x > 0 and (not occupied(x - 1, y + 1) or not occupied(x, y + 1) or not occupied(x - 1, y)): south = occupied(x, y + 1) west = occupied(x - 1, y) if south or west: if south and west: draw(self.inner_corners["WS"], px, py + half_cell) elif south and not west: draw(self.edges["W"], px, py + half_cell) else: draw(self.edges["S"], px, py + half_cell) else: draw(self.corners["SW"], px, py + half_cell) elif cell == maze.MAP_TUNNEL: above = occupied(x, y - 1) below = occupied(x, y + 1) left = occupied(x - 1, y) right = occupied(x + 1, y) if above: if below: if left: if right: if random.randrange(10) != 0: draw_cave(random_wall_texture(), px + half_cell, py + half_cell, None) else: draw_cave(random_flower_texture(), px + half_cell, py + half_cell, None) else: draw_cave(self.caves["RIGHT"], px, py, "RIGHT") else: draw(self.grasses[0], px + half_cell, py + half_cell) draw_cave(self.caves["LEFT"], px, py, "LEFT") else: draw_cave(self.caves["DOWN"], px, py, "DOWN") else: draw(self.grasses[0], px + half_cell, py + half_cell) draw_cave(self.caves["UP"], px, py, "UP") # Blood stains now handled separately as overlay layer self.background_texture = self.render_engine.create_texture(texture_tiles, fill_color=(128, 128, 128)) def add_blood_stain(self, position): """Add a blood stain as sprite overlay (opti mized - no background regeneration)""" # Pick random blood texture from pre-generated pool if not self.blood_stain_textures: return blood_texture = random.choice(self.blood_stain_textures) x = position[0] * self.cell_size y = position[1] * self.cell_size # Add to blood layer sprites instead of regenerating background self.blood_layer_sprites.append((blood_texture, x, y)) def scroll_cursor(self, x=0, y=0): if self.pointer[0] + x > self.map.width or self.pointer[1] + y > self.map.height: return self.pointer = ( max(1, min(self.map.width-2, self.pointer[0] + x)), max(1, min(self.map.height-2, self.pointer[1] + y)) ) self.render_engine.scroll_view(self.pointer)