import pygame
import sys
import random
from Levels import Level1, Level2, Level3, Level4, Level5, Level6, Level7, Level8, Level9, Level10, Level11, Level12, Level13, Level14, Level15, Level16, Level17, Level18, Level19, Level20, Level21, Level22, Level23, Level24, Level25
from Player.Player import Player, Projectile
from enemies.enemy import Enemy
from enemies.jumping_enemy import JumpingEnemy
from enemies.Shotting_enemie import Shooting_enemie
from enemies.Turrent import Turret
from enemies.SlowTurret import SlowTurret
from Bosses.Boss3 import Boss3
from Bosses.Boss1 import Boss1
from Bosses.Boss2 import Boss2
from Bosses.Boss4 import Boss4
from Bosses.Boss5 import Boss5  # NEW

pygame.init()
audio_enabled = False
try:
    pygame.mixer.init()
    audio_enabled = True
except Exception:
    audio_enabled = False

# NEW: track if music/volume have been fully initialized and guard against re‑entry
audio_ready = False
_audio_init_busy = False

# small console hint so you can see if audio is on/off
try:
    print(f"[RARL] mixer_init={bool(pygame.mixer.get_init())}")
except Exception:
    pass

# Set up the max fps for the game
clock = pygame.time.Clock()
fps = 60

# TARGET grid size required: 30 x 20
TARGET_COLS = 30
TARGET_ROWS = 20

game_over = 0
main_menu = True          # already exists but now actually used
ALLOW_UPSCALE = True
# NEW: in‑game pause state
paused = False

# NEW/EXISTING: global volume state
master_volume = 0.6      # 0.0–1.0, UI shows percentage
mute_audio = False        # derived from master_volume == 0 in practice
# NEW: remember last non-zero volume to base percent UI on
last_nonzero_volume = master_volume

# NEW/EXISTING: global registry for all SFX
REGISTERED_SOUNDS: list[pygame.mixer.Sound] = []

# NEW: track current music kind + fadeback for priority SFX
current_music_kind = None          # "normal" | "boss" | None
_music_restore_time_ms = 0        # when to restore music after ducking
_music_ducked = False

def register_sound(snd):
    """Register a pygame.mixer.Sound so menu volume can control it (kept for compatibility)."""
    if not pygame.mixer.get_init():
        return
    try:
        import pygame as _pg
        if isinstance(snd, _pg.mixer.Sound) and snd not in REGISTERED_SOUNDS:
            REGISTERED_SOUNDS.append(snd)
    except Exception:
        pass

# NEW: tiny helper that other modules can import safely
def safe_register_sound(snd):
    """Kept for backward compatibility; gameplay code should NOT import from RARL anymore."""
    register_sound(snd)

# NEW: helper to pick correct music file for given level
def _get_music_path_for_level(level: int):
    """Return (kind, path_list) where kind is 'boss' or 'normal', path_list is list of candidates."""
    boss_levels = {5, 10, 15, 20, 25}
    if level in boss_levels:
        # Boss music candidates
        return "boss", [
            r"Sounds/boss_music.mp3",
            r"Sounds\boss_music.mp3",
            r"../Sounds/boss_music.mp3",
            r"..\Sounds\boss_music.mp3",
        ]
    # Normal music
    return "normal", [
        r"Sounds/music.mp3",
        r"Sounds\music.mp3",
        r"../Sounds/music.mp3",
        r"..\Sounds/music.mp3",
    ]

# NEW: start or switch music for current level
def play_music_for_level(level: int):
    global audio_enabled, current_music_kind
    if not audio_enabled or not pygame.mixer.get_init():
        return
    kind, paths = _get_music_path_for_level(level)
    # If the same kind is already playing, just ensure volume and return
    if current_music_kind == kind and pygame.mixer.music.get_busy():
        apply_volume()
        return
    # Try to load one of the candidate files
    for p in paths:
        try:
            pygame.mixer.music.load(p)
            pygame.mixer.music.play(-1)
            current_music_kind = kind
            apply_volume()
            return
        except Exception:
            continue
    # If loading fails, just mark as no music
    current_music_kind = None

# NEW: generic safe sound-play wrapper used by RARL (do NOT import from gameplay modules)
def play_safe_sound(snd: pygame.mixer.Sound, priority: bool = False, duck_ms: int = 0):
    """
    Play a pygame.mixer.Sound safely:
    - checks mixer init
    - catches all play() errors
    - optionally ducks music for 'priority' sounds

    IMPORTANT:
    - This is for internal use in RARL only; Player / Bosses must NOT import it
      to avoid circular imports when sounds first play.
    """
    global _music_restore_time_ms, _music_ducked

    if snd is None or not isinstance(snd, pygame.mixer.Sound):
        return
    if not pygame.mixer.get_init():
        return
    try:
        snd.play()
    except Exception:
        return
    if priority and not mute_audio and pygame.mixer.music.get_busy():
        try:
            pygame.mixer.music.set_volume(0.0)
        except Exception:
            pass
        _music_ducked = True
        _music_restore_time_ms = pygame.time.get_ticks() + max(duck_ms, 50)

# NEW: 'priority' SFX play helper (e.g. damage sound)
def play_priority_sfx(snd: pygame.mixer.Sound, duck_ms: int = 250):
    """
    Play a high-priority sound:
    - If music is playing, temporarily duck it.
    - Resume music after `duck_ms`.
    """
    # just wrap play_safe_sound in priority mode
    play_safe_sound(snd, priority=True, duck_ms=duck_ms)

def _update_music_duck():
    """Internal: restore music volume after priority SFX."""
    global _music_ducked, _music_restore_time_ms
    if not _music_ducked:
        return
    if pygame.time.get_ticks() >= _music_restore_time_ms:
        _music_ducked = False
        apply_volume()  # restore normal volume based on master_volume/mute_audio

# NEW: ensure_audio_ready() strictly one‑shot (with a `busy` flag and `audio_ready` set early), and keep it lightweight so imports from other modules are safe
def ensure_audio_ready():
    """
    Make sure mixer is initialized, music loaded and volume applied.

    This is SAFE to call from any module:
    - It is strictly one-shot.
    - It never re-enters if already running.
    """
    global audio_enabled, audio_ready, _audio_init_busy

    # already done or currently in progress -> do nothing
    if audio_ready or _audio_init_busy:
        return

    _audio_init_busy = True
    try:
        # (re)init mixer if needed
        if not pygame.mixer.get_init():
            try:
                pygame.mixer.init()
                audio_enabled = True
            except Exception:
                audio_enabled = False

        if not audio_enabled:
            # permanently mark as "not available" for this run
            audio_ready = False
            return

        # CHANGED: do not hardcode music path here; just mark ready.
        apply_volume()
        audio_ready = True
    finally:
        _audio_init_busy = False

# Get user's display resolution for initial window
display_info = pygame.display.Info()

# NEW: small margins so the window does not exactly match the desktop (keeps title bar visible)
FRAME_MARGIN_SIDE = 8
FRAME_MARGIN_TOP = 64
MIN_WINDOW_W = 800
MIN_WINDOW_H = 600

def clamp_window_size(w, h):
    """Clamp requested window size to desktop minus margins and enforce minimums."""
    info = pygame.display.Info()
    max_w = max(MIN_WINDOW_W, info.current_w - FRAME_MARGIN_SIDE)
    max_h = max(MIN_WINDOW_H, info.current_h - FRAME_MARGIN_TOP)
    cw = max(MIN_WINDOW_W, min(w, max_w))
    ch = max(MIN_WINDOW_H, min(h, max_h))
    return cw, ch

# NEW: simple helper to fully heal the player
def heal_player_full(player):
    try:
        player.health = getattr(player, "max_health", player.health)
    except Exception:
        pass

# NEW: helper to build a door rect at player's spawn tile (fallback to center-bottom)
def make_door_at_player_spawn(world):
    """
    Create a door rect anchored at the player's spawn tile for this world.

    world.player_spawn is stored as the TOP-LEFT pixel of the spawn tile.
    We want the door 1 tile wide and 2 tiles tall, bottom-aligned so the
    player can "walk into" the door without being blocked.
    """
    ts = world.tile_size

    # default: centre-bottom (used only if player_spawn missing for some reason)
    door_x = world.pixel_width // 2 - ts // 2
    door_y = world.pixel_height - 2 * ts

    spawn = getattr(world, "player_spawn", None)
    if spawn:
        # spawn is recorded as (tile_x, tile_y) in pixels at tile top-left
        sx, sy = spawn
        door_x = sx
        # make door 2 tiles high, bottom aligned with the spawn tile bottom
        door_y = sy - ts  # door covers [sy - ts, sy + ts)

    return pygame.Rect(door_x+30, door_y, ts, 2 * ts)

# REPLACE initial size with clamped desktop-sized window (keeps top bar visible)
initial_w, initial_h = clamp_window_size(display_info.current_w, display_info.current_h)

# NOTE: windowed, resizable only (no fullscreen)
screen_width, screen_height = initial_w, initial_h
WINDOW_FLAGS = pygame.RESIZABLE | pygame.SHOWN
screen = pygame.display.set_mode((screen_width, screen_height))
pygame.display.set_caption("RALR")
pygame.event.pump()

