365 lines
17 KiB
Python
365 lines
17 KiB
Python
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 getattr(self, "startup_loading_active", False):
|
|
self._update_startup_loading(
|
|
"Loading graphics",
|
|
detail=f"Preparing theme {theme_index}",
|
|
progress=0.3,
|
|
)
|
|
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...")
|
|
if getattr(self, "startup_loading_active", False):
|
|
self._update_startup_loading(
|
|
"Loading graphics",
|
|
detail="Decoding sprites and tiles",
|
|
progress=0.4,
|
|
)
|
|
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)),
|
|
scale=False,
|
|
)
|
|
texture = self.render_engine.load_image(
|
|
f"Rat/BMP_{sex}_{direction}.png",
|
|
transparent_color=((125, 125, 125), (128, 128, 128)),
|
|
surface=False,
|
|
scale=False,
|
|
)
|
|
self.rat_assets_textures[sex][direction] = texture
|
|
self.rat_image_sizes[sex][direction] = (texture.size[0] // 4, texture.size[1])
|
|
|
|
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...")
|
|
if getattr(self, "startup_loading_active", False):
|
|
self._update_startup_loading(
|
|
"Loading graphics",
|
|
detail="Generating blood pool",
|
|
progress=0.58,
|
|
)
|
|
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}...")
|
|
if getattr(self, "startup_loading_active", False):
|
|
self._update_startup_loading(
|
|
"Loading graphics",
|
|
detail=f"Loading theme {theme_index} art",
|
|
progress=0.74,
|
|
)
|
|
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}")
|
|
|
|
if getattr(self, "startup_loading_active", False):
|
|
self._update_startup_loading(
|
|
"Loading graphics",
|
|
detail="Finishing render setup",
|
|
progress=0.84,
|
|
)
|
|
|
|
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) |