# REMOVED: adjust_window_position() and related Windows-specific nudges
# ...existing code...

# NEW: helper to re-show window if minimized
def ensure_window_shown():
    try:
        if not pygame.display.get_active():
            pygame.display.set_mode((screen_width, screen_height), WINDOW_FLAGS)
            pygame.display.set_caption("RALR")
    except Exception:
        pass

# NEW: load background image safely (was missing -> NameError / not loading)
try:
    scree_image = pygame.image.load(r"Textures\screen_background.png").convert()
except Exception:
    try:
        scree_image = pygame.image.load(r"Textures/screen_background.png").convert()
    except Exception:
        try:
            scree_image = pygame.image.load(r"Textures\screen1.png").convert()
        except Exception:
            try:
                scree_image = pygame.image.load(r"Textures/screen1.png").convert()
            except Exception:
                # Final fallback: simple gradient placeholder
                scree_image = pygame.Surface((800, 600))
                for y in range(scree_image.get_height()):
                    c = 30 + int(70 * (y / scree_image.get_height()))
                    pygame.draw.line(scree_image, (c, c, c), (0, y), (scree_image.get_width(), y))

# compute tile size (integer) so a TARGET_COLS x TARGET_ROWS grid fits the window
def compute_tile_size(w, h, cols=None, rows=None):
    # SAFE: do not assume 'world' exists yet
    if cols is None or rows is None:
        try:
            cols = world.cols
            rows = world.rows
        except Exception:
            cols = TARGET_COLS
            rows = TARGET_ROWS
    return max(8, min(w // cols, h // rows))

def get_viewport_layout(w, h):
    ts = compute_tile_size(w, h, TARGET_COLS, TARGET_ROWS)
    content_w = ts * TARGET_COLS
    content_h = ts * TARGET_ROWS
    off_x = (w - content_w) // 2
    off_y = (h - content_h) // 2
    return ts, off_x, off_y, content_w, content_h

# NEW: center the rendered grid based on actual world size (rounded to avoid 1px bias)
def update_viewport_offsets():
    global viewport_off_x, viewport_off_y, screen_width, screen_height
    try:
        surf = pygame.display.get_surface()
        if surf:
            screen_w, screen_h = surf.get_size()
        else:
            screen_w, screen_h = screen_width, screen_height

        # Use actual pixel dimensions of the world
        content_w = world.pixel_width
        content_h = world.pixel_height

        off_x = int(round((screen_w - content_w) / 2.0))
        off_y = int(round((screen_h - content_h) / 2.0))

        # Clamp offsets so content is not fully pushed off-screen.
        # If content is smaller than screen, keep offsets between 0..max_off (centered region).
        # If content is larger, allow negative offsets but not beyond the max negative (so at least part remains visible).
        max_off_x = screen_w - content_w
        max_off_y = screen_h - content_h

        if max_off_x >= 0:
            off_x = min(max(off_x, 0), max_off_x)
        else:
            off_x = max(max_off_x, off_x)

        if max_off_y >= 0:
            off_y = min(max(off_y, 0), max_off_y)
        else:
            off_y = max(max_off_y, off_y)

        viewport_off_x = off_x
        viewport_off_y = off_y
        # propagate to world for draw calls
        world.viewport_off_x = viewport_off_x
        world.viewport_off_y = viewport_off_y
    except Exception:
        viewport_off_x = 0
        viewport_off_y = 0

# UI font
font = pygame.font.SysFont(None, 30)
big_menu_font = pygame.font.SysFont(None, 72)
small_menu_font = pygame.font.SysFont(None, 32)

# NEW: pause button rect helper (used in game loop)
def get_pause_button_rect(screen_w, screen_h):
    """Return rect for the in‑game pause button in top‑right corner."""
    w, h = 110, 36
    x = screen_w - w - 12
    y = 10
    return pygame.Rect(x, y, w, h)

# -------------------------------------------------------------------
# MENU HELPERS
# -------------------------------------------------------------------
def apply_volume():
    """Apply current master_volume / mute_audio to mixer (music + key SFX classes)."""
    global master_volume, mute_audio
    if not pygame.mixer.get_init():
        return

    effective_volume = 0.0 if mute_audio or master_volume <= 0.0 else master_volume

    # Music volume (skip if ducked for priority SFX)
    try:
        if not _music_ducked:
            pygame.mixer.music.set_volume(effective_volume)
    except Exception:
        pass

    # Registered generic sounds
    for snd in list(REGISTERED_SOUNDS):
        try:
            snd.set_volume(effective_volume)
        except Exception:
            try:
                REGISTERED_SOUNDS.remove(snd)
            except Exception:
                pass

    # projectile SFX
    try:
        from Player.Player import Projectile as _PProj
        if hasattr(_PProj, "apply_master_volume"):
            _PProj.apply_master_volume(effective_volume)
    except Exception:
        pass
    try:
        from Bosses.Boss3 import BossProjectile as _B3Proj
        if hasattr(_B3Proj, "apply_master_volume"):
            _B3Proj.apply_master_volume(effective_volume)
    except Exception:
        pass
    try:
        from Bosses.Boss5 import Boss5Projectile as _B5Proj
        if hasattr(_B5Proj, "apply_master_volume"):
            _B5Proj.apply_master_volume(effective_volume)
    except Exception:
        pass

    # Player damage sound volume
    try:
        from Player.Player import Player as _P
        if isinstance(getattr(_P, "damage_sound", None), pygame.mixer.Sound):
            _P.damage_sound.set_volume(effective_volume)
    except Exception:
        pass

def draw_button(screen, rect, text, font,
                base_color=(80, 80, 80),
                hover_color=(120, 120, 120),
                text_color=(255, 255, 255)):
    # NOTE: keep this single definition; remove any duplicate later in file.
    mouse_pos = pygame.mouse.get_pos()
    is_hover = rect.collidepoint(mouse_pos)
    color = hover_color if is_hover else base_color
    pygame.draw.rect(screen, color, rect)
    pygame.draw.rect(screen, (0, 0, 0), rect, 2)
    label = font.render(text, True, text_color)
    lx = rect.x + rect.width // 2 - label.get_width() // 2
    ly = rect.y + rect.height // 2 - label.get_height() // 2
    screen.blit(label, (lx, ly))
    return is_hover

def draw_slider(screen, x, y, w, h, value):
    """Simple horizontal slider 0..1, returns (track_rect, knob_rect)."""
    # track
    track_rect = pygame.Rect(x, y + h // 2 - 4, w, 8)
    pygame.draw.rect(screen, (60, 60, 60), track_rect)
    pygame.draw.rect(screen, (0, 0, 0), track_rect, 1)
    # knob
    knob_x = x + int(round(w * value))
    knob_rect = pygame.Rect(knob_x - 6, y, 12, h)
    pygame.draw.rect(screen, (180, 180, 40), knob_rect)
    pygame.draw.rect(screen, (0, 0, 0), knob_rect, 1)
    return track_rect, knob_rect

def run_level_select_menu():
    """Level selector 1..25; returns selected level or None if canceled."""
    global screen_width, screen_height
    selecting = True
    selected_level = None
    cols = 5
    rows = 5
    margin = 20
    btn_w = 90
    btn_h = 50
    while selecting:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
            elif event.type == pygame.VIDEORESIZE:
                # CHANGED: only resize window/background here; do NOT rebuild world/level yet
                new_w, new_h = clamp_window_size(event.w, event.h)
                screen_width, screen_height = new_w, new_h
                pygame.display.set_mode((screen_width, screen_height), WINDOW_FLAGS)
                background_cache["size"] = None
            elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
                mx, my = event.pos
                # check which level button was clicked
                for i in range(25):
                    r, c = divmod(i, cols)
                    x = screen_width // 2 - (cols * (btn_w + margin) - margin) // 2 + c * (btn_w + margin)
                    y = screen_height // 2 - (rows * (btn_h + margin) - margin) // 2 + r * (btn_h + margin)
                    rect = pygame.Rect(x, y, btn_w, btn_h)
                    if rect.collidepoint(mx, my):
                        selected_level = i + 1
                        selecting = False
                        break
                # small back button
                back_rect = pygame.Rect(20, screen_height - 70, 120, 40)
                if back_rect.collidepoint(mx, my):
                    selecting = False
                if not selecting:
                    break

        # draw background + buttons
        if background_cache["size"] != (screen_width, screen_height):
            background_cache["size"] = (screen_width, screen_height)
            background_cache["image"] = pygame.transform.smoothscale(
                scree_image, (screen_width, screen_height)
            )
        screen.blit(background_cache["image"], (0, 0))
        title = big_menu_font.render("Select Level", True, (255, 255, 255))
        screen.blit(title, (screen_width // 2 - title.get_width() // 2, 60))

        # draw level buttons
        for i in range(25):
            r, c = divmod(i, cols)
            x = screen_width // 2 - (cols * (btn_w + margin) - margin) // 2 + c * (btn_w + margin)
            y = screen_height // 2 - (rows * (btn_h + margin) - margin) // 2 + r * (btn_h + margin)
            rect = pygame.Rect(x, y, btn_w, btn_h)
            draw_button(screen, rect, str(i + 1), small_menu_font)

        back_rect = pygame.Rect(20, screen_height - 70, 120, 40)
        draw_button(screen, back_rect, "Back", small_menu_font)

        pygame.display.update()
        clock.tick(fps)

    return selected_level

# -------------------------------------------------------------------
# WORLD / PLAYER CREATION
# -------------------------------------------------------------------

# NEW: define levels table and initial level/data BEFORE using data
levels = {
    1: Level1,
    2: Level2,
    3: Level3,
    4: Level4,
    5: Level5,
    6: Level6,
    7: Level7,
    8: Level8,
    9: Level9,
    10: Level10,
    11: Level11,
    12: Level12,
    13: Level13,
    14: Level14,
    15: Level15,
    16: Level16,
    17: Level17,
    18: Level18,
    19: Level19,
    20: Level20,
    21: Level21,
    22: Level22,
    23: Level23,
    24: Level24,
    25: Level25,
}
level = 1
data = levels[level].data_svet

# compute initial tile size using actual level grid (now data is defined)
velkost_plochy = compute_tile_size(screen_width, screen_height, cols=len(data[0]), rows=len(data))

class World:
    def __init__(self, data, current_level, tile_size):
        self.tile_list = []
        self.enemies = pygame.sprite.Group()
        # NEW: player spawn stored in logic-space (pixels)
        self.player_spawn = None
        # NEW: list of door rects (unscaled, logic-space)
        self.door_list = []
        self.current_level = current_level
        self.boss = None
        self.boss_door_locked = False
        self.boss_door_info = None
        self.boss1 = None
        self.boss1_door_spawned = False
        self.boss2 = None
        self.boss2_door_spawned = False
        self.boss4 = None
        self.boss4_door_spawned = False
        self.boss5 = None            # NEW: worm boss reference
        self.boss5_door_spawned = False  # NEW: track if Boss5 door already spawned

        self.hazard_list = []  # list[pygame.Rect] – each entry is a hitbox rect
        # (img, rect, col, row) when locked

        # replace direct loads with two-attempt tries
        try:
            block1 = pygame.image.load(r"Textures\ground_block_normal.png").convert_alpha()
        except Exception:
            try:
                block1 = pygame.image.load("Textures/ground_block_normal.png").convert_alpha()
            except Exception:
                block1 = pygame.Surface((tile_size, tile_size))
        try:
            block2 = pygame.image.load(r"Textures\ground_block_cracked.png").convert_alpha()
        except Exception:
            try:
                block2 = pygame.image.load("Textures/ground_block_cracked.png").convert_alpha()
            except Exception:
                block2 = block1
        try:
            block3 = pygame.image.load(r"Textures\ground_block_cracked2.png").convert_alpha()
        except Exception:
            try:
                block3 = pygame.image.load("Textures/ground_block_cracked2.png").convert_alpha()
            except Exception:
                block3 = block1
        try:
            block4 = pygame.image.load(r"Textures\ground_block_hole.png").convert_alpha()
        except Exception:
            try:
                block4 = pygame.image.load("Textures/ground_block_hole.png").convert_alpha()
            except Exception:
                block4 = block1
        try:
            block5 = pygame.image.load(r"Textures\ground_block_hole2.png").convert_alpha()
        except Exception:
            try:
                block5 = pygame.image.load("Textures/ground_block_hole2.png").convert_alpha()
            except Exception:
                block5 = block1
        try:
            block6 = pygame.image.load(r"Textures\ground_block_hole3.png").convert_alpha()
        except Exception:
            try:
                block6 = pygame.image.load("Textures/ground_block_hole3.png").convert_alpha()
            except Exception:
                block6 = block1

        try:
            spike_tall = pygame.image.load(r"Textures\spike_tall.png").convert_alpha()
        except Exception:
            try:
                spike_tall = pygame.image.load(r"Textures/spike_tall.png").convert_alpha()
            except Exception:
                spike_tall = None

        try:
            door = pygame.image.load(r"Textures\door.png").convert_alpha()
        except Exception:
            try:
                door = pygame.image.load(r"Textures/door.png").convert_alpha()
            except Exception:
                door = None
        # self.door_texture = door   # NEW: keep door texture for dynamic spawn
        # FIX: always have a visible fallback door texture
        if door is None:
            door = pygame.Surface((tile_size, tile_size * 2), pygame.SRCALPHA)
            door.fill((0, 80, 200))
            pygame.draw.rect(door, (255, 255, 255), door.get_rect(), 2)
        self.door_texture = door

        try:
            spike_short = pygame.image.load(r"Textures\spike_short.png").convert_alpha()
        except Exception:
            try:
                spike_short = pygame.image.load(r"Textures/spike_short.png").convert_alpha()
            except Exception:
                spike_short = None
        
        try:
            pistol_enemy = pygame.image.load(r"Textures\enemy_pistol.png").convert_alpha()
        except Exception:
            try:
                pistol_enemy = pygame.image.load(r"Textures/enemy_pistol.png").convert_alpha()
            except Exception:
                pistol_enemy = None

        row_count = 0
        for row in data:
            a = 0
            for tile in row:
                if tile == 1:
                    # store ORIGINAL tile surface (not pre-scaled) + logic-space rect
                    block = random.choice((block1, block2, block3, block4, block5, block6))
                    img = block  # keep raw
                    img_rect = pygame.Rect(a * tile_size, row_count * tile_size, tile_size, tile_size)
                    self.tile_list.append((img, img_rect))

                elif tile == 4:
                    img = pistol_enemy
                    x = a * velkost_plochy
                    y = row_count * velkost_plochy  # FIX
                    self.enemies.add(Shooting_enemie(x, y, speed=2, patrol_blocks=2, image_path=r"Textures\enemy_pistol.png",tile_size = tile_size))

                elif tile == 5:
                    x = a * velkost_plochy
                    y = row_count * velkost_plochy  # FIX
                    self.enemies.add(Turret(x, y, speed=0, patrol_blocks=0, image_path=r"Textures\Turret.png",tile_size=tile_size))
                
                elif tile == 55:
                    x = a * velkost_plochy
                    y = row_count * velkost_plochy  # FIX
                    self.enemies.add(SlowTurret(x, y, speed=0, patrol_blocks=0, image_path=r"Textures\Turret.png",tile_size=tile_size))

                elif tile == 6:
                    img = spike_short
                    img_rect = pygame.Rect(a * tile_size, row_count * tile_size, tile_size, tile_size)
                    self.tile_list.append((img, img_rect))
                    # base 50% height hitbox centered vertically
                    base_h = tile_size // 2
                    base_y = row_count * tile_size + (tile_size - base_h) // 2
                    # move hitbox 10px up and grow height by 10px
                    hit_y = base_y - 10
                    hit_h = base_h + 10
                    # clamp to stay inside the tile bounds
                    tile_top = row_count * tile_size
                    tile_bottom = tile_top + tile_size
                    if hit_y < tile_top:
                        # shift height down so bottom stays inside tile
                        overflow = tile_top - hit_y
                        hit_y = tile_top
                        hit_h = min(tile_size, hit_h - overflow)
                    if hit_y + hit_h > tile_bottom:
                        hit_h = tile_bottom - hit_y
                    hit_rect = pygame.Rect(a * tile_size, hit_y, tile_size, hit_h)
                    self.hazard_list.append(hit_rect)

                elif tile == 7:
                    img = spike_tall
                    img_rect = pygame.Rect(a * tile_size, row_count * tile_size, tile_size, tile_size)
                    self.tile_list.append((img, img_rect))
                    # tall spike: full-tile rect as hazard
                    self.hazard_list.append(img_rect)

                elif tile == 9:
                    img = door
                    # Make door visual span 2 tiles high, bottom-aligned to the tile cell
                    # img_rect.top = row_count*tile_size - tile_size, height = 2*tile_size
                    img_rect = pygame.Rect(a * tile_size, row_count * tile_size - tile_size, tile_size, tile_size*2)

                    if self.current_level == 15:
                        # LOCK door until boss defeated; store tile coords so unlock can add proper crects
                        self.boss_door_locked = True
                        self.boss_door_info = (img, img_rect, a, row_count)
                    else:
                        # Add visual only; *do not* add to collision_rects so player isn't blocked
                        self.tile_list.append((img, img_rect))
                        # Logic door rects go only into door_list (used for detecting entering)
                        top_row = max(0, (img_rect.y) // tile_size)
                        bottom_row = (img_rect.y + img_rect.height - 1) // tile_size
                        for rr in range(top_row, bottom_row + 1):
                            crect = pygame.Rect(a * tile_size, rr * tile_size, tile_size, tile_size)
                            self.door_list.append(crect)

                elif tile == 99:
                    # PLAYER SPAWN MARKER: record logic-space position but do not add an actual tile
                    spawn_x = a * tile_size
                    spawn_y = row_count * tile_size
                    # Store top-left of the spawn tile in pixels.
                    self.player_spawn = (spawn_x, spawn_y)
                    # leave the cell empty visually / collision-wise

                elif tile == 10:
                    x = a * tile_size
                    y = row_count * tile_size
                    # pass tile_size to Enemy so patrol distances align
                    self.enemies.add(Enemy(x, y, speed=2, patrol_blocks=2, image_path=r"Textures\enemy.png", tile_size=tile_size))
                elif tile == 11:
                    x = a * tile_size
                    y = row_count * tile_size
                    self.enemies.add(JumpingEnemy(x, y, speed=3, patrol_blocks=15, image_path=r"Textures\crawler2.png", tile_size=tile_size))
                elif tile == 12:
                    x = a * tile_size
                    y = row_count * tile_size
                    self.enemies.add(JumpingEnemy(x, y, speed=2, patrol_blocks=2, image_path=r"Textures\crawler2.png", tile_size=tile_size))

                # NEW: Boss1 spawn tile - only active on level 5
                elif tile == 21 and self.current_level == 5:
                    # Place Boss1 centered on this tile; Boss1 will resize to 3x3 tiles itself
                    boss_x = a * tile_size + tile_size // 2
                    boss_y = row_count * tile_size + tile_size // 2
                    b1 = Boss1(boss_x, boss_y, hp=50)
                    self.enemies.add(b1)
                    self.boss1 = b1  # NEW: store reference

                # NEW: Boss2 spawn tile - only active on level 10
                elif tile == 22 and self.current_level == 10:
                    # Place Boss2 centered on this tile; Boss2 will resize to 2x2 tiles itself
                    boss_x = a * tile_size + tile_size // 2
                    boss_y = row_count * tile_size + tile_size // 2
                    b2 = Boss2(boss_x, boss_y, hp=25)
                    self.enemies.add(b2)
                    self.boss2 = b2  # NEW: store reference

                # NEW: Boss4 spawn tile – only on level 20 when tile == 24
                elif tile == 24 and self.current_level == 20:
                    boss_x = a * tile_size + tile_size // 2
                    boss_y = row_count * tile_size + tile_size // 2
                    b4 = Boss4(boss_x, boss_y, hp=60, projectile_group=None)
                    self.enemies.add(b4)
                    self.boss4 = b4

                # NEW: Boss5 spawn tile – only on level 25, tile == 25
                elif tile == 25 and self.current_level == 25:
                    boss_x = a * tile_size + tile_size // 2
                    boss_y = row_count * tile_size + tile_size // 2
                    b5 = Boss5(boss_x, boss_y, hp=30, segment_count=25,
                               projectile_group=None, tile_size=tile_size)
                    self.enemies.add(b5)
                    self.boss5 = b5

                a += 1
            row_count += 1

        self.cols = len(data[0]) if data else 0
        self.rows = len(data)
        self.pixel_width = self.cols * tile_size
        self.pixel_height = self.rows * tile_size
        self.tile_size = tile_size

        # NEW: precompute which (col,row) cells are small spikes (tile 6)
        spike6_cells = set()
        r_i = 0
        for row in data:
            c_i = 0
            for t in row:
                if t == 6:
                    spike6_cells.add((c_i, r_i))
                c_i += 1
            r_i += 1

        # NEW: collision rects used for physics/collision checks (integer-aligned)
        self.collision_rects = []
        for img, rect in self.tile_list:
            col = rect.x // tile_size
            top_row = rect.y // tile_size
            bottom_row = (rect.y + rect.height - 1) // tile_size
            for r in range(max(0, top_row), min(self.rows - 1, bottom_row) + 1):
                # SKIP door rectangles here; only solid ground should be in collision_rects
                # Door visuals are still in tile_list, but their logical rects live in door_list.
                # A simple way: if this (col,row) cell belongs to a door, don't add to collision_rects.
                door_here = False
                for drect in self.door_list:
                    if drect.collidepoint(col * tile_size + 1, r * tile_size + 1):
                        door_here = True
                        break
                if door_here:
                    continue
                # NEW: skip small spike tiles (tile == 6) so you can walk through them
                if (col, r) in spike6_cells:
                    continue
                crect = pygame.Rect(col * tile_size, r * tile_size, tile_size, tile_size)
                self.collision_rects.append(crect)

        # NEW: current scale and viewport offsets for drawing
        self.scale = 1.0
        self.viewport_off_x = 0
        self.viewport_off_y = 0
        # NEW: cache of scaled tiles for current scale
        self._tile_cache = {"scale": None, "surfaces": []}

    def _rebuild_tile_cache(self, scale):
        # Rebuild all scaled tiles once per scale; use grid-aligned positions
        self._tile_cache["scale"] = scale
        self._tile_cache["surfaces"] = []
        # rounded, uniform tile size -> no seams/gaps when scaling
        s = max(1, int(round(self.tile_size * scale)))  # scaled tile px
        for img, rect in self.tile_list:
            # compute grid coordinates and any vertical offset relative to the tile
            col = rect.x // self.tile_size
            row = rect.y // self.tile_size
            offset_y = rect.y - (row * self.tile_size)
            # scale offset proportionally so door/offset visuals are preserved
            offset_y_scaled = int(round((offset_y / max(1, self.tile_size)) * s))
            sx = col * s
            sy = row * s + offset_y_scaled
            # scale the ORIGINAL image to the scaled tile size once
            srf = pygame.transform.smoothscale(img, (s, rect.height // self.tile_size * s)) if rect.height != self.tile_size else pygame.transform.smoothscale(img, (s, s))
            self._tile_cache["surfaces"].append((srf, (sx, sy)))

    def draw(self, scale, off_x=0, off_y=0):
        # scale assumed 1 (tiles already sized); apply offsets
        self.scale = scale
        if self._tile_cache["scale"] != scale:
            self._rebuild_tile_cache(scale)
        screen = pygame.display.get_surface()
        for srf, (sx, sy) in self._tile_cache["surfaces"]:
            screen.blit(srf, (sx + off_x, sy + off_y))
        for enemy in self.enemies:
            try:
                enemy.draw(screen, scale, off_x, off_y)
            except TypeError:
                # fallback if draw signature old
                ex = int(round(enemy.rect.x * scale)) + off_x
                ey = int(round(enemy.rect.y * scale)) + off_y
                ei = pygame.transform.smoothscale(enemy.image, (
                    max(1, int(round(enemy.image.get_width() * scale))),
                    max(1, int(round(enemy.image.get_height() * scale))),
                ))
                screen.blit(ei, (ex, ey))

    def unlock_boss_door(self):
        # NEW: make hidden door appear (only once)
        if self.boss_door_locked and self.boss_door_info:
            img, rect, col_idx, row_idx = self.boss_door_info
            # append visual
            self.tile_list.append((img, rect))
            # add door rects ONLY to door_list, NOT to collision_rects
            col = rect.x // self.tile_size
            top_row = rect.y // self.tile_size
            bottom_row = (rect.y + rect.height - 1) // self.tile_size
            for rr in range(max(0, top_row), min(self.rows - 1, bottom_row) + 1):
                crect = pygame.Rect(col * self.tile_size, rr * self.tile_size, self.tile_size, self.tile_size)
                # ADD to door_list so player can enter
                self.door_list.append(crect)
                # DO NOT append to collision_rects -> door does not block movement
            self.boss_door_locked = False
            self._tile_cache["scale"] = None

# --- CREATE INITIAL WORLD INSTANCE HERE (fixes NameError on `world`) ---
world = World(data, level, velkost_plochy)

# NEW: compute initial viewport offsets so the level is centered correctly
update_viewport_offsets()

# Create required sprite groups (empty placeholders for now)
groups = {
    'slimak': pygame.sprite.Group(),
    'pheonix': pygame.sprite.Group(),
    'spike': pygame.sprite.Group(),
    'jumping_slime': pygame.sprite.Group(),
    'lava': pygame.sprite.Group(),
    'door': pygame.sprite.Group(),
    'projectILES'.lower(): pygame.sprite.Group(),  # this becomes 'projectiles'
}
# Normalize to explicit keys for clarity
groups['projectiles'] = groups.pop('projectILES'.lower())
groups['enemies'] = world.enemies

# group for boss projectiles
groups['boss_projectiles'] = pygame.sprite.Group()

# If starting directly on level 4, spawn the boss (AFTER groups exist)
if level == 15:
    try:
        boss_x, boss_y = getattr(Level15, "boss_coords", (world.pixel_width // 2, world.pixel_height // 2))
    except Exception:
        boss_x, boss_y = (world.pixel_width // 2, world.pixel_height // 2)
    b = Boss3(boss_x, boss_y, hp=20, projectile_group=groups['boss_projectiles'])
    world.enemies.add(b)
    world.boss = b
    # no need to set b.projectile_group separately now

# NEW: if we ever start on level 20 and Boss4 exists, wire its projectile_group
if level == 20 and getattr(world, "boss4", None) is not None:
    try:
        world.boss4.projectile_group = groups['boss_projectiles']
    except Exception:
        pass

# NEW: if we start directly on level 25 and Boss5 exists, wire its projectile group
if level == 25 and getattr(world, "boss5", None) is not None:
    try:
        world.boss5.projectile_group = groups['boss_projectiles']
    except Exception:
        pass

# Create the player (use world's player_spawn or fallback)
start_spawn = getattr(world, "player_spawn", None)
if start_spawn:
    player = Player(start_spawn[0], start_spawn[1], velkost_plochy)
else:
    # default bottom-left-ish spawn scaled by tile size
    default_x = int(round(1 * velkost_plochy))
    default_y = int(round((TARGET_ROWS - 2) * velkost_plochy) - int(velkost_plochy * 0.2))
    player = Player(default_x, default_y, velkost_plochy)

run = True
# cache for scaled bg (must exist before menus and run_level_select_menu)
background_cache = {"size": None, "image": None}
# helper to rebuild world and groups when window size or fullscreen toggles
def rebuild_world_for_window(new_w, new_h, fullscreen_flag):
    # Replace previous tile_size recompute with layout
    global screen, screen_width, screen_height, velkost_plochy, world, groups, player
    prev_tile_size = velkost_plochy
    # capture current tile under player (feet) with old size
    try:
        px = player.rect.centerx
        py = player.rect.bottom - 1
        keep_tile = (px // prev_tile_size, py // prev_tile_size)
    except Exception:
        keep_tile = None

    # CLAMP requested new size so title bar remains visible
    new_w, new_h = clamp_window_size(new_w, new_h)
    screen_width, screen_height = new_w, new_h

    # SAFE: compute with current world's cols/rows (world exists now)
    velkost_plochy = compute_tile_size(screen_width, screen_height)
    data_local = levels[level].data_svet
    world = World(data_local, level, velkost_plochy)
    groups['enemies'] = world.enemies
    groups['projectiles'] = pygame.sprite.Group()
    groups['boss_projectiles'] = pygame.sprite.Group()
    if level == 15:
        coords = getattr(Level15, "boss_coords", (world.pixel_width // 2, world.pixel_height // 2))
        b = Boss3(coords[0], coords[1], hp=20, projectile_group=groups['boss_projectiles'])
        world.enemies.add(b)
        world.boss = b
    # Ensure the set_mode uses the clamped sizes
    screen = pygame.display.set_mode((screen_width, screen_height), WINDOW_FLAGS)
    update_viewport_offsets()  # keep centered after rebuild
    ensure_window_shown()  # keep ensuring window shown after rebuild

# NEW: reset everything back to level 1 start
def reset_game_to_start():
    global level, data, world, groups, player, velkost_plochy
    # restart from level 1
    level = 1
    data = levels[level].data_svet

    # recompute tile size for new level grid
    velkost_plochy = compute_tile_size(screen_width, screen_height, cols=len(data[0]), rows=len(data))

    # rebuild world for level 1
    world = World(data, level, velkost_plochy)

    # (re)create dynamic groups
    groups['enemies'] = world.enemies
    groups['projectiles'] = pygame.sprite.Group()
    groups['boss_projectiles'] = pygame.sprite.Group()

    # wire bosses for current level (same logic as on level change)
    if level == 15:
        coords = getattr(Level15, "boss_coords", (world.pixel_width // 2, world.pixel_height // 2))
        boss_x, boss_y = coords
        b = Boss3(boss_x, boss_y, hp=20, projectile_group=groups['boss_projectiles'])
        world.enemies.add(b)
        world.boss = b
    if level == 20 and getattr(world, "boss4", None) is not None:
        try:
            world.boss4.projectile_group = groups['boss_projectiles']
        except Exception:
            pass
    if level == 25 and getattr(world, "boss5", None) is not None:
        try:
            world.boss5.projectile_group = groups['boss_projectiles']
        except Exception:
            pass

    # recreate player at new world's spawn
    start_spawn = getattr(world, "player_spawn", None)
    if start_spawn:
        player = Player(start_spawn[0], start_spawn[1], velkost_plochy)
    else:
        player_x = int(round(1 * velkost_plochy))
        player_y = int(round((TARGET_ROWS - 2) * velkost_plochy) - int(velkost_plochy * 0.2))
        player = Player(player_x, player_y, velkost_plochy)

    # update viewport offsets to center new world
    update_viewport_offsets()
    # NEW: make sure correct music is playing for new level
    ensure_audio_ready()
    play_music_for_level(level)

# NEW/EXISTING: track end screen state; None | "lost" | "won"
end_state = None

# -------------------------------------------------------------------
# SLIDESHOW HELPERS
# -------------------------------------------------------------------
# NEW: comic-style viewer — shows images from Images/ and requires clicking Next to advance.
def show_intro_slideshow(image_paths=None, final_label=None, final_action='start'):
    """
    Show a sequence of images that the user advances with Next/Skip.

    Returns a string indicating how the slideshow ended:
      - 'start' if the final button (default "Start Game") was used
      - 'menu' if a caller provided final_action='menu' and final button was used
      - 'skip' if the user pressed Skip
      - 'quit' if the user closed the window

    Parameters:
      image_paths: list of image paths (defaults to image_1..image_6)
      final_label: override label for the final button (defaults to "Start Game")
      final_action: action string to return when final button clicked (default 'start')
    """
    global screen_width, screen_height, screen
    if image_paths is None:
        image_paths = [r"Images\\image_1.png", r"Images\\image_2.png", r"Images\\image_3.png",
                       r"Images\\image_4.png", r"Images\\image_5.png", r"Images\\image_6.png"]

    if final_label is None:
        final_label = "Start Game"

    # Load images (skip missing)
    images = []
    for p in image_paths:
        try:
            img = pygame.image.load(p).convert()
            images.append(img)
        except Exception:
            try:
                img = pygame.image.load(p.replace('\\', '/')).convert()
                images.append(img)
            except Exception:
                continue

    if not images:
        return None

    idx = 0
    while idx < len(images):
        img = images[idx]
        # scale to fit while keeping aspect ratio
        sw, sh = screen_width, screen_height
        iw, ih = img.get_size()
        scale = min(sw / iw, sh / ih)
        new_w = max(1, int(iw * scale))
        new_h = max(1, int(ih * scale))
        surf = pygame.transform.smoothscale(img, (new_w, new_h))

        waiting = True
        while waiting:
            for ev in pygame.event.get():
                if ev.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()
                    return 'quit'
                elif ev.type == pygame.VIDEORESIZE:
                    try:
                        new_wi, new_hi = clamp_window_size(ev.w, ev.h)
                        screen_width, screen_height = new_wi, new_hi
                        pygame.display.set_mode((screen_width, screen_height), WINDOW_FLAGS)
                        # break inner loop to recompute scaled surface
                        waiting = False
                        break
                    except Exception:
                        pass
                elif ev.type == pygame.MOUSEBUTTONDOWN and ev.button == 1:
                    mx, my = ev.pos
                    btn_w, btn_h = 160, 48
                    padding = 16
                    next_rect = pygame.Rect(screen_width - btn_w - padding, screen_height - btn_h - padding, btn_w,
                                            btn_h)
                    skip_rect = pygame.Rect(padding, screen_height - btn_h - padding, btn_w, btn_h)
                    if next_rect.collidepoint(mx, my):
                        # final image -> return the requested final_action
                        if idx == len(images) - 1:
                            return final_action
                        idx += 1
                        waiting = False
                        break
                    if skip_rect.collidepoint(mx, my):
                        return 'skip'
                elif ev.type == pygame.KEYDOWN:
                    if ev.key in (pygame.K_RETURN, pygame.K_SPACE):
                        if idx == len(images) - 1:
                            return final_action
                        idx += 1
                        waiting = False
                        break
                    elif ev.key == pygame.K_ESCAPE:
                        return 'skip'

            # draw background (use existing background image if available)
            if background_cache.get("image"):
                try:
                    screen.blit(background_cache["image"], (0, 0))
                except Exception:
                    screen.fill((0, 0, 0))
            else:
                screen.fill((0, 0, 0))

            # draw image centered
            x = (screen_width - new_w) // 2
            y = (screen_height - new_h) // 2
            screen.blit(surf, (x, y))

            # draw UI buttons
            btn_w, btn_h = 160, 48
            padding = 16
            next_rect = pygame.Rect(screen_width - btn_w - padding, screen_height - btn_h - padding, btn_w, btn_h)
            skip_rect = pygame.Rect(padding, screen_height - btn_h - padding, btn_w, btn_h)
            # last image shows custom final_label
            lbl = final_label if idx == len(images) - 1 else "Next"
            draw_button(screen, skip_rect, "Skip", small_menu_font)
            draw_button(screen, next_rect, lbl, small_menu_font)

            pygame.display.update()
            clock.tick(fps)
    return None


# -------------------------------------------------------------------
# BOSS IMAGE OVERLAY (show when boss is killed)
# -------------------------------------------------------------------
# Load Boss1 image once (fall back to None if missing)
try:
    boss1_image = pygame.image.load(r"Images\\Boss_1.png").convert_alpha()
except Exception:
    try:
        boss1_image = pygame.image.load(r"Images/Boss_1.png").convert_alpha()
    except Exception:
        boss1_image = None

try:
    boss2_image = pygame.image.load(r"Images\\Boss_2.png").convert_alpha()
except Exception:
    try:
        boss2_image = pygame.image.load(r"Images/Boss_2.png").convert_alpha()
    except Exception:
        boss2_image = None

try:
    boss3_image = pygame.image.load(r"Images\\Boss_3.png").convert_alpha()
except Exception:
    try:
        boss3_image = pygame.image.load(r"Images/Boss_3.png").convert_alpha()
    except Exception:
        boss3_image = None

try:
    boss4_image = pygame.image.load(r"Images\\Boss_4.png").convert_alpha()
except Exception:
    try:
        boss4_image = pygame.image.load(r"Images/Boss_4.png").convert_alpha()
    except Exception:
        boss4_image = None

try:
    boss5_image = pygame.image.load(r"Images\\Boss_5.png").convert_alpha()
except Exception:
    try:
        boss5_image = pygame.image.load(r"Images/Boss_5.png").convert_alpha()
    except Exception:
        boss5_image = None

# Load overlay images for other milestone floors from Images/ (use Images/image_2..image_5)
overlay_images = {}
_overlay_paths = {
    5: r"Images\\Boss_1.png",
    10: r"Images\\Boss_2.png",
    15: r"Images\\Boss_3.png",
    20: r"Images\\Boss_4.png",
    25: r"Images\\Boss_5.png",
}
for lvl, p in _overlay_paths.items():
    try:
        overlay_images[lvl] = pygame.image.load(p).convert()
    except Exception:
        try:
            overlay_images[lvl] = pygame.image.load(p.replace('\\', '/')).convert()
        except Exception:
            overlay_images[lvl] = None


# Helper to show a single image fullscreen with a clickable Continue button (blocks until clicked)
def show_image_overlay(img_surf, button_label="Continue"):
    global screen_width, screen_height, screen
    if img_surf is None:
        return
    # scale preserving aspect
    iw, ih = img_surf.get_size()
    sw, sh = screen.get_size()
    scale = min(sw / iw, sh / ih)
    new_w = max(1, int(iw * scale))
    new_h = max(1, int(ih * scale))
    surf = pygame.transform.smoothscale(img_surf, (new_w, new_h))

    btn_w, btn_h = 180, 52
    padding = 20
    waiting = True
    while waiting:
        for ev in pygame.event.get():
            if ev.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
            elif ev.type == pygame.VIDEORESIZE:
                try:
                    nw, nh = clamp_window_size(ev.w, ev.h)
                    screen_width, screen_height = nw, nh
                    pygame.display.set_mode((screen_width, screen_height), WINDOW_FLAGS)
                    # recompute scaled surf
                    sw2, sh2 = screen.get_size()
                    scale2 = min(sw2 / iw, sh2 / ih)
                    new_w = max(1, int(iw * scale2))
                    new_h = max(1, int(ih * scale2))
                    surf = pygame.transform.smoothscale(img_surf, (new_w, new_h))
                except Exception:
                    pass
            elif ev.type == pygame.MOUSEBUTTONDOWN and ev.button == 1:
                mx, my = ev.pos
                next_rect = pygame.Rect(screen_width - btn_w - padding, screen_height - btn_h - padding, btn_w, btn_h)
                skip_rect = pygame.Rect(padding, screen_height - btn_h - padding, btn_w, btn_h)
                if next_rect.collidepoint(mx, my):
                    waiting = False
                    break
                if skip_rect.collidepoint(mx, my):
                    waiting = False
                    break
            elif ev.type == pygame.KEYDOWN:
                # Enter/Space also continue
                if ev.key in (pygame.K_RETURN, pygame.K_SPACE, pygame.K_ESCAPE):
                    waiting = False
                    break

        # draw overlay
        overlay = pygame.Surface((screen_width, screen_height))
        overlay.fill((0, 0, 0))
        overlay.set_alpha(200)
        screen.blit(overlay, (0, 0))

        # draw image centered
        x = (screen_width - new_w) // 2
        y = (screen_height - new_h) // 2
        screen.blit(surf, (x, y))

        # draw button
        next_rect = pygame.Rect(screen_width - btn_w - padding, screen_height - btn_h - padding, btn_w, btn_h)
        draw_button(screen, next_rect, button_label, small_menu_font)

        pygame.display.update()
        clock.tick(fps)
# -------------------------------------------------------------------
# MAIN LOOP WITH MAIN MENU
# -------------------------------------------------------------------
while run:
    clock.tick(fps)
    screen_width, screen_height = screen.get_size()
    # restore ducked music when needed
    _update_music_duck()

    # --- MAIN MENU STATE ---
    if main_menu:
        events = pygame.event.get()
        for event in events:
            if event.type == pygame.QUIT:
                run = False
            elif event.type == pygame.VIDEORESIZE:
                rebuild_world_for_window(event.w, event.h, False)
                background_cache["size"] = None
            # DO NOT handle clicks here, we handle them after drawing when we know rects

        # draw background
        if background_cache["size"] != (screen_width, screen_height):
            background_cache["size"] = (screen_width, screen_height)
            background_cache["image"] = pygame.transform.smoothscale(
                scree_image, (screen_width, screen_height)
            )
        screen.blit(background_cache["image"], (0, 0))

        # title
        title = big_menu_font.render("RALR", True, (255, 255, 255))
        screen.blit(title, (screen_width // 2 - title.get_width() // 2, 80))

        # buttons layout
        btn_w, btn_h, gap = 260, 60, 20
        start_rect = pygame.Rect(
            screen_width // 2 - btn_w // 2,
            200,
            btn_w,
            btn_h,
        )
        level_select_rect = pygame.Rect(
            screen_width // 2 - btn_w // 2,
            200 + btn_h + gap,
            btn_w,
            btn_h,
        )
        exit_rect = pygame.Rect(
            screen_width // 2 - btn_w // 2,
            200 + 2 * (btn_h + gap),
            btn_w,
            btn_h,
        )

        draw_button(screen, start_rect, "Start Game", small_menu_font)
        draw_button(screen, level_select_rect, "Level Select", small_menu_font)
        draw_button(screen, exit_rect, "Exit", small_menu_font)

        # --- VOLUME UI (percent, controls master_volume 0–1) ---
        vol_label = small_menu_font.render("Volume", True, (255, 255, 255))
        vx = screen_width // 2 - 200
        vy = 200 + 3 * (btn_h + gap) + 40
        screen.blit(vol_label, (vx, vy))

        slider_x = vx + vol_label.get_width() + 20
        slider_y = vy
        slider_w = 240
        slider_h = 24
        track_rect, knob_rect = draw_slider(screen, slider_x, slider_y, slider_w, slider_h, master_volume)

        vol_percent = int(round(master_volume * 100))
        vol_text_surf = small_menu_font.render(f"{vol_percent}%", True, (255, 255, 255))
        screen.blit(vol_text_surf, (slider_x + slider_w + 10, slider_y))

        mute_rect = pygame.Rect(
            screen_width // 2 - 80,
            slider_y + 50,
            160,
            40,
        )
        # Button text reflects current mute state
        mute_text = "Unmute" if mute_audio or master_volume <= 0.0 else "Mute"
        draw_button(screen, mute_rect, mute_text, small_menu_font)

        # --- handle clicks using these rects ---
        for event in events:
            if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
                mx, my = event.pos

                if start_rect.collidepoint(mx, my):
                    # Show comic-style intro images one by one; final button should START the game.
                    result = show_intro_slideshow()
                    # Only actually start the game if the slideshow returned 'start'
                    if result == 'start' or result is None:
                    # (None means the slideshow completed with default action)
                        main_menu = False
                        end_state = None
                        game_over = 0
                        reset_game_to_start()
                        # ensure audio + correct music
                        ensure_audio_ready()
                        play_music_for_level(level)
                        apply_volume()
                elif level_select_rect.collidepoint(mx, my):
                    sel = run_level_select_menu()
                    if sel is not None:
                        # CHANGED: actually switch to selected level and rebuild world/player/groups
                        level = int(sel)
                        data = levels[level].data_svet
                        velkost_plochy = compute_tile_size(
                            screen_width,
                            screen_height,
                            cols=len(data[0]),
                            rows=len(data),
                        )
                        world = World(data, level, velkost_plochy)
                        groups['enemies'] = world.enemies
                        groups['projectiles'] = pygame.sprite.Group()
                        groups['boss_projectiles'] = pygame.sprite.Group()
                        # boss wiring same as in door change
                        if level == 15:
                            coords = getattr(
                                Level15,
                                "boss_coords",
                                (world.pixel_width // 2, world.pixel_height // 2),
                            )
                            b = Boss3(coords[0], coords[1], hp=20, projectile_group=groups['boss_projectiles'])
                            world.enemies.add(b)
                            world.boss = b
                        if level == 20 and getattr(world, "boss4", None) is not None:
                            world.boss4.projectile_group = groups['boss_projectiles']
                        if level == 25 and getattr(world, "boss5", None) is not None:
                            world.boss5.projectile_group = groups['boss_projectiles']
                        # create player for selected level
                        spawn = getattr(world, "player_spawn", None)
                        if spawn:
                            player = Player(spawn[0], spawn[1], velkost_plochy)
                        else:
                            default_x = int(round(1 * velkost_plochy))
                            default_y = int(
                                round((TARGET_ROWS - 2) * velkost_plochy)
                                - int(velkost_plochy * 0.2)
                            )
                            player = Player(default_x, default_y, velkost_plochy)
                        update_viewport_offsets()
                        main_menu = False
                        end_state = None
                        game_over = 0
                        ensure_audio_ready()
                        play_music_for_level(level)
                        apply_volume()  # same when entering via level select
                elif exit_rect.collidepoint(mx, my):
                    run = False
                elif mute_rect.collidepoint(mx, my):
                    # Toggle mute: if currently muted, restore last_nonzero_volume;
                    # if not muted, set volume to 0.
                    if mute_audio or master_volume <= 0.0:
                        # unmute -> restore remembered volume (or leave at 0 if never set)
                        mute_audio = False
                        master_volume = last_nonzero_volume
                    else:
                        mute_audio = True
                        # remember current volume before muting
                        if master_volume > 0.0:
                            last_nonzero_volume = master_volume
                        master_volume = 0.0
                    apply_volume()
                # slider: click anywhere on track to set master_volume in 0..1
                if track_rect.collidepoint(mx, my):
                    rel = (mx - track_rect.x) / max(1, track_rect.width)
                    master_volume = max(0.0, min(1.0, rel))
                    if master_volume > 0.0:
                        last_nonzero_volume = master_volume
                        # moving slider above 0 implicitly un-mutes
                        mute_audio = False
                    apply_volume()

        pygame.display.update()
        continue

    # ----------------- GAME LOOP (IN‑LEVEL) -----------------
    events = pygame.event.get()
    for event in events:
        if event.type == pygame.QUIT:
            run = False
        elif event.type == pygame.VIDEORESIZE:
            rebuild_world_for_window(event.w, event.h, False)
            background_cache["size"] = None
        elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1 and not game_over:
            # Check clicks on pause button only when not game_over
            pause_btn = get_pause_button_rect(screen_width, screen_height)
            if pause_btn.collidepoint(event.pos):
                paused = not paused
                # when opening pause, just freeze; volume is adjusted inside pause UI
                continue

    # ensure background cache valid
    if background_cache["size"] != (screen_width, screen_height):
        background_cache["size"] = (screen_width, screen_height)
        background_cache["image"] = pygame.transform.smoothscale(
            scree_image, (screen_width, screen_height)
        )

    # draw background
    screen.blit(background_cache["image"], (0, 0))

    # ALWAYS draw world and entities behind overlays
    world.draw(1, world.viewport_off_x, world.viewport_off_y)

    # --- IN‑GAME PAUSE BUTTON (TOP‑RIGHT) ---
    pause_btn = get_pause_button_rect(screen_width, screen_height)
    draw_button(screen, pause_btn, "Pause" if not paused else "Resume", small_menu_font)

    if paused and game_over == 0 and end_state is None:
        # PAUSE OVERLAY: freeze gameplay and show pause menu
        overlay = pygame.Surface((screen_width, screen_height))
        overlay.fill((0, 0, 0))
        overlay.set_alpha(180)
        screen.blit(overlay, (0, 0))

        # title
        title = big_menu_font.render("Paused", True, (255, 255, 255))
        screen.blit(title, (screen_width // 2 - title.get_width() // 2, 80))

        # warning
        warn_text = small_menu_font.render(
            "Warning: Progress is NOT saved when returning to main menu.",
            True, (255, 80, 80)
        )
        screen.blit(warn_text, (screen_width // 2 - warn_text.get_width() // 2, 150))

        # layout for pause buttons
        btn_w, btn_h, gap = 260, 60, 20
        resume_rect = pygame.Rect(
            screen_width // 2 - btn_w // 2,
            200,
            btn_w,
            btn_h,
        )
        main_menu_rect = pygame.Rect(
            screen_width // 2 - btn_w // 2,
            200 + btn_h + gap,
            btn_w,
            btn_h,
        )

        draw_button(screen, resume_rect, "Resume", small_menu_font)
        draw_button(screen, main_menu_rect, "Main Menu", small_menu_font)

        # volume UI inside pause menu
        vol_label = small_menu_font.render("Volume", True, (255, 255, 255))
        vx = screen_width // 2 - 220
        vy = 200 + 2 * (btn_h + gap) + 40
        screen.blit(vol_label, (vx, vy))

        slider_x = vx + vol_label.get_width() + 20
        slider_y = vy
        slider_w = 240
        slider_h = 24
        track_rect, knob_rect = draw_slider(screen, slider_x, slider_y, slider_w, slider_h, master_volume)

        vol_percent = int(round(master_volume * 100))
        vol_text_surf = small_menu_font.render(f"{vol_percent}%", True, (255, 255, 255))
        screen.blit(vol_text_surf, (slider_x + slider_w + 10, slider_y))

        mute_rect = pygame.Rect(
            screen_width // 2 - 80,
            slider_y + 50,
            160,
            40,
        )
        mute_text = "Unmute" if mute_audio or master_volume <= 0.0 else "Mute"
        draw_button(screen, mute_rect, mute_text, small_menu_font)

        # handle pause‑menu clicks (use this frame's events)
        for event in events:
            if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
                mx, my = event.pos
                if resume_rect.collidepoint(mx, my):
                    paused = False
                elif main_menu_rect.collidepoint(mx, my):
                    # go back to main menu; WARNING: game not saved
                    main_menu = True
                    paused = False
                    end_state = None
                    game_over = 0
                    # NEW: stop any level music when returning to menu via pause
                    try:
                        pygame.mixer.music.stop()
                    except Exception:
                        pass
                    current_music_kind = None
                elif mute_rect.collidepoint(mx, my):
                    if mute_audio or master_volume <= 0.0:
                        mute_audio = False
                        master_volume = last_nonzero_volume
                    else:
                        mute_audio = True
                        if master_volume > 0.0:
                            last_nonzero_volume = master_volume
                        master_volume = 0.0
                    apply_volume()
                if track_rect.collidepoint(mx, my):
                    rel = (mx - track_rect.x) / max(1, track_rect.width)
                    master_volume = max(0.0, min(1.0, rel))
                    if master_volume > 0.0:
                        last_nonzero_volume = master_volume
                        mute_audio = False
                    apply_volume()

        pygame.display.update()
        continue  # skip gameplay updates when paused

    # ----------------- NORMAL (UNPAUSED) GAMEPLAY -----------------
    # --- UPDATE ENEMIES / BOSSES CORRECTLY ---
    for enemy in list(world.enemies):
        try:
            enemy.update(player, groups.get('projectiles', pygame.sprite.Group()), world)
        except TypeError:
            # fallback: some old enemies may expect only (world) or no args
            try:
                enemy.update(world)
            except TypeError:
                try:
                    enemy.update()
                except Exception:
                    pass

    # update boss projectiles (group) and other misc groups that only need world
    for b in list(groups['boss_projectiles']):
        try:
            b.update(world)
        except Exception:
            pass

    # NEW: update player/enemy projectiles AND then draw them
    proj_group = groups.get('projectiles')
    if isinstance(proj_group, pygame.sprite.Group):
        for p in list(proj_group):
            try:
                p.update(world, screen_width, screen_height)
            except TypeError:
                try:
                    p.update(world)
                except Exception:
                    pass
        # draw all projectiles after update
        for p in proj_group:
            try:
                p.draw(screen, 1, world.viewport_off_x, world.viewport_off_y)
            except Exception:
                # very simple fallback draw if draw() missing
                sx = p.rect.x + world.viewport_off_x
                sy = p.rect.y + world.viewport_off_y
                screen.blit(p.image, (sx, sy))

    # if you still have other enemy types in groups (slimak, etc.), keep simple update
    for g_name, g in groups.items():
        if g_name in ('enemies', 'projectiles', 'boss_projectiles'):
            continue
        try:
            g.update(world)
        except TypeError:
            try:
                g.update()
            except Exception:
                pass

    # player update handles most game logic
    game_over = player.update(game_over, world, groups, events)

    # --- BOSS DOOR SPAWN (Boss3 + others) ---
    if getattr(world, "boss", None) and not world.boss.alive():
        world.unlock_boss_door()
        # door spawns at the player's spawn point for this level
        door_rect = make_door_at_player_spawn(world)
        door = pygame.sprite.Sprite()
        door.image = world.door_texture
        door.rect = door_rect
        groups['door'].add(door)
        world.door_list.append(door_rect)
        # NEW: also add as a visual tile so World.draw renders it
        world.tile_list.append((world.door_texture, door_rect))
        world._tile_cache["scale"] = None  # rebuild tile cache next draw
        heal_player_full(player)

    # Boss1 (level 5, tile 21)
    if getattr(world, "boss1", None) is not None and not world.boss1.alive() and not world.boss1_door_spawned:
        # Show overlay for level 5 if available
        try:
            if overlay_images.get(5) is not None:
                show_image_overlay(overlay_images.get(5), button_label="Continue")
        except Exception:
            pass
        door_rect = make_door_at_player_spawn(world)
        door = pygame.sprite.Sprite()
        door.image = world.door_texture
        door.rect = door_rect
        groups['door'].add(door)
        world.door_list.append(door_rect)
        # NEW: visual tile
        world.tile_list.append((world.door_texture, door_rect))
        world._tile_cache["scale"] = None
        world.boss1_door_spawned = True
        heal_player_full(player)

    # Boss2 (level 10, tile 22)
    if getattr(world, "boss2", None) is not None and not world.boss2.alive() and not world.boss2_door_spawned:
        try:
            if overlay_images.get(10) is not None:
                show_image_overlay(overlay_images.get(10), button_label="Continue")
        except Exception:
            pass
        door_rect = make_door_at_player_spawn(world)
        door = pygame.sprite.Sprite()
        door.image = world.door_texture
        door.rect = door_rect
        groups['door'].add(door)
        world.door_list.append(door_rect)
        # NEW: visual tile
        world.tile_list.append((world.door_texture, door_rect))
        world._tile_cache["scale"] = None
        world.boss2_door_spawned = True
        heal_player_full(player)

    # Boss4 (level 20)
    if getattr(world, "boss4", None) is not None and not world.boss4.alive() and not world.boss4_door_spawned:
        # Show overlay for level 20 if available
        try:
            if overlay_images.get(20) is not None:
                show_image_overlay(overlay_images.get(20), button_label="Continue")
        except Exception:
            pass
        door_rect = make_door_at_player_spawn(world)
        door = pygame.sprite.Sprite()
        door.image = world.door_texture
        door.rect = door_rect
        groups['door'].add(door)
        world.door_list.append(door_rect)
        # NEW: visual tile
        world.tile_list.append((world.door_texture, door_rect))
        world._tile_cache["scale"] = None
        world.boss4_door_spawned = True
        heal_player_full(player)

    # Boss5 (level 25)
    if getattr(world, "boss5", None) is not None and (
            getattr(world.boss5, "dead", False) or not world.boss5.alive()) and not world.boss5_door_spawned:
        # Show overlay for level 25 if available
        try:
            if overlay_images.get(25) is not None:
                show_image_overlay(overlay_images.get(25), button_label="Continue")
        except Exception:
            pass
        door_rect = make_door_at_player_spawn(world)
        door = pygame.sprite.Sprite()
        door.image = world.door_texture
        door.rect = door_rect
        groups['door'].add(door)
        world.door_list.append(door_rect)
        # NEW: visual tile
        world.tile_list.append((world.door_texture, door_rect))
        world._tile_cache["scale"] = None
        world.boss5_door_spawned = True
        heal_player_full(player)

    # --- DOOR COLLISION -> LEVEL CHANGE OR WIN ---
    # Use both dynamic doors (groups['door']) AND static tile doors (world.door_list).
    reached_door = False

    # 1) dynamic doors (sprites we spawned)
    for door in list(groups['door']):
        if player.rect.colliderect(door.rect):
            reached_door = True
            break

    # 2) static doors from world.door_list (tile-based)
    if not reached_door:
        for drect in getattr(world, "door_list", []):
            if player.rect.colliderect(drect):
                reached_door = True
                break

    if reached_door:
        if level == 25:
            # Show outro images when finishing floor 25 (Outro_1..Outro_4).
            # Final button will be "Main Menu" and should return player to the lobby.
            try:
                outro_paths = [r"Images\\Outro_1.png", r"Images\\Outro_2.png", r"Images\\Outro_3.png",
                               r"Images\\Outro_4.png"]
                res = show_intro_slideshow(outro_paths, final_label="Main Menu", final_action='menu')
            except Exception:
                res = None

            # If player chose to return to menu (or skipped), go back to main menu.
            if res == 'menu' or res == 'skip':
                main_menu = True
                end_state = None
                game_over = 0
                reset_game_to_start()
                try:
                    pygame.mixer.music.stop()
                except Exception:
                    pass
                current_music_kind = None
            else:
                # Default: show win overlay after outro
                end_state = "won"
        else:
            level += 1
            data = levels[level].data_svet
            velkost_plochy = compute_tile_size(screen_width, screen_height, cols=len(data[0]), rows=len(data))
            world = World(data, level, velkost_plochy)
            # clear old doors/sprites so only new level doors are active
            groups['door'].empty()
            world.door_list = getattr(world, "door_list", [])
            groups['enemies'] = world.enemies
            groups['projectiles'] = pygame.sprite.Group()
            groups['boss_projectiles'] = pygame.sprite.Group()
            # boss wiring after level change
            if level == 15:
                coords = getattr(
                    Level15,
                    "boss_coords",
                    (world.pixel_width // 2, world.pixel_height // 2),
                )
                b = Boss3(coords[0], coords[1], hp=20, projectile_group=groups['boss_projectILES'.lower()] if 'boss_projectILES'.lower() in groups else groups['boss_projectiles'])
                world.enemies.add(b)
                world.boss = b
            if level == 20 and getattr(world, "boss4", None) is not None:
                world.boss4.projectile_group = groups['boss_projectiles']
            if level == 25 and getattr(world, "boss5", None) is not None:
                world.boss5.projectile_group = groups['boss_projectiles']
            # recreate player for new level
            spawn = getattr(world, "player_spawn", None)
            if spawn:
                player = Player(spawn[0], spawn[1], velkost_plochy)
            else:
                default_x = int(round(1 * velkost_plochy))
                default_y = int(
                    round((TARGET_ROWS - 2) * velkost_plochy)
                    - int(velkost_plochy * 0.2)
                )
                player = Player(default_x, default_y, velkost_plochy)
            update_viewport_offsets()
            # NEW: switch music when level changes
            ensure_audio_ready()
            play_music_for_level(level)
            apply_volume()

    # --- YOU DIED overlay / restart ---
    if game_over != 0:
        overlay = pygame.Surface((screen_width, screen_height))
        overlay.fill((0, 0, 0))
        overlay.set_alpha(192)
        screen.blit(overlay, (0, 0))

        die_surface = big_menu_font.render("YOU DIED", True, (255, 0, 0))
        screen.blit(die_surface, (screen_width // 2 - die_surface.get_width() // 2,
                                  screen_height // 2 - 100))

        restart_rect = pygame.Rect(screen_width // 2 - 100, screen_height // 2 + 20, 200, 50)
        draw_button(screen, restart_rect, "Restart", small_menu_font, text_color=(255, 255, 255))

        # click-to-restart detection
        for event in events:
            if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
                if restart_rect.collidepoint(event.pos):
                    reset_game_to_start()
                    game_over = 0
                    end_state = None
                    break

    # --- WIN overlay ---
    if end_state == "won":
        overlay = pygame.Surface((screen_width, screen_height))
        overlay.fill((0, 0, 0))
        overlay.set_alpha(220)
        screen.blit(overlay, (0, 0))
        win_surface = big_menu_font.render("YOU WIN", True, (0, 200, 0))
        screen.blit(win_surface, (screen_width // 2 - win_surface.get_width() // 2,
                                  screen_height // 2 - 100))
        exit_rect = pygame.Rect(screen_width // 2 - 100, screen_height // 2 + 20, 200, 50)
        draw_button(screen, exit_rect, "Exit", small_menu_font)
        for event in events:
            if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
                if exit_rect.collidepoint(event.pos):
                    # NEW: ensure music is stopped when exiting from win screen
                    try:
                        pygame.mixer.music.stop()
                    except Exception:
                        pass
                    current_music_kind = None
                    run = False
                    break

    # --- Health / Ammo bars + event loop (your original snippet) ---
    # Health bar (green; red if last health; orange if 2 HP)
    hb_w = min(300, max(140, int(screen_width * 0.22)))
    hb_h = 18
    hb_x, hb_y = 10, 10
    max_hp = getattr(player, "max_health", 3)
    hp = max(0, min(max_hp, player.health))
    hp_ratio = 0 if max_hp <= 0 else (hp / max_hp)
    if hp <= 1:
        hp_color = (200, 0, 0)       # red
    elif hp == 2:
        hp_color = (255, 165, 0)     # orange
    else:
        hp_color = (0, 200, 0)       # green
    pygame.draw.rect(screen, (50, 50, 50), (hb_x, hb_y, hb_w, hb_h))
    pygame.draw.rect(screen, hp_color, (hb_x, hb_y, int(hb_w * hp_ratio), hb_h))
    if max_hp > 1:
        seg_w = hb_w / max_hp
        for i in range(1, max_hp):
            tx = hb_x + int(round(i * seg_w))
            pygame.draw.line(screen, (0, 0, 0), (tx, hb_y), (tx, hb_y + hb_h), 1)
    pygame.draw.rect(screen, (0, 0, 0), (hb_x, hb_y, hb_w, hb_h), 2)

    # Ammo bar (yellow)
    ab_w = hb_w
    ab_h = 12
    ab_x, ab_y = hb_x, hb_y + hb_h + 8
    max_ammo = max(1, getattr(player, "max_ammo", 10))
    ammo = max(0, min(max_ammo, player.ammo))
    ammo_ratio = ammo / max_ammo
    pygame.draw.rect(screen, (50, 50, 50), (ab_x, ab_y, ab_w, ab_h))
    pygame.draw.rect(screen, (255, 220, 0), (ab_x, ab_y, int(ab_w * ammo_ratio), ab_h))
    if max_ammo > 1:
        seg_w = ab_w / max_ammo
        for i in range(1, max_ammo):
            tx = ab_x + int(round(i * seg_w))
            pygame.draw.line(screen, (0, 0, 0), (tx, ab_y), (tx, ab_y + ab_h), 1)
    pygame.draw.rect(screen, (0, 0, 0), (ab_x, ab_y, ab_w, ab_h), 2)

    # NEW: check player collisions with all hazard tiles (spikes, etc.)
    for hrect in world.hazard_list:
        if player.rect.colliderect(hrect):
            # damage player: fallback to health attribute if take_damage() missing
            try:
                player.take_damage(1)
            except Exception:
                player.health -= 1
                if player.health <= 0:
                    game_over = 2  # set game over state (lost)

    pygame.display.update()

pygame.quit()
