# ===== VERSION: COMBINED-6 (Full File for Pyxel Studio) =====
# =====================================================================
# HARE HURRY: EASTER RIVAL  (Combined Edition v6)
# Neu: neue Sprites (88x64 Schule, Boss 56x64, Osterei, Herz, Hund 32x32),
# HotBunny Attraction, Boss mit Plattformen + Osterei-Projektile,
# Story-Intros pro Level, Prolog nach Loading, mehr Sounds, Cage-Break
# via Stomp/Throw/Slam, MazeDog fix, Cannons unregelmäßig.
# =====================================================================
import pyxel
import random
import math

# ---------------------------------------------------------------------
# DISPLAY
# ---------------------------------------------------------------------
SCREEN_W = 256
SCREEN_H = 256
FPS = 30
COLKEY = 3

# ---------------------------------------------------------------------
# STATES
# ---------------------------------------------------------------------
ST_LOADING       = "loading"
ST_PROLOGUE      = "prologue"
ST_TITLE         = "title"
ST_INTRO         = "intro"
ST_LOBBY         = "lobby"
ST_SHOP          = "shop"
ST_SETTINGS      = "settings"
ST_LEVEL_INTRO   = "level_intro"
ST_LEVEL_MEADOW  = "meadow"
ST_LEVEL_KIDS    = "kids"
ST_LEVEL_MAZE1   = "maze1"
ST_LEVEL_MAZE2   = "maze2"
ST_LEVEL_PLAT_CANNON  = "plat_cannon"
ST_LEVEL_PLAT_ATTRACT = "plat_attract"
ST_LEVEL_PLAT_FENCE   = "plat_fence"
ST_LEVEL_FROZEN  = "frozen"
ST_LEVEL_BOSS    = "boss"
ST_LEVEL_LOVE    = "love"
ST_LEVEL_MAZE    = ST_LEVEL_MAZE1
ST_LEVEL_COMPLETE = "level_complete"
ST_WIN           = "win"
ST_CREDITS       = "credits"
ST_GAME_OVER     = "game_over"
ST_PAUSED        = "paused"

# ---------------------------------------------------------------------
# COLORS
# ---------------------------------------------------------------------
COL_BLACK = 0; COL_NAVY = 1; COL_PURPLE = 2; COL_GREEN = 3
COL_BROWN = 4; COL_DGRAY = 5; COL_LGRAY = 6; COL_WHITE = 7
COL_RED = 8;   COL_ORANGE = 9; COL_YELLOW = 10; COL_LGREEN = 11
COL_LBLUE = 12; COL_INDIGO = 13; COL_PINK = 14; COL_PEACH = 15

# ---------------------------------------------------------------------
# SPRITE COORDINATES
# ---------------------------------------------------------------------
SPR_PLAYER_BIG       = (0,  80, 48, 32)
SPR_PLAYER_L_IDLE    = (0, 176, 32, 32)
SPR_PLAYER_L_RUN1    = (0, 208, 32, 32)
SPR_PLAYER_L_RUN2    = (32, 208, 32, 32)
SPR_PLAYER_R_IDLE    = (32, 176, 32, 32)
SPR_PLAYER_R_RUN1    = (32, 144, 32, 32)
SPR_PLAYER_R_RUN2    = (64, 208, 32, 32)

SPR_RIVAL            = (48, 64, 16, 16)
SPR_RIVAL_HEAD       = (48, 64, 16, 16)
SPR_RIVAL_R_1        = (96, 176, 32, 32)
SPR_RIVAL_R_2        = (64, 176, 32, 32)
SPR_RIVAL_L_1        = (96, 144, 32, 32)
SPR_RIVAL_L_2        = (64, 144, 32, 32)

# NEW: 88x64 school
SPR_SCHOOL           = (120, 0, 88, 64)
SCHOOL_W             = 88
SCHOOL_H             = 64

SPR_PROFESSOR        = (0, 144, 32, 32)
SPR_COIN             = (32, 64, 8, 8)
SPR_FARMER           = (32, 32, 32, 32)
SPR_WALL_TALL        = (48, 0, 16, 32)
SPR_WALL_SHORT       = (64, 0, 16, 16)

SPR_BABY_L_1         = (0, 16, 16, 16)
SPR_BABY_L_2         = (0, 48, 16, 16)
SPR_BABY_L_CARROT_1  = (0, 64, 16, 16)
SPR_BABY_L_CARROT_2  = (16, 64, 16, 16)
SPR_BABY_R_1         = (16, 16, 16, 16)
SPR_BABY_R_2         = (32, 16, 16, 16)
SPR_BABY_R_CARROT_1  = (16, 0, 16, 16)
SPR_BABY_R_CARROT_2  = (32, 0, 16, 16)

# NEW sprites
SPR_DOG              = (80, 0, 32, 32)
SPR_BOSS_NORMAL      = (128, 144, 56, 64)
SPR_BOSS_ATTACK      = (184, 144, 56, 64)
SPR_EASTER_EGG       = (48, 80, 16, 16)
SPR_HEART            = (48, 96, 8, 8)

# Lila — the lying-down heart-eyed bunny (same sprite shown on the title
# screen as SPR_PLAYER_BIG). 48x32, image bank 0.
SPR_LILA             = (0, 80, 48, 32)

# Female bunny "Lila" — drawn procedurally on a floating cloud
LILA_NAME = "LILA"

# Image 1
SPR_CARROT           = (0, 16, 16, 16)
SPR_FOX_LEFT         = (0, 32, 56, 32)
SPR_FOX_RIGHT        = (0, 96, 56, 32)

# ---------------------------------------------------------------------
# GAMEPLAY CONSTANTS
# ---------------------------------------------------------------------
PLAYER_VIS_W = 32
PLAYER_VIS_H = 32
PLAYER_HIT_W = 22
PLAYER_HIT_H = 24

MAP_MEADOW_W = 512
MAP_MEADOW_H = 512

TILE = 16
MAZE_COLS = 24
MAZE_ROWS = 24
MAP_MAZE_W = MAZE_COLS * TILE
MAP_MAZE_H = MAZE_ROWS * TILE

RIVAL_BAR_MAX = 1000


# ---------------------------------------------------------------------
# UTILITIES
# ---------------------------------------------------------------------
def clamp(v, lo, hi):
    return max(lo, min(hi, v))


def rects_overlap(a, b):
    return (a[0] < b[0] + b[2] and a[0] + a[2] > b[0]
            and a[1] < b[1] + b[3] and a[1] + a[3] > b[1])


def dist(x1, y1, x2, y2):
    return math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)


def normalize(dx, dy):
    d = math.sqrt(dx * dx + dy * dy)
    if d < 0.0001:
        return 0.0, 0.0
    return dx / d, dy / d


def approach(value, target, step):
    if value < target:
        return min(target, value + step)
    if value > target:
        return max(target, value - step)
    return value


def draw_centered(text, y, col=COL_WHITE, x_mid=SCREEN_W // 2):
    pyxel.text(int(x_mid - len(text) * 2), y, text, col)


def draw_panel(x, y, w, h, border=COL_WHITE, fill=COL_BLACK):
    pyxel.rect(x, y, w, h, fill)
    pyxel.rectb(x, y, w, h, border)


def draw_story_window(lines, line_idx, char_idx, panel_x, panel_y,
                      panel_w, panel_h, max_lines=4, line_h=10,
                      highlight_words=("RIVAL", "OLD BUNNY", "OLD EASTER")):
    """Render story text inside a panel with a SLIDING WINDOW.

    Only the most recent `max_lines` revealed lines are shown. As new lines
    appear, older ones scroll up and out of the top — so text never
    overflows the box. The line currently being typed is always visible at
    the bottom of the window.

    lines:      full list of story strings
    line_idx:   index of the line currently being typed (0-based)
    char_idx:   how many chars of the current line are revealed
    """
    # Which lines are currently visible: a window ending at line_idx.
    end = min(line_idx, len(lines) - 1)
    start = max(0, end - max_lines + 1)
    visible = lines[start:end + 1]

    # Vertically center the window block inside the panel.
    block_h = len(visible) * line_h
    base_y = panel_y + (panel_h - block_h) // 2

    for vi, line in enumerate(visible):
        actual_idx = start + vi
        if actual_idx == line_idx:
            shown = line[:char_idx]
        else:
            shown = line
        col = COL_WHITE
        for hw in highlight_words:
            if hw in shown:
                col = COL_RED
                break
        if actual_idx == 0:
            # First line is the title — make it pop
            col = COL_YELLOW if col == COL_WHITE else col
        y = base_y + vi * line_h
        draw_centered(shown, y, col, x_mid=panel_x + panel_w // 2)


def btn_any(*keys):
    return any(pyxel.btn(k) for k in keys)


def btnp_any(*keys):
    return any(pyxel.btnp(k) for k in keys)


# ---------------------------------------------------------------------
# ASSETS
# ---------------------------------------------------------------------
class Assets:
    loaded = False

    @staticmethod
    def setup():
        try:
            pyxel.load("assets.pyxres")
            Assets.loaded = True
        except Exception:
            Assets.loaded = False
            Assets._build_fallback()

    @staticmethod
    def _build_fallback():
        img0 = pyxel.image(0)
        for y in range(256):
            for x in range(256):
                img0.pset(x, y, COL_GREEN)

        def draw_bunny(ox, oy, run=0, facing=1):
            for yy in range(oy + 14, oy + 28):
                for xx in range(ox + 8, ox + 24):
                    img0.pset(xx, yy, COL_WHITE)
            for yy in range(oy + 6, oy + 18):
                for xx in range(ox + 10, ox + 22):
                    img0.pset(xx, yy, COL_WHITE)
            for yy in range(oy, oy + 9):
                for xx in range(ox + 11, ox + 14):
                    img0.pset(xx, yy, COL_WHITE)
                for xx in range(ox + 18, ox + 21):
                    img0.pset(xx, yy, COL_WHITE)
                if oy + 2 < yy < oy + 7:
                    img0.pset(ox + 12, yy, COL_PINK)
                    img0.pset(ox + 19, yy, COL_PINK)
            img0.pset(ox + 13, oy + 11, COL_BLACK)
            img0.pset(ox + 19, oy + 11, COL_BLACK)
            if run == 1:
                for yy in range(oy + 26, oy + 30):
                    img0.pset(ox + 9, yy, COL_WHITE)
                    img0.pset(ox + 22, yy, COL_WHITE)
            elif run == 2:
                for yy in range(oy + 26, oy + 30):
                    img0.pset(ox + 10, yy, COL_WHITE)
                    img0.pset(ox + 23, yy, COL_WHITE)

        draw_bunny(32, 176, 0, 1)
        draw_bunny(32, 144, 1, 1)
        draw_bunny(64, 208, 2, 1)
        draw_bunny(0, 176, 0, -1)
        draw_bunny(0, 208, 1, -1)
        draw_bunny(32, 208, 2, -1)
        draw_bunny(0, 80, 0, 1)

        # Rival head 16x16
        for yy in range(64 + 4, 64 + 16):
            for xx in range(48 + 3, 48 + 13):
                img0.pset(xx, yy, COL_BROWN)
        img0.pset(48 + 6, 64 + 8, COL_RED)
        img0.pset(48 + 10, 64 + 8, COL_RED)

        # New school 88x64 placeholder at (120, 0)
        for yy in range(0, 64):
            for xx in range(120, 120 + 88):
                img0.pset(xx, yy, COL_PEACH)
        # Roof
        for yy in range(0, 18):
            for xx in range(120, 120 + 88):
                img0.pset(xx, yy, COL_RED)
        # Door
        for yy in range(40, 64):
            for xx in range(120 + 38, 120 + 50):
                img0.pset(xx, yy, COL_BROWN)
        # Windows
        for yy in range(22, 36):
            for xx in range(120 + 12, 120 + 28):
                img0.pset(xx, yy, COL_YELLOW)
            for xx in range(120 + 60, 120 + 76):
                img0.pset(xx, yy, COL_YELLOW)
        # Bell
        for yy in range(4, 10):
            for xx in range(120 + 40, 120 + 48):
                img0.pset(xx, yy, COL_YELLOW)

        # Professor 32x32
        for yy in range(144 + 8, 144 + 30):
            for xx in range(4, 28):
                img0.pset(xx, yy, COL_LGRAY)
        for yy in range(144, 144 + 10):
            for xx in range(8, 12):
                img0.pset(xx, yy, COL_LGRAY)
            for xx in range(20, 24):
                img0.pset(xx, yy, COL_LGRAY)
        for xx in range(8, 12):
            img0.pset(xx, 144 + 14, COL_BLACK)
            img0.pset(xx, 144 + 17, COL_BLACK)
        for xx in range(20, 24):
            img0.pset(xx, 144 + 14, COL_BLACK)
            img0.pset(xx, 144 + 17, COL_BLACK)

        # Coin 8x8
        for yy in range(64 + 1, 64 + 7):
            for xx in range(32 + 1, 32 + 7):
                img0.pset(xx, yy, COL_YELLOW)
        img0.pset(32 + 3, 64 + 3, COL_PEACH)

        # Farmer
        for yy in range(32 + 12, 32 + 28):
            for xx in range(32 + 6, 32 + 26):
                img0.pset(xx, yy, COL_NAVY)
        for yy in range(32 + 12, 32 + 18):
            for xx in range(32 + 6, 32 + 26):
                img0.pset(xx, yy, COL_RED)
        for yy in range(32 + 5, 32 + 12):
            for xx in range(32 + 10, 32 + 22):
                img0.pset(xx, yy, COL_PEACH)
        for yy in range(32 + 1, 32 + 5):
            for xx in range(32 + 8, 32 + 24):
                img0.pset(xx, yy, COL_BROWN)
        img0.pset(32 + 14, 32 + 9, COL_BLACK)
        img0.pset(32 + 18, 32 + 9, COL_BLACK)

        # Walls
        for yy in range(0, 32):
            for xx in range(48, 64):
                img0.pset(xx, yy, COL_DGRAY)
            if yy % 8 == 0:
                for xx in range(48, 64):
                    img0.pset(xx, yy, COL_BLACK)
        for yy in range(0, 16):
            for xx in range(64, 80):
                img0.pset(xx, yy, COL_DGRAY)
            if yy % 8 == 0:
                for xx in range(64, 80):
                    img0.pset(xx, yy, COL_BLACK)

        # Baby slots
        baby_slots = [
            (0, 16, COL_WHITE, False),
            (0, 48, COL_WHITE, False),
            (0, 64, COL_WHITE, True),
            (16, 64, COL_WHITE, True),
            (16, 16, COL_WHITE, False),
            (32, 16, COL_WHITE, False),
            (16, 0, COL_WHITE, True),
            (32, 0, COL_WHITE, True),
        ]
        for sx, sy, body, carrot in baby_slots:
            for yy in range(sy + 6, sy + 14):
                for xx in range(sx + 3, sx + 13):
                    img0.pset(xx, yy, body)
            for yy in range(sy + 2, sy + 8):
                for xx in range(sx + 4, sx + 12):
                    img0.pset(xx, yy, body)
            img0.pset(sx + 6, sy + 5, COL_BLACK)
            img0.pset(sx + 10, sy + 5, COL_BLACK)
            if carrot:
                img0.pset(sx + 8, sy + 9, COL_ORANGE)
                img0.pset(sx + 9, sy + 9, COL_ORANGE)
                img0.pset(sx + 8, sy + 10, COL_ORANGE)

        # NEW Dog 32x32 at (80, 0)
        for yy in range(8, 28):
            for xx in range(82, 110):
                img0.pset(xx, yy, COL_BROWN)
        for yy in range(4, 16):
            for xx in range(95, 110):
                img0.pset(xx, yy, COL_BROWN)
        img0.pset(105, 9, COL_BLACK)
        img0.pset(107, 11, COL_RED)
        for yy in range(28, 32):
            img0.pset(85, yy, COL_BROWN)
            img0.pset(105, yy, COL_BROWN)

        # NEW Boss Normal 56x64 at (128, 144)
        bx0, by0 = 128, 144
        for yy in range(by0 + 16, by0 + 56):
            for xx in range(bx0 + 8, bx0 + 48):
                img0.pset(xx, yy, COL_WHITE)
        for yy in range(by0 + 4, by0 + 24):
            for xx in range(bx0 + 12, bx0 + 44):
                img0.pset(xx, yy, COL_WHITE)
        # Ears
        for yy in range(by0, by0 + 14):
            for xx in range(bx0 + 14, bx0 + 19):
                img0.pset(xx, yy, COL_WHITE)
            for xx in range(bx0 + 37, bx0 + 42):
                img0.pset(xx, yy, COL_WHITE)
        # Crown
        for xx in range(bx0 + 14, bx0 + 42):
            img0.pset(xx, by0 + 2, COL_YELLOW)
            img0.pset(xx, by0 + 3, COL_YELLOW)
        for xx in (bx0 + 16, bx0 + 24, bx0 + 32, bx0 + 40):
            img0.pset(xx, by0, COL_YELLOW)
        # Eyes
        for yy in range(by0 + 10, by0 + 13):
            for xx in range(bx0 + 18, bx0 + 21):
                img0.pset(xx, yy, COL_BLACK)
            for xx in range(bx0 + 35, bx0 + 38):
                img0.pset(xx, yy, COL_BLACK)
        # Robe
        for yy in range(by0 + 38, by0 + 44):
            for xx in range(bx0 + 8, bx0 + 48):
                img0.pset(xx, yy, COL_RED)
        # Beard
        for yy in range(by0 + 24, by0 + 32):
            for xx in range(bx0 + 18, bx0 + 38):
                img0.pset(xx, yy, COL_LGRAY)

        # NEW Boss Attack 56x64 at (184, 144) — like normal but red eyes + raised arms
        bx1, by1 = 184, 144
        for yy in range(by1 + 16, by1 + 56):
            for xx in range(bx1 + 8, bx1 + 48):
                img0.pset(xx, yy, COL_WHITE)
        for yy in range(by1 + 4, by1 + 24):
            for xx in range(bx1 + 12, bx1 + 44):
                img0.pset(xx, yy, COL_WHITE)
        for yy in range(by1, by1 + 14):
            for xx in range(bx1 + 14, bx1 + 19):
                img0.pset(xx, yy, COL_WHITE)
            for xx in range(bx1 + 37, bx1 + 42):
                img0.pset(xx, yy, COL_WHITE)
        # Arms up
        for yy in range(by1 + 18, by1 + 30):
            for xx in range(bx1 + 2, bx1 + 8):
                img0.pset(xx, yy, COL_WHITE)
            for xx in range(bx1 + 48, bx1 + 54):
                img0.pset(xx, yy, COL_WHITE)
        # Crown
        for xx in range(bx1 + 14, bx1 + 42):
            img0.pset(xx, by1 + 2, COL_YELLOW)
            img0.pset(xx, by1 + 3, COL_YELLOW)
        # Angry red eyes
        for yy in range(by1 + 10, by1 + 13):
            for xx in range(bx1 + 18, bx1 + 21):
                img0.pset(xx, yy, COL_RED)
            for xx in range(bx1 + 35, bx1 + 38):
                img0.pset(xx, yy, COL_RED)
        for yy in range(by1 + 38, by1 + 44):
            for xx in range(bx1 + 8, bx1 + 48):
                img0.pset(xx, yy, COL_RED)
        for yy in range(by1 + 24, by1 + 32):
            for xx in range(bx1 + 18, bx1 + 38):
                img0.pset(xx, yy, COL_LGRAY)

        # NEW Easter Egg 16x16 at (48, 80)
        ex, ey = 48, 80
        for yy in range(ey + 2, ey + 14):
            for xx in range(ex + 3, ex + 13):
                d_top = abs(yy - (ey + 5))
                d_bot = abs(yy - (ey + 11))
                if d_top < 4 and abs(xx - (ex + 8)) < 5:
                    img0.pset(xx, yy, COL_LBLUE)
                elif d_bot < 4 and abs(xx - (ex + 8)) < 5:
                    img0.pset(xx, yy, COL_LBLUE)
        # Pattern
        for xx in range(ex + 4, ex + 12, 2):
            img0.pset(xx, ey + 6, COL_PINK)
            img0.pset(xx + 1, ey + 9, COL_YELLOW)

        # NEW Heart 8x8 at (48, 96)
        hx, hy = 48, 96
        heart_pixels = [
            (1,1),(2,1),(5,1),(6,1),
            (0,2),(1,2),(2,2),(3,2),(4,2),(5,2),(6,2),(7,2),
            (1,3),(2,3),(3,3),(4,3),(5,3),(6,3),
            (2,4),(3,4),(4,4),(5,4),
            (3,5),(4,5),
        ]
        for dx, dy in heart_pixels:
            img0.pset(hx + dx, hy + dy, COL_RED)
        img0.pset(hx + 1, hy + 1, COL_PINK)

        # Image 1
        img1 = pyxel.image(1)
        for y in range(256):
            for x in range(256):
                img1.pset(x, y, COL_GREEN)
        for yy in range(16 + 4, 16 + 14):
            for xx in range(4, 12):
                if (xx - 4) + (yy - (16 + 4)) < 12:
                    img1.pset(xx, yy, COL_ORANGE)
        img1.pset(7, 17, COL_LGREEN)
        img1.pset(8, 16, COL_LGREEN)
        img1.pset(9, 17, COL_LGREEN)
        # Fox left
        for yy in range(32 + 8, 32 + 26):
            for xx in range(0, 56):
                img1.pset(xx, yy, COL_ORANGE)
        for yy in range(32 + 4, 32 + 14):
            for xx in range(2, 18):
                img1.pset(xx, yy, COL_ORANGE)
        img1.pset(5, 32 + 8, COL_BLACK)
        for yy in range(32 + 10, 32 + 18):
            for xx in range(50, 56):
                img1.pset(xx, yy, COL_ORANGE)
        img1.pset(55, 32 + 12, COL_WHITE)
        # Fox right
        for yy in range(96 + 8, 96 + 26):
            for xx in range(0, 56):
                img1.pset(xx, yy, COL_ORANGE)
        for yy in range(96 + 4, 96 + 14):
            for xx in range(38, 54):
                img1.pset(xx, yy, COL_ORANGE)
        img1.pset(50, 96 + 8, COL_BLACK)
        for yy in range(96 + 10, 96 + 18):
            for xx in range(0, 6):
                img1.pset(xx, yy, COL_ORANGE)
        img1.pset(0, 96 + 12, COL_WHITE)


# ---------------------------------------------------------------------
# SOUND
# ---------------------------------------------------------------------
class Sound:
    _enabled = True

    @staticmethod
    def setup(vol_main="7", vol_soft="5", vol_tick="4"):
        pyxel.sounds[0].set("c2e2g2c3", "t", vol_tick, "n", 20)
        pyxel.sounds[1].set("c3e3g3", "p", vol_main, "s", 5)
        pyxel.sounds[2].set("g3c4e4c4", "p", vol_main, "s", 6)
        pyxel.sounds[3].set("g3 r e3 r c3", "t", vol_main, "n", 40)
        pyxel.sounds[4].set("c4g4c4", "s", vol_soft, "v", 10)
        pyxel.sounds[5].set("e2d2c2", "p", vol_main, "f", 8)
        pyxel.sounds[6].set("a2e3a3", "t", vol_soft, "n", 15)
        pyxel.sounds[7].set("c4r c4r g4", "p", vol_soft, "s", 12)
        pyxel.sounds[8].set("g4e4c4g3", "p", vol_main, "f", 18)
        pyxel.sounds[9].set("g3", "n", vol_tick, "n", 2)
        pyxel.sounds[10].set("c2c2", "n", vol_soft, "f", 10)
        # NEW
        pyxel.sounds[11].set("c4e4g4c4", "p", vol_main, "s", 4)
        pyxel.sounds[12].set("g3f3e3d3c3", "t", vol_main, "f", 8)
        pyxel.sounds[13].set("c4d4e4f4g4", "s", vol_soft, "n", 5)
        pyxel.sounds[14].set("g2 r g2 r g2", "n", vol_main, "f", 6)
        pyxel.sounds[15].set("c3 e3 g3", "p", vol_main, "s", 10)
        pyxel.sounds[16].set("a3 g3 f3 e3", "t", vol_soft, "f", 8)
        pyxel.sounds[17].set("g4c4", "p", vol_soft, "s", 4)
        pyxel.sounds[18].set("d3g3d4", "s", vol_main, "n", 8)

    @staticmethod
    def play(ch, s):
        if not Sound._enabled:
            return
        try:
            pyxel.play(ch, s)
        except Exception:
            pass

    @staticmethod
    def set_enabled(b):
        Sound._enabled = b

    @staticmethod
    def apply_volume_level(lvl):
        if lvl == 0:
            Sound._enabled = False
            return
        Sound._enabled = True
        vols = ["0", "3", "5", "7"]
        v = vols[lvl]
        try:
            Sound.setup(vol_main=v,
                        vol_soft=str(max(3, int(v) - 2)),
                        vol_tick=str(max(2, int(v) - 3)))
        except Exception:
            pass


# ---------------------------------------------------------------------
# CAMERA
# ---------------------------------------------------------------------
class Camera:
    def __init__(self):
        self.x = 0.0
        self.y = 0.0
        self.bounds_w = MAP_MEADOW_W
        self.bounds_h = MAP_MEADOW_H

    def set_bounds(self, w, h):
        self.bounds_w = w
        self.bounds_h = h

    def follow(self, target_x, target_y):
        self.x = clamp(target_x - SCREEN_W / 2, 0,
                       max(0, self.bounds_w - SCREEN_W))
        self.y = clamp(target_y - SCREEN_H / 2, 0,
                       max(0, self.bounds_h - SCREEN_H))

    def offset(self):
        return (self.x, self.y)
# =====================================================================
# ENTITIES
# =====================================================================

class Player:
    def __init__(self, x, y):
        self.x = float(x)
        self.y = float(y)
        self.w = PLAYER_HIT_W
        self.h = PLAYER_HIT_H
        self.vx = 0.0
        self.vy = 0.0
        self.facing = 1
        self.anim_tick = 0
        self.anim_frame = 0
        self.carrying_carrots = 0
        self.max_carry = 3
        self.coins = 0
        self.alive = True
        self.hurt_timer = 0
        self.health = 3
        self.max_health = 3
        self.has_dash = False
        self.has_special_attack = False
        self.dash_cooldown = 0
        self.dash_active = 0
        self.dash_dir_x = 0.0
        self.dash_dir_y = 0.0
        self.attack_cooldown = 0
        self.effects = {}
        self.mode = "topdown"
        self.on_ground = False
        self.jump_buffer = 0
        self.coyote = 0
        self.jumps_left = 0
        self.max_jumps = 1
        self.fly_mode = False  # god-mode free flight

    def rect(self):
        return (int(self.x), int(self.y), self.w, self.h)

    def center(self):
        return (self.x + self.w / 2, self.y + self.h / 2)

    def sprite_pos(self):
        sx = self.x - (PLAYER_VIS_W - self.w) // 2
        sy = self.y - (PLAYER_VIS_H - self.h)
        return (sx, sy)

    def damage(self, amount=1):
        if self.hurt_timer > 0 or not self.alive:
            return False
        if "shield" in self.effects:
            return False
        self.health = max(0, self.health - amount)
        self.hurt_timer = 30
        Sound.play(0, 5)
        if self.health <= 0:
            self.alive = False
        return True

    def update(self, speed, blocked_fn=None):
        if not self.alive:
            return False
        if self.fly_mode:
            fly_speed = 4.0
            if btn_any(pyxel.KEY_LEFT, pyxel.KEY_A):
                self.x -= fly_speed; self.facing = -1
            if btn_any(pyxel.KEY_RIGHT, pyxel.KEY_D):
                self.x += fly_speed; self.facing = 1
            if btn_any(pyxel.KEY_UP, pyxel.KEY_W):
                self.y -= fly_speed
            if btn_any(pyxel.KEY_DOWN, pyxel.KEY_S):
                self.y += fly_speed
            if self.hurt_timer > 0:
                self.hurt_timer -= 1
            self._tick_effects()
            return True
        if self.hurt_timer > 0:
            self.hurt_timer -= 1
        self._tick_effects()
        if "speed" in self.effects:
            speed *= 1.5
        if self.dash_active > 0:
            self.dash_active -= 1
        if self.dash_cooldown > 0:
            self.dash_cooldown -= 1
        if self.attack_cooldown > 0:
            self.attack_cooldown -= 1

        moving = False
        dx = 0.0
        dy = 0.0
        if btn_any(pyxel.KEY_LEFT, pyxel.KEY_A):
            dx -= speed; self.facing = -1; moving = True
        if btn_any(pyxel.KEY_RIGHT, pyxel.KEY_D):
            dx += speed; self.facing = 1; moving = True
        if btn_any(pyxel.KEY_UP, pyxel.KEY_W):
            dy -= speed; moving = True
        if btn_any(pyxel.KEY_DOWN, pyxel.KEY_S):
            dy += speed; moving = True
        if dx != 0 and dy != 0:
            dx *= 0.7071; dy *= 0.7071

        if (self.has_dash and pyxel.btnp(pyxel.KEY_SHIFT)
                and self.dash_cooldown <= 0):
            self.dash_active = 8
            self.dash_cooldown = 45
            if abs(dx) < 0.01 and abs(dy) < 0.01:
                self.dash_dir_x = self.facing
                self.dash_dir_y = 0
            else:
                nx, ny = normalize(dx, dy)
                self.dash_dir_x = nx; self.dash_dir_y = ny
            Sound.play(0, 4)

        if self.dash_active > 0:
            dx = self.dash_dir_x * 5.5
            dy = self.dash_dir_y * 5.5

        if blocked_fn is None:
            self.x += dx
            self.y += dy
        else:
            new_x = self.x + dx
            if not blocked_fn(new_x, self.y, self.w, self.h):
                self.x = new_x
            new_y = self.y + dy
            if not blocked_fn(self.x, new_y, self.w, self.h):
                self.y = new_y

        if moving or self.dash_active > 0:
            self.anim_tick += 1
            if self.anim_tick >= 6:
                self.anim_tick = 0
                self.anim_frame = 1 if self.anim_frame != 1 else 2
        else:
            self.anim_frame = 0
            self.anim_tick = 0
        return moving

    def update_platformer(self, base_speed, gravity, max_fall,
                          jump_vel, blocked_fn, allow_drop_through=False):
        if not self.alive:
            return False
        if self.fly_mode:
            # Free flight — ignore gravity & walls (god mode)
            fly_speed = 4.0
            if btn_any(pyxel.KEY_LEFT, pyxel.KEY_A):
                self.x -= fly_speed; self.facing = -1
            if btn_any(pyxel.KEY_RIGHT, pyxel.KEY_D):
                self.x += fly_speed; self.facing = 1
            if btn_any(pyxel.KEY_UP, pyxel.KEY_W):
                self.y -= fly_speed
            if btn_any(pyxel.KEY_DOWN, pyxel.KEY_S):
                self.y += fly_speed
            self.vy = 0
            self.on_ground = True
            self.jumps_left = self.max_jumps
            if self.hurt_timer > 0:
                self.hurt_timer -= 1
            self._tick_effects()
            return True
        if self.hurt_timer > 0:
            self.hurt_timer -= 1
        self._tick_effects()
        speed = base_speed
        if "speed" in self.effects:
            speed *= 1.4
        if self.dash_active > 0:
            self.dash_active -= 1
        if self.dash_cooldown > 0:
            self.dash_cooldown -= 1
        if self.attack_cooldown > 0:
            self.attack_cooldown -= 1
        if self.jump_buffer > 0:
            self.jump_buffer -= 1
        if self.coyote > 0:
            self.coyote -= 1

        moving = False
        vx = 0.0
        if btn_any(pyxel.KEY_LEFT, pyxel.KEY_A):
            vx -= speed; self.facing = -1; moving = True
        if btn_any(pyxel.KEY_RIGHT, pyxel.KEY_D):
            vx += speed; self.facing = 1; moving = True

        if btnp_any(pyxel.KEY_SPACE, pyxel.KEY_UP, pyxel.KEY_W):
            self.jump_buffer = 6

        if self.jump_buffer > 0:
            jump_pwr = jump_vel
            if "jump" in self.effects:
                jump_pwr = jump_vel - 1.5
            if self.on_ground or self.coyote > 0:
                self.vy = jump_pwr
                self.jumps_left = self.max_jumps - 1
                Sound.play(0, 4)
                self.jump_buffer = 0
                self.coyote = 0
            elif self.jumps_left > 0:
                self.vy = jump_pwr * 0.85
                self.jumps_left -= 1
                Sound.play(0, 4)
                self.jump_buffer = 0

        if self.vy < 0 and not btn_any(pyxel.KEY_SPACE, pyxel.KEY_UP, pyxel.KEY_W):
            self.vy *= 0.86

        if (self.has_dash and pyxel.btnp(pyxel.KEY_SHIFT)
                and self.dash_cooldown <= 0):
            self.dash_active = 8
            self.dash_cooldown = 45
            self.dash_dir_x = self.facing
            self.dash_dir_y = 0
            self.vy = 0
            Sound.play(0, 4)

        if self.dash_active <= 0:
            self.vy += gravity
            if self.vy > max_fall:
                self.vy = max_fall
        else:
            vx = self.dash_dir_x * 5.5

        was_on_ground = self.on_ground
        new_x = self.x + vx
        if not blocked_fn(new_x, self.y, self.w, self.h):
            self.x = new_x
        elif vx != 0:
            # Snap flush against the wall instead of leaving a gap / clipping
            step = 0.5 if vx > 0 else -0.5
            while not blocked_fn(self.x + step, self.y, self.w, self.h):
                self.x += step
            if self.dash_active > 0:
                self.dash_active = 0  # cancel dash on wall hit

        self.on_ground = False
        new_y = self.y + self.vy
        if not blocked_fn(self.x, new_y, self.w, self.h):
            self.y = new_y
        else:
            if self.vy > 0:
                step = 0.5
                while not blocked_fn(self.x, self.y + step, self.w, self.h):
                    self.y += step
                self.vy = 0
                self.on_ground = True
                self.jumps_left = self.max_jumps
            else:
                self.vy = 0

        if was_on_ground and not self.on_ground and self.vy >= 0:
            self.coyote = 5

        if moving and self.on_ground:
            self.anim_tick += 1
            if self.anim_tick >= 5:
                self.anim_tick = 0
                self.anim_frame = 1 if self.anim_frame != 1 else 2
        elif not self.on_ground:
            self.anim_frame = 0
        else:
            self.anim_frame = 0
            self.anim_tick = 0
        return moving

    def apply_potion(self, name, duration):
        self.effects[name] = duration
        if name == "heal":
            self.health = self.max_health
        elif name == "stamina":
            self.effects.pop("stamina", None)
        elif name == "double_jump":
            self.max_jumps = 2

    def _tick_effects(self):
        expired = []
        for k in list(self.effects.keys()):
            self.effects[k] -= 1
            if self.effects[k] <= 0:
                expired.append(k)
        for k in expired:
            del self.effects[k]
            if k == "double_jump":
                self.max_jumps = 1
                self.jumps_left = min(self.jumps_left, 1)

    def draw(self, cam_x, cam_y):
        if not self.alive:
            return
        sx, sy = self.sprite_pos()
        dx = int(sx - cam_x)
        dy = int(sy - cam_y)
        if dx < -32 or dx > SCREEN_W or dy < -32 or dy > SCREEN_H:
            return

        if self.facing >= 0:
            frames = [SPR_PLAYER_R_IDLE, SPR_PLAYER_R_RUN1, SPR_PLAYER_R_RUN2]
        else:
            frames = [SPR_PLAYER_L_IDLE, SPR_PLAYER_L_RUN1, SPR_PLAYER_L_RUN2]
        u, v, w, h = frames[self.anim_frame]

        flash = self.hurt_timer > 0 and self.hurt_timer % 4 < 2
        if flash:
            for c in range(1, 16):
                pyxel.pal(c, COL_WHITE)
        pyxel.blt(dx, dy, 0, u, v, w, h, COLKEY)
        if flash:
            pyxel.pal()

        if "shield" in self.effects:
            r = 18 + int(math.sin(pyxel.frame_count * 0.4) * 2)
            pyxel.circb(dx + 16, dy + 16, r, COL_YELLOW)

        if self.dash_active > 0:
            for i in range(3):
                pyxel.pset(dx + 12 + i * 2, dy + 16, COL_LBLUE)
                pyxel.pset(dx + 12 + i * 2, dy + 18, COL_WHITE)

        # NEW: Carrots float higher above the head and are bigger
        if self.carrying_carrots > 0:
            bob = int(math.sin(pyxel.frame_count * 0.18) * 1)
            base_y = dy - 18 + bob
            for i in range(self.carrying_carrots):
                pyxel.blt(dx + 2 + i * 8, base_y, 1,
                          SPR_CARROT[0], SPR_CARROT[1], 16, 16, COLKEY)


# ---------------------------------------------------------------------
class Carrot:
    def __init__(self, x, y):
        self.x = float(x)
        self.y = float(y)
        self.w = 12
        self.h = 12
        self.alive = True
        self.bob = random.random() * math.pi * 2

    def rect(self):
        return (int(self.x), int(self.y), self.w, self.h)

    def update(self):
        self.bob += 0.1

    def draw(self, cam_x, cam_y):
        if not self.alive:
            return
        u, v, w, h = SPR_CARROT
        offset = int(math.sin(self.bob) * 2)
        pyxel.blt(int(self.x - cam_x - 2),
                  int(self.y - cam_y - 2) + offset,
                  1, u, v, w, h, COLKEY)


# ---------------------------------------------------------------------
class Coin:
    def __init__(self, x, y):
        self.x = float(x)
        self.y = float(y)
        self.w = 8
        self.h = 8
        self.alive = True
        self.t = random.random() * math.pi * 2

    def rect(self):
        return (int(self.x), int(self.y), self.w, self.h)

    def update(self):
        self.t += 0.15

    def draw(self, cam_x, cam_y):
        if not self.alive:
            return
        u, v, w, h = SPR_COIN
        offset = int(math.sin(self.t) * 1.5)
        pyxel.blt(int(self.x - cam_x), int(self.y - cam_y) + offset,
                  0, u, v, w, h, COLKEY)


# ---------------------------------------------------------------------
class BabyHare:
    def __init__(self, x, y):
        self.x = float(x)
        self.y = float(y)
        self.w = 14
        self.h = 14
        self.alive = True
        self.facing = random.choice([-1, 1])
        self.anim_tick = 0
        self.anim_frame = 0
        self.has_carrot = False
        self.bob = random.random() * math.pi * 2

    def rect(self):
        return (int(self.x), int(self.y), self.w, self.h)

    def update(self):
        self.bob += 0.08
        self.anim_tick += 1
        if self.anim_tick >= 12:
            self.anim_tick = 0
            self.anim_frame = 1 - self.anim_frame

    def draw(self, cam_x, cam_y):
        if not self.alive:
            return
        if self.has_carrot:
            if self.facing < 0:
                slot = SPR_BABY_L_CARROT_1 if self.anim_frame == 0 else SPR_BABY_L_CARROT_2
            else:
                slot = SPR_BABY_R_CARROT_1 if self.anim_frame == 0 else SPR_BABY_R_CARROT_2
        else:
            if self.facing < 0:
                slot = SPR_BABY_L_1 if self.anim_frame == 0 else SPR_BABY_L_2
            else:
                slot = SPR_BABY_R_1 if self.anim_frame == 0 else SPR_BABY_R_2
        u, v, w, h = slot
        offset = int(math.sin(self.bob) * 1.5)
        pyxel.blt(int(self.x - cam_x), int(self.y - cam_y) + offset,
                  0, u, v, w, h, COLKEY)


# ---------------------------------------------------------------------
class Fox:
    def __init__(self, x, y):
        self.x = float(x)
        self.y = float(y)
        self.w = 40
        self.h = 22
        self.vx = 0.0
        self.vy = 0.0
        self.speed = 1.1
        self.facing = -1
        self.alive = True
        self.growl_timer = 0
        self.aware = False

    def rect(self):
        return (int(self.x) + 8, int(self.y) + 6, self.w, self.h)

    def update(self, player):
        if not self.alive or not player.alive:
            return
        d = dist(self.x, self.y, player.x, player.y)
        if d < 220:
            dx = player.x - self.x
            dy = player.y - self.y
            nx, ny = normalize(dx, dy)
            self.x += nx * self.speed
            self.y += ny * self.speed
            self.facing = 1 if dx > 0 else -1
            if not self.aware:
                self.aware = True
                Sound.play(2, 10)
        else:
            self.aware = False

        self.x = clamp(self.x, 0, MAP_MEADOW_W - 56)
        self.y = clamp(self.y, 0, MAP_MEADOW_H - 32)
        if self.growl_timer > 0:
            self.growl_timer -= 1

    def draw(self, cam_x, cam_y):
        if not self.alive:
            return
        u, v, w, h = SPR_FOX_RIGHT if self.facing >= 0 else SPR_FOX_LEFT
        sx = int(self.x - cam_x)
        sy = int(self.y - cam_y)
        if sx < -64 or sx > SCREEN_W or sy < -40 or sy > SCREEN_H:
            return
        pyxel.blt(sx, sy, 1, u, v, w, h, COLKEY)
        if self.aware and pyxel.frame_count % 20 < 10:
            pyxel.text(sx + 26, sy - 6, "!", COL_RED)


# ---------------------------------------------------------------------
class Farmer:
    def __init__(self, x, y, maze):
        self.x = float(x)
        self.y = float(y)
        self.w = 24
        self.h = 28
        self.maze = maze
        self.speed_patrol = 0.7
        self.speed_chase = 1.45
        self.state = "patrol"
        self.alert_timer = 0
        self.facing = 1
        self.route = self._build_route()
        self.route_index = 0
        self.damage_cooldown = 0

    def _build_route(self):
        opens = self.maze.open_tiles
        if len(opens) < 4:
            return [(self.x, self.y)]
        tile = getattr(self.maze, "tile", TILE)
        route_tiles = []
        step = max(1, len(opens) // 6)
        for i in range(0, len(opens), step):
            tx, ty = opens[i]
            route_tiles.append((tx * tile + 2, ty * tile + 2))
            if len(route_tiles) >= 6:
                break
        return route_tiles

    def rect(self):
        return (int(self.x) + 4, int(self.y) + 4, self.w, self.h)

    def center(self):
        return (self.x + 16, self.y + 16)

    def can_see_player(self, player):
        fx, fy = self.center()
        px, py = player.x + player.w / 2, player.y + player.h / 2
        dx = px - fx
        dy = py - fy
        if self.facing == 1 and dx < -6:
            return False
        if self.facing == -1 and dx > 6:
            return False
        d = math.sqrt(dx * dx + dy * dy)
        if d > 80:
            return False
        if abs(dy) > 20 + abs(dx) * 0.3:
            return False
        return self.maze.line_clear(fx, fy, px, py)

    def update(self, player):
        if self.damage_cooldown > 0:
            self.damage_cooldown -= 1
        seen = self.can_see_player(player)
        if seen:
            self.state = "chase"
            if self.alert_timer < 90:
                Sound.play(2, 10)
            self.alert_timer = 90
        else:
            if self.state == "chase":
                self.alert_timer -= 1
                if self.alert_timer <= 0:
                    self.state = "patrol"

        if self.state == "chase":
            goal_x = player.x
            goal_y = player.y
            speed = self.speed_chase
        else:
            tx, ty = self.route[self.route_index]
            if dist(self.x, self.y, tx, ty) < 4:
                self.route_index = (self.route_index + 1) % len(self.route)
            goal_x, goal_y = self.route[self.route_index]
            speed = self.speed_patrol

        # Smart axis-aligned movement with sidestep fallback when blocked
        dx = goal_x - self.x
        dy = goal_y - self.y
        moved_x = False
        moved_y = False

        # X first
        if abs(dx) > 0.5:
            step_x = (1 if dx > 0 else -1) * speed
            if not self.maze.collides(self.x + step_x, self.y, self.w, self.h):
                self.x += step_x
                self.facing = 1 if step_x > 0 else -1
                moved_x = True

        # Y second
        if abs(dy) > 0.5:
            step_y = (1 if dy > 0 else -1) * speed
            if not self.maze.collides(self.x, self.y + step_y, self.w, self.h):
                self.y += step_y
                moved_y = True

        # If totally blocked, try sidestep perpendicular to goal
        if not moved_x and not moved_y:
            for sidestep_dy in (-speed, speed):
                if not self.maze.collides(self.x, self.y + sidestep_dy, self.w, self.h):
                    self.y += sidestep_dy
                    moved_y = True
                    break
            if not moved_y:
                for sidestep_dx in (-speed, speed):
                    if not self.maze.collides(self.x + sidestep_dx, self.y, self.w, self.h):
                        self.x += sidestep_dx
                        moved_x = True
                        break

    def draw(self, cam_x, cam_y):
        u, v, w, h = SPR_FARMER
        sx = int(self.x - cam_x)
        sy = int(self.y - cam_y)
        if sx < -32 or sx > SCREEN_W or sy < -32 or sy > SCREEN_H:
            return
        flip_w = -w if self.facing < 0 else w
        flash = self.state == "chase" and pyxel.frame_count % 10 < 5
        if flash:
            for c in range(1, 16):
                pyxel.pal(c, COL_RED)
        pyxel.blt(sx, sy, 0, u, v, flip_w, h, COLKEY)
        if flash:
            pyxel.pal()

        cone_col = COL_RED if self.state == "chase" else COL_YELLOW
        cone_x = sx + (24 if self.facing > 0 else 8)
        cone_y = sy + 16
        for r in range(20, 80, 8):
            tip_x = cone_x + r * self.facing
            pyxel.line(cone_x, cone_y, tip_x, cone_y - r // 4, cone_col)
            pyxel.line(cone_x, cone_y, tip_x, cone_y + r // 4, cone_col)
        if self.state == "chase":
            pyxel.text(sx + 10, sy - 8, "!", COL_RED)


# ---------------------------------------------------------------------
# RIVAL — head sits ON TOP of bar edge
# ---------------------------------------------------------------------
class Rival:
    def __init__(self):
        self.x = 0.0
        self.y = 0.0
        self.w = 16
        self.h = 16
        self.sprite_size = 32
        self.facing = 1
        self.last_x = 0.0
        self.is_moving = False
        self.progress = 0.0
        self.fill_rate = 1.0 / (45 * FPS)
        self.delay_frames = 0
        self.attack_mode = False
        self.attack_frames = 0
        self.retreat_frames = 0
        self.anim_frame = 0
        self.anim_tick = 0
        self.wander_angle = 0.0
        self.wander_timer = 0
        self.failed = False
        # When True, the rival physically chases the player (early levels).
        self.chase_enabled = False

    def rect(self):
        return (int(self.x), int(self.y), self.w, self.h)

    def reset_for_level(self, level_num):
        self.progress = 0.0
        self.attack_mode = False
        self.attack_frames = 0
        self.retreat_frames = 0
        # Constant fill: bar reaches full in a fixed time. 45s default.
        self.fill_rate = 1.0 / (45 * FPS)
        self.delay_frames = 0
        self.failed = False
        self.chase_enabled = False

    def set_time_limit(self, seconds):
        """Set how long until the bar is full (level failed)."""
        self.fill_rate = 1.0 / (seconds * FPS)

    def head_start_delay(self, frames):
        self.delay_frames = frames

    def update_meter(self):
        dx = self.x - self.last_x
        self.is_moving = abs(dx) > 0.1
        if dx > 0.05:
            self.facing = 1
        elif dx < -0.05:
            self.facing = -1
        self.last_x = self.x
        self.anim_tick += 1
        anim_speed = 8
        if self.is_moving and self.anim_tick >= anim_speed:
            self.anim_tick = 0
            self.anim_frame = (self.anim_frame + 1) % 2
        elif not self.is_moving:
            self.anim_frame = 0
        if self.delay_frames > 0:
            self.delay_frames -= 1
            return
        # Constant fill — bar rises steadily. Full = level failed.
        self.progress = clamp(self.progress + self.fill_rate, 0.0, 1.0)
        if self.progress >= 1.0:
            self.failed = True

    def punish(self, amount=0.05):
        self.progress = clamp(self.progress + amount, 0.0, 1.0)
        if self.progress >= 1.0:
            self.failed = True

    def reward(self, amount=0.04):
        self.progress = clamp(self.progress - amount, 0.0, 1.0)

    def end_attack(self, reset_to=0.78):
        self.attack_mode = False
        self.attack_frames = 0
        self.retreat_frames = FPS
        self.progress = reset_to

    def wander(self, map_w, map_h):
        self.wander_timer -= 1
        if self.wander_timer <= 0:
            self.wander_timer = random.randint(40, 90)
            self.wander_angle += random.uniform(-1.2, 1.2)
        speed = 0.6 + self.progress * 0.4
        self.x += math.cos(self.wander_angle) * speed
        self.y += math.sin(self.wander_angle) * speed
        self.x = clamp(self.x, 0, map_w - self.w)
        self.y = clamp(self.y, 0, map_h - self.h)

    def chase_player(self, player, speed=2.5):
        nx, ny = normalize(player.x - self.x, player.y - self.y)
        self.x += nx * speed
        self.y += ny * speed

    def retreat_from_player(self, player, speed=1.6):
        nx, ny = normalize(self.x - player.x, self.y - player.y)
        self.x += nx * speed
        self.y += ny * speed

    def draw(self, cam_x, cam_y, dim=False):
        offset = (self.sprite_size - self.w) // 2
        sx = int(self.x - cam_x) - offset
        sy = int(self.y - cam_y) - offset
        if self.facing >= 0:
            sprite = SPR_RIVAL_R_2 if self.anim_frame == 1 else SPR_RIVAL_R_1
        else:
            sprite = SPR_RIVAL_L_2 if self.anim_frame == 1 else SPR_RIVAL_L_1
        u, v, w, h = sprite
        if dim:
            for c in range(1, 16):
                pyxel.pal(c, COL_DGRAY)
        if self.attack_mode and pyxel.frame_count % 6 < 3:
            for c in range(1, 16):
                pyxel.pal(c, COL_RED)
        pyxel.blt(sx, sy, 0, u, v, w, h, COLKEY)
        if dim or (self.attack_mode and pyxel.frame_count % 6 < 3):
            pyxel.pal()
        if self.attack_mode:
            cx = sx + self.sprite_size // 2
            cy = sy + self.sprite_size // 2
            r = self.sprite_size // 2 + 2 + int(math.sin(pyxel.frame_count * 0.5) * 2)
            pyxel.circb(cx, cy, r, COL_RED)

    def draw_meter(self):
        # Bar at right edge, head sits ON TOP of bar (upper edge cap)
        bar_x = SCREEN_W - 14
        bar_y = 28
        bar_h = 180
        bar_w = 10
        head_size = 16

        # Background panel
        panel_x = bar_x - 4
        panel_y = bar_y - 4
        panel_w = bar_w + 8
        panel_h = bar_h + 8
        pyxel.rect(panel_x, panel_y, panel_w, panel_h, COL_BLACK)
        pyxel.rectb(panel_x, panel_y, panel_w, panel_h, COL_WHITE)

        # Bar background
        pyxel.rect(bar_x, bar_y, bar_w, bar_h, COL_NAVY)

        # Fill bottom-up
        fill_h = int(bar_h * self.progress)
        if self.attack_mode:
            col = COL_RED
        elif self.progress > 0.7:
            col = COL_ORANGE
        elif self.progress > 0.35:
            col = COL_YELLOW
        else:
            col = COL_LGREEN
        if self.delay_frames > 0:
            col = COL_LGRAY
        pyxel.rect(bar_x, bar_y + bar_h - fill_h, bar_w, fill_h, col)
        pyxel.rectb(bar_x, bar_y, bar_w, bar_h, COL_WHITE)

        # Tick marks like a level indicator
        for tick_pct in (0.25, 0.5, 0.75):
            ty = bar_y + bar_h - int(bar_h * tick_pct)
            pyxel.line(bar_x - 2, ty, bar_x, ty, COL_LGRAY)

        # Head sits AT top edge of bar (its bottom flush with bar top)
        head_x = bar_x + bar_w // 2 - head_size // 2
        head_y = bar_y - head_size + 2

        if self.attack_mode and pyxel.frame_count % 6 < 3:
            for c in range(1, 16):
                pyxel.pal(c, COL_RED)
        u, v, w, h = SPR_RIVAL_HEAD
        pyxel.blt(head_x, head_y, 0, u, v, w, h, COLKEY)
        if self.attack_mode and pyxel.frame_count % 6 < 3:
            pyxel.pal()

        if self.attack_mode and pyxel.frame_count % 6 < 3:
            pyxel.rectb(bar_x - 1, bar_y - 1, bar_w + 2, bar_h + 2, COL_RED)


# ---------------------------------------------------------------------
# HotBunnyAttraction + LovePullMeter (for Attraction level)
# ---------------------------------------------------------------------
class LovePullMeter:
    def __init__(self):
        self.progress = 0.0
        self.fill_rate = 0.0022
        self.delay_frames = 0
        self.attack_frames = 0
        self.retreat_frames = 0
        self.attack_mode = False
        self.pulse = 0

    def configure(self, level_no=3):
        self.progress = 0.0
        self.attack_frames = 0
        self.retreat_frames = 0
        self.attack_mode = False
        self.delay_frames = 0
        self.fill_rate = 0.0020 + 0.00075 * max(0, level_no - 1)

    def update_fill(self):
        self.pulse += 1
        if self.delay_frames > 0:
            self.delay_frames -= 1
            return
        if not self.attack_mode:
            self.progress = clamp(self.progress + self.fill_rate, 0.0, 1.0)
            if self.progress >= 1.0:
                self.attack_mode = True
                self.attack_frames = 5 * FPS
                Sound.play(2, 16)

    def end_attack(self, reset_to=0.83):
        self.attack_mode = False
        self.attack_frames = 0
        self.retreat_frames = FPS
        self.progress = reset_to


class HotBunnyAttraction:
    """Lila — the female bunny who floats on a cloud like a goddess.
    Her AI actively tries to drag the player into pits."""

    def __init__(self, x=70, y=80):
        self.x = float(x)
        self.y = float(y)
        self.w = 48          # bigger goddess-scale hitbox for the "meet" check
        self.h = 28
        self.anim = 0
        self.anim_timer = 0
        self.love_heart_timer = 0
        self.float_base_y = y
        # Lila stays AHEAD of the player so the pull works
        self.normal_offset_from_player = 110
        # Hazard awareness — set by the level so Lila can pull toward holes
        self.hazard_zones = []  # list of (x_min, x_max) where pits are
        self.target_hazard = None
        self.hazard_check_timer = 0
        self.blocked_fn = None  # set by the level for wall-safe pulling

    def set_hazards(self, hazards):
        """Pass list of (x_min, x_max) hole zones for Lila to target."""
        self.hazard_zones = hazards

    def set_blocked_fn(self, fn):
        """Pass the level's collision test so the pull never clips walls."""
        self.blocked_fn = fn

    def _animate(self):
        self.anim_timer += 1
        if self.anim_timer > 9:
            self.anim_timer = 0
            self.anim = (self.anim + 1) % 2

    def _pick_hazard_target(self, player):
        # Periodically pick the closest pit AHEAD of the player to lure toward
        self.hazard_check_timer -= 1
        if self.hazard_check_timer > 0:
            return
        self.hazard_check_timer = 60
        best = None
        best_d = 999999
        for x_min, x_max in self.hazard_zones:
            mid = (x_min + x_max) / 2
            if mid > player.x and mid - player.x < 200:
                d = mid - player.x
                if d < best_d:
                    best_d = d
                    best = mid
        self.target_hazard = best

    def update_background_position(self, player, meter):
        # Decide target: hover ahead of player, OR hover over a pit
        target_x = player.x + self.normal_offset_from_player
        if meter.attack_mode and self.target_hazard is not None:
            # Float directly over a pit to drag the player in
            target_x = self.target_hazard
        elif meter.progress > 0.5 and self.target_hazard is not None:
            # Even in normal mode, drift toward nearest hazard when meter is high
            target_x = self.target_hazard
        self.x = approach(self.x, target_x, 1.6 + meter.progress * 2.4)
        # Float bob on the cloud
        self.y = self.float_base_y + math.sin(pyxel.frame_count / 18) * 6
        self._animate()

    def apply_pull_to_player(self, player, meter):
        # Pull horizontally toward Lila — stronger AND now also vertical down-pull
        strength = meter.progress * 0.075
        if meter.attack_mode:
            if meter.attack_frames > 3 * FPS:
                meter.attack_frames = 3 * FPS
            strength = 0.32
            meter.attack_frames -= 1
            if meter.attack_frames <= 0:
                meter.end_attack(0.78)
        pull_dir = 0
        if self.x > player.x:
            pull_dir = 1
        elif self.x < player.x:
            pull_dir = -1
        dx = pull_dir * strength * FPS / 10
        # Respect walls — never drag the player INTO solid tiles
        new_x = player.x + dx
        if self.blocked_fn is None or not self.blocked_fn(
                new_x, player.y, player.w, player.h):
            player.x = new_x
        # Small downward nudge during attacks — makes pit traps lethal
        if meter.attack_mode and player.mode == "platformer":
            player.vy = max(player.vy, 1.5)

    def update(self, player, meter):
        meter.update_fill()
        self._pick_hazard_target(player)
        self.update_background_position(player, meter)
        self.apply_pull_to_player(player, meter)

    def draw_heart_chain(self, cam_x, cam_y, player, strength):
        if strength <= 0.02:
            return
        self.love_heart_timer += 1
        count = 2 + int(strength * 6)
        for i in range(count):
            t = (i + 1) / (count + 1)
            hx = player.x * (1 - t) + self.x * t - cam_x
            hy = (player.y * (1 - t) + self.y * t - cam_y
                  - 8 + math.sin((self.love_heart_timer + i * 7) / 6) * 3)
            u, v, w, h = SPR_HEART
            pyxel.blt(int(hx) - 4, int(hy) - 4, 0, u, v, w, h, COLKEY)

    def draw(self, cam_x, cam_y, player, meter):
        # ============================================================
        # LILA — the goddess on the cloud. Drawn BIG and dramatic.
        # Everything is 2x the old scale + divine light effects.
        # ============================================================
        cx = int(self.x - cam_x)
        cy = int(self.y - cam_y)
        fc = pyxel.frame_count
        intensity = meter.progress  # 0..1 how strong her pull is

        # --- DIVINE LIGHT RAYS radiating from behind her ---
        center_x = cx + 28
        center_y = cy + 16
        ray_count = 12
        ray_len = 40 + int(intensity * 30) + int(math.sin(fc * 0.1) * 4)
        for i in range(ray_count):
            ang = (i / ray_count) * math.tau + fc * 0.012
            ex = center_x + math.cos(ang) * ray_len
            ey = center_y + math.sin(ang) * ray_len * 0.7
            ray_col = COL_YELLOW if (i + fc // 6) % 2 == 0 else COL_PEACH
            if meter.attack_mode:
                ray_col = COL_RED if (i + fc // 4) % 2 == 0 else COL_ORANGE
            pyxel.line(center_x, center_y, int(ex), int(ey), ray_col)

        # --- OUTER GLOW AURA (pulsing rings) ---
        for ring in range(3):
            r = 24 + ring * 7 + int(math.sin(fc * 0.15 + ring) * 3)
            aura_col = COL_PEACH if not meter.attack_mode else COL_PINK
            pyxel.circb(center_x, center_y, r, aura_col)

        # --- BIG GLOWING CLOUD ---
        cloud_col = COL_WHITE
        if meter.attack_mode and fc % 8 < 4:
            cloud_col = COL_PINK
        glow_col = COL_LBLUE if not meter.attack_mode else COL_RED
        # Cloud glow underlay
        pyxel.circ(cx + 12, cy + 30, 14, glow_col)
        pyxel.circ(cx + 30, cy + 28, 16, glow_col)
        pyxel.circ(cx + 46, cy + 30, 14, glow_col)
        # Main cloud body (bigger)
        pyxel.rect(cx - 4, cy + 26, 64, 12, cloud_col)
        pyxel.rect(cx + 2, cy + 34, 52, 6, cloud_col)
        pyxel.circ(cx + 6, cy + 28, 10, cloud_col)
        pyxel.circ(cx + 20, cy + 25, 12, cloud_col)
        pyxel.circ(cx + 36, cy + 25, 12, cloud_col)
        pyxel.circ(cx + 50, cy + 28, 10, cloud_col)
        # Cloud shadow line
        pyxel.rect(cx + 2, cy + 40, 52, 2, COL_LGRAY)

        # --- LILA HERSELF — the lying-down heart-eyed bunny (SPR_LILA), ---
        # the exact same sprite shown on the title screen. She rests on
        # the cloud with all the divine effects around her.
        bob = int(math.sin(fc * 0.08) * 2)
        ly = cy + bob
        lu, lv, lw, lh = SPR_LILA   # 48x32 lying-down bunny
        # Center her on the cloud (cloud spans roughly cx-4 .. cx+60)
        sprite_x = cx + 4
        sprite_y = ly + 8
        # Soft pink shimmer when she's actively pulling (attack mode)
        if meter.attack_mode and fc % 8 < 4:
            pyxel.pal(COL_LGRAY, COL_PINK)
            pyxel.pal(COL_WHITE, COL_PINK)
        pyxel.blt(sprite_x, sprite_y, 0, lu, lv, lw, lh, COLKEY)
        if meter.attack_mode and fc % 8 < 4:
            pyxel.pal()

        # --- HALO / CROWN OF LIGHT above her head (head is on the RIGHT) ---
        halo_x = sprite_x + 34
        halo_y = sprite_y - 2
        halo_r = 11 + int(math.sin(fc * 0.2) * 2)
        halo_col = COL_YELLOW if not meter.attack_mode else COL_RED
        pyxel.circb(halo_x, halo_y, halo_r, halo_col)
        pyxel.circb(halo_x, halo_y, halo_r - 3, halo_col)

        # --- SPARKLES floating around her ---
        for i in range(6):
            sp_ang = (i / 6) * math.tau + fc * 0.05
            sp_r = 30 + math.sin(fc * 0.1 + i) * 6
            sx = center_x + int(math.cos(sp_ang) * sp_r)
            sy = center_y + int(math.sin(sp_ang) * sp_r * 0.6)
            if (fc + i * 5) % 20 < 12:
                pyxel.pset(sx, sy, COL_WHITE)
                pyxel.pset(sx + 1, sy, COL_YELLOW)
                pyxel.pset(sx - 1, sy, COL_YELLOW)
                pyxel.pset(sx, sy + 1, COL_YELLOW)
                pyxel.pset(sx, sy - 1, COL_YELLOW)

        # --- FLOATING HEARTS rising from her ---
        for i in range(4):
            hb_t = (fc + i * 30) % 120
            hh_x = center_x - 10 + i * 7 + int(math.sin((fc + i * 20) * 0.1) * 4)
            hh_y = center_y - 10 - hb_t // 4
            if hb_t < 100:
                u, v, w, h = SPR_HEART
                pyxel.blt(hh_x, hh_y, 0, u, v, w, h, COLKEY)

        # --- NAME BANNER ---
        name = LILA_NAME
        banner_w = len(name) * 4 + 8
        banner_x = center_x - banner_w // 2
        banner_y = cy - 16
        pyxel.rect(banner_x, banner_y, banner_w, 9, COL_PINK)
        pyxel.rectb(banner_x, banner_y, banner_w, 9, COL_WHITE)
        pyxel.text(banner_x + 4, banner_y + 2, name, COL_WHITE)

        # --- ATTACK MODE: dramatic warning ---
        if meter.attack_mode:
            if fc % 10 < 5:
                draw_centered("LILA CALLS YOU!", banner_y - 10, COL_RED,
                              x_mid=center_x)
            # Screen-edge red vignette pulse handled by level

        # Heart chain connecting her to the player (the "pull")
        self.draw_heart_chain(cam_x, cam_y, player, meter.progress)
# =====================================================================
# RANDOM MAZE GENERATOR
# =====================================================================
class RandomMaze:
    def __init__(self, cols=16, rows=16, tile_size=24, seed=None):
        self.cols = cols
        self.rows = rows
        self.tile = tile_size
        self.width = cols * tile_size
        self.height = rows * tile_size
        if seed is not None:
            random.seed(seed)
        self.grid = self._generate()
        self.open_tiles = []
        self.dead_ends = []
        for ty in range(rows):
            for tx in range(cols):
                if self.grid[ty][tx] == 0:
                    self.open_tiles.append((tx, ty))
                    walls = sum(1 for d in [(1, 0), (-1, 0), (0, 1), (0, -1)]
                                if self._is_wall_at(tx + d[0], ty + d[1]))
                    if walls >= 3:
                        self.dead_ends.append((tx, ty))
        if len(self.dead_ends) < 6:
            self.dead_ends = random.sample(self.open_tiles,
                                           min(10, len(self.open_tiles)))

    def _is_wall_at(self, tx, ty):
        if 0 <= ty < self.rows and 0 <= tx < self.cols:
            return self.grid[ty][tx] == 1
        return True

    def _generate(self):
        g = [[1 for _ in range(self.cols)] for _ in range(self.rows)]
        stack = [(1, 1)]
        g[1][1] = 0
        while stack:
            cx, cy = stack[-1]
            dirs = [(2, 0), (-2, 0), (0, 2), (0, -2)]
            random.shuffle(dirs)
            moved = False
            for dx, dy in dirs:
                nx, ny = cx + dx, cy + dy
                if (1 <= nx < self.cols - 1 and 1 <= ny < self.rows - 1
                        and g[ny][nx] == 1):
                    g[cy + dy // 2][cx + dx // 2] = 0
                    g[ny][nx] = 0
                    stack.append((nx, ny))
                    moved = True
                    break
            if not moved:
                stack.pop()
        breaks = (self.cols * self.rows) // 24
        for _ in range(breaks):
            bx = random.randint(2, self.cols - 3)
            by = random.randint(2, self.rows - 3)
            if g[by][bx] == 1:
                g[by][bx] = 0
        for x in range(self.cols):
            g[0][x] = 1
            g[self.rows - 1][x] = 1
        for y in range(self.rows):
            g[y][0] = 1
            g[y][self.cols - 1] = 1
        return g

    def collides(self, x, y, w, h):
        pad = 4  # was 2 — tighter movement, less stuck-in-wall feel
        left = int((x + pad) // self.tile)
        right = int((x + w - pad - 1) // self.tile)
        top = int((y + pad) // self.tile)
        bottom = int((y + h - pad - 1) // self.tile)
        if right < left or bottom < top:
            return False
        for ty in range(top, bottom + 1):
            for tx in range(left, right + 1):
                if not self.is_open_tile(tx, ty):
                    return True
        return False

    def is_open_tile(self, tx, ty):
        if 0 <= ty < self.rows and 0 <= tx < self.cols:
            return self.grid[ty][tx] == 0
        return False

    def line_clear(self, ax, ay, bx, by):
        steps = int(max(abs(ax - bx), abs(ay - by)) // 4) + 1
        for i in range(steps + 1):
            t = i / max(1, steps)
            x = ax * (1 - t) + bx * t
            y = ay * (1 - t) + by * t
            if self.collides(x, y, 4, 4):
                return False
        return True

    def spawn_tile(self):
        for ty in range(1, min(5, self.rows)):
            for tx in range(1, min(5, self.cols)):
                if self.is_open_tile(tx, ty):
                    return tx, ty
        return self.open_tiles[0] if self.open_tiles else (1, 1)

    def goal_tile(self):
        for ty in range(self.rows - 2, max(0, self.rows - 6), -1):
            for tx in range(self.cols - 2, max(0, self.cols - 6), -1):
                if self.is_open_tile(tx, ty):
                    return tx, ty
        return self.open_tiles[-1] if self.open_tiles else (self.cols - 2, self.rows - 2)

    def random_open(self):
        return random.choice(self.open_tiles)

    def draw(self, cam_x, cam_y, wall_palette=None):
        pyxel.cls(COL_BROWN)
        for ty in range(self.rows):
            for tx in range(self.cols):
                x = tx * self.tile - cam_x
                y = ty * self.tile - cam_y
                if x < -self.tile or x > SCREEN_W or y < -self.tile or y > SCREEN_H:
                    continue
                ix, iy = int(x), int(y)
                ch = self.grid[ty][tx]
                if ch == 0:
                    pyxel.rect(ix, iy, self.tile, self.tile, COL_LGREEN)
                    if (tx + ty) % 3 == 0:
                        pyxel.pset(ix + 6, iy + 8, COL_GREEN)
                        pyxel.pset(ix + 14, iy + 12, COL_GREEN)
                    if (tx + ty) % 7 == 0:
                        pyxel.pset(ix + 10, iy + 6, COL_PINK)
                        pyxel.pset(ix + 11, iy + 6, COL_PINK)
                else:
                    self._draw_wall(ix, iy, wall_palette)

    def _draw_wall(self, ix, iy, palette):
        u, v, w, h = SPR_WALL_SHORT
        if palette:
            for c in range(1, 16):
                pyxel.pal(c, palette.get(c, c))
        pyxel.blt(ix, iy, 0, u, v, w, h, COLKEY)
        pyxel.rect(ix + 16, iy, 8, 24, COL_DGRAY)
        pyxel.rect(ix, iy + 16, 24, 8, COL_DGRAY)
        for hx in range(0, 24, 6):
            pyxel.pset(ix + hx + 1, iy + 16 + 2, COL_LGRAY)
            pyxel.pset(ix + 16 + 2, iy + hx + 1, COL_LGRAY)
        if palette:
            pyxel.pal()


# =====================================================================
# MAZE BLUEPRINT (Level Maze legacy)
# =====================================================================
class MazeBlueprint:
    ROWS = [
        "111111111111111111111111",
        "100000100000000010000001",
        "101110101111111010111101",
        "101000100000001010100001",
        "101011111011101010101111",
        "101010000010001000100001",
        "101010111110111111111101",
        "100010100000100000000101",
        "111110101111101111110101",
        "100000101000001000010101",
        "101111101011111011010101",
        "101000001010000010010001",
        "101011111010111110111101",
        "101010000010100000100001",
        "101010111110101111101101",
        "100010100000101000001001",
        "111010101111101011111101",
        "100010001000001010000001",
        "101111101011111010111101",
        "100000001010000010100001",
        "101111111010111110101111",
        "100000000010000000100001",
        "101111111111101111111101",
        "111111111111111111111111",
    ]

    def __init__(self):
        self.width = MAP_MAZE_W
        self.height = MAP_MAZE_H
        self.tile = TILE
        self.open_tiles = []
        for ty, row in enumerate(self.ROWS):
            for tx, ch in enumerate(row):
                if ch == "0":
                    self.open_tiles.append((tx, ty))
        self.carrot_tiles = [(1, 1), (22, 1), (21, 5), (1, 9), (15, 11),
                             (21, 13), (1, 17), (21, 17), (1, 21), (17, 21)]
        self.coin_tiles = [(3, 3), (12, 5), (19, 7), (5, 11), (14, 13),
                           (8, 17), (12, 19), (19, 21)]
        self.school_tile = (11, 11)

    def is_open_tile(self, tx, ty):
        if 0 <= ty < len(self.ROWS) and 0 <= tx < len(self.ROWS[ty]):
            return self.ROWS[ty][tx] == "0"
        return False

    def collides(self, x, y, w, h):
        pad = 4
        left = int((x + pad) // TILE)
        right = int((x + w - pad - 1) // TILE)
        top = int((y + pad) // TILE)
        bottom = int((y + h - pad - 1) // TILE)
        if right < left or bottom < top:
            return False
        for ty in range(top, bottom + 1):
            for tx in range(left, right + 1):
                if not self.is_open_tile(tx, ty):
                    return True
        return False

    def line_clear(self, ax, ay, bx, by):
        steps = int(max(abs(ax - bx), abs(ay - by)) // 4) + 1
        for i in range(steps + 1):
            t = i / max(1, steps)
            x = ax * (1 - t) + bx * t
            y = ay * (1 - t) + by * t
            if self.collides(x, y, 4, 4):
                return False
        return True

    def draw(self, cam_x, cam_y):
        pyxel.cls(COL_BROWN)
        for ty in range(MAZE_ROWS):
            for tx in range(MAZE_COLS):
                x = tx * TILE - cam_x
                y = ty * TILE - cam_y
                if x < -TILE or x > SCREEN_W or y < -TILE or y > SCREEN_H:
                    continue
                ix, iy = int(x), int(y)
                ch = self.ROWS[ty][tx]
                if ch == "0":
                    pyxel.rect(ix, iy, TILE, TILE, COL_LGREEN)
                    if (tx + ty) % 4 == 0:
                        pyxel.pset(ix + 4, iy + 6, COL_GREEN)
                        pyxel.pset(ix + 11, iy + 9, COL_GREEN)
                else:
                    u, v, w, h = SPR_WALL_SHORT
                    pyxel.blt(ix, iy, 0, u, v, w, h, COLKEY)


# =====================================================================
# MazeDog — REWRITTEN: proper collision, damage cooldown, no tunneling
# =====================================================================
class MazeDog:
    def __init__(self, x, y, maze):
        self.x = float(x)
        self.y = float(y)
        self.w = 22
        self.h = 18
        self.maze = maze
        self.speed = 1.3
        self.detect = 100
        self.alive = True
        self.facing = 1
        self.anim_tick = 0
        self.anim_frame = 0
        self.damage_cooldown = 0
        self.wander_dir = (0.0, 0.0)
        self.wander_timer = 0

    def rect(self):
        return (int(self.x) + 5, int(self.y) + 8, self.w, self.h)

    def _try_move(self, dx, dy):
        moved = False
        nx = self.x + dx
        if not self.maze.collides(nx, self.y, 32, 32):
            self.x = nx
            moved = True
        ny = self.y + dy
        if not self.maze.collides(self.x, ny, 32, 32):
            self.y = ny
            moved = True
        return moved

    def update(self, player):
        if self.damage_cooldown > 0:
            self.damage_cooldown -= 1
        self.anim_tick += 1
        if self.anim_tick >= 6:
            self.anim_tick = 0
            self.anim_frame = 1 - self.anim_frame

        cx = self.x + 16
        cy = self.y + 16
        px = player.x + player.w / 2
        py = player.y + player.h / 2
        d = dist(cx, cy, px, py)
        visible = d < self.detect and self.maze.line_clear(cx, cy, px, py)

        if visible:
            nx, ny = normalize(px - cx, py - cy)
            self.facing = 1 if nx > 0 else -1
            speed = self.speed * 1.5
            moved = self._try_move(nx * speed, ny * speed)
            # If totally blocked, try perpendicular sidestep so we don't grind
            # into walls.
            if not moved:
                for s in (speed, -speed):
                    if self._try_move(-ny * s, nx * s):
                        break
        else:
            self.wander_timer -= 1
            if self.wander_timer <= 0:
                self.wander_timer = 40
                ang = random.uniform(0, math.pi * 2)
                self.wander_dir = (math.cos(ang), math.sin(ang))
                self.facing = 1 if self.wander_dir[0] > 0 else -1
            wdx, wdy = self.wander_dir
            if not self._try_move(wdx * self.speed, wdy * self.speed):
                # Pick a new direction immediately when wandering hits a wall
                self.wander_timer = 0

        if rects_overlap(player.rect(), self.rect()) and self.damage_cooldown <= 0:
            if player.damage(1):
                self.damage_cooldown = 45
                Sound.play(2, 5)
                nx, ny = normalize(player.x - self.x, player.y - self.y)
                push = 12
                if not self.maze.collides(player.x + nx * push, player.y,
                                          player.w, player.h):
                    player.x += nx * push
                if not self.maze.collides(player.x, player.y + ny * push,
                                          player.w, player.h):
                    player.y += ny * push

    def draw(self, cam_x, cam_y):
        sx = int(self.x - cam_x)
        sy = int(self.y - cam_y)
        if sx < -32 or sx > SCREEN_W or sy < -32 or sy > SCREEN_H:
            return
        u, v, w, h = SPR_DOG
        flip_w = -w if self.facing < 0 else w
        bob = self.anim_frame
        pyxel.blt(sx, sy + bob, 0, u, v, flip_w, h, COLKEY)
        if pyxel.frame_count % 60 < 20:
            pyxel.text(sx + 8, sy - 6, "WUFF", COL_RED)


# =====================================================================
# PLATFORMER WORLD + Cannon + Projectile
# =====================================================================
PLAT_TILE = 16
PLAT_GRAVITY = 0.55
PLAT_MAX_FALL = 8.0
PLAT_JUMP_VEL = -7.5


class PlatformerWorld:
    def __init__(self, cols, rows, tile=PLAT_TILE):
        self.cols = cols
        self.rows = rows
        self.tile = tile
        self.width = cols * tile
        self.height = rows * tile
        self.grid = [[0] * cols for _ in range(rows)]

    def set_tile(self, tx, ty, val):
        if 0 <= tx < self.cols and 0 <= ty < self.rows:
            self.grid[ty][tx] = val

    def fill_floor(self, ground_y_tiles=2):
        for ty in range(self.rows - ground_y_tiles, self.rows):
            for tx in range(self.cols):
                self.grid[ty][tx] = 1

    def collides(self, x, y, w, h):
        left = int(x // self.tile)
        right = int((x + w - 1) // self.tile)
        top = int(y // self.tile)
        bottom = int((y + h - 1) // self.tile)
        for ty in range(top, bottom + 1):
            for tx in range(left, right + 1):
                if 0 <= ty < self.rows and 0 <= tx < self.cols:
                    t = self.grid[ty][tx]
                    if t == 1 or t == 2:
                        return True
                else:
                    if ty >= self.rows or tx < 0 or tx >= self.cols:
                        return True
        return False

    def tile_at(self, x, y):
        tx = int(x // self.tile)
        ty = int(y // self.tile)
        if 0 <= tx < self.cols and 0 <= ty < self.rows:
            return self.grid[ty][tx]
        return 0

    def draw(self, cam_x, theme="green"):
        if theme == "green":
            sky = COL_LBLUE; bg_mid = COL_LGREEN; ground_col = COL_BROWN; top_col = COL_GREEN
        elif theme == "stone":
            sky = COL_PURPLE; bg_mid = COL_DGRAY; ground_col = COL_DGRAY; top_col = COL_LGRAY
        else:
            sky = COL_ORANGE; bg_mid = COL_PURPLE; ground_col = COL_BROWN; top_col = COL_PEACH

        pyxel.cls(sky)
        for i in range(8):
            mx = (i * 60 - cam_x // 3) % (SCREEN_W + 100) - 50
            pyxel.tri(int(mx), 160, int(mx) + 40, 90,
                      int(mx) + 80, 160, bg_mid)
        for i in range(5):
            cx = (i * 80 + 30 - cam_x // 4) % (SCREEN_W + 60) - 30
            cy = 30 + (i * 13) % 40
            pyxel.rect(int(cx), int(cy), 18, 5, COL_WHITE)
            pyxel.rect(int(cx) + 3, int(cy) - 2, 12, 4, COL_WHITE)

        T = self.tile
        start_tx = max(0, int(cam_x // T) - 1)
        end_tx = min(self.cols, int((cam_x + SCREEN_W) // T) + 2)
        for ty in range(self.rows):
            for tx in range(start_tx, end_tx):
                t = self.grid[ty][tx]
                if t == 0:
                    continue
                sx = tx * T - cam_x
                sy = ty * T
                if t == 1 or t == 2:
                    pyxel.rect(sx, sy, T, T, ground_col)
                    if ty == 0 or self.grid[ty - 1][tx] == 0:
                        pyxel.rect(sx, sy, T, 3, top_col)
                        if (tx + ty) % 2 == 0:
                            pyxel.pset(sx + 2, sy - 1, top_col)
                            pyxel.pset(sx + 8, sy - 1, top_col)
                    if (tx * 3 + ty) % 5 == 0:
                        pyxel.pset(sx + 3, sy + 6, COL_LGRAY)
                        pyxel.pset(sx + 9, sy + 10, COL_LGRAY)
                elif t == 3:
                    pyxel.tri(sx, sy + T, sx + T // 2, sy,
                              sx + T, sy + T, COL_RED)
                    pyxel.pset(sx + T // 2, sy + 3, COL_WHITE)
                elif t == 4:
                    pyxel.rect(sx + 6, sy - 8, 2, T + 8, COL_BROWN)
                    pyxel.tri(sx + 8, sy - 6, sx + 8, sy + 2,
                              sx + 14, sy - 2, COL_YELLOW)


# Less regular cannon
class CarrotCannon:
    def __init__(self, x, y, direction=-1, interval=130):
        self.x = x
        self.y = y
        self.w = 18
        self.h = 12
        self.direction = direction
        self.base_interval = interval
        self.timer = random.randint(40, interval)

    def update(self, projectiles):
        self.timer -= 1
        if self.timer <= 0:
            self.timer = self.base_interval + random.randint(-30, 60)
            vx = 2.5 * self.direction
            projectiles.append(CarrotProjectile(
                self.x + (-4 if self.direction < 0 else self.w),
                self.y + 2, vx))
            Sound.play(0, 5)

    def draw(self, cam_x):
        sx = int(self.x - cam_x)
        if sx < -20 or sx > SCREEN_W:
            return
        sy = int(self.y)
        pyxel.rect(sx + 4, sy + 2, 10, 9, COL_DGRAY)
        pyxel.rect(sx + 4, sy + 2, 10, 2, COL_LGRAY)
        if self.direction < 0:
            pyxel.rect(sx, sy + 4, 6, 6, COL_DGRAY)
            pyxel.rect(sx - 1, sy + 5, 2, 4, COL_BLACK)
        else:
            pyxel.rect(sx + 12, sy + 4, 6, 6, COL_DGRAY)
            pyxel.rect(sx + 17, sy + 5, 2, 4, COL_BLACK)
        pyxel.circ(sx + 5, sy + 12, 2, COL_BROWN)
        pyxel.circ(sx + 13, sy + 12, 2, COL_BROWN)


class CarrotProjectile:
    def __init__(self, x, y, vx):
        self.x = float(x)
        self.y = float(y)
        self.vx = vx
        self.w = 10
        self.h = 6
        self.alive = True
        self.life = 200

    def update(self):
        self.x += self.vx
        self.life -= 1
        if self.life <= 0 or self.x < -50:
            self.alive = False

    def rect(self):
        return (int(self.x), int(self.y), self.w, self.h)

    def draw(self, cam_x):
        sx = int(self.x - cam_x)
        sy = int(self.y)
        if sx < -20 or sx > SCREEN_W:
            return
        if self.vx < 0:
            pyxel.tri(sx, sy + 3, sx + 4, sy, sx + 4, sy + 6, COL_ORANGE)
            pyxel.rect(sx + 4, sy + 1, 6, 5, COL_ORANGE)
            pyxel.pset(sx + 9, sy, COL_GREEN)
            pyxel.pset(sx + 10, sy - 1, COL_GREEN)
        else:
            pyxel.tri(sx + 10, sy + 3, sx + 6, sy, sx + 6, sy + 6, COL_ORANGE)
            pyxel.rect(sx, sy + 1, 6, 5, COL_ORANGE)
            pyxel.pset(sx, sy, COL_GREEN)
            pyxel.pset(sx - 1, sy - 1, COL_GREEN)


# =====================================================================
# Fence / Cage / CageBomb
# =====================================================================
class Fence:
    def __init__(self, x, y, height_tiles=2):
        self.x = x
        self.y = y
        self.w = 12
        self.h = height_tiles * PLAT_TILE

    def rect(self):
        return (self.x, self.y, self.w, self.h)

    def draw(self, cam_x):
        sx = int(self.x - cam_x)
        sy = int(self.y)
        if sx < -20 or sx > SCREEN_W:
            return
        pyxel.rect(sx + 1, sy, 2, self.h, COL_BROWN)
        pyxel.rect(sx + 9, sy, 2, self.h, COL_BROWN)
        pyxel.rect(sx, sy + 4, 12, 2, COL_BROWN)
        pyxel.rect(sx, sy + self.h - 8, 12, 2, COL_BROWN)
        pyxel.pset(sx + 1, sy + 1, COL_PEACH)
        pyxel.pset(sx + 9, sy + 1, COL_PEACH)


class RabbitCage:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.w = 24
        self.h = 24
        self.broken = False

    def rect(self):
        return (self.x, self.y, self.w, self.h)

    def draw(self, cam_x):
        sx = int(self.x - cam_x)
        sy = int(self.y)
        if sx < -30 or sx > SCREEN_W:
            return
        if self.broken:
            pyxel.rect(sx + 2, sy + self.h - 4, 4, 3, COL_DGRAY)
            pyxel.rect(sx + 18, sy + self.h - 4, 4, 3, COL_DGRAY)
            return
        pyxel.rectb(sx, sy, self.w, self.h, COL_DGRAY)
        pyxel.rectb(sx + 1, sy + 1, self.w - 2, self.h - 2, COL_LGRAY)
        for bx in (sx + 5, sx + 11, sx + 17):
            pyxel.line(bx, sy + 1, bx, sy + self.h - 2, COL_DGRAY)
        # Baby inside
        pyxel.rect(sx + 8, sy + 12, 8, 8, COL_WHITE)
        pyxel.rect(sx + 9, sy + 8, 2, 6, COL_WHITE)
        pyxel.rect(sx + 13, sy + 8, 2, 6, COL_WHITE)
        pyxel.pset(sx + 10, sy + 15, COL_BLACK)
        pyxel.pset(sx + 13, sy + 15, COL_BLACK)
        pyxel.pset(sx + 11, sy + 17, COL_PINK)


class CageBomb:
    """Carrot thrown at cages — alternative to F-slam."""
    def __init__(self, x, y, vx):
        self.x = float(x)
        self.y = float(y)
        self.vx = vx
        self.vy = -3.0
        self.w = 10
        self.h = 10
        self.alive = True

    def update(self):
        self.x += self.vx
        self.vy += 0.35
        self.y += self.vy
        if self.y > 400 or self.x < -50 or self.x > 100000:
            self.alive = False

    def rect(self):
        return (int(self.x), int(self.y), self.w, self.h)

    def draw(self, cam_x):
        sx = int(self.x - cam_x)
        sy = int(self.y)
        if sx < -20 or sx > SCREEN_W:
            return
        u, v, w, h = SPR_CARROT
        pyxel.blt(sx, sy, 1, u, v, w, h, COLKEY)


# =====================================================================
# BossEgg (new sprite-based projectile)
# =====================================================================
class BossEgg:
    def __init__(self, x, y, vx, vy):
        self.x = float(x)
        self.y = float(y)
        self.vx = vx
        self.vy = vy
        self.w = 12
        self.h = 12
        self.alive = True
        self.life = 180
        self.rot = 0.0

    def rect(self):
        return (int(self.x) + 2, int(self.y) + 2, self.w, self.h)

    def update(self):
        self.x += self.vx
        self.y += self.vy
        self.vy += 0.06  # slight arc
        self.life -= 1
        self.rot += 0.2
        if self.life <= 0 or self.x < -30 or self.x > SCREEN_W + 30 \
                or self.y > SCREEN_H + 30:
            self.alive = False

    def draw(self):
        u, v, w, h = SPR_EASTER_EGG
        pyxel.blt(int(self.x) - 2, int(self.y) - 2, 0, u, v, w, h, COLKEY)


# =====================================================================
# OldEasterBunnyBoss — fixed Y, lateral walk, new sprites
# =====================================================================
class OldEasterBunnyBoss:
    BOSS_Y = 60
    # Walkway constraint — boss never drifts off his platform
    WALK_X_MIN = 30
    WALK_X_MAX = SCREEN_W - 30 - 56

    def __init__(self):
        self.x = SCREEN_W // 2 - 28
        self.y = self.BOSS_Y
        self.w = 56
        self.h = 64
        self.hp = 9
        self.max_hp = 9
        self.attack_timer = 90
        self.move_timer = 0
        self.target_x = self.x
        self.hit_flash = 0
        self.invuln = 0
        self.anim_tick = 0
        self.attacking_anim = 0
        self.facing = -1
        # Facing-flip cooldown: prevents twitchy mid-frame sprite swap
        self._facing_cooldown = 0

    def rect(self):
        return (int(self.x) + 10, int(self.y) + 10, self.w - 20, self.h - 20)

    def center(self):
        return (self.x + self.w / 2, self.y + self.h / 2)

    def current_phase(self):
        if self.hp >= 7:
            return 1
        if self.hp >= 4:
            return 2
        return 3

    def take_hit(self):
        if self.invuln > 0:
            return False
        self.hp = max(0, self.hp - 1)
        self.hit_flash = 12
        self.invuln = 22
        Sound.play(2, 5)
        return True

    def update(self, player, eggs):
        self.anim_tick += 1
        if self.invuln > 0:
            self.invuln -= 1
        if self.hit_flash > 0:
            self.hit_flash -= 1
        if self.attacking_anim > 0:
            self.attacking_anim -= 1
        if self._facing_cooldown > 0:
            self._facing_cooldown -= 1

        self.move_timer += 1
        if self.move_timer > 60:
            self.move_timer = 0
            # Pick a target that's meaningfully far so he commits to a walk
            attempts = 0
            new_target = random.randint(self.WALK_X_MIN, self.WALK_X_MAX)
            while abs(new_target - self.x) < 30 and attempts < 5:
                new_target = random.randint(self.WALK_X_MIN, self.WALK_X_MAX)
                attempts += 1
            self.target_x = new_target

        # Dead-zone: don't move (or flip) when essentially at target.
        diff = self.target_x - self.x
        new_x = self.x
        if diff > 1.5:
            new_x = self.x + 1.1
            if self._facing_cooldown <= 0 and self.facing != 1:
                self.facing = 1
                self._facing_cooldown = 18
        elif diff < -1.5:
            new_x = self.x - 1.1
            if self._facing_cooldown <= 0 and self.facing != -1:
                self.facing = -1
                self._facing_cooldown = 18
        else:
            # Snap to target, stop jittering
            new_x = self.target_x
        # Hard clamp to walkway — never leaves the platform
        self.x = clamp(new_x, self.WALK_X_MIN, self.WALK_X_MAX)
        self.y = self.BOSS_Y + math.sin(self.anim_tick * 0.06) * 2

        self.attack_timer -= 1
        if self.attack_timer <= 0:
            phase = self.current_phase()
            self._attack(phase, player, eggs)
            rates = {1: 100, 2: 75, 3: 55}
            self.attack_timer = rates[phase]

    def _attack(self, phase, player, eggs):
        cx, cy = self.center()
        self.attacking_anim = 20
        Sound.play(1, 5)
        if phase == 1:
            dx = player.x - cx
            dy = player.y - cy
            nx, ny = normalize(dx, dy)
            eggs.append(BossEgg(cx, cy + 10, nx * 2.2, ny * 1.6 - 0.5))
        elif phase == 2:
            dx = player.x - cx
            dy = player.y - cy
            nx, ny = normalize(dx, dy)
            for off in (-0.25, 0, 0.25):
                cs, sn = math.cos(off), math.sin(off)
                vx = (nx * cs - ny * sn) * 2.4
                vy = (nx * sn + ny * cs) * 1.6
                eggs.append(BossEgg(cx, cy + 10, vx, vy))
        else:
            for off in (-0.5, -0.25, 0, 0.25, 0.5):
                dx = player.x - cx
                dy = player.y - cy
                nx, ny = normalize(dx, dy)
                cs, sn = math.cos(off), math.sin(off)
                vx = (nx * cs - ny * sn) * 2.6
                vy = (nx * sn + ny * cs) * 1.5
                eggs.append(BossEgg(cx, cy + 10, vx, vy))

    def draw(self):
        # Stable integer position — avoids sub-pixel jitter frame to frame
        bx = round(self.x)
        by = round(self.y)
        # Choose sprite — attack frame held steady during attacking_anim
        if self.attacking_anim > 0:
            u, v, w, h = SPR_BOSS_ATTACK
        else:
            u, v, w, h = SPR_BOSS_NORMAL
        flash = self.hit_flash > 0 and self.hit_flash % 2 == 0
        if flash:
            for c in range(1, 16):
                pyxel.pal(c, COL_RED)
        # Flip handling: pyxel anchors a negative-width blt at the SAME x,
        # but visually mirrors within [x, x+|w|]. To keep the boss centered
        # on the walkway we always draw at bx and only flip the source width.
        if self.facing < 0:
            pyxel.blt(bx, by, 0, u, v, -w, h, COLKEY)
        else:
            pyxel.blt(bx, by, 0, u, v, w, h, COLKEY)
        if flash:
            pyxel.pal()

    def draw_hp_bar(self):
        bar_w = SCREEN_W - 100
        bar_x = 50
        bar_y = SCREEN_H - 18
        pyxel.rect(bar_x, bar_y, bar_w, 8, COL_BLACK)
        ratio = self.hp / self.max_hp
        fill = int((bar_w - 2) * ratio)
        phase = self.current_phase()
        col = [0, COL_LGREEN, COL_YELLOW, COL_RED][phase]
        pyxel.rect(bar_x + 1, bar_y + 1, fill, 6, col)
        pyxel.rectb(bar_x, bar_y, bar_w, 8, COL_WHITE)
        draw_centered("OLD EASTER BUNNY", bar_y - 10, COL_PINK)
# =====================================================================
# LEVEL: MEADOW
# =====================================================================
class LevelMeadow:
    def __init__(self, game):
        self.game = game
        self.target_carrots = 10
        self.delivered = 0
        self.player = Player(MAP_MEADOW_W // 2, MAP_MEADOW_H // 2)
        self.school_x = MAP_MEADOW_W - SCHOOL_W - 16
        self.school_y = MAP_MEADOW_H - SCHOOL_H - 16
        self.carrots = self._spawn_carrots(14)
        self.coins = self._spawn_coins(16)
        self.foxes = [
            Fox(80, 80),
            Fox(MAP_MEADOW_W - 120, 100),
            Fox(120, MAP_MEADOW_H - 140),
            Fox(MAP_MEADOW_W - 160, MAP_MEADOW_H - 80),
        ]
        self.babies = []

    def _spawn_carrots(self, n):
        out = []
        for _ in range(n):
            x = random.randint(40, MAP_MEADOW_W - 60)
            y = random.randint(40, MAP_MEADOW_H - 60)
            if dist(x, y, self.school_x, self.school_y) < 100:
                continue
            if dist(x, y, MAP_MEADOW_W // 2, MAP_MEADOW_H // 2) < 50:
                continue
            out.append(Carrot(x, y))
        return out

    def _spawn_coins(self, n):
        out = []
        for _ in range(n):
            x = random.randint(20, MAP_MEADOW_W - 30)
            y = random.randint(20, MAP_MEADOW_H - 30)
            out.append(Coin(x, y))
        return out

    def school_rect(self):
        return (self.school_x, self.school_y, SCHOOL_W, SCHOOL_H)

    def update(self):
        speed = 2.2
        if self.game.upgrade_fast_paws:
            speed += 0.5
        self.player.update(speed, blocked_fn=self._blocked)

        for c in self.carrots:
            c.update()
            if c.alive and rects_overlap(self.player.rect(), c.rect()):
                if self.player.carrying_carrots < self.player.max_carry:
                    c.alive = False
                    self.player.carrying_carrots += 1
                    Sound.play(1, 1)
                    self.game.rival.reward(0.03)
        self.carrots = [c for c in self.carrots if c.alive]
        if len(self.carrots) < 4:
            self.carrots.extend(self._spawn_carrots(4))

        for coin in self.coins:
            coin.update()
            if coin.alive and rects_overlap(self.player.rect(), coin.rect()):
                coin.alive = False
                self.player.coins += 1
                Sound.play(1, 1)
        self.coins = [c for c in self.coins if c.alive]

        if (self.player.carrying_carrots > 0
                and rects_overlap(self.player.rect(), self.school_rect())):
            self.delivered += self.player.carrying_carrots
            for _ in range(self.player.carrying_carrots):
                bx = self.school_x + random.randint(8, SCHOOL_W - 24)
                by = self.school_y + SCHOOL_H + random.randint(-4, 12)
                self.babies.append(BabyHare(bx, by))
            self.player.carrying_carrots = 0
            Sound.play(1, 2)
            self.game.rival.reward(0.08)
            self.game.flash_message(f"DELIVERED! {self.delivered}/{self.target_carrots}", 50)

        for b in self.babies:
            b.update()

        for f in self.foxes:
            f.update(self.player)
            if rects_overlap(self.player.rect(), f.rect()):
                if self.player.damage(1):
                    self.game.rival.punish(0.08)
                    if self.player.carrying_carrots > 0:
                        for _ in range(self.player.carrying_carrots):
                            drop_x = self.player.x + random.randint(-20, 20)
                            drop_y = self.player.y + random.randint(-20, 20)
                            self.carrots.append(Carrot(drop_x, drop_y))
                        self.player.carrying_carrots = 0
                    nx, ny = normalize(self.player.x - f.x, self.player.y - f.y)
                    self.player.x += nx * 30
                    self.player.y += ny * 30
                    self.player.x = clamp(self.player.x, 0, MAP_MEADOW_W - self.player.w)
                    self.player.y = clamp(self.player.y, 0, MAP_MEADOW_H - self.player.h)

        self._update_rival()

        cx, cy = self.player.center()
        self.game.camera.set_bounds(MAP_MEADOW_W, MAP_MEADOW_H)
        self.game.camera.follow(cx, cy)

        if self.delivered >= self.target_carrots:
            self.game.complete_level(ST_LOBBY, "MEADOW CLEAR!")
            return
        if not self.player.alive:
            self.game.game_over("THE FOX CAUGHT YOU")
            return

    def _update_rival(self):
        r = self.game.rival
        r.update_meter()
        if r.chase_enabled:
            # Rival hunts the player. Speed scales slightly with the timer
            # so the pressure ramps up over the 45 seconds.
            speed = 1.4 + r.progress * 1.2
            r.chase_player(self.player, speed=speed)
            r.x = clamp(r.x, 0, MAP_MEADOW_W - 16)
            r.y = clamp(r.y, 0, MAP_MEADOW_H - 16)
            if rects_overlap(self.player.rect(), r.rect()):
                self.game.game_over("THE RIVAL OVERTOOK YOU")
        else:
            r.wander(MAP_MEADOW_W, MAP_MEADOW_H)

    def _blocked(self, x, y, w, h):
        if rects_overlap((x, y, w, h), self.school_rect()):
            return False
        if x < 0 or y < 0 or x + w > MAP_MEADOW_W or y + h > MAP_MEADOW_H:
            return True
        return False

    def draw(self):
        cam_x, cam_y = self.game.camera.offset()
        pyxel.cls(COL_LGREEN)
        for i in range(60):
            seed = (i * 173 + 11) % 1024
            wx = (seed * 17) % MAP_MEADOW_W
            wy = (seed * 23) % MAP_MEADOW_H
            sx = int(wx - cam_x); sy = int(wy - cam_y)
            if 0 <= sx < SCREEN_W and 0 <= sy < SCREEN_H:
                col = [COL_GREEN, COL_GREEN, COL_PEACH, COL_RED][seed % 4]
                pyxel.pset(sx, sy, col)
                pyxel.pset(sx + 1, sy, col)
        for i in range(30):
            seed = (i * 41 + 7) % 256
            wx = (seed * 13) % MAP_MEADOW_W
            wy = (seed * 19) % MAP_MEADOW_H
            sx = int(wx - cam_x); sy = int(wy - cam_y)
            if -8 < sx < SCREEN_W and -8 < sy < SCREEN_H:
                pyxel.rect(sx, sy, 6, 4, COL_GREEN)

        sxr = int(self.school_x - cam_x)
        syr = int(self.school_y - cam_y)
        if -SCHOOL_W < sxr < SCREEN_W and -SCHOOL_H < syr < SCREEN_H:
            u, v, w, h = SPR_SCHOOL
            pyxel.blt(sxr, syr, 0, u, v, w, h, COLKEY)
            if self.player.carrying_carrots > 0:
                arrow_y = syr - 14 + int(math.sin(pyxel.frame_count * 0.2) * 2)
                pyxel.tri(sxr + 36, arrow_y, sxr + 52, arrow_y,
                          sxr + 44, arrow_y + 6, COL_YELLOW)

        for coin in self.coins:
            coin.draw(cam_x, cam_y)
        for c in self.carrots:
            c.draw(cam_x, cam_y)
        for b in self.babies:
            b.draw(cam_x, cam_y)
        for f in self.foxes:
            f.draw(cam_x, cam_y)
        self.game.rival.draw(cam_x, cam_y)
        self.player.draw(cam_x, cam_y)
        self._draw_hud()

    def _draw_hud(self):
        for i in range(self.player.max_health):
            col = COL_RED if i < self.player.health else COL_DGRAY
            x = 4 + i * 10; y = 4
            pyxel.rect(x, y + 1, 3, 3, col)
            pyxel.rect(x + 4, y + 1, 3, 3, col)
            pyxel.rect(x + 1, y + 4, 5, 2, col)
            pyxel.pset(x + 2, y + 6, col)
            pyxel.pset(x + 3, y + 7, col)
            pyxel.pset(x + 4, y + 6, col)
        pyxel.blt(4, 18, 0, SPR_COIN[0], SPR_COIN[1], 8, 8, COLKEY)
        pyxel.text(14, 19, str(self.player.coins), COL_YELLOW)
        obj = f"CARROTS {self.delivered}/{self.target_carrots}"
        draw_panel(SCREEN_W // 2 - 50, 2, 100, 9, COL_WHITE, COL_BLACK)
        draw_centered(obj, 4, COL_YELLOW)
        if self.player.carrying_carrots > 0:
            txt = f"HOLDING: {self.player.carrying_carrots}"
            draw_panel(4, 32, 70, 9, COL_WHITE, COL_BLACK)
            pyxel.text(8, 34, txt, COL_ORANGE)


# =====================================================================
# LEVEL: KIDS RESCUE
# =====================================================================
class LevelKids:
    def __init__(self, game):
        self.game = game
        self.target_kids = 8
        self.delivered = 0
        self.player = Player(MAP_MEADOW_W // 2, MAP_MEADOW_H // 2)
        self.school_x = 60
        self.school_y = 60
        self.kids_lost = []
        for _ in range(12):
            x = random.randint(40, MAP_MEADOW_W - 60)
            y = random.randint(40, MAP_MEADOW_H - 60)
            if dist(x, y, self.school_x, self.school_y) < 100:
                continue
            self.kids_lost.append(BabyHare(x, y))
        self.kids_following = []
        self.babies_safe = []
        self.coins = []
        for _ in range(20):
            self.coins.append(Coin(random.randint(20, MAP_MEADOW_W - 30),
                                   random.randint(20, MAP_MEADOW_H - 30)))
        self.foxes = [
            Fox(random.randint(60, MAP_MEADOW_W - 80),
                random.randint(60, MAP_MEADOW_H - 80))
            for _ in range(5)
        ]

    def school_rect(self):
        return (self.school_x, self.school_y, SCHOOL_W, SCHOOL_H)

    def update(self):
        speed = 2.2
        if self.game.upgrade_fast_paws:
            speed += 0.5
        self.player.update(speed, blocked_fn=self._blocked)

        for coin in self.coins:
            coin.update()
            if coin.alive and rects_overlap(self.player.rect(), coin.rect()):
                coin.alive = False
                self.player.coins += 1
                Sound.play(1, 1)
        self.coins = [c for c in self.coins if c.alive]

        prect = self.player.rect()
        for kid in self.kids_lost:
            if kid.alive and rects_overlap(prect, kid.rect()):
                kid.alive = False
                follower = BabyHare(self.player.x, self.player.y)
                follower.has_carrot = True
                self.kids_following.append(follower)
                Sound.play(1, 6)
                self.game.rival.reward(0.04)
                self.game.flash_message("KID RESCUED!", 30)
        self.kids_lost = [k for k in self.kids_lost if k.alive]

        if self.kids_following:
            target = (self.player.x - 16, self.player.y)
            for i, kid in enumerate(self.kids_following):
                if i == 0:
                    tx, ty = target
                else:
                    prev = self.kids_following[i - 1]
                    tx, ty = prev.x - 12, prev.y
                nx, ny = normalize(tx - kid.x, ty - kid.y)
                d = dist(kid.x, kid.y, tx, ty)
                if d > 16:
                    kid.x += nx * 1.8
                    kid.y += ny * 1.8
                kid.facing = self.player.facing
                kid.update()

        if (self.kids_following
                and rects_overlap(prect, self.school_rect())):
            delivered_now = len(self.kids_following)
            self.delivered += delivered_now
            for kid in self.kids_following:
                bx = self.school_x + random.randint(8, SCHOOL_W - 24)
                by = self.school_y + SCHOOL_H + random.randint(-4, 12)
                safe = BabyHare(bx, by)
                self.babies_safe.append(safe)
            self.kids_following = []
            Sound.play(1, 2)
            self.game.rival.reward(0.12)
            self.game.flash_message(f"DELIVERED! {self.delivered}/{self.target_kids}", 50)

        for b in self.babies_safe:
            b.update()

        for f in self.foxes:
            f.update(self.player)
            if rects_overlap(self.player.rect(), f.rect()):
                if self.player.damage(1):
                    self.game.rival.punish(0.08)
                    if self.kids_following:
                        lost = self.kids_following.pop()
                        lost.alive = True
                        lost.x = self.player.x + random.randint(-40, 40)
                        lost.y = self.player.y + random.randint(-40, 40)
                        lost.has_carrot = False
                        self.kids_lost.append(lost)
                    nx, ny = normalize(self.player.x - f.x, self.player.y - f.y)
                    self.player.x += nx * 30
                    self.player.y += ny * 30
                    self.player.x = clamp(self.player.x, 0, MAP_MEADOW_W - self.player.w)
                    self.player.y = clamp(self.player.y, 0, MAP_MEADOW_H - self.player.h)

        r = self.game.rival
        r.update_meter()
        if r.chase_enabled:
            speed = 1.5 + r.progress * 1.3
            r.chase_player(self.player, speed=speed)
            r.x = clamp(r.x, 0, MAP_MEADOW_W - 16)
            r.y = clamp(r.y, 0, MAP_MEADOW_H - 16)
            if rects_overlap(self.player.rect(), r.rect()):
                self.game.game_over("THE RIVAL CAUGHT YOU")
                return
        else:
            r.wander(MAP_MEADOW_W, MAP_MEADOW_H)

        cx, cy = self.player.center()
        self.game.camera.set_bounds(MAP_MEADOW_W, MAP_MEADOW_H)
        self.game.camera.follow(cx, cy)

        if self.delivered >= self.target_kids:
            self.game.complete_level(ST_LOBBY, "KIDS LEVEL CLEAR!")
            return
        if not self.player.alive:
            self.game.game_over("THE FOX CAUGHT YOU")
            return

    def _blocked(self, x, y, w, h):
        if rects_overlap((x, y, w, h), self.school_rect()):
            return False
        if x < 0 or y < 0 or x + w > MAP_MEADOW_W or y + h > MAP_MEADOW_H:
            return True
        return False

    def draw(self):
        cam_x, cam_y = self.game.camera.offset()
        pyxel.cls(COL_LGREEN)
        for i in range(60):
            seed = (i * 173 + 31) % 1024
            wx = (seed * 19) % MAP_MEADOW_W
            wy = (seed * 23) % MAP_MEADOW_H
            sx = int(wx - cam_x); sy = int(wy - cam_y)
            if 0 <= sx < SCREEN_W and 0 <= sy < SCREEN_H:
                col = [COL_GREEN, COL_PEACH, COL_PINK, COL_YELLOW][seed % 4]
                pyxel.pset(sx, sy, col)
                pyxel.pset(sx + 1, sy, col)

        sxr = int(self.school_x - cam_x); syr = int(self.school_y - cam_y)
        if -SCHOOL_W < sxr < SCREEN_W and -SCHOOL_H < syr < SCREEN_H:
            u, v, w, h = SPR_SCHOOL
            pyxel.blt(sxr, syr, 0, u, v, w, h, COLKEY)
            if self.kids_following:
                arrow_y = syr - 14 + int(math.sin(pyxel.frame_count * 0.2) * 2)
                pyxel.tri(sxr + 36, arrow_y, sxr + 52, arrow_y,
                          sxr + 44, arrow_y + 6, COL_YELLOW)

        for coin in self.coins:
            coin.draw(cam_x, cam_y)
        for kid in self.kids_lost:
            kid.draw(cam_x, cam_y)
            sx = int(kid.x - cam_x); sy = int(kid.y - cam_y)
            if -16 < sx < SCREEN_W and -16 < sy < SCREEN_H:
                if pyxel.frame_count % 20 < 10:
                    pyxel.text(sx + 4, sy - 8, "?", COL_RED)
        for kid in self.kids_following:
            kid.draw(cam_x, cam_y)
        for b in self.babies_safe:
            b.draw(cam_x, cam_y)
        for f in self.foxes:
            f.draw(cam_x, cam_y)
        self.game.rival.draw(cam_x, cam_y)
        self.player.draw(cam_x, cam_y)
        self._draw_hud()

    def _draw_hud(self):
        for i in range(self.player.max_health):
            col = COL_RED if i < self.player.health else COL_DGRAY
            x = 4 + i * 10; y = 4
            pyxel.rect(x, y + 1, 3, 3, col)
            pyxel.rect(x + 4, y + 1, 3, 3, col)
            pyxel.rect(x + 1, y + 4, 5, 2, col)
            pyxel.pset(x + 2, y + 6, col)
            pyxel.pset(x + 3, y + 7, col)
            pyxel.pset(x + 4, y + 6, col)
        pyxel.blt(4, 18, 0, SPR_COIN[0], SPR_COIN[1], 8, 8, COLKEY)
        pyxel.text(14, 19, str(self.player.coins), COL_YELLOW)
        obj = f"KIDS {self.delivered}/{self.target_kids}"
        draw_panel(SCREEN_W // 2 - 50, 2, 100, 9, COL_WHITE, COL_BLACK)
        draw_centered(obj, 4, COL_PINK)
        if self.kids_following:
            txt = f"FOLLOWING: {len(self.kids_following)}"
            draw_panel(4, 32, 90, 9, COL_WHITE, COL_BLACK)
            pyxel.text(8, 34, txt, COL_PEACH)


# =====================================================================
# LEVEL: RANDOM MAZE  (variant 1 & 2)
# =====================================================================
class LevelRandomMaze:
    def __init__(self, game, variant=1, seed=None):
        self.game = game
        self.variant = variant
        if variant == 1:
            self.maze = RandomMaze(cols=16, rows=16, tile_size=24,
                                   seed=seed or random.randint(0, 99999))
            self.target_carrots = 6
            self.num_farmers = 1
            self.num_dogs = 1
        else:
            self.maze = RandomMaze(cols=18, rows=18, tile_size=24,
                                   seed=seed or random.randint(100, 99999))
            self.target_carrots = 8
            self.num_farmers = 2
            self.num_dogs = 2

        T = self.maze.tile
        self.map_w = self.maze.width
        self.map_h = self.maze.height

        sx, sy = self.maze.spawn_tile()
        self.player = Player(sx * T + 4, sy * T + 4)
        gx, gy = self.maze.goal_tile()
        self.school_x = gx * T - SCHOOL_W // 2
        self.school_y = gy * T - SCHOOL_H // 2
        # Keep school within map
        self.school_x = clamp(self.school_x, 0, self.map_w - SCHOOL_W)
        self.school_y = clamp(self.school_y, 0, self.map_h - SCHOOL_H)
        self.carrots = []
        spots = list(self.maze.dead_ends)
        random.shuffle(spots)
        for tx, ty in spots[:max(self.target_carrots + 2, 8)]:
            self.carrots.append(Carrot(tx * T + (T - 12) // 2,
                                       ty * T + (T - 12) // 2))
        self.coins = []
        for tx, ty in random.sample(self.maze.open_tiles,
                                    min(15, len(self.maze.open_tiles))):
            self.coins.append(Coin(tx * T + T // 2 - 4,
                                   ty * T + T // 2 - 4))
        self.farmers = []
        opens = self.maze.open_tiles
        for i in range(self.num_farmers):
            idx = (i + 1) * len(opens) // (self.num_farmers + 1)
            fx, fy = opens[idx]
            self.farmers.append(Farmer(fx * T, fy * T, self.maze))
        self.dogs = []
        for i in range(self.num_dogs):
            idx = (len(opens) - 1 - i)
            dx, dy = opens[idx % len(opens)]
            self.dogs.append(MazeDog(dx * T, dy * T, self.maze))
        self.delivered = 0
        self.babies = []
        if variant == 1:
            self.wall_palette = None
        else:
            self.wall_palette = {COL_DGRAY: COL_PURPLE, COL_LGRAY: COL_PINK}

    def school_rect(self):
        return (self.school_x, self.school_y, SCHOOL_W, SCHOOL_H)

    def update(self):
        speed = 1.7
        if self.game.upgrade_fast_paws:
            speed += 0.3
        self.player.update(speed,
                           blocked_fn=lambda x, y, w, h:
                               self.maze.collides(x, y, w, h))

        prect = self.player.rect()
        for c in self.carrots:
            c.update()
            if c.alive and rects_overlap(prect, c.rect()):
                if self.player.carrying_carrots < self.player.max_carry:
                    c.alive = False
                    self.player.carrying_carrots += 1
                    Sound.play(1, 1)
                    self.game.rival.reward(0.03)
        self.carrots = [c for c in self.carrots if c.alive]

        for coin in self.coins:
            coin.update()
            if coin.alive and rects_overlap(prect, coin.rect()):
                coin.alive = False
                self.player.coins += 1
                Sound.play(1, 1)
        self.coins = [c for c in self.coins if c.alive]

        if (self.player.carrying_carrots > 0
                and rects_overlap(prect, self.school_rect())):
            self.delivered += self.player.carrying_carrots
            for _ in range(self.player.carrying_carrots):
                bx = self.school_x + random.randint(8, SCHOOL_W - 24)
                by = self.school_y + SCHOOL_H + random.randint(-4, 12)
                self.babies.append(BabyHare(bx, by))
            self.player.carrying_carrots = 0
            Sound.play(1, 2)
            self.game.rival.reward(0.10)
            self.game.flash_message(f"DELIVERED! {self.delivered}/{self.target_carrots}", 50)

        for b in self.babies:
            b.update()

        for farmer in self.farmers:
            farmer.update(self.player)
            if rects_overlap(prect, farmer.rect()):
                if self.player.damage(1):
                    self.game.rival.punish(0.12)
                    if self.player.carrying_carrots > 0:
                        self.player.carrying_carrots = 0
                    nx, ny = normalize(self.player.x - farmer.x,
                                       self.player.y - farmer.y)
                    for _ in range(6):
                        new_x = self.player.x + nx * 4
                        new_y = self.player.y + ny * 4
                        if not self.maze.collides(new_x, new_y, self.player.w, self.player.h):
                            self.player.x = new_x
                            self.player.y = new_y
                        else:
                            break
                    break

        for dog in self.dogs:
            dog.update(self.player)

        self._update_rival()

        cx, cy = self.player.center()
        self.game.camera.set_bounds(self.map_w, self.map_h)
        self.game.camera.follow(cx, cy)

        if self.delivered >= self.target_carrots:
            self.game.complete_level(ST_LOBBY,
                f"MAZE {self.variant} CLEAR!")
            return
        if not self.player.alive:
            self.game.game_over("CAUGHT IN THE MAZE")
            return

    def _update_rival(self):
        r = self.game.rival
        r.update_meter()
        if not r.attack_mode and r.retreat_frames <= 0:
            r.wander_timer -= 1
            if r.wander_timer <= 0:
                r.wander_timer = 80
                tx, ty = random.choice(self.maze.open_tiles)
                r.wander_angle = math.atan2(ty * self.maze.tile - r.y,
                                            tx * self.maze.tile - r.x)
            speed = 0.7 + r.progress * 0.4
            dx = math.cos(r.wander_angle) * speed
            dy = math.sin(r.wander_angle) * speed
            if not self.maze.collides(r.x + dx, r.y, r.w, r.h):
                r.x += dx
            if not self.maze.collides(r.x, r.y + dy, r.w, r.h):
                r.y += dy
        elif r.attack_mode:
            nx, ny = normalize(self.player.x - r.x, self.player.y - r.y)
            speed = 2.2
            if not self.maze.collides(r.x + nx * speed, r.y, r.w, r.h):
                r.x += nx * speed
            if not self.maze.collides(r.x, r.y + ny * speed, r.w, r.h):
                r.y += ny * speed
            if rects_overlap(self.player.rect(), r.rect()):
                self.game.game_over("THE RIVAL CAUGHT YOU")
                return
            r.attack_frames -= 1
            if r.attack_frames <= 0:
                r.end_attack(0.78)
        elif r.retreat_frames > 0:
            r.retreat_frames -= 1

    def draw(self):
        cam_x, cam_y = self.game.camera.offset()
        self.maze.draw(cam_x, cam_y, self.wall_palette)
        sx = int(self.school_x - cam_x)
        sy = int(self.school_y - cam_y)
        if -SCHOOL_W < sx < SCREEN_W and -SCHOOL_H < sy < SCREEN_H:
            u, v, w, h = SPR_SCHOOL
            pyxel.blt(sx, sy, 0, u, v, w, h, COLKEY)
            if self.player.carrying_carrots > 0:
                arrow_y = sy - 14 + int(math.sin(pyxel.frame_count * 0.2) * 2)
                pyxel.tri(sx + 36, arrow_y, sx + 52, arrow_y,
                          sx + 44, arrow_y + 6, COL_YELLOW)
        for coin in self.coins:
            coin.draw(cam_x, cam_y)
        for c in self.carrots:
            c.draw(cam_x, cam_y)
        for b in self.babies:
            b.draw(cam_x, cam_y)
        for farmer in self.farmers:
            farmer.draw(cam_x, cam_y)
        for dog in self.dogs:
            dog.draw(cam_x, cam_y)
        self.game.rival.draw(cam_x, cam_y)
        self.player.draw(cam_x, cam_y)
        self._draw_hud()

    def _draw_hud(self):
        for i in range(self.player.max_health):
            col = COL_RED if i < self.player.health else COL_DGRAY
            x = 4 + i * 10; y = 4
            pyxel.rect(x, y + 1, 3, 3, col)
            pyxel.rect(x + 4, y + 1, 3, 3, col)
            pyxel.rect(x + 1, y + 4, 5, 2, col)
            pyxel.pset(x + 2, y + 6, col)
            pyxel.pset(x + 3, y + 7, col)
            pyxel.pset(x + 4, y + 6, col)
        pyxel.blt(4, 18, 0, SPR_COIN[0], SPR_COIN[1], 8, 8, COLKEY)
        pyxel.text(14, 19, str(self.player.coins), COL_YELLOW)
        obj = f"CARROTS {self.delivered}/{self.target_carrots}"
        draw_panel(SCREEN_W // 2 - 60, 2, 120, 9, COL_WHITE, COL_BLACK)
        draw_centered(obj, 4, COL_YELLOW)
        title_txt = f"MAZE {self.variant}"
        draw_centered(title_txt, 12, COL_PEACH)
        if self.player.carrying_carrots > 0:
            txt = f"HOLDING: {self.player.carrying_carrots}"
            draw_panel(4, 32, 70, 9, COL_WHITE, COL_BLACK)
            pyxel.text(8, 34, txt, COL_ORANGE)


# =====================================================================
# LEVEL: PLATFORMER CANNON — always-on double jump + less regular shots
# =====================================================================
class LevelPlatformCannon:
    def __init__(self, game):
        self.game = game
        self.world = PlatformerWorld(110, 16, PLAT_TILE)
        self._build_world()
        self.player = Player(32, (self.world.rows - 4) * PLAT_TILE)
        self.player.mode = "platformer"
        # Double jump only if bought from shop
        self.player.max_jumps = 2 if self.game.has_double_jump else 1
        self.player.jumps_left = self.player.max_jumps
        self.cannons = self._spawn_cannons()
        self.projectiles = []
        self.coins = self._spawn_coins()
        self.carrots = []
        # Spread 3 carrots across the longer level
        for cx in (30 * PLAT_TILE, 60 * PLAT_TILE, 90 * PLAT_TILE):
            self.carrots.append(Carrot(cx, (self.world.rows - 5) * PLAT_TILE - 4))
        self.target_carrots = 3
        self.collected = 0
        self.goal_x = (self.world.cols - 4) * PLAT_TILE
        self.goal_y = (self.world.rows - 4) * PLAT_TILE
        self.cam_x = 0

    def _build_world(self):
        w = self.world
        w.fill_floor(2)
        # Gaps spread across the longer level
        gaps = [(13, 15), (22, 24), (32, 34), (44, 46),
                (56, 58), (70, 73), (84, 86), (96, 98)]
        for gx1, gx2 in gaps:
            for tx in range(gx1, gx2 + 1):
                w.set_tile(tx, w.rows - 1, 0)
                w.set_tile(tx, w.rows - 2, 0)
        # Varied platform staircase
        plats = [
            (8, w.rows - 5, 4), (16, w.rows - 7, 3), (20, w.rows - 5, 3),
            (26, w.rows - 8, 3), (32, w.rows - 6, 4), (40, w.rows - 5, 3),
            (46, w.rows - 7, 3), (52, w.rows - 9, 3), (58, w.rows - 6, 4),
            (64, w.rows - 5, 3), (70, w.rows - 8, 3), (76, w.rows - 6, 4),
            (82, w.rows - 5, 3), (88, w.rows - 9, 3), (94, w.rows - 7, 4),
            (100, w.rows - 5, 4),
        ]
        for tx, ty, length in plats:
            for i in range(length):
                w.set_tile(tx + i, ty, 1)
        # Random spikes scattered
        for sx in (20, 34, 48, 62, 76, 90, 102):
            w.set_tile(sx, w.rows - 3, 3)
        # Goal flag at the end
        w.set_tile(w.cols - 3, w.rows - 3, 4)

    def _spawn_cannons(self):
        cannons = []
        # More cannons across the wider level, mix of directions/heights
        cannon_specs = [
            (random.randint(12, 16), -1, -4),
            (random.randint(20, 24), -1, -7),
            (random.randint(28, 32),  1, -8),
            (random.randint(36, 40), -1, -4),
            (random.randint(44, 48),  1, -7),
            (random.randint(52, 56), -1, -9),
            (random.randint(60, 64),  1, -6),
            (random.randint(68, 72), -1, -4),
            (random.randint(76, 80),  1, -8),
            (random.randint(84, 88), -1, -5),
            (random.randint(92, 96),  1, -7),
            (random.randint(100, 104), -1, -4),
        ]
        for tx, dir_, ty_off in cannon_specs:
            x = tx * PLAT_TILE
            y = (self.world.rows + ty_off) * PLAT_TILE + 4
            interval = random.randint(110, 180)
            cannons.append(CarrotCannon(x, y, dir_, interval))
        return cannons

    def _spawn_coins(self):
        coins = []
        for _ in range(20):
            tx = random.randint(8, self.world.cols - 8)
            ty = random.randint(self.world.rows - 9, self.world.rows - 4)
            coins.append(Coin(tx * PLAT_TILE + 4, ty * PLAT_TILE + 4))
        return coins

    def update(self):
        speed = 1.9
        if self.game.upgrade_fast_paws:
            speed += 0.4
        self.player.update_platformer(
            base_speed=speed, gravity=PLAT_GRAVITY, max_fall=PLAT_MAX_FALL,
            jump_vel=PLAT_JUMP_VEL, blocked_fn=self.world.collides)

        if self.player.y > self.world.height + 40:
            if self.player.damage(1):
                self.game.rival.punish(0.10)
                Sound.play(2, 5)
            # Respawn just behind where you fell (not all the way to start)
            self.player.x = max(32, self.player.x - 48)
            self.player.y = (self.world.rows - 5) * PLAT_TILE
            self.player.vy = 0

        prect = self.player.rect()
        if self.world.tile_at(prect[0] + prect[2] / 2,
                              prect[1] + prect[3] - 2) == 3:
            if self.player.damage(1):
                self.game.rival.punish(0.06)
                self.player.x -= 30 * self.player.facing
                self.player.vy = -4

        for c in self.cannons:
            c.update(self.projectiles)
        for p in self.projectiles:
            p.update()
            if p.alive and rects_overlap(prect, p.rect()):
                if self.player.damage(1):
                    self.game.rival.punish(0.05)
                p.alive = False
        self.projectiles = [p for p in self.projectiles if p.alive]

        for coin in self.coins:
            coin.update()
            if coin.alive and rects_overlap(prect, coin.rect()):
                coin.alive = False
                self.player.coins += 1
                Sound.play(1, 1)
        self.coins = [c for c in self.coins if c.alive]

        for c in self.carrots:
            c.update()
            if c.alive and rects_overlap(prect, c.rect()):
                c.alive = False
                self.collected += 1
                Sound.play(1, 1)
                self.game.rival.reward(0.08)
                self.game.flash_message(
                    f"CARROT {self.collected}/{self.target_carrots}", 30)
        self.carrots = [c for c in self.carrots if c.alive]

        if self.collected >= self.target_carrots:
            gx, gy = self.goal_x, self.goal_y
            if abs(self.player.x - gx) < 24 and abs(self.player.y - gy) < 30:
                self.game.complete_level(ST_LOBBY, "CANNON LEVEL CLEAR!")
                return

        self.game.rival.update_meter()
        target_cam = self.player.x - SCREEN_W // 2
        self.cam_x = clamp(target_cam, 0, self.world.width - SCREEN_W)

        if not self.player.alive:
            self.game.game_over("BLASTED BY A CANNON")
            return

    def draw(self):
        cam_x = int(self.cam_x)
        self.world.draw(cam_x, theme="green")
        for c in self.cannons:
            c.draw(cam_x)
        for p in self.projectiles:
            p.draw(cam_x)
        for coin in self.coins:
            coin.draw(cam_x, 0)
        for c in self.carrots:
            c.draw(cam_x, 0)
        if self.collected >= self.target_carrots:
            gx = self.goal_x - cam_x
            gy = self.goal_y
            pyxel.rectb(int(gx) - 4, int(gy) - 4, 24, 32, COL_YELLOW)
        self.player.draw(cam_x, 0)
        self._draw_hud()

    def _draw_hud(self):
        for i in range(self.player.max_health):
            col = COL_RED if i < self.player.health else COL_DGRAY
            x = 4 + i * 10; y = 4
            pyxel.rect(x, y + 1, 3, 3, col)
            pyxel.rect(x + 4, y + 1, 3, 3, col)
            pyxel.rect(x + 1, y + 4, 5, 2, col)
            pyxel.pset(x + 2, y + 6, col)
        pyxel.blt(4, 18, 0, SPR_COIN[0], SPR_COIN[1], 8, 8, COLKEY)
        pyxel.text(14, 19, str(self.player.coins), COL_YELLOW)
        obj = f"CARROTS {self.collected}/{self.target_carrots}"
        draw_panel(SCREEN_W // 2 - 60, 2, 120, 9, COL_WHITE, COL_BLACK)
        draw_centered(obj, 4, COL_ORANGE)
        draw_centered("LEVEL 5: CANNONS", 12, COL_PEACH)
        if pyxel.frame_count < 240:
            if self.game.has_double_jump:
                pyxel.text(4, SCREEN_H - 12, "SPACE = DOUBLE JUMP", COL_WHITE)
            else:
                pyxel.text(4, SCREEN_H - 12, "BUY DOUBLE JUMP AT SHOP!", COL_YELLOW)


# =====================================================================
# LEVEL: PLATFORMER ATTRACT — uses HotBunny + LovePullMeter
# =====================================================================
class LevelPlatformAttract:
    def __init__(self, game):
        self.game = game
        self.world = PlatformerWorld(110, 16, PLAT_TILE)
        self._build_world()
        self.player = Player(32, (self.world.rows - 4) * PLAT_TILE)
        self.player.mode = "platformer"
        self.player.max_jumps = 2 if self.game.has_double_jump else 1
        self.player.jumps_left = self.player.max_jumps
        # Lila on cloud, high in the sky
        bunny_x = (self.world.cols - 5) * PLAT_TILE
        bunny_y = 60  # high up — cloud altitude
        self.hot_bunny = HotBunnyAttraction(bunny_x, bunny_y)
        # Give Lila the pit zones to target
        self.hot_bunny.set_hazards(self._collect_pit_zones())
        self.hot_bunny.set_blocked_fn(self.world.collides)
        self.love_meter = LovePullMeter()
        self.love_meter.configure(level_no=3)
        # Carrots spread across the longer level
        self.carrots = []
        for tx in (10, 22, 36, 48, 62, 76, 90, 100):
            ty = self.world.rows - random.randint(5, 8)
            self.carrots.append(Carrot(tx * PLAT_TILE + 2,
                                       ty * PLAT_TILE - 8))
        self.target_carrots = 6
        self.collected = 0
        self.coins = []
        for _ in range(22):
            tx = random.randint(8, self.world.cols - 8)
            ty = random.randint(self.world.rows - 9, self.world.rows - 4)
            self.coins.append(Coin(tx * PLAT_TILE + 4, ty * PLAT_TILE + 4))
        self.cam_x = 0

    def _collect_pit_zones(self):
        """Find x-ranges where there's no floor → Lila will hover over these."""
        zones = []
        w = self.world
        bottom = w.rows - 1
        in_pit = False
        pit_start = 0
        for tx in range(w.cols):
            empty = w.grid[bottom][tx] == 0
            if empty and not in_pit:
                in_pit = True
                pit_start = tx
            elif not empty and in_pit:
                in_pit = False
                zones.append((pit_start * PLAT_TILE,
                              tx * PLAT_TILE))
        return zones

    def _build_world(self):
        w = self.world
        w.fill_floor(2)
        # Many holes — Lila will lure player into these
        holes = [(14, 16), (22, 25), (32, 35), (44, 47),
                 (56, 58), (66, 69), (78, 81), (90, 92), (98, 101)]
        for hx1, hx2 in holes:
            for tx in range(hx1, hx2 + 1):
                w.set_tile(tx, w.rows - 1, 0)
                w.set_tile(tx, w.rows - 2, 0)
        # Spikes guarding SOME pit edges — being pulled is risky but fair.
        # Only every other pit gets a guard spike, on one side.
        for i, (hx1, hx2) in enumerate(holes):
            if i % 2 == 0 and w.grid[w.rows - 1][hx1 - 1] != 0:
                w.set_tile(hx1 - 1, w.rows - 3, 3)
        # A few sparse ground spikes to dodge while being pulled
        for sx in (28, 60, 86):
            if w.grid[w.rows - 1][sx] != 0:
                w.set_tile(sx, w.rows - 3, 3)
        # Platforms spanning the longer world
        for tx, ty, length in [
            (8, w.rows - 5, 3), (14, w.rows - 7, 3), (20, w.rows - 5, 4),
            (28, w.rows - 8, 3), (35, w.rows - 5, 3), (42, w.rows - 7, 4),
            (50, w.rows - 6, 3), (57, w.rows - 8, 3), (63, w.rows - 5, 4),
            (70, w.rows - 7, 3), (77, w.rows - 5, 3), (84, w.rows - 8, 4),
            (90, w.rows - 5, 3), (96, w.rows - 7, 3), (102, w.rows - 5, 4),
        ]:
            for i in range(length):
                w.set_tile(tx + i, ty, 1)
        w.set_tile(w.cols - 4, w.rows - 3, 4)

    def update(self):
        speed = 1.9
        if self.game.upgrade_fast_paws:
            speed += 0.4
        self.hot_bunny.update(self.player, self.love_meter)

        self.player.update_platformer(
            base_speed=speed, gravity=PLAT_GRAVITY, max_fall=PLAT_MAX_FALL,
            jump_vel=PLAT_JUMP_VEL, blocked_fn=self.world.collides)

        if self.player.y > self.world.height + 40:
            if self.player.damage(1):
                self.game.rival.punish(0.10)
                Sound.play(2, 5)
            # Respawn just behind where you fell (not all the way to start)
            self.player.x = max(32, self.player.x - 48)
            self.player.y = (self.world.rows - 5) * PLAT_TILE
            self.player.vy = 0

        prect = self.player.rect()
        # Spike contact — deadly especially while being pulled by Lila
        if self.world.tile_at(prect[0] + prect[2] / 2,
                              prect[1] + prect[3] - 2) == 3:
            if self.player.damage(1):
                self.game.rival.punish(0.06)
                self.player.x -= 24 * self.player.facing
                self.player.vy = -4
                Sound.play(2, 14)
        for coin in self.coins:
            coin.update()
            if coin.alive and rects_overlap(prect, coin.rect()):
                coin.alive = False
                self.player.coins += 1
                Sound.play(1, 1)
        self.coins = [c for c in self.coins if c.alive]

        for c in self.carrots:
            c.update()
            if c.alive and rects_overlap(prect, c.rect()):
                c.alive = False
                self.collected += 1
                Sound.play(1, 1)
                self.game.rival.reward(0.08)
                self.love_meter.progress = max(
                    0.0, self.love_meter.progress - 0.06)
                self.game.flash_message(
                    f"CARROT {self.collected}/{self.target_carrots}", 30)
        self.carrots = [c for c in self.carrots if c.alive]

        if (self.collected >= self.target_carrots
                and dist(self.player.x, self.player.y,
                         self.hot_bunny.x, self.hot_bunny.y) < 30):
            self.game.complete_level(ST_LOBBY, "LOVE BUNNY MET!")
            return

        self.game.rival.update_meter()
        self.cam_x = clamp(self.player.x - SCREEN_W // 2,
                           0, self.world.width - SCREEN_W)

        if not self.player.alive:
            self.game.game_over("LILA DRAGGED YOU DOWN")

    def draw(self):
        cam_x = int(self.cam_x)
        self.world.draw(cam_x, theme="sunset")
        for coin in self.coins:
            coin.draw(cam_x, 0)
        for c in self.carrots:
            c.draw(cam_x, 0)
        self.hot_bunny.draw(cam_x, 0, self.player, self.love_meter)
        self.player.draw(cam_x, 0)
        # Red vignette pulse when Lila is actively dragging you
        if self.love_meter.attack_mode:
            fc = pyxel.frame_count
            if fc % 8 < 4:
                pyxel.rectb(0, 0, SCREEN_W, SCREEN_H, COL_RED)
                pyxel.rectb(1, 1, SCREEN_W - 2, SCREEN_H - 2, COL_PINK)
        self._draw_hud()

    def _draw_hud(self):
        for i in range(self.player.max_health):
            col = COL_RED if i < self.player.health else COL_DGRAY
            x = 4 + i * 10; y = 4
            pyxel.rect(x, y + 1, 3, 3, col)
            pyxel.rect(x + 4, y + 1, 3, 3, col)
            pyxel.rect(x + 1, y + 4, 5, 2, col)
            pyxel.pset(x + 2, y + 6, col)
        pyxel.blt(4, 18, 0, SPR_COIN[0], SPR_COIN[1], 8, 8, COLKEY)
        pyxel.text(14, 19, str(self.player.coins), COL_YELLOW)
        obj = f"CARROTS {self.collected}/{self.target_carrots}"
        draw_panel(SCREEN_W // 2 - 60, 2, 120, 9, COL_WHITE, COL_BLACK)
        draw_centered(obj, 4, COL_PINK)
        draw_centered("LEVEL 6: LILA'S PULL", 12, COL_PEACH)

        # Love pull bar (left)
        x = 5; y = 35; h = 100; w = 6
        pyxel.rect(x - 2, y - 11, w + 4, h + 14, COL_BLACK)
        pyxel.rectb(x - 2, y - 11, w + 4, h + 14, COL_PINK)
        label_col = COL_RED if self.love_meter.attack_mode \
                    and self.love_meter.pulse % 10 < 5 else COL_PINK
        pyxel.text(x - 1, y - 9, "H", label_col)
        pyxel.rect(x, y, w, h, COL_NAVY)
        fill_h = int(h * self.love_meter.progress)
        fill_col = COL_RED if self.love_meter.attack_mode else COL_PINK
        pyxel.rect(x, y + h - fill_h, w, fill_h, fill_col)
        pyxel.rectb(x, y, w, h, COL_WHITE)
        if self.love_meter.attack_mode:
            border_col = COL_RED if self.love_meter.pulse % 8 < 4 else COL_YELLOW
            pyxel.rectb(x - 1, y - 1, w + 2, h + 2, border_col)


# =====================================================================
# LEVEL: PLATFORMER FENCE — three cage-break methods + baby sprites
# =====================================================================
class LevelPlatformFence:
    def __init__(self, game):
        self.game = game
        self.world = PlatformerWorld(110, 16, PLAT_TILE)
        self._build_world()
        self.player = Player(32, (self.world.rows - 4) * PLAT_TILE)
        self.player.mode = "platformer"
        self.player.max_jumps = 2 if self.game.has_double_jump else 1
        self.player.jumps_left = self.player.max_jumps
        self.fences = []
        x_cursor = 6
        while x_cursor < self.world.cols - 8:
            x_cursor += random.randint(4, 7)
            if x_cursor >= self.world.cols - 8:
                break
            h = random.choice([1, 2, 2])
            fy = (self.world.rows - 2) * PLAT_TILE - h * PLAT_TILE
            self.fences.append(Fence(x_cursor * PLAT_TILE, fy, h))
        self.cages = []
        # More cages spread across the longer level
        for _ in range(6):
            cx = random.randint(12, self.world.cols - 12)
            self.cages.append(RabbitCage(cx * PLAT_TILE,
                                          (self.world.rows - 4) * PLAT_TILE - 8))
        self.coins = []
        for _ in range(30):
            tx = random.randint(6, self.world.cols - 6)
            ty = random.randint(self.world.rows - 9, self.world.rows - 4)
            self.coins.append(Coin(tx * PLAT_TILE + 4, ty * PLAT_TILE + 4))
        # Carrot ammo spread out
        self.carrot_pickups = []
        for _ in range(6):
            tx = random.randint(8, self.world.cols - 8)
            self.carrot_pickups.append(
                Carrot(tx * PLAT_TILE + 2,
                       (self.world.rows - 3) * PLAT_TILE - 6))
        self.bombs = []
        self.ammo = 0
        self.rescued = []
        self.cages_broken = 0
        self.target_cages = 3
        self.goal_x = (self.world.cols - 4) * PLAT_TILE
        self.cam_x = 0

    def _build_world(self):
        w = self.world
        w.fill_floor(2)
        # More plateaus across the longer world
        for tx, ty, length in [
            (15, w.rows - 4, 3), (28, w.rows - 5, 3),
            (42, w.rows - 4, 3), (55, w.rows - 5, 4),
            (68, w.rows - 4, 3), (82, w.rows - 5, 3),
            (96, w.rows - 4, 3),
        ]:
            for i in range(length):
                w.set_tile(tx + i, ty, 1)
        w.set_tile(w.cols - 3, w.rows - 3, 4)

    def update(self):
        speed = 2.0
        if self.game.upgrade_fast_paws:
            speed += 0.4

        def blocked(x, y, w, h):
            if self.world.collides(x, y, w, h):
                return True
            rect = (x, y, w, h)
            for f in self.fences:
                if rects_overlap(rect, f.rect()):
                    return True
            for c in self.cages:
                if not c.broken and rects_overlap(rect, c.rect()):
                    return True
            return False

        self.player.update_platformer(
            base_speed=speed, gravity=PLAT_GRAVITY, max_fall=PLAT_MAX_FALL,
            jump_vel=PLAT_JUMP_VEL, blocked_fn=blocked)

        # F-Slam (if owned)
        if (self.game.has_special_attack and pyxel.btnp(pyxel.KEY_F)
                and self.player.attack_cooldown <= 0):
            self.player.attack_cooldown = 45
            self.player.vy = max(self.player.vy, 4.0)
            Sound.play(0, 5)
            prect = self.player.rect()
            for c in self.cages:
                if not c.broken and dist(prect[0], prect[1],
                                          c.x, c.y) < 32:
                    self._break_cage(c, "SLAM")
                    break

        if self.player.y > self.world.height + 40:
            if self.player.damage(1):
                self.game.rival.punish(0.10)
                Sound.play(2, 5)
            # Respawn just behind where you fell (not all the way to start)
            self.player.x = max(32, self.player.x - 48)
            self.player.y = (self.world.rows - 5) * PLAT_TILE
            self.player.vy = 0

        prect = self.player.rect()

        # Pick up carrot ammo
        for c in self.carrot_pickups:
            c.update()
            if c.alive and rects_overlap(prect, c.rect()):
                c.alive = False
                self.ammo = min(3, self.ammo + 1)
                Sound.play(0, 17)
        self.carrot_pickups = [c for c in self.carrot_pickups if c.alive]

        # Throw bomb with E / RETURN
        if (pyxel.btnp(pyxel.KEY_E) or pyxel.btnp(pyxel.KEY_RETURN)) \
                and self.ammo > 0:
            self.ammo -= 1
            self.bombs.append(CageBomb(
                self.player.x + 12,
                self.player.y + 4,
                3.5 * self.player.facing))
            Sound.play(1, 4)

        # Stomp from above
        if self.player.vy > 1.5:
            for c in self.cages:
                if c.broken:
                    continue
                pr = self.player.rect()
                cr = c.rect()
                if (pr[0] + pr[2] > cr[0] + 4
                        and pr[0] < cr[0] + cr[2] - 4
                        and pr[1] + pr[3] >= cr[1]
                        and pr[1] + pr[3] <= cr[1] + 10):
                    self._break_cage(c, "STOMPED")
                    self.player.vy = -5
                    Sound.play(2, 18)
                    break

        # Bomb collisions
        for b in self.bombs:
            b.update()
            if not b.alive:
                continue
            for c in self.cages:
                if c.broken:
                    continue
                if rects_overlap(b.rect(), c.rect()):
                    self._break_cage(c, "SMASHED")
                    b.alive = False
                    break
        self.bombs = [b for b in self.bombs if b.alive]

        for coin in self.coins:
            coin.update()
            if coin.alive and rects_overlap(prect, coin.rect()):
                coin.alive = False
                self.player.coins += 1
                Sound.play(1, 1)
        self.coins = [c for c in self.coins if c.alive]

        if self.cages_broken >= self.target_cages and self.player.x > self.goal_x:
            self.game.complete_level(ST_LOBBY, "FENCE LEVEL CLEAR!")
            return

        self.game.rival.update_meter()
        self.cam_x = clamp(self.player.x - SCREEN_W // 2,
                           0, self.world.width - SCREEN_W)
        if not self.player.alive:
            self.game.game_over("OUTRUN BY THE FENCES")

    def _break_cage(self, cage, verb):
        cage.broken = True
        self.cages_broken += 1
        self.rescued.append((cage.x + 4, cage.y + 8))
        Sound.play(2, 15)
        self.game.rival.reward(0.10)
        self.game.flash_message(
            f"CAGE {verb}! {self.cages_broken}", 40)

    def draw(self):
        cam_x = int(self.cam_x)
        self.world.draw(cam_x, theme="green")
        for coin in self.coins:
            coin.draw(cam_x, 0)
        for c in self.carrot_pickups:
            c.draw(cam_x, 0)
        for f in self.fences:
            f.draw(cam_x)
        for c in self.cages:
            c.draw(cam_x)
        # Rescued babies using BabyHare sprites + Heart
        for (rx, ry) in self.rescued:
            sx = int(rx - cam_x)
            sy = int(ry + math.sin(pyxel.frame_count * 0.1) * 2)
            frame = SPR_BABY_R_1 if (pyxel.frame_count // 12) % 2 == 0 \
                    else SPR_BABY_R_2
            u, v, w, h = frame
            pyxel.blt(sx, sy, 0, u, v, w, h, COLKEY)
            if pyxel.frame_count % 30 < 15:
                hu, hv, hw, hh = SPR_HEART
                pyxel.blt(sx + 4, sy - 9, 0, hu, hv, hw, hh, COLKEY)
        for b in self.bombs:
            b.draw(cam_x)
        self.player.draw(cam_x, 0)
        self._draw_hud()

    def _draw_hud(self):
        for i in range(self.player.max_health):
            col = COL_RED if i < self.player.health else COL_DGRAY
            x = 4 + i * 10; y = 4
            pyxel.rect(x, y + 1, 3, 3, col)
            pyxel.rect(x + 4, y + 1, 3, 3, col)
            pyxel.rect(x + 1, y + 4, 5, 2, col)
            pyxel.pset(x + 2, y + 6, col)
        pyxel.blt(4, 18, 0, SPR_COIN[0], SPR_COIN[1], 8, 8, COLKEY)
        pyxel.text(14, 19, str(self.player.coins), COL_YELLOW)
        obj = f"CAGES {self.cages_broken}/{self.target_cages}"
        draw_panel(SCREEN_W // 2 - 60, 2, 120, 9, COL_WHITE, COL_BLACK)
        draw_centered(obj, 4, COL_LBLUE)
        draw_centered("LEVEL 7: FENCES & CAGES", 12, COL_PEACH)
        pyxel.text(4, SCREEN_H - 20, f"E/ENTER: THROW ({self.ammo}/3)",
                   COL_ORANGE)
        pyxel.text(4, SCREEN_H - 12, "STOMP CAGES FROM ABOVE", COL_LBLUE)
        if self.game.has_special_attack:
            pyxel.text(140, SCREEN_H - 12, "F: SLAM", COL_PINK)


# =====================================================================
# LEVEL: FROZEN GARDEN — Time-freeze puzzle platformer (Trial 8)
# Hold Q to freeze time — falling Easter eggs stop mid-air. Limited
# time-mana. Collect carrots while dodging the egg rain.
# =====================================================================
class FallingEgg:
    """Easter egg that falls from the sky. Freezes when time is stopped.
    Uses world-space x (camera-scrolled), screen-space y (falls top->bottom)."""
    def __init__(self, x, y, vx=0.0, vy=1.6):
        self.x = float(x)
        self.y = float(y)
        self.vx = vx
        self.vy = vy
        self.w = 12
        self.h = 12
        self.alive = True
        self.frozen = False
        self.spin = random.random() * 6.28

    def rect(self):
        return (int(self.x) + 2, int(self.y) + 2, self.w, self.h)

    def update(self):
        if self.frozen:
            return
        self.x += self.vx
        self.y += self.vy
        self.spin += 0.18
        self.vx += math.sin(self.spin * 0.7) * 0.04
        # Despawn when it falls below screen (y is screen-height space)
        if self.y > SCREEN_H + 20:
            self.alive = False

    def draw(self, cam_x):
        sx = int(self.x - cam_x)
        sy = int(self.y)
        if sx < -20 or sx > SCREEN_W + 20:
            return
        u, v, w, h = SPR_EASTER_EGG
        if self.frozen:
            # Cyan freeze outline + shimmer
            pyxel.rectb(sx - 2, sy - 2, 20, 20, COL_LBLUE)
            if pyxel.frame_count % 6 < 3:
                pyxel.pset(sx + 6, sy - 4, COL_WHITE)
        pyxel.blt(sx, sy, 0, u, v, w, h, COLKEY)


class LevelFrozenGarden:
    """Time-freeze platformer between Fences and Boss. Hold Q to stop time.
    Short freeze window, long cooldown — use it wisely."""

    MAX_MANA = 90       # 3 seconds of freeze (was 6)
    MANA_DRAIN = 1.5    # drains faster — freeze lasts ~2 seconds of use
    COOLDOWN_FRAMES = 150  # full 5-second cooldown after freeze runs out
    MANA_REGEN = 0.0    # no passive regen — only refills after cooldown

    def __init__(self, game):
        self.game = game
        # MUCH longer world
        self.world = PlatformerWorld(160, 16, PLAT_TILE)
        self._build_world()
        self.player = Player(32, (self.world.rows - 4) * PLAT_TILE)
        self.player.mode = "platformer"
        self.player.max_jumps = 2 if self.game.has_double_jump else 1
        self.player.jumps_left = self.player.max_jumps
        self.eggs = []
        self.spawn_timer = 0
        self.spawn_interval = 14  # denser egg rain
        self.mana = float(self.MAX_MANA)
        self.freezing = False
        self.cooldown = 0  # when >0, cannot freeze (mana locked at 0)
        # Carrots spread across the long world
        self.carrots = []
        for tx in (16, 34, 52, 72, 92, 112, 132, 150):
            ty = self.world.rows - random.randint(4, 6)
            self.carrots.append(Carrot(tx * PLAT_TILE + 2,
                                       ty * PLAT_TILE - 8))
        self.target_carrots = 6
        self.collected = 0
        # Coins
        self.coins = []
        for _ in range(30):
            tx = random.randint(8, self.world.cols - 8)
            ty = random.randint(self.world.rows - 6, self.world.rows - 4)
            self.coins.append(Coin(tx * PLAT_TILE + 4, ty * PLAT_TILE + 4))
        self.cam_x = 0
        # Visual snowflakes
        self.snowflakes = []
        for _ in range(40):
            self.snowflakes.append([
                random.uniform(0, SCREEN_W * 4),
                random.uniform(0, SCREEN_H),
                random.uniform(0.3, 0.9),
            ])

    def _build_world(self):
        w = self.world
        # Ground only — NO protective platforms (must dodge eggs on the ground)
        w.fill_floor(2)
        # Damage-dealing pits scattered across the long level
        pit_zones = [(20, 22), (38, 40), (54, 57), (72, 74),
                     (90, 93), (108, 110), (126, 129), (144, 146)]
        for gx1, gx2 in pit_zones:
            for tx in range(gx1, gx2 + 1):
                w.set_tile(tx, w.rows - 1, 0)
                w.set_tile(tx, w.rows - 2, 0)
        w.set_tile(w.cols - 3, w.rows - 3, 4)

    def update(self):
        speed = 1.95
        if self.game.upgrade_fast_paws:
            speed += 0.4

        # Cooldown ticking — locks freeze until done
        if self.cooldown > 0:
            self.cooldown -= 1
            if self.cooldown == 0:
                self.mana = float(self.MAX_MANA)  # refill after cooldown

        # Time freeze input — only if not in cooldown and have mana
        want_freeze = pyxel.btn(pyxel.KEY_Q)
        self.freezing = want_freeze and self.mana > 0 and self.cooldown == 0
        if self.freezing:
            self.mana = max(0.0, self.mana - self.MANA_DRAIN)
            if self.mana <= 0:
                self.mana = 0.0
                self.freezing = False
                self.cooldown = self.COOLDOWN_FRAMES  # start long cooldown

        # Apply frozen state to all eggs
        for e in self.eggs:
            e.frozen = self.freezing

        self.player.update_platformer(
            base_speed=speed, gravity=PLAT_GRAVITY, max_fall=PLAT_MAX_FALL,
            jump_vel=PLAT_JUMP_VEL, blocked_fn=self.world.collides)

        # Falling into a pit deals damage
        if self.player.y > self.world.height + 40:
            if self.player.damage(1):
                self.game.rival.punish(0.10)
                Sound.play(2, 5)
            self.player.x = max(32, self.player.x - 40)
            self.player.y = (self.world.rows - 5) * PLAT_TILE
            self.player.vy = 0

        # Spawn eggs across the WHOLE visible screen width (rain everywhere)
        self.spawn_timer -= 1
        if self.spawn_timer <= 0:
            self.spawn_timer = self.spawn_interval
            # Spawn 2-3 eggs spread across the full camera view each tick
            for _ in range(random.randint(2, 3)):
                spawn_x = self.cam_x + random.randint(0, SCREEN_W)
                spawn_x = clamp(spawn_x, 8, self.world.width - 8)
                vx = random.uniform(-0.5, 0.5)
                vy = random.uniform(1.6, 2.4)
                self.eggs.append(FallingEgg(spawn_x, -16, vx, vy))

        prect = self.player.rect()
        for e in self.eggs:
            e.update()
            if e.alive and not e.frozen and rects_overlap(prect, e.rect()):
                if self.player.damage(1):
                    self.game.rival.punish(0.04)
                    Sound.play(2, 14)
                e.alive = False
        self.eggs = [e for e in self.eggs if e.alive]

        for coin in self.coins:
            coin.update()
            if coin.alive and rects_overlap(prect, coin.rect()):
                coin.alive = False
                self.player.coins += 1
                Sound.play(1, 1)
        self.coins = [c for c in self.coins if c.alive]

        for c in self.carrots:
            c.update()
            if c.alive and rects_overlap(prect, c.rect()):
                c.alive = False
                self.collected += 1
                Sound.play(1, 1)
                self.game.rival.reward(0.06)
                self.game.flash_message(
                    f"CARROT {self.collected}/{self.target_carrots}", 30)
        self.carrots = [c for c in self.carrots if c.alive]

        # Goal
        if self.collected >= self.target_carrots \
                and self.player.x > (self.world.cols - 5) * PLAT_TILE:
            self.game.complete_level(ST_LOBBY, "FROZEN GARDEN CLEAR!")
            return

        self.game.rival.update_meter()
        self.cam_x = clamp(self.player.x - SCREEN_W // 2,
                           0, self.world.width - SCREEN_W)

        for sf in self.snowflakes:
            if not self.freezing:
                sf[0] += sf[2] * 0.3
                sf[1] += sf[2]
                if sf[1] > SCREEN_H:
                    sf[1] = -5
                    sf[0] = random.uniform(self.cam_x, self.cam_x + SCREEN_W * 2)

        if not self.player.alive:
            self.game.game_over("FROZEN BY THE EGGS")

    def draw(self):
        cam_x = int(self.cam_x)
        # Cold sky gradient
        for y in range(SCREEN_H):
            t = y / SCREEN_H
            if t < 0.4:
                col = COL_LBLUE
            elif t < 0.7:
                col = COL_WHITE
            else:
                col = COL_LGREEN
            pyxel.rect(0, y, SCREEN_W, 1, col)
        # Crystal mountains parallax
        for i in range(6):
            mx = (i * 80 - cam_x // 3) % (SCREEN_W + 120) - 60
            pyxel.tri(int(mx), 160, int(mx) + 40, 80,
                      int(mx) + 80, 160, COL_PEACH)
            pyxel.tri(int(mx) + 20, 160, int(mx) + 40, 95,
                      int(mx) + 60, 160, COL_WHITE)
        # Snowflakes (frozen-tinted when freezing)
        for sf in self.snowflakes:
            sx = int(sf[0] - cam_x * 0.5) % (SCREEN_W + 20) - 10
            sy = int(sf[1])
            col = COL_WHITE if not self.freezing else COL_LBLUE
            pyxel.pset(sx, sy, col)

        # World tiles — override base world.draw bg with our sky
        T = self.world.tile
        start_tx = max(0, int(cam_x // T) - 1)
        end_tx = min(self.world.cols, int((cam_x + SCREEN_W) // T) + 2)
        for ty in range(self.world.rows):
            for tx in range(start_tx, end_tx):
                t = self.world.grid[ty][tx]
                if t == 0:
                    continue
                sx = tx * T - cam_x
                sy = ty * T
                if t == 1 or t == 2:
                    pyxel.rect(sx, sy, T, T, COL_PURPLE)
                    if ty == 0 or self.world.grid[ty - 1][tx] == 0:
                        pyxel.rect(sx, sy, T, 3, COL_LBLUE)
                    if (tx * 3 + ty) % 5 == 0:
                        pyxel.pset(sx + 3, sy + 6, COL_WHITE)
                        pyxel.pset(sx + 9, sy + 10, COL_WHITE)
                elif t == 4:
                    pyxel.rect(sx + 6, sy - 8, 2, T + 8, COL_BROWN)
                    pyxel.tri(sx + 8, sy - 6, sx + 8, sy + 2,
                              sx + 14, sy - 2, COL_YELLOW)

        # Coins
        for coin in self.coins:
            coin.draw(cam_x, 0)
        # Carrots
        for c in self.carrots:
            c.draw(cam_x, 0)
        # Eggs
        for e in self.eggs:
            e.draw(cam_x)
        # Player
        self.player.draw(cam_x, 0)
        # Freeze tint overlay when active
        if self.freezing:
            for y in range(0, SCREEN_H, 4):
                pyxel.line(0, y, SCREEN_W, y, COL_LBLUE)
        self._draw_hud()

    def _draw_hud(self):
        for i in range(self.player.max_health):
            col = COL_RED if i < self.player.health else COL_DGRAY
            x = 4 + i * 10; y = 4
            pyxel.rect(x, y + 1, 3, 3, col)
            pyxel.rect(x + 4, y + 1, 3, 3, col)
            pyxel.rect(x + 1, y + 4, 5, 2, col)
            pyxel.pset(x + 2, y + 6, col)
        pyxel.blt(4, 18, 0, SPR_COIN[0], SPR_COIN[1], 8, 8, COLKEY)
        pyxel.text(14, 19, str(self.player.coins), COL_YELLOW)
        obj = f"CARROTS {self.collected}/{self.target_carrots}"
        draw_panel(SCREEN_W // 2 - 60, 2, 120, 9, COL_WHITE, COL_BLACK)
        draw_centered(obj, 4, COL_LBLUE)
        draw_centered("LEVEL 8: FROZEN GARDEN", 12, COL_PEACH)
        # Time freeze gauge (left side)
        mx, my, mw, mh = 4, 34, 60, 6
        pyxel.rect(mx, my, mw, mh, COL_NAVY)
        if self.cooldown > 0:
            # Cooldown — bar fills back up over COOLDOWN_FRAMES
            frac = 1.0 - (self.cooldown / self.COOLDOWN_FRAMES)
            fill = int(mw * frac)
            pyxel.rect(mx, my, fill, mh, COL_DGRAY)
            pyxel.rectb(mx, my, mw, mh, COL_WHITE)
            pyxel.text(mx, my - 7, "COOLDOWN...", COL_RED)
        else:
            fill = int(mw * (self.mana / self.MAX_MANA))
            col = COL_WHITE if self.freezing else COL_LBLUE
            pyxel.rect(mx, my, fill, mh, col)
            pyxel.rectb(mx, my, mw, mh, COL_WHITE)
            pyxel.text(mx, my - 7, "FREEZE READY", COL_LBLUE)
        pyxel.text(4, SCREEN_H - 12, "HOLD Q TO FREEZE - MIND THE PITS!",
                   COL_LBLUE)


# =====================================================================
# LEVEL: BOSS — platforms only, lateral boss, egg projectiles
# =====================================================================
BOSS_FLOOR_Y = SCREEN_H - 24
BOSS_PLATFORMS = [
    (32,                  SCREEN_H - 90,  56, 6),
    (SCREEN_W // 2 - 28,  SCREEN_H - 130, 56, 6),
    (SCREEN_W - 88,       SCREEN_H - 90,  56, 6),
]


class LevelBoss:
    def __init__(self, game):
        self.game = game
        self.player = Player(SCREEN_W // 2 - 12, BOSS_FLOOR_Y - 40)
        self.player.mode = "platformer"
        self.player.max_health = 4
        self.player.health = 4
        self.player.max_jumps = 2
        self.player.jumps_left = 2
        self.player.vy = 0
        self.player.on_ground = False
        self.boss = OldEasterBunnyBoss()
        self.eggs = []
        self.intro_timer = 80
        self.win_timer = 0
        self.carrots = []
        for i in range(4):
            x = 30 + i * 60
            y = BOSS_FLOOR_Y - 12
            self.carrots.append(Carrot(x, y))
        self.ammo = 0
        self.thrown = []

    def _blocked(self, x, y, w, h):
        # Side walls
        if x < 8 or x + w > SCREEN_W - 8:
            return True
        # Top wall
        if y < 12:
            return True
        # Floor blocks
        if y + h > BOSS_FLOOR_Y:
            return True
        # Platforms — solid from all sides
        for (px, py, pw, ph) in BOSS_PLATFORMS:
            if rects_overlap((x, y, w, h), (px, py, pw, ph)):
                return True
        return False

    def update(self):
        if self.intro_timer > 0:
            self.intro_timer -= 1
            if self.intro_timer == 60:
                Sound.play(2, 10)
            return
        if self.win_timer > 0:
            self.win_timer -= 1
            if self.win_timer <= 0:
                self.game.complete_level(ST_LOBBY, "BOSS DEFEATED!")
            return

        speed = 2.2
        if self.game.upgrade_fast_paws:
            speed += 0.4
        self.player.update_platformer(
            base_speed=speed, gravity=PLAT_GRAVITY, max_fall=PLAT_MAX_FALL,
            jump_vel=PLAT_JUMP_VEL, blocked_fn=self._blocked)

        prect = self.player.rect()
        for c in self.carrots:
            c.update()
            if c.alive and rects_overlap(prect, c.rect()):
                c.alive = False
                self.ammo = min(3, self.ammo + 1)
                Sound.play(0, 17)
        self.carrots = [c for c in self.carrots if c.alive]
        if not self.carrots and self.ammo == 0:
            for i in range(3):
                self.carrots.append(
                    Carrot(40 + i * 80, BOSS_FLOOR_Y - 12))

        if (pyxel.btnp(pyxel.KEY_RETURN) or pyxel.btnp(pyxel.KEY_E)) \
                and self.ammo > 0:
            self.ammo -= 1
            dx = (self.boss.x + 28) - self.player.x
            dy = (self.boss.y + 32) - self.player.y
            nx, ny = normalize(dx, dy)
            self.thrown.append({
                "x": self.player.x + 8, "y": self.player.y,
                "vx": nx * 4.5, "vy": ny * 3.5 - 1.2,
                "alive": True,
            })
            Sound.play(1, 4)

        for t in self.thrown:
            t["x"] += t["vx"]
            t["y"] += t["vy"]
            t["vy"] += 0.16
            rect = (int(t["x"]), int(t["y"]), 10, 10)
            if rects_overlap(rect, self.boss.rect()):
                if self.boss.take_hit():
                    self.game.rival.reward(0.08)
                    if self.boss.hp <= 0:
                        self.win_timer = 90
                        Sound.play(2, 7)
                t["alive"] = False
            if (t["y"] > SCREEN_H or t["x"] < -20
                    or t["x"] > SCREEN_W + 20):
                t["alive"] = False
        self.thrown = [t for t in self.thrown if t["alive"]]

        self.boss.update(self.player, self.eggs)

        for e in self.eggs:
            e.update()
            if e.alive and rects_overlap(self.player.rect(), e.rect()):
                if self.player.damage(1):
                    self.game.rival.punish(0.05)
                    Sound.play(2, 14)
                e.alive = False
        self.eggs = [e for e in self.eggs if e.alive]

        self.game.rival.update_meter()
        self.game.camera.x = 0
        self.game.camera.y = 0

        if not self.player.alive:
            self.game.game_over("THE OLD BUNNY CRUSHED YOU")

    def draw(self):
        pyxel.cls(COL_PURPLE)
        for y in range(SCREEN_H):
            t = y / SCREEN_H
            if t > 0.7:
                pyxel.rect(0, y, SCREEN_W, 1, COL_NAVY)
        for i in range(40):
            sx = (i * 173 + 7) % SCREEN_W
            sy = (i * 97 + 13) % SCREEN_H
            if (pyxel.frame_count + i * 5) % 30 < 15:
                pyxel.pset(sx, sy, COL_PEACH)

        pyxel.rect(0, BOSS_FLOOR_Y, SCREEN_W, SCREEN_H - BOSS_FLOOR_Y,
                   COL_DGRAY)
        for x in range(0, SCREEN_W, 8):
            pyxel.pset(x, BOSS_FLOOR_Y, COL_WHITE)
        pyxel.rect(0, 0, 8, SCREEN_H, COL_DGRAY)
        pyxel.rect(SCREEN_W - 8, 0, 8, SCREEN_H, COL_DGRAY)
        pyxel.rect(0, 0, SCREEN_W, 12, COL_DGRAY)

        for (px, py, pw, ph) in BOSS_PLATFORMS:
            pyxel.rect(px, py, pw, ph, COL_BROWN)
            pyxel.rect(px, py, pw, 2, COL_PEACH)

        # Boss walkway — visible cloud-platform under the Old Bunny
        walk_y = OldEasterBunnyBoss.BOSS_Y + 60
        walk_x_min = OldEasterBunnyBoss.WALK_X_MIN - 6
        walk_x_max = OldEasterBunnyBoss.WALK_X_MAX + 56 + 6
        walk_w = walk_x_max - walk_x_min
        pyxel.rect(walk_x_min, walk_y, walk_w, 6, COL_WHITE)
        pyxel.rect(walk_x_min + 4, walk_y - 3, walk_w - 8, 3, COL_WHITE)
        pyxel.rect(walk_x_min + 8, walk_y + 6, walk_w - 16, 2, COL_LGRAY)

        self.boss.draw()
        for c in self.carrots:
            c.draw(0, 0)
        for t in self.thrown:
            u, v, w, h = SPR_CARROT
            pyxel.blt(int(t["x"]), int(t["y"]), 1, u, v, w, h, COLKEY)
        for e in self.eggs:
            e.draw()
        self.player.draw(0, 0)
        self._draw_hud()
        self.boss.draw_hp_bar()

        if self.intro_timer > 0:
            if (self.intro_timer // 6) % 2 == 0:
                draw_panel(SCREEN_W // 2 - 100, SCREEN_H // 2 - 8, 200, 16,
                           COL_RED, COL_BLACK)
                draw_centered("THE OLD EASTER BUNNY APPEARS!",
                              SCREEN_H // 2 - 4, COL_RED)
        if self.win_timer > 0:
            draw_panel(SCREEN_W // 2 - 70, SCREEN_H // 2 - 8, 140, 16,
                       COL_YELLOW, COL_BLACK)
            draw_centered("BOSS DEFEATED!", SCREEN_H // 2 - 4, COL_YELLOW)

    def _draw_hud(self):
        for i in range(self.player.max_health):
            col = COL_RED if i < self.player.health else COL_DGRAY
            x = 4 + i * 10; y = 4
            pyxel.rect(x, y + 1, 3, 3, col)
            pyxel.rect(x + 4, y + 1, 3, 3, col)
            pyxel.rect(x + 1, y + 4, 5, 2, col)
            pyxel.pset(x + 2, y + 6, col)
        draw_panel(4, 18, 60, 10, COL_ORANGE, COL_BLACK)
        pyxel.text(7, 20, f"AMMO {self.ammo}/3", COL_ORANGE)
        draw_centered("E/ENTER: THROW CARROT", 32, COL_PEACH)
        draw_centered("DODGE THE EGGS!", 40, COL_LBLUE)


# =====================================================================
# LEVEL: LOVE — finale
# =====================================================================
class LevelLove:
    def __init__(self, game):
        self.game = game
        self.player = Player(40, SCREEN_H - 60)
        game.rival.x = SCREEN_W - 60
        game.rival.y = SCREEN_H - 60
        game.rival.progress = 0.3
        self.school_x = SCREEN_W // 2 - SCHOOL_W // 2
        self.school_y = 30
        self.target_carrots = 5
        self.delivered = 0
        self.carrots = []
        spots = [(50, 110), (SCREEN_W - 60, 110), (SCREEN_W // 2 - 8, 150),
                 (60, 180), (SCREEN_W - 80, 170), (SCREEN_W // 2 + 30, 200),
                 (30, 220), (SCREEN_W - 40, 220), (SCREEN_W // 2 - 70, 160),
                 (SCREEN_W // 2 + 70, 160)]
        for x, y in spots:
            self.carrots.append(Carrot(x, y))
        self.babies = []
        self.hearts = []
        for _ in range(8):
            self.hearts.append({
                "x": random.randint(0, SCREEN_W),
                "y": random.randint(0, SCREEN_H),
                "vy": -random.uniform(0.3, 0.8),
                "life": 200
            })

    def school_rect(self):
        return (self.school_x, self.school_y, SCHOOL_W, SCHOOL_H)

    def update(self):
        speed = 2.0
        if self.game.upgrade_fast_paws:
            speed += 0.4

        r = self.game.rival
        pull = 0.0
        if not r.attack_mode:
            pull = r.progress * 1.2
        else:
            pull = 2.5
        if pull > 0.05:
            nx, ny = normalize(r.x - self.player.x, r.y - self.player.y)
            self.player.x += nx * pull * 0.4
            self.player.y += ny * pull * 0.4

        self.player.update(speed, blocked_fn=self._blocked)

        for c in self.carrots:
            c.update()
            if c.alive and rects_overlap(self.player.rect(), c.rect()):
                if self.player.carrying_carrots < self.player.max_carry:
                    c.alive = False
                    self.player.carrying_carrots += 1
                    Sound.play(1, 1)
                    self.game.rival.reward(0.06)
        self.carrots = [c for c in self.carrots if c.alive]

        if (self.player.carrying_carrots > 0
                and rects_overlap(self.player.rect(), self.school_rect())):
            self.delivered += self.player.carrying_carrots
            for _ in range(self.player.carrying_carrots):
                bx = self.school_x + random.randint(8, SCHOOL_W - 24)
                by = self.school_y + SCHOOL_H + random.randint(-4, 12)
                self.babies.append(BabyHare(bx, by))
            self.player.carrying_carrots = 0
            Sound.play(1, 2)
            self.game.rival.reward(0.15)
            self.game.flash_message(f"{self.delivered}/{self.target_carrots} DELIVERED!", 50)

        for b in self.babies:
            b.update()

        for h in self.hearts:
            h["y"] += h["vy"]
            h["life"] -= 1
            if h["y"] < -10 or h["life"] <= 0:
                h["x"] = random.randint(0, SCREEN_W)
                h["y"] = SCREEN_H + 5
                h["life"] = 200

        r.update_meter()
        if r.attack_mode:
            r.chase_player(self.player, speed=2.2)
            if rects_overlap(self.player.rect(), r.rect()):
                self.game.game_over("THE OLD BUNNY TOOK LILA")
                return
            r.attack_frames -= 1
            if r.attack_frames <= 0:
                r.end_attack(0.78)
        else:
            r.x = approach(r.x, self.player.x + 60, 0.6 + r.progress)
            r.y = approach(r.y, self.player.y, 0.4)
            r.x = clamp(r.x, 0, SCREEN_W - 16)
            r.y = clamp(r.y, 20, SCREEN_H - 20)

        self.game.camera.x = 0
        self.game.camera.y = 0

        if self.delivered >= self.target_carrots:
            self.game.set_state(ST_WIN)
            self.game.rival.reset_for_level(1)
            return
        if not self.player.alive:
            self.game.game_over("YOU LOST LILA")
            return

    def _blocked(self, x, y, w, h):
        if x < 0 or y < 12 or x + w > SCREEN_W or y + h > SCREEN_H:
            return True
        return False

    def draw(self):
        for y in range(SCREEN_H):
            t = y / SCREEN_H
            if t < 0.25:
                col = COL_PURPLE
            elif t < 0.5:
                col = COL_RED
            elif t < 0.72:
                col = COL_ORANGE
            else:
                col = COL_LGREEN
            pyxel.rect(0, y, SCREEN_W, 1, col)
        sun_x = SCREEN_W // 2
        sun_y = 110
        pyxel.circ(sun_x, sun_y, 22, COL_YELLOW)
        pyxel.circ(sun_x, sun_y, 16, COL_PEACH)

        for h in self.hearts:
            x = int(h["x"]); y = int(h["y"])
            u, v, w, h_ = SPR_HEART
            pyxel.blt(x, y, 0, u, v, w, h_, COLKEY)

        u, v, w, h = SPR_SCHOOL
        pyxel.blt(self.school_x, self.school_y, 0, u, v, w, h, COLKEY)

        for b in self.babies:
            b.draw(0, 0)
        for c in self.carrots:
            c.draw(0, 0)

        r = self.game.rival
        if r.progress > 0.2:
            steps = 10
            for i in range(steps):
                t = i / steps
                lx = int(self.player.x * (1 - t) + r.x * t)
                ly = int(self.player.y * (1 - t) + r.y * t)
                if (pyxel.frame_count + i * 3) % 8 < 4:
                    col = COL_RED if r.attack_mode else COL_PINK
                    pyxel.pset(lx, ly, col)
                    pyxel.pset(lx + 1, ly, col)
        r.draw(0, 0)
        self.player.draw(0, 0)
        self._draw_hud()

    def _draw_hud(self):
        for i in range(self.player.max_health):
            col = COL_RED if i < self.player.health else COL_DGRAY
            x = 4 + i * 10; y = 4
            pyxel.rect(x, y + 1, 3, 3, col)
            pyxel.rect(x + 4, y + 1, 3, 3, col)
            pyxel.rect(x + 1, y + 4, 5, 2, col)
            pyxel.pset(x + 2, y + 6, col)
            pyxel.pset(x + 3, y + 7, col)
            pyxel.pset(x + 4, y + 6, col)
        pyxel.blt(4, 18, 0, SPR_COIN[0], SPR_COIN[1], 8, 8, COLKEY)
        pyxel.text(14, 19, str(self.player.coins), COL_YELLOW)
        obj = f"FINAL TRIAL {self.delivered}/{self.target_carrots}"
        draw_panel(SCREEN_W // 2 - 60, 2, 120, 9, COL_PINK, COL_BLACK)
        draw_centered(obj, 4, COL_PINK)
        if self.player.carrying_carrots > 0:
            txt = f"HOLDING: {self.player.carrying_carrots}"
            draw_panel(4, 32, 70, 9, COL_WHITE, COL_BLACK)
            pyxel.text(8, 34, txt, COL_ORANGE)
# =====================================================================
# MUSIC SYSTEM
# =====================================================================
SND_STUPSI_MELODY = 19
SND_STUPSI_BASS   = 20
SND_SONG2_MELODY  = 21
SND_SONG2_BASS    = 22


def _notes_from(tokens):
    """Build a Pyxel note string from (note, length) tuples.
    A length-N note becomes the note followed by (N-1) 'r-extend' via
    repeating the note value (Pyxel sustains by repeating the note)."""
    out = []
    for note, length in tokens:
        out.append(note)
        # Sustain: repeat the same note so it holds (sounds connected),
        # except for rests which simply repeat 'r'.
        out.extend([note] * (length - 1))
    return "".join(out), len(out)


def setup_song_stupsi(vol_char="5"):
    # ------------------------------------------------------------------
    # "SPRING HOP" — bright, bouncy major-key theme in C major.
    # Melody and bass are both exactly 32 sixteenth-steps per phrase and
    # loop cleanly. Chords: C  - G  - Am - F  (classic happy progression).
    # ------------------------------------------------------------------
    melody = [
        # bar 1 (C)         bar 2 (G)
        ("c3", 2), ("e3", 2), ("g3", 2), ("e3", 2),
        ("d3", 2), ("g3", 2), ("b3", 2), ("g3", 2),
        # bar 3 (Am)        bar 4 (F)
        ("c3", 2), ("a3", 2), ("e3", 2), ("a3", 2),
        ("f3", 2), ("a3", 2), ("c3", 4),
        # bar 5 (C)         bar 6 (G)
        ("e3", 2), ("g3", 2), ("c3", 2), ("g3", 2),
        ("d3", 2), ("b3", 2), ("g3", 2), ("d3", 2),
        # bar 7 (Am)        bar 8 (F -> G turnaround)
        ("a3", 2), ("c3", 2), ("e3", 2), ("c3", 2),
        ("f3", 2), ("g3", 2), ("c3", 4),
    ]
    bass = [
        ("c2", 4), ("c2", 4), ("g1", 4), ("g1", 4),
        ("a1", 4), ("a1", 4), ("f1", 4), ("f1", 4),
        ("c2", 4), ("c2", 4), ("g1", 4), ("g1", 4),
        ("a1", 4), ("a1", 4), ("f1", 2), ("g1", 2), ("g1", 4),
    ]
    m_str, m_len = _notes_from(melody)
    b_str, b_len = _notes_from(bass)
    try:
        # Melody: soft square ('s'), bass: triangle ('t') for warmth.
        # Slower tempo (speed 11) so it's relaxed, not frantic.
        pyxel.sounds[SND_STUPSI_MELODY].set(
            m_str, "s" * m_len, vol_char * m_len, "n" * m_len, 11)
        pyxel.sounds[SND_STUPSI_BASS].set(
            b_str, "t" * b_len, vol_char * b_len, "n" * b_len, 11)
    except Exception:
        pass


def setup_song_two(vol_char="5"):
    # ------------------------------------------------------------------
    # "MOONLIT MEADOW" — calm, gentle theme in A minor. Flowing melody
    # with a steady walking bass. Am - F - C - G progression.
    # Both lines are phase-locked to 32 steps.
    # ------------------------------------------------------------------
    melody = [
        # Am                 F
        ("a3", 3), ("c3", 1), ("e3", 2), ("c3", 2),
        ("f3", 3), ("a3", 1), ("c3", 2), ("a3", 2),
        # C                  G
        ("g3", 3), ("c3", 1), ("e3", 2), ("g3", 2),
        ("d3", 3), ("b3", 1), ("g3", 2), ("d3", 2),
        # Am                 F
        ("a3", 2), ("e3", 2), ("d3", 2), ("c3", 2),
        ("f3", 2), ("c3", 2), ("a3", 2), ("f3", 2),
        # C                  G -> Am turnaround
        ("e3", 2), ("g3", 2), ("c3", 2), ("e3", 2),
        ("d3", 2), ("b3", 2), ("a3", 4),
    ]
    bass = [
        ("a1", 4), ("a1", 4), ("f1", 4), ("f1", 4),
        ("c2", 4), ("c2", 4), ("g1", 4), ("g1", 4),
        ("a1", 4), ("a1", 4), ("f1", 4), ("f1", 4),
        ("c2", 4), ("c2", 4), ("g1", 4), ("a1", 4),
    ]
    m_str, m_len = _notes_from(melody)
    b_str, b_len = _notes_from(bass)
    try:
        # Slow, mellow tempo (speed 14) + triangle melody for a soft tone.
        pyxel.sounds[SND_SONG2_MELODY].set(
            m_str, "t" * m_len, vol_char * m_len, "n" * m_len, 14)
        pyxel.sounds[SND_SONG2_BASS].set(
            b_str, "t" * b_len, vol_char * b_len, "n" * b_len, 14)
    except Exception:
        pass


class MusicManager:
    TRACK_NONE = 0
    TRACK_STUPSI = 1
    TRACK_SONG2 = 2

    def __init__(self):
        self.current_track = self.TRACK_NONE
        self.volume_level = 2
        self._setup_with_volume()

    def _vol_char(self):
        return ["0", "3", "5", "7"][self.volume_level]

    def _setup_with_volume(self):
        setup_song_stupsi(self._vol_char())
        setup_song_two(self._vol_char())

    def set_volume_level(self, lvl):
        lvl = clamp(lvl, 0, 3)
        if lvl == self.volume_level:
            return
        self.volume_level = lvl
        self._setup_with_volume()
        old = self.current_track
        self.stop()
        if lvl > 0 and old != self.TRACK_NONE:
            self.play(old)

    def play(self, track):
        self.stop()
        if self.volume_level == 0 or track == self.TRACK_NONE:
            return
        self.current_track = track
        try:
            if track == self.TRACK_STUPSI:
                pyxel.play(3, SND_STUPSI_MELODY, loop=True)
                pyxel.play(2, SND_STUPSI_BASS, loop=True)
            elif track == self.TRACK_SONG2:
                pyxel.play(3, SND_SONG2_MELODY, loop=True)
                pyxel.play(2, SND_SONG2_BASS, loop=True)
        except Exception:
            pass

    def stop(self):
        try:
            pyxel.stop(2)
            pyxel.stop(3)
        except Exception:
            pass
        self.current_track = self.TRACK_NONE


# =====================================================================
# SETTINGS
# =====================================================================
SETTINGS_ITEMS = [
    ("music_vol",   "MUSIC VOLUME"),
    ("sfx_vol",     "SFX VOLUME"),
    ("music_track", "MUSIC TRACK"),
    ("controls",    "CONTROL HINT"),
    ("back",        "BACK TO LOBBY"),
]
VOL_LABELS = ["OFF", "LOW", "MID", "HIGH"]
TRACK_LABELS = ["NONE", "SPRING HOP", "MOONLIT"]
CONTROL_LABELS = ["ARROWS", "WASD"]


class SettingsScreen:
    def __init__(self, game):
        self.game = game
        self.cursor = 0
        self.flash_text = ""
        self.flash_t = 0

    def update(self):
        if self.flash_t > 0:
            self.flash_t -= 1
        n = len(SETTINGS_ITEMS)
        if btnp_any(pyxel.KEY_DOWN, pyxel.KEY_S):
            self.cursor = (self.cursor + 1) % n
            Sound.play(0, 11)
        if btnp_any(pyxel.KEY_UP, pyxel.KEY_W):
            self.cursor = (self.cursor - 1) % n
            Sound.play(0, 11)
        delta = 0
        if btnp_any(pyxel.KEY_LEFT, pyxel.KEY_A):
            delta = -1
        if btnp_any(pyxel.KEY_RIGHT, pyxel.KEY_D):
            delta = 1
        if delta != 0:
            self._adjust(delta)
        if btnp_any(pyxel.KEY_RETURN, pyxel.KEY_SPACE, pyxel.KEY_E):
            key, _ = SETTINGS_ITEMS[self.cursor]
            if key == "back":
                Sound.play(0, 12)
                self.game.exit_settings()
            else:
                self._adjust(1)
        if pyxel.btnp(pyxel.KEY_ESCAPE):
            Sound.play(0, 12)
            self.game.exit_settings()

    def _adjust(self, delta):
        key, _ = SETTINGS_ITEMS[self.cursor]
        if key == "music_vol":
            new = clamp(self.game.music.volume_level + delta, 0, 3)
            self.game.music.set_volume_level(new)
        elif key == "sfx_vol":
            new = clamp(self.game.sfx_volume + delta, 0, 3)
            self.game.set_sfx_volume(new)
        elif key == "music_track":
            self.game.music_track_pref = (
                self.game.music_track_pref + delta) % 3
            self.game.music.play(self.game.music_track_pref)
        elif key == "controls":
            self.game.control_scheme = (self.game.control_scheme + delta) % 2
        Sound.play(0, 11)

    def draw(self):
        pyxel.cls(COL_NAVY)
        for i in range(30):
            sx = (i * 27 + 13) % SCREEN_W
            sy = (i * 13 + 7) % SCREEN_H
            if (pyxel.frame_count + i * 5) % 60 < 30:
                pyxel.pset(sx, sy, COL_PURPLE)
        draw_panel(20, 4, SCREEN_W - 40, 11, COL_YELLOW, COL_BLACK)
        draw_centered("SETTINGS", 6, COL_YELLOW)
        list_x = 16
        list_y = 28
        for i, (key, label) in enumerate(SETTINGS_ITEMS):
            row_y = list_y + i * 22
            is_sel = i == self.cursor
            if is_sel:
                pyxel.rect(list_x - 4, row_y - 4, SCREEN_W - 24, 19,
                           COL_PURPLE)
                pyxel.rectb(list_x - 4, row_y - 4, SCREEN_W - 24, 19,
                            COL_YELLOW)
            label_col = COL_YELLOW if is_sel else COL_WHITE
            pyxel.text(list_x, row_y, label, label_col)
            value_x = list_x + 110
            value_y = row_y
            if key == "music_vol":
                self._draw_volume_bar(value_x, value_y,
                                      self.game.music.volume_level)
            elif key == "sfx_vol":
                self._draw_volume_bar(value_x, value_y,
                                      self.game.sfx_volume)
            elif key == "music_track":
                txt = TRACK_LABELS[self.game.music_track_pref]
                col = COL_PINK if is_sel else COL_PEACH
                pyxel.text(value_x, value_y, f"< {txt} >", col)
            elif key == "controls":
                txt = CONTROL_LABELS[self.game.control_scheme]
                col = COL_LBLUE if is_sel else COL_PEACH
                pyxel.text(value_x, value_y, f"< {txt} >", col)
            elif key == "back":
                if is_sel:
                    pyxel.text(value_x, value_y, "[ENTER]", COL_YELLOW)
            if is_sel and key != "back":
                pyxel.text(list_x, row_y + 8,
                           "LEFT/RIGHT TO ADJUST", COL_LGRAY)
        pyxel.text(4, SCREEN_H - 18, "ESC: BACK", COL_LGRAY)

    def _draw_volume_bar(self, x, y, level):
        for i in range(4):
            col = COL_DGRAY
            if i < level:
                col = [COL_DGRAY, COL_LGREEN, COL_YELLOW, COL_RED][level]
            pyxel.rect(x + i * 8, y + 2, 6, 4, col)
            pyxel.rectb(x + i * 8, y + 2, 6, 4, COL_LGRAY)
        pyxel.text(x + 36, y, VOL_LABELS[level], COL_PEACH)


# =====================================================================
# LOBBY
# =====================================================================
LOBBY_LEVELS = [
    (ST_LEVEL_MEADOW,       "1. MEADOW",          COL_LGREEN),
    (ST_LEVEL_KIDS,         "2. KIDS RESCUE",     COL_PINK),
    (ST_LEVEL_MAZE1,        "3. MAZE I",          COL_BROWN),
    (ST_LEVEL_MAZE2,        "4. MAZE II",         COL_PURPLE),
    (ST_LEVEL_PLAT_CANNON,  "5. CANNONS",         COL_ORANGE),
    (ST_LEVEL_PLAT_ATTRACT, "6. LILA'S PULL",     COL_PINK),
    (ST_LEVEL_PLAT_FENCE,   "7. FENCES",          COL_LBLUE),
    (ST_LEVEL_FROZEN,       "8. FROZEN GARDEN",   COL_LBLUE),
    (ST_LEVEL_BOSS,         "9. THE OLD BUNNY",   COL_RED),
    (ST_LEVEL_LOVE,         "10. WIN LILA",       COL_YELLOW),
]


class Lobby:
    def __init__(self, game):
        self.game = game
        self.cursor_pos = 0
        self.scroll = 0
        self.entered_msg_timer = 90
        self.tree_positions = [(40, 200), (210, 195), (130, 205)]
        # Walkable hero avatar
        self.avatar_x = 30.0
        self.avatar_y = float(SCREEN_H - 56)
        self.avatar_facing = 1
        self.avatar_walking = False
        self.avatar_anim = 0
        self.avatar_anim_tick = 0
        # Signposts: one per level, lined up along the meadow. Walk onto
        # one and press E to enter that level. Plus shop/settings/credits.
        self._build_signposts()
        self.active_post = -1

    def _build_signposts(self):
        self.posts = []
        n = len(LOBBY_LEVELS)
        # Spread level posts across the ground in two rows
        for i in range(n):
            col = i % 5
            row = i // 5
            px = 24 + col * 44
            py = SCREEN_H - 72 + row * 22
            self.posts.append({"kind": "level", "idx": i,
                               "x": px, "y": py})
        # Shop / Settings / Credits posts on the right side
        extras = [("shop", "SHOP"), ("settings", "SET"), ("credits", "CRED")]
        for j, (kind, _) in enumerate(extras):
            self.posts.append({"kind": kind, "idx": -1,
                               "x": SCREEN_W - 44, "y": SCREEN_H - 70 + j * 16})

    def update(self):
        if self.entered_msg_timer > 0:
            self.entered_msg_timer -= 1

        # --- Free-walking hero avatar (arrow keys / WASD) ---
        speed = 1.9
        moving = False
        if btn_any(pyxel.KEY_LEFT, pyxel.KEY_A):
            self.avatar_x -= speed
            self.avatar_facing = -1
            moving = True
        if btn_any(pyxel.KEY_RIGHT, pyxel.KEY_D):
            self.avatar_x += speed
            self.avatar_facing = 1
            moving = True
        if btn_any(pyxel.KEY_UP, pyxel.KEY_W):
            self.avatar_y -= speed
            moving = True
        if btn_any(pyxel.KEY_DOWN, pyxel.KEY_S):
            self.avatar_y += speed
            moving = True
        self.avatar_x = clamp(self.avatar_x, 4, SCREEN_W - 36)
        self.avatar_y = clamp(self.avatar_y, SCREEN_H - 84, SCREEN_H - 36)
        self.avatar_walking = moving
        self.avatar_anim_tick += 1
        if moving and self.avatar_anim_tick >= 6:
            self.avatar_anim_tick = 0
            self.avatar_anim = 1 - self.avatar_anim

        # Which signpost is the avatar standing on?
        self.active_post = -1
        acx = self.avatar_x + 16
        acy = self.avatar_y + 16
        for pi, post in enumerate(self.posts):
            if abs(acx - (post["x"] + 8)) < 22 and abs(acy - (post["y"] + 8)) < 24:
                self.active_post = pi
                break

        # Confirm with E / Enter / Space when on a post
        if btnp_any(pyxel.KEY_RETURN, pyxel.KEY_SPACE, pyxel.KEY_E):
            if self.active_post >= 0:
                self._select_post(self.posts[self.active_post])

    def _select_post(self, post):
        kind = post["kind"]
        if kind == "level":
            idx = post["idx"]
            if not self.game.is_level_unlocked(idx):
                self.game.flash_message("LEVEL LOCKED", 40)
                Sound.play(2, 3)
                return
            state, label, _ = LOBBY_LEVELS[idx]
            self.game.begin_level_with_intro(state)
        elif kind == "shop":
            self.game.enter_shop()
        elif kind == "settings":
            self.game.enter_settings()
        elif kind == "credits":
            self.game.set_state(ST_CREDITS)
            self.game.credits_screen = CreditsScreen()

    def draw(self):
        pyxel.cls(COL_LBLUE)
        pyxel.rect(0, SCREEN_H - 96, SCREEN_W, 96, COL_LGREEN)
        for i in range(40):
            seed = (i * 173 + 31) % 1024
            wx = (seed * 7) % SCREEN_W
            wy = SCREEN_H - 96 + (seed * 11) % 90
            col = [COL_GREEN, COL_PEACH, COL_PINK, COL_YELLOW][seed % 4]
            pyxel.pset(int(wx), int(wy), col)
        for i in range(4):
            cx = (i * 80 + (pyxel.frame_count // 4) % 320) % (SCREEN_W + 60) - 30
            cy = 18 + (i * 13) % 25
            pyxel.rect(int(cx), int(cy), 22, 6, COL_WHITE)
            pyxel.rect(int(cx) + 4, int(cy) - 2, 14, 4, COL_WHITE)
        pyxel.circ(SCREEN_W - 28, 28, 12, COL_YELLOW)
        pyxel.circ(SCREEN_W - 28, 28, 10, COL_ORANGE)

        # School in the back
        sxr = SCREEN_W - 110
        syr = SCREEN_H - 134
        u, v, w, h = SPR_SCHOOL
        pyxel.blt(sxr, syr, 0, u, v, w, h, COLKEY)

        # --- Draw signposts ---
        for pi, post in enumerate(self.posts):
            px, py = post["x"], post["y"]
            highlighted = (pi == self.active_post)
            if post["kind"] == "level":
                idx = post["idx"]
                unlocked = self.game.is_level_unlocked(idx)
                completed = self.game.is_level_completed(idx)
                _, label, lcol = LOBBY_LEVELS[idx]
                # post pole
                pyxel.rect(px + 7, py + 6, 2, 10, COL_BROWN)
                # sign board
                board_col = lcol if unlocked else COL_DGRAY
                pyxel.rect(px, py, 16, 8, board_col)
                pyxel.rectb(px, py, 16, 8,
                            COL_YELLOW if highlighted else COL_BLACK)
                # number on the sign
                num = str(idx + 1)
                pyxel.text(px + (4 if idx < 9 else 2), py + 1, num,
                           COL_BLACK if unlocked else COL_LGRAY)
                if completed:
                    pyxel.text(px + 11, py - 6, "*", COL_YELLOW)
                elif not unlocked:
                    pyxel.text(px + 5, py - 7, "L", COL_RED)
            else:
                # shop / settings / credits posts
                tag = {"shop": "SHOP", "settings": "SET",
                       "credits": "CRED"}[post["kind"]]
                tcol = {"shop": COL_PINK, "settings": COL_LBLUE,
                        "credits": COL_PEACH}[post["kind"]]
                pyxel.rect(px + 7, py + 6, 2, 10, COL_BROWN)
                pyxel.rect(px - 4, py, 24, 8, tcol)
                pyxel.rectb(px - 4, py, 24, 8,
                            COL_YELLOW if highlighted else COL_BLACK)
                pyxel.text(px - 2, py + 1, tag, COL_BLACK)

        # --- Walking hero avatar ---
        if self.avatar_walking:
            if self.avatar_facing >= 0:
                spr = SPR_PLAYER_R_RUN1 if self.avatar_anim == 0 \
                    else SPR_PLAYER_R_RUN2
            else:
                spr = SPR_PLAYER_L_RUN1 if self.avatar_anim == 0 \
                    else SPR_PLAYER_L_RUN2
        else:
            spr = SPR_PLAYER_R_IDLE if self.avatar_facing >= 0 \
                else SPR_PLAYER_L_IDLE
        au, av, aw, ah = spr
        pyxel.blt(int(self.avatar_x), int(self.avatar_y), 0,
                  au, av, aw, ah, COLKEY)

        # --- HUD ---
        draw_panel(4, 2, 70, 9, COL_WHITE, COL_BLACK)
        pyxel.text(8, 4, "LOBBY", COL_YELLOW)
        draw_panel(SCREEN_W - 60, 2, 56, 9, COL_WHITE, COL_BLACK)
        pyxel.blt(SCREEN_W - 56, 3, 0, SPR_COIN[0], SPR_COIN[1], 8, 8, COLKEY)
        pyxel.text(SCREEN_W - 46, 4, str(self.game.persistent_coins),
                   COL_YELLOW)

        # Prompt: what's the avatar standing on?
        if self.active_post >= 0:
            post = self.posts[self.active_post]
            if post["kind"] == "level":
                idx = post["idx"]
                _, label, _ = LOBBY_LEVELS[idx]
                if self.game.is_level_unlocked(idx):
                    msg = "PRESS E: " + label
                    mcol = COL_LGREEN
                else:
                    msg = "LOCKED - FINISH PREVIOUS LEVEL"
                    mcol = COL_RED
            else:
                names = {"shop": "ENTER SHOP", "settings": "SETTINGS",
                         "credits": "CREDITS"}
                msg = "PRESS E: " + names[post["kind"]]
                mcol = COL_LGREEN
            draw_panel(SCREEN_W // 2 - 90, 14, 180, 11, COL_WHITE, COL_BLACK)
            draw_centered(msg, 16, mcol)

        pyxel.text(6, SCREEN_H - 10, "ARROWS/WASD: WALK   E: ENTER",
                   COL_WHITE)
        if self.entered_msg_timer > 0:
            if (self.entered_msg_timer // 4) % 2 == 0:
                draw_centered("WALK TO A SIGN AND PRESS E",
                              SCREEN_H - 26, COL_PINK)


# =====================================================================
# SHOP
# =====================================================================
SHOP_ITEMS = [
    ("dash",         "DASH SKILL",        12, "SHIFT TO DASH FAST", "skill"),
    ("double_jump",  "DOUBLE JUMP",       18, "EXTRA AIR JUMP",      "skill"),
    ("special",      "SPECIAL ATTACK",    20, "F KEY: SLAM ATK",     "skill"),
    ("max_health",   "EXTRA HEART",       15, "MAX HEALTH +1",       "skill"),
    ("speed_potion", "SPEED POTION",       5, "FAST PAWS 10 SEC",    "potion"),
    ("shield_potion","SHIELD POTION",      6, "INVULN 6 SEC",        "potion"),
    ("heal_potion",  "HEAL POTION",        4, "REFILL HEARTS",       "potion"),
    ("jump_potion",  "JUMP POTION",        5, "HIGHER JUMPS 10 SEC", "potion"),
]


class Shop:
    def __init__(self, game):
        self.game = game
        self.cursor = 0
        self.flash_text = ""
        self.flash_t = 0

    def update(self):
        if self.flash_t > 0:
            self.flash_t -= 1
        n = len(SHOP_ITEMS) + 1
        if btnp_any(pyxel.KEY_DOWN, pyxel.KEY_S):
            self.cursor = (self.cursor + 1) % n
            Sound.play(0, 11)
        if btnp_any(pyxel.KEY_UP, pyxel.KEY_W):
            self.cursor = (self.cursor - 1) % n
            Sound.play(0, 11)
        if btnp_any(pyxel.KEY_RETURN, pyxel.KEY_SPACE, pyxel.KEY_E):
            if self.cursor == len(SHOP_ITEMS):
                Sound.play(0, 12)
                self.game.exit_shop()
                return
            self._buy(self.cursor)
        if pyxel.btnp(pyxel.KEY_ESCAPE):
            Sound.play(0, 12)
            self.game.exit_shop()

    def _buy(self, idx):
        key, name, price, desc, kind = SHOP_ITEMS[idx]
        if kind == "skill" and self.game.has_skill(key):
            self.flash_text = "ALREADY OWNED"
            self.flash_t = 50
            Sound.play(2, 3)
            return
        if self.game.persistent_coins < price:
            self.flash_text = "NOT ENOUGH COINS"
            self.flash_t = 50
            Sound.play(2, 3)
            return
        self.game.persistent_coins -= price
        if kind == "skill":
            self.game.unlock_skill(key)
            self.flash_text = f"BOUGHT: {name}"
        else:
            self.game.add_potion(key)
            self.flash_text = "POTION ACQUIRED"
        self.flash_t = 50
        Sound.play(1, 2)

    def draw(self):
        pyxel.cls(COL_NAVY)
        for y in range(SCREEN_H - 30, SCREEN_H, 5):
            pyxel.rect(0, y, SCREEN_W, 1, COL_BROWN)
        for x in range(0, SCREEN_W, 32):
            pyxel.line(x, SCREEN_H - 30, x, SCREEN_H - 1, COL_DGRAY)
        draw_panel(20, 4, SCREEN_W - 40, 11, COL_YELLOW, COL_BLACK)
        draw_centered("PROFESSOR'S SHOP", 6, COL_RED)
        u, v, w, h = SPR_PROFESSOR
        pyxel.blt(SCREEN_W - 48, SCREEN_H - 58, 0, u, v, w, h, COLKEY)
        pyxel.blt(8, 18, 0, SPR_COIN[0], SPR_COIN[1], 8, 8, COLKEY)
        pyxel.text(18, 19, f"COINS: {self.game.persistent_coins}", COL_YELLOW)
        list_x = 8
        list_y = 32
        for i, (key, name, price, desc, kind) in enumerate(SHOP_ITEMS):
            row_y = list_y + i * 18
            is_sel = i == self.cursor
            owned = kind == "skill" and self.game.has_skill(key)
            if is_sel:
                pyxel.rect(list_x - 2, row_y - 2, SCREEN_W - 16, 17, COL_PURPLE)
                pyxel.rectb(list_x - 2, row_y - 2, SCREEN_W - 16, 17, COL_YELLOW)
            else:
                pyxel.rect(list_x - 2, row_y - 2, SCREEN_W - 16, 17, COL_BLACK)
            icon_col = COL_LGREEN if kind == "skill" else COL_PINK
            if owned:
                icon_col = COL_DGRAY
            pyxel.rect(list_x, row_y, 6, 6, icon_col)
            name_col = COL_YELLOW if is_sel else (COL_LGRAY if owned else COL_WHITE)
            pyxel.text(list_x + 10, row_y - 1, name, name_col)
            pyxel.text(list_x + 10, row_y + 7, desc, COL_PEACH)
            if owned:
                pyxel.text(SCREEN_W - 48, row_y + 2, "OWNED", COL_LGRAY)
            else:
                pyxel.text(SCREEN_W - 48, row_y + 2, f"{price}C", COL_YELLOW)
        back_y = list_y + len(SHOP_ITEMS) * 18 + 4
        is_back = self.cursor == len(SHOP_ITEMS)
        if is_back:
            pyxel.rect(list_x - 2, back_y - 2, SCREEN_W - 16, 11, COL_PURPLE)
            pyxel.rectb(list_x - 2, back_y - 2, SCREEN_W - 16, 11, COL_YELLOW)
            pyxel.text(list_x + 10, back_y + 1, "< BACK TO LOBBY", COL_YELLOW)
        else:
            pyxel.text(list_x + 10, back_y + 1, "< BACK TO LOBBY", COL_WHITE)
        if self.flash_t > 0:
            draw_panel(SCREEN_W // 2 - 70, SCREEN_H // 2 - 8, 140, 14,
                       COL_RED, COL_BLACK)
            draw_centered(self.flash_text, SCREEN_H // 2 - 4, COL_YELLOW)


# =====================================================================
# SCREENS
# =====================================================================
class LoadingScreen:
    def __init__(self):
        self.timer = 0
        self.duration = 120
        self.bunny_bounce = 0.0
        self.carrot_rot = 0.0
        self.hearts = []
        for _ in range(12):
            self._spawn_heart()

    def _spawn_heart(self):
        self.hearts.append({
            "x": random.randint(0, SCREEN_W),
            "y": SCREEN_H + random.randint(0, 30),
            "vy": -random.uniform(0.4, 1.0),
            "vx": random.uniform(-0.3, 0.3),
            "life": 180 + random.randint(0, 60),
        })

    def update(self):
        self.timer += 1
        self.bunny_bounce = abs(math.sin(self.timer * 0.18)) * 6
        self.carrot_rot += 0.15
        for h in self.hearts:
            h["x"] += h["vx"]
            h["y"] += h["vy"]
            h["life"] -= 1
        self.hearts = [h for h in self.hearts if h["life"] > 0]
        while len(self.hearts) < 14:
            self._spawn_heart()
        if self.timer % 18 == 0:
            Sound.play(2, 9)

    def done(self):
        return self.timer >= self.duration or btnp_any(pyxel.KEY_RETURN, pyxel.KEY_SPACE)

    def draw(self):
        pyxel.cls(COL_BLACK)
        for i in range(40):
            sx = (i * 173 + self.timer) % SCREEN_W
            sy = (i * 97 + self.timer // 2) % SCREEN_H
            pyxel.pset(sx, sy, COL_NAVY if i % 3 else COL_PURPLE)
        draw_centered("HARE HURRY", 30, COL_PINK)
        draw_centered("EASTER RIVAL", 42, COL_YELLOW)
        for h in self.hearts:
            x = int(h["x"]); y = int(h["y"])
            u, v, w, h_ = SPR_HEART
            pyxel.blt(x, y, 0, u, v, w, h_, COLKEY)
        cx = SCREEN_W // 2
        cy = SCREEN_H // 2
        scale_offset = int(math.cos(self.carrot_rot) * 4)
        u, v, w, h = SPR_CARROT
        pyxel.blt(cx - 8 + scale_offset, cy - 8, 1, u, v, w, h, COLKEY)
        bunny_y = SCREEN_H - 60 - int(self.bunny_bounce)
        bunny_x = SCREEN_W // 2 - 16
        bu, bv, bw, bh = SPR_PLAYER_R_IDLE
        pyxel.blt(bunny_x, bunny_y, 0, bu, bv, bw, bh, COLKEY)
        bar_w = 160
        bar_x = SCREEN_W // 2 - bar_w // 2
        bar_y = SCREEN_H - 30
        ratio = min(1.0, self.timer / self.duration)
        pyxel.rect(bar_x, bar_y, bar_w, 6, COL_DGRAY)
        pyxel.rect(bar_x, bar_y, int(bar_w * ratio), 6, COL_LGREEN)
        pyxel.rectb(bar_x, bar_y, bar_w, 6, COL_WHITE)
        draw_centered("LOADING EASTER TRIALS...", bar_y - 8, COL_PEACH)
        draw_centered("ENTER TO SKIP", SCREEN_H - 14, COL_DGRAY)


# =====================================================================
# PROLOGUE — animated story after Loading, before Title
# =====================================================================
class Prologue:
    LINES = [
        "YOU ARE A YOUNG HARE.",
        "AND YOU LOVE LILA.",
        "BUT LILA LOVES ONLY ONE:",
        "THE EASTER BUNNY HIMSELF.",
        "",
        "PASS NINE TRIALS.",
        "TAKE THE CROWN.",
        "THEN WIN HER HEART.",
        "",
        "VISIT THE SCHOOL'S SHOP",
        "BETWEEN TRIALS FOR SKILLS.",
    ]

    def __init__(self):
        self.timer = 0
        self.line_idx = 0
        self.char_idx = 0
        self.hare_x = -40.0
        self.rival_x = SCREEN_W + 40.0
        self.done_flag = False

    def update(self):
        self.timer += 1
        self.hare_x = approach(self.hare_x, 60, 0.9)
        self.rival_x = approach(self.rival_x, SCREEN_W - 90, 0.9)
        if btnp_any(pyxel.KEY_S, pyxel.KEY_ESCAPE):
            self.done_flag = True
            return
        if btnp_any(pyxel.KEY_RETURN, pyxel.KEY_SPACE):
            if self.line_idx < len(self.LINES):
                cur = self.LINES[self.line_idx]
                if self.char_idx < len(cur):
                    self.char_idx = len(cur)
                else:
                    self.line_idx += 1
                    self.char_idx = 0
                    Sound.play(2, 13)
            else:
                self.done_flag = True
                return
        if self.line_idx < len(self.LINES):
            cur = self.LINES[self.line_idx]
            if self.char_idx < len(cur):
                if self.timer % 2 == 0:
                    self.char_idx += 1
                    if cur and self.char_idx % 5 == 0:
                        Sound.play(2, 9)
            else:
                if self.timer % 70 == 0:
                    self.line_idx += 1
                    self.char_idx = 0
        else:
            if self.timer > 60:
                self.done_flag = True

    def done(self):
        return self.done_flag

    def draw(self):
        for y in range(SCREEN_H):
            t = y / SCREEN_H
            if t < 0.45:
                col = COL_PURPLE
            elif t < 0.65:
                col = COL_ORANGE
            elif t < 0.85:
                col = COL_PEACH
            else:
                col = COL_LGREEN
            pyxel.rect(0, y, SCREEN_W, 1, col)
        pyxel.circ(SCREEN_W // 2, 130, 26, COL_YELLOW)
        pyxel.circ(SCREEN_W // 2, 130, 20, COL_PEACH)
        # School in background using new big sprite
        u, v, w, h = SPR_SCHOOL
        pyxel.blt(SCREEN_W // 2 - 44, SCREEN_H - 100, 0, u, v, w, h, COLKEY)
        # Hero approaching from left
        bu, bv, bw, bh = SPR_PLAYER_R_IDLE
        pyxel.blt(int(self.hare_x), SCREEN_H - 60, 0, bu, bv, bw, bh, COLKEY)
        # Rival approaching from right
        if (self.timer // 14) % 2 == 0:
            ru, rv, rw, rh = SPR_RIVAL_L_1
        else:
            ru, rv, rw, rh = SPR_RIVAL_L_2
        pyxel.blt(int(self.rival_x), SCREEN_H - 60, 0,
                  ru, rv, rw, rh, COLKEY)
        draw_panel(16, 14, SCREEN_W - 32, 60, COL_PINK, COL_BLACK)
        draw_story_window(self.LINES, self.line_idx, self.char_idx,
                          16, 14, SCREEN_W - 32, 60, max_lines=4)
        draw_centered("[S] SKIP   [ENTER] NEXT", SCREEN_H - 12, COL_DGRAY)


# =====================================================================
# LEVEL INTROS — per-level animated story screens
# =====================================================================
LEVEL_STORIES = {
    ST_LEVEL_MEADOW: [
        "TRIAL 1: THE MEADOW",
        "",
        "PROVE YOURSELF FIRST.",
        "BRING TEN CARROTS HOME.",
        "WATCH OUT FOR THE FOX.",
    ],
    ST_LEVEL_KIDS: [
        "TRIAL 2: KIDS RESCUE",
        "",
        "BABY HARES ESCAPED.",
        "GUIDE THEM TO SCHOOL.",
        "LILA WILL HEAR OF YOUR DEEDS.",
    ],
    ST_LEVEL_MAZE1: [
        "TRIAL 3: THE MAZE",
        "",
        "THE FARMER BUILT A MAZE.",
        "STAY OUT OF HIS SIGHT.",
        "FIND THE HIDDEN CARROTS.",
    ],
    ST_LEVEL_MAZE2: [
        "TRIAL 4: DEEPER MAZE",
        "",
        "TWO FARMERS NOW PATROL.",
        "AND THE DOGS HAVE TEETH.",
        "ALMOST HALFWAY TO LILA.",
    ],
    ST_LEVEL_PLAT_CANNON: [
        "TRIAL 5: CARROT CANNONS",
        "",
        "THE FARMER FOUND ARTILLERY.",
        "DODGE THE FLYING CARROTS.",
        "BUY DOUBLE JUMP IN THE SHOP.",
    ],
    ST_LEVEL_PLAT_ATTRACT: [
        "TRIAL 6: ATTRACTION",
        "",
        "LILA APPEARS ON A CLOUD.",
        "HER LOVE WILL DRAG YOU DOWN.",
        "RESIST. YOU ARE NOT WORTHY YET.",
    ],
    ST_LEVEL_PLAT_FENCE: [
        "TRIAL 7: FENCES AND CAGES",
        "",
        "THE OLD BUNNY IMPRISONS BABIES.",
        "STOMP OR THROW TO FREE THEM.",
        "EVERY CAGE BREAKS HIS POWER.",
    ],
    ST_LEVEL_FROZEN: [
        "TRIAL 8: THE FROZEN GARDEN",
        "",
        "TIME ITSELF BENDS HERE.",
        "HOLD Q TO FREEZE TIME.",
        "EASTER EGGS RAIN FROM THE SKY.",
        "ONLY THE WORTHY CAN PASS.",
    ],
    ST_LEVEL_BOSS: [
        "TRIAL 9: THE OLD BUNNY",
        "",
        "HE WILL NOT YIELD HIS CROWN.",
        "THROW CARROTS WITH E.",
        "DODGE HIS EASTER EGGS.",
        "TAKE WHAT IS YOURS.",
    ],
    ST_LEVEL_LOVE: [
        "FINAL TRIAL: LILA",
        "",
        "YOU ARE THE EASTER BUNNY NOW.",
        "LILA COMES TO YOU.",
        "BRING FIVE LAST CARROTS HOME.",
        "WIN HER, FOREVER.",
    ],
}


class LevelIntro:
    def __init__(self, lines, next_state, game):
        self.lines = lines
        self.next_state = next_state
        self.game = game
        self.timer = 0
        self.line_idx = 0
        self.char_idx = 0
        self.hop = 0.0
        self.bunny_x = -40.0

    def update(self):
        self.timer += 1
        if self.timer == 1:
            Sound.play(2, 13)
        self.hop = abs(math.sin(self.timer * 0.18)) * 4
        self.bunny_x = approach(self.bunny_x, 32, 1.2)
        if btnp_any(pyxel.KEY_S, pyxel.KEY_ESCAPE):
            self._launch()
            return
        if btnp_any(pyxel.KEY_RETURN, pyxel.KEY_SPACE):
            if self.line_idx < len(self.lines):
                cur = self.lines[self.line_idx]
                if self.char_idx < len(cur):
                    self.char_idx = len(cur)
                else:
                    self.line_idx += 1
                    self.char_idx = 0
                    if self.line_idx < len(self.lines):
                        Sound.play(2, 9)
            else:
                self._launch()
                return
        if self.line_idx < len(self.lines):
            cur = self.lines[self.line_idx]
            if self.char_idx < len(cur):
                if self.timer % 2 == 0:
                    self.char_idx += 1
                    if cur and self.char_idx % 5 == 0:
                        Sound.play(2, 9)
            else:
                if self.timer % 50 == 0:
                    self.line_idx += 1
                    self.char_idx = 0

    def _launch(self):
        self.game.begin_level_by_state(self.next_state)

    def draw(self):
        pyxel.cls(COL_NAVY)
        for i in range(50):
            sx = (i * 173 + 7) % SCREEN_W
            sy = (i * 97 + 13) % SCREEN_H
            twinkle = (self.timer + i * 7) % 30 < 15
            pyxel.pset(sx, sy, COL_WHITE if twinkle else COL_PEACH)
        pyxel.circ(SCREEN_W - 40, 40, 18, COL_PEACH)
        pyxel.circ(SCREEN_W - 35, 35, 16, COL_YELLOW)
        # Big school
        u, v, w, h = SPR_SCHOOL
        pyxel.blt(SCREEN_W // 2 - 44, SCREEN_H - 80, 0, u, v, w, h, COLKEY)
        # Hopping hero
        bunny_y = SCREEN_H - 80 - int(self.hop)
        bu, bv, bw, bh = SPR_PLAYER_R_IDLE
        pyxel.blt(int(self.bunny_x), bunny_y, 0, bu, bv, bw, bh, COLKEY)
        # Heart trail
        for hi in range(3):
            hx = int(self.bunny_x) - 4 - hi * 8
            if hx > -8 and (self.timer + hi * 7) % 24 < 12:
                hu, hv, hw, hh = SPR_HEART
                pyxel.blt(hx, bunny_y + 12, 0, hu, hv, hw, hh, COLKEY)
        draw_panel(20, 20, SCREEN_W - 40, 120, COL_PINK, COL_BLACK)
        draw_story_window(self.lines, self.line_idx, self.char_idx,
                          20, 20, SCREEN_W - 40, 120, max_lines=5, line_h=11)
        draw_centered("[S] SKIP   [ENTER] NEXT", SCREEN_H - 14, COL_DGRAY)


# =====================================================================
# TITLE
# =====================================================================
class TitleScreen:
    OPTIONS = ["START GAME", "CONTROLS", "CREDITS"]

    def __init__(self):
        self.cursor = 0
        self.timer = 0
        self.confirmed = False
        self.confirm_anim = 0
        self.clouds = []
        for _ in range(5):
            self.clouds.append({
                "x": random.randint(0, SCREEN_W),
                "y": random.randint(10, 80),
                "speed": random.uniform(0.15, 0.4),
                "w": random.randint(20, 36),
            })
        self.show_controls = False

    def update(self):
        self.timer += 1
        if self.confirmed:
            self.confirm_anim += 1
            return
        if self.show_controls:
            if btnp_any(pyxel.KEY_ESCAPE, pyxel.KEY_RETURN, pyxel.KEY_SPACE):
                self.show_controls = False
                Sound.play(0, 12)
            return
        if btnp_any(pyxel.KEY_UP, pyxel.KEY_W):
            self.cursor = (self.cursor - 1) % len(self.OPTIONS)
            Sound.play(0, 11)
        if btnp_any(pyxel.KEY_DOWN, pyxel.KEY_S):
            self.cursor = (self.cursor + 1) % len(self.OPTIONS)
            Sound.play(0, 11)
        if btnp_any(pyxel.KEY_RETURN, pyxel.KEY_SPACE):
            choice = self.OPTIONS[self.cursor]
            if choice == "CONTROLS":
                self.show_controls = True
                Sound.play(2, 4)
            else:
                Sound.play(2, 4)
                self.confirmed = True
        for c in self.clouds:
            c["x"] += c["speed"]
            if c["x"] > SCREEN_W + 10:
                c["x"] = -c["w"] - 5

    def is_done(self):
        return self.confirmed and self.confirm_anim > 12

    def selected(self):
        return self.OPTIONS[self.cursor]

    def draw(self):
        for y in range(SCREEN_H):
            t = y / SCREEN_H
            if t < 0.45:
                col = COL_LBLUE
            elif t < 0.75:
                col = COL_PEACH
            else:
                col = COL_LGREEN
            pyxel.rect(0, y, SCREEN_W, 1, col)
        sun_x = SCREEN_W - 36
        sun_y = 36
        for r in (20, 16, 12):
            col = [COL_YELLOW, COL_ORANGE, COL_PEACH][(self.timer // 6) % 3] if r == 20 else COL_YELLOW
            pyxel.circ(sun_x, sun_y, r, col)
        for c in self.clouds:
            cx = int(c["x"]); cy = int(c["y"])
            pyxel.rect(cx, cy, c["w"], 5, COL_WHITE)
            pyxel.rect(cx + 4, cy - 3, c["w"] - 8, 3, COL_WHITE)
            pyxel.rect(cx + 8, cy - 6, c["w"] - 16, 3, COL_WHITE)
        pyxel.rect(0, SCREEN_H - 60, SCREEN_W, 60, COL_LGREEN)
        for i in range(0, SCREEN_W, 4):
            yoff = int(math.sin((i + self.timer * 0.3) * 0.4))
            pyxel.pset(i, SCREEN_H - 60 + yoff, COL_GREEN)
        # Big school
        u, v, w, h = SPR_SCHOOL
        pyxel.blt(10, SCREEN_H - 120, 0, u, v, w, h, COLKEY)

        if self.show_controls:
            self._draw_controls_overlay()
            return

        bx = SCREEN_W // 2 - 24
        by = SCREEN_H - 110 + (2 if (self.timer // 18) % 2 else 0)
        pu, pv, pw, ph = SPR_PLAYER_BIG
        pyxel.blt(bx, by, 0, pu, pv, pw, ph, COLKEY)
        rx = SCREEN_W - 80
        ry = SCREEN_H - 100
        if (self.timer // 12) % 2 == 0:
            ru, rv, rw, rh = SPR_RIVAL_L_1
        else:
            ru, rv, rw, rh = SPR_RIVAL_L_2
        pyxel.blt(rx, ry, 0, ru, rv, rw, rh, COLKEY)
        if (self.timer // 30) % 2 == 0:
            pyxel.text(rx + 4, ry - 8, "RIVAL", COL_RED)

        title = "HARE HURRY"
        bob = int(math.sin(self.timer * 0.08) * 3)
        tx = SCREEN_W // 2 - len(title) * 4
        ty = 14 + bob
        for ci, ch in enumerate(title):
            cxx = tx + ci * 8
            pyxel.text(cxx + 1, ty + 1, ch, COL_RED)
            pyxel.text(cxx, ty, ch, COL_YELLOW)
        sub = "EASTER RIVAL"
        draw_centered(sub, ty + 14, COL_PINK)

        menu_y = SCREEN_H - 50
        for i, opt in enumerate(self.OPTIONS):
            col = COL_WHITE
            prefix = "  "
            if i == self.cursor:
                col = COL_YELLOW if (self.timer // 4) % 2 == 0 else COL_PEACH
                prefix = "> "
            txt = prefix + opt
            x = SCREEN_W // 2 - len(txt) * 2
            y = menu_y + i * 9
            if i == self.cursor:
                pyxel.rect(x - 4, y - 1, len(txt) * 4 + 8, 8, COL_BLACK)
            pyxel.text(x, y, txt, col)
        if self.confirmed and self.confirm_anim % 4 < 2:
            pyxel.rectb(0, 0, SCREEN_W, SCREEN_H, COL_PEACH)
        draw_centered("ARROWS + ENTER", SCREEN_H - 8, COL_DGRAY)

    def _draw_controls_overlay(self):
        draw_panel(20, 30, SCREEN_W - 40, SCREEN_H - 60, COL_PINK, COL_BLACK)
        draw_centered("CONTROLS", 38, COL_YELLOW)
        lines = [
            ("WASD / ARROWS", "MOVE"),
            ("SPACE", "JUMP"),
            ("E / ENTER", "INTERACT / THROW"),
            ("F", "SLAM ATTACK"),
            ("P", "PAUSE"),
            ("1-4", "USE POTIONS"),
            ("ESC", "BACK"),
        ]
        for i, (a, b) in enumerate(lines):
            y = 60 + i * 14
            pyxel.text(40, y, a, COL_LGREEN)
            pyxel.text(160, y, b, COL_WHITE)
        draw_centered("PRESS ENTER TO RETURN", SCREEN_H - 50, COL_PEACH)


# =====================================================================
# INTRO (after Title — original cutscene)
# =====================================================================
class IntroCutscene:
    LINES = [
        "EVERY SPRING THE HARES COMPETE",
        "FOR THE EASTER CROWN.",
        "",
        "YOU ARE THE YOUNGEST HARE.",
        "YOUR HEART BELONGS TO LILA.",
        "BUT LILA WAITS FOR ONE BUNNY ONLY:",
        "THE NEW EASTER BUNNY.",
        "",
        "BRING CARROTS TO THE SCHOOL.",
        "INSIDE THE SCHOOL IS A SHOP.",
        "BUY SKILLS BETWEEN TRIALS.",
        "",
        "DEFEAT THE OLD BUNNY.",
        "TAKE HIS CROWN.",
        "THEN LILA WILL BE YOURS.",
        "",
        "PRESS S TO SKIP.",
    ]

    def __init__(self):
        self.timer = 0
        self.line_idx = 0
        self.char_idx = 0

    def update(self):
        self.timer += 1
        if btnp_any(pyxel.KEY_S, pyxel.KEY_ESCAPE):
            self.line_idx = len(self.LINES)
            return
        if btnp_any(pyxel.KEY_RETURN, pyxel.KEY_SPACE):
            if self.char_idx < len(self.LINES[self.line_idx]):
                self.char_idx = len(self.LINES[self.line_idx])
            else:
                self.line_idx += 1
                self.char_idx = 0
            return
        if self.line_idx < len(self.LINES):
            cur = self.LINES[self.line_idx]
            if self.char_idx < len(cur):
                if self.timer % 2 == 0:
                    self.char_idx += 1
                    if cur and self.char_idx % 4 == 0:
                        Sound.play(2, 9)
            else:
                if self.timer % 90 == 0:
                    self.line_idx += 1
                    self.char_idx = 0

    def done(self):
        return self.line_idx >= len(self.LINES)

    def draw(self):
        pyxel.cls(COL_NAVY)
        for i in range(50):
            sx = (i * 173 + 7) % SCREEN_W
            sy = (i * 97 + 13) % SCREEN_H
            twinkle = (self.timer + i * 7) % 30 < 15
            pyxel.pset(sx, sy, COL_WHITE if twinkle else COL_PEACH)
        # Moon
        pyxel.circ(SCREEN_W - 40, 36, 20, COL_PEACH)
        pyxel.circ(SCREEN_W - 35, 32, 18, COL_YELLOW)

        fc = self.timer
        stage_y = SCREEN_H - 66

        # --- HERO HARE (left), looking up longingly ---
        hop = int(abs(math.sin(fc * 0.06)) * 3)
        pu, pv, pw, ph = SPR_PLAYER_R_IDLE
        pyxel.blt(20, stage_y - hop, 0, pu, pv, pw, ph, COLKEY)
        # little heart floating from the hero toward Lila
        hb = (fc % 80)
        if hb < 60:
            hx = 40 + hb
            hy = stage_y - 6 - hb // 3
            hu, hv, hw, hh = SPR_HEART
            pyxel.blt(int(hx), int(hy), 0, hu, hv, hw, hh, COLKEY)

        # --- LILA on her cloud (center, floating like in the level) ---
        lcx = SCREEN_W // 2 - 24
        lcy = stage_y - 16 + int(math.sin(fc * 0.08) * 3)
        # glow rays behind her
        for i in range(8):
            ang = (i / 8) * math.tau + fc * 0.02
            ex = lcx + 24 + math.cos(ang) * 26
            ey = lcy + 6 + math.sin(ang) * 16
            pyxel.line(lcx + 24, lcy + 6, int(ex), int(ey),
                       COL_YELLOW if (i + fc // 8) % 2 == 0 else COL_PEACH)
        # cloud
        pyxel.circ(lcx + 6, lcy + 18, 9, COL_WHITE)
        pyxel.circ(lcx + 20, lcy + 16, 11, COL_WHITE)
        pyxel.circ(lcx + 36, lcy + 18, 9, COL_WHITE)
        pyxel.rect(lcx, lcy + 18, 44, 6, COL_WHITE)
        pyxel.rect(lcx + 2, lcy + 23, 40, 2, COL_LGRAY)
        # Lila sprite (the lying-down heart-eyed bunny)
        lu, lv, lw, lh = SPR_LILA
        pyxel.blt(lcx, lcy - 2, 0, lu, lv, lw, lh, COLKEY)
        # halo over her head (right side)
        halo_r = 9 + int(math.sin(fc * 0.2) * 2)
        pyxel.circb(lcx + 34, lcy - 4, halo_r, COL_YELLOW)

        # --- OLD EASTER BUNNY / boss (right), with crown ---
        bob = int(math.sin(fc * 0.08 + 1) * 2)
        if self.timer % 40 < 20:
            bu, bv, bw, bh = SPR_BOSS_NORMAL
        else:
            bu, bv, bw, bh = SPR_BOSS_ATTACK
        bx = SCREEN_W - 60
        by = stage_y - 14 + bob
        pyxel.blt(bx, by, 0, bu, bv, bw, bh, COLKEY)
        # golden crown above him
        crown_x = bx + 16
        crown_y = by - 8
        pyxel.rect(crown_x, crown_y + 3, 14, 4, COL_YELLOW)
        pyxel.tri(crown_x, crown_y + 3, crown_x, crown_y - 2,
                  crown_x + 4, crown_y + 3, COL_YELLOW)
        pyxel.tri(crown_x + 5, crown_y + 3, crown_x + 7, crown_y - 3,
                  crown_x + 9, crown_y + 3, COL_YELLOW)
        pyxel.tri(crown_x + 10, crown_y + 3, crown_x + 14, crown_y - 2,
                  crown_x + 14, crown_y + 3, COL_YELLOW)
        pyxel.pset(crown_x + 7, crown_y, COL_RED)

        # --- TEXT PANEL (shorter, sits above the character stage) ---
        draw_panel(18, 22, SCREEN_W - 36, 96, COL_PINK, COL_BLACK)
        draw_story_window(self.LINES, self.line_idx, self.char_idx,
                          18, 22, SCREEN_W - 36, 96, max_lines=5, line_h=11,
                          highlight_words=("RIVAL",))
        draw_centered("[S] SKIP   [ENTER] NEXT", SCREEN_H - 10, COL_DGRAY)


# =====================================================================
# WIN / CREDITS / GAME OVER
# =====================================================================
class WinScreen:
    def __init__(self):
        self.timer = 0
        self.hearts = []
        for _ in range(30):
            self.hearts.append({
                "x": random.randint(0, SCREEN_W),
                "y": random.randint(0, SCREEN_H),
                "vx": random.uniform(-0.4, 0.4),
                "vy": -random.uniform(0.4, 1.0),
            })

    def update(self):
        self.timer += 1
        for h in self.hearts:
            h["x"] += h["vx"]
            h["y"] += h["vy"]
            if h["y"] < -5:
                h["y"] = SCREEN_H + 5
                h["x"] = random.randint(0, SCREEN_W)

    def is_done(self):
        return self.timer > 180 and btnp_any(pyxel.KEY_RETURN, pyxel.KEY_SPACE)

    def draw(self):
        for y in range(SCREEN_H):
            t = y / SCREEN_H
            if t < 0.3:
                col = COL_PURPLE
            elif t < 0.55:
                col = COL_RED
            elif t < 0.78:
                col = COL_ORANGE
            else:
                col = COL_LGREEN
            pyxel.rect(0, y, SCREEN_W, 1, col)
        pyxel.circ(SCREEN_W // 2, 110, 32, COL_YELLOW)
        pyxel.circ(SCREEN_W // 2, 110, 24, COL_PEACH)
        for h in self.hearts:
            x = int(h["x"]); y = int(h["y"])
            u, v, w, h_ = SPR_HEART
            pyxel.blt(x, y, 0, u, v, w, h_, COLKEY)
        bu, bv, bw, bh = SPR_PLAYER_BIG
        pyxel.blt(SCREEN_W // 2 - 24, SCREEN_H - 80, 0, bu, bv, bw, bh, COLKEY)
        cx = SCREEN_W // 2 - 8
        cy = SCREEN_H - 86
        pyxel.tri(cx, cy, cx + 4, cy - 6, cx + 8, cy, COL_YELLOW)
        pyxel.tri(cx + 8, cy, cx + 12, cy - 6, cx + 16, cy, COL_YELLOW)
        pyxel.rect(cx, cy, 17, 3, COL_YELLOW)
        title = "YOU ARE THE"
        sub = "EASTER BUNNY!"
        bob = int(math.sin(self.timer * 0.06) * 3)
        draw_centered(title, 30 + bob, COL_WHITE)
        col = COL_YELLOW if (self.timer // 6) % 2 == 0 else COL_PEACH
        draw_centered(sub, 42 + bob, col)
        draw_centered("LILA IS YOURS, FOREVER", 60, COL_PINK)
        if self.timer > 180 and (self.timer // 30) % 2 == 0:
            draw_centered("PRESS ENTER FOR CREDITS", SCREEN_H - 12, COL_WHITE)


class CreditsScreen:
    LINES = [
        "HARE HURRY: EASTER RIVAL",
        "",
        "A PYXEL STUDIO ADVENTURE",
        "",
        "- - - - - - - - - -",
        "",
        "PROGRAMMING",
        "MATS",
        "",
        "GAME DESIGN",
        "MATS",
        "",
        "LEVEL DESIGN",
        "MATS",
        "",
        "GAME MECHANICS",
        "MATS",
        "",
        "VISUALS & SPRITE ART",
        "LAURA",
        "",
        "CHARACTER DESIGN",
        "LAURA",
        "",
        "CO-CODER",
        "CLAUDE",
        "",
        "SOUND & MUSIC",
        "MATS",
        "",
        "STORY & WRITING",
        "MATS",
        "",
        "QUALITY ASSURANCE",
        "MATS",
        "",
        "CAFFEINE SUPPLY",
        "MATS",
        "",
        "BUG CREATION DEPT.",
        "MATS",
        "",
        "BUG REMOVAL DEPT.",
        "MATS & CLAUDE",
        "",
        "CHIEF HARE OFFICER",
        "MATS",
        "",
        "- - - - - - - - - -",
        "",
        "STARRING",
        "THE YOUNG HARE",
        "AND LILA",
        "",
        "VILLAIN",
        "THE OLD EASTER BUNNY",
        "",
        "SPECIAL THANKS",
        "TO EVERY BUNNY",
        "WHO HOPPED ALONG",
        "",
        "AND TO YOU",
        "FOR PLAYING",
        "",
        "THE END",
        "",
        "",
        "PRESS ENTER",
        "TO RETURN TO TITLE",
    ]

    def __init__(self):
        self.scroll = float(SCREEN_H + 10)
        self.timer = 0

    def update(self):
        self.timer += 1
        self.scroll -= 0.5

    def is_done(self):
        if self.timer < 60:
            return False
        return (btnp_any(pyxel.KEY_RETURN, pyxel.KEY_SPACE)
                or self.scroll < -900)

    def draw(self):
        pyxel.cls(COL_NAVY)
        for i in range(50):
            sx = (i * 173 + 17) % SCREEN_W
            sy = (i * 97 + 23) % SCREEN_H
            twinkle = (self.timer + i * 7) % 30 < 15
            pyxel.pset(sx, sy, COL_WHITE if twinkle else COL_PEACH)
        y = int(self.scroll)
        headers = (
            "PROGRAMMING", "GAME DESIGN", "LEVEL DESIGN", "GAME MECHANICS",
            "VISUALS & SPRITE ART", "CHARACTER DESIGN", "CO-CODER",
            "SOUND & MUSIC", "STORY & WRITING", "QUALITY ASSURANCE",
            "CAFFEINE SUPPLY", "BUG CREATION DEPT.", "BUG REMOVAL DEPT.",
            "CHIEF HARE OFFICER", "STARRING", "VILLAIN", "SPECIAL THANKS",
        )
        for line in self.LINES:
            if -10 < y < SCREEN_H + 10:
                col = COL_WHITE
                if line == self.LINES[0]:
                    col = COL_YELLOW
                elif line in headers:
                    col = COL_PINK
                elif line == "MATS" or line == "MATS & CLAUDE":
                    col = COL_LGREEN
                elif line == "LAURA":
                    col = COL_LBLUE
                elif line == "CLAUDE":
                    col = COL_ORANGE
                elif line in ("THE END", "AND TO YOU"):
                    col = COL_PEACH
                draw_centered(line, y, col)
            y += 10


class GameOverScreen:
    def __init__(self):
        self.timer = 0
        self.reason = ""

    def set_reason(self, reason):
        self.reason = reason
        self.timer = 0

    def update(self):
        self.timer += 1

    def can_continue(self):
        return self.timer > 45 and btnp_any(pyxel.KEY_RETURN, pyxel.KEY_R, pyxel.KEY_SPACE)

    def draw(self):
        pyxel.cls(COL_BLACK)
        for i in range(30):
            sx = (i * 113 + self.timer) % SCREEN_W
            sy = (i * 71 + self.timer // 3) % SCREEN_H
            pyxel.pset(sx, sy, COL_PURPLE)
        title = "GAME OVER"
        tx = SCREEN_W // 2 - len(title) * 4
        ty = 50
        for ci, ch in enumerate(title):
            cxx = tx + ci * 8
            pyxel.text(cxx + 1, ty + 1, ch, COL_NAVY)
            pyxel.text(cxx, ty, ch, COL_RED)
        bu, bv, bw, bh = SPR_PLAYER_L_IDLE
        pyxel.blt(SCREEN_W // 2 - 60, 90, 0, bu, bv, bw, bh, COLKEY)
        if (self.timer // 8) % 4 < 3:
            tear_y = 110 + (self.timer % 12)
            pyxel.pset(SCREEN_W // 2 - 48, tear_y, COL_LBLUE)
            pyxel.pset(SCREEN_W // 2 - 40, tear_y, COL_LBLUE)
        if (self.timer // 8) % 2 == 0:
            ru, rv, rw, rh = SPR_RIVAL_L_1
        else:
            ru, rv, rw, rh = SPR_RIVAL_L_2
        pyxel.blt(SCREEN_W // 2 + 20, 92, 0, ru, rv, rw, rh, COLKEY)
        if (self.timer // 6) % 2:
            pyxel.text(SCREEN_W // 2 + 56, 96, "HA!", COL_YELLOW)
        if self.reason:
            draw_centered(self.reason, 140, COL_PEACH)
        draw_centered("THE RIVAL WON", 152, COL_ORANGE)
        if self.timer > 45:
            hint = "PRESS ENTER" if (self.timer // 20) % 2 == 0 else "TO RETRY"
            draw_centered(hint, SCREEN_H - 20, COL_WHITE)
# =====================================================================
# MAIN GAME
# =====================================================================
class HareHurryGame:
    def __init__(self):
        pyxel.init(SCREEN_W, SCREEN_H, title="Hare Hurry: Easter Rival",
                   fps=FPS, display_scale=2)
        Assets.setup()
        Sound.setup()

        self.camera = Camera()
        self.rival = Rival()

        self.loading_screen = LoadingScreen()
        self.prologue = None
        self.title_screen = None
        self.intro_scene = None
        self.win_screen = None
        self.credits_screen = None
        self.game_over_screen = GameOverScreen()

        self.level_meadow = None
        self.level_kids = None
        self.level_maze1 = None
        self.level_maze2 = None
        self.level_plat_cannon = None
        self.level_plat_attract = None
        self.level_plat_fence = None
        self.level_frozen = None
        self.level_boss = None
        self.level_love = None
        self.lobby = None
        self.shop = None
        self.settings_screen = None
        self.level_intro = None

        self.music = MusicManager()
        self.sfx_volume = 2
        Sound.apply_volume_level(self.sfx_volume)
        self.music_track_pref = MusicManager.TRACK_STUPSI
        self.control_scheme = 0

        self.persistent_coins = 0
        self.completed_levels = set()
        self.skills = set()
        self.potions = {}
        self.level_maze = None

        self.state = ST_LOADING
        self.prev_state = None
        self.flash_msg = ""
        self.flash_timer = 0

        self.upgrade_fast_paws = False
        self.upgrade_extra_time = False
        self.upgrade_head_start = False

        self.god_mode = False
        self.god_fly = False
        self.god_stage = 0
        self.god_seq_timer = 0
        self.god_code_buffer = ""
        self.god_msg = ""
        self.god_msg_timer = 0

        self.complete_timer = 0
        self.complete_msg = ""
        self.complete_next_state = ""
        self.complete_next_label = None
        self.complete_cursor = 0

        pyxel.run(self.update, self.draw)

    # -----------------------------------------------------------------
    # STATE TRANSITIONS
    # -----------------------------------------------------------------
    def set_state(self, new_state):
        self.prev_state = self.state
        self.state = new_state

    def goto_title(self):
        self.title_screen = TitleScreen()
        if self.music.current_track != self.music_track_pref:
            self.music.play(self.music_track_pref)
        self.set_state(ST_TITLE)

    def start_new_game(self):
        self.rival.reset_for_level(1)
        if self.upgrade_head_start:
            self.rival.head_start_delay(5 * FPS)
        self.intro_scene = IntroCutscene()
        self.set_state(ST_INTRO)

    # ---------- Persistence / Skills ----------
    @property
    def has_dash(self):
        return "dash" in self.skills

    @property
    def has_double_jump(self):
        return "double_jump" in self.skills

    @property
    def has_special_attack(self):
        return "special" in self.skills

    def has_skill(self, key):
        return key in self.skills

    def unlock_skill(self, key):
        self.skills.add(key)

    def add_potion(self, key):
        self.potions[key] = self.potions.get(key, 0) + 1

    def use_potion(self, key, player):
        if self.potions.get(key, 0) <= 0:
            return False
        self.potions[key] -= 1
        if key == "speed_potion":
            player.apply_potion("speed", 300)
        elif key == "shield_potion":
            player.apply_potion("shield", 180)
        elif key == "heal_potion":
            player.apply_potion("heal", 1)
        elif key == "jump_potion":
            player.apply_potion("jump", 300)
        Sound.play(1, 6)
        return True

    def is_level_unlocked(self, idx):
        if idx == 0:
            return True
        return (idx - 1) in self.completed_levels

    def is_level_completed(self, idx):
        return idx in self.completed_levels

    def _apply_skills_to_player(self, player):
        player.has_dash = self.has_dash
        player.has_special_attack = self.has_special_attack
        if self.has_skill("max_health"):
            player.max_health = 4
            player.health = 4
        if self.has_double_jump and player.mode == "platformer":
            player.max_jumps = 2
            player.jumps_left = 2

    # ---------- Lobby flow ----------
    def goto_lobby(self):
        if self.lobby is None:
            self.lobby = Lobby(self)
        else:
            self.lobby.entered_msg_timer = 60
        if self.music.current_track != self.music_track_pref:
            self.music.play(self.music_track_pref)
        self.set_state(ST_LOBBY)

    def enter_shop(self):
        self.shop = Shop(self)
        self.set_state(ST_SHOP)

    def exit_shop(self):
        self.set_state(ST_LOBBY)

    def enter_settings(self):
        self.settings_screen = SettingsScreen(self)
        self.set_state(ST_SETTINGS)

    def exit_settings(self):
        self.set_state(ST_LOBBY)

    def set_sfx_volume(self, lvl):
        self.sfx_volume = max(0, min(3, lvl))
        Sound.apply_volume_level(self.sfx_volume)

    def begin_level_with_intro(self, state):
        lines = LEVEL_STORIES.get(state)
        if not lines:
            self.begin_level_by_state(state)
            return
        self.level_intro = LevelIntro(lines, state, self)
        self.set_state(ST_LEVEL_INTRO)

    def begin_level_by_state(self, state):
        self.music.stop()
        if state == ST_LEVEL_MEADOW:
            self.begin_meadow()
        elif state == ST_LEVEL_KIDS:
            self.begin_kids()
        elif state == ST_LEVEL_MAZE1:
            self.begin_maze1()
        elif state == ST_LEVEL_MAZE2:
            self.begin_maze2()
        elif state == ST_LEVEL_PLAT_CANNON:
            self.begin_plat_cannon()
        elif state == ST_LEVEL_PLAT_ATTRACT:
            self.begin_plat_attract()
        elif state == ST_LEVEL_PLAT_FENCE:
            self.begin_plat_fence()
        elif state == ST_LEVEL_FROZEN:
            self.begin_frozen()
        elif state == ST_LEVEL_BOSS:
            self.begin_boss()
        elif state == ST_LEVEL_LOVE:
            self.begin_love()

    def begin_meadow(self):
        self.rival.reset_for_level(1)
        self.rival.chase_enabled = True  # first level: rival physically chases
        if self.upgrade_head_start:
            self.rival.head_start_delay(5 * FPS)
        self.level_meadow = LevelMeadow(self)
        self._apply_skills_to_player(self.level_meadow.player)
        self.level_meadow.player.coins = self.persistent_coins
        self.rival.x = self.level_meadow.player.x - 60
        self.rival.y = self.level_meadow.player.y - 60
        self.set_state(ST_LEVEL_MEADOW)

    def begin_kids(self):
        self.rival.reset_for_level(2)
        self.rival.chase_enabled = True  # second level: rival chases too
        self.level_kids = LevelKids(self)
        self._apply_skills_to_player(self.level_kids.player)
        self.level_kids.player.coins = self.persistent_coins
        self.rival.x = self.level_kids.player.x - 80
        self.rival.y = self.level_kids.player.y - 80
        self.set_state(ST_LEVEL_KIDS)

    def begin_maze1(self):
        self.rival.reset_for_level(2)
        self.level_maze1 = LevelRandomMaze(self, variant=1)
        self._apply_skills_to_player(self.level_maze1.player)
        self.level_maze1.player.coins = self.persistent_coins
        opens = self.level_maze1.maze.open_tiles
        rx, ry = opens[0]
        self.rival.x = rx * self.level_maze1.maze.tile
        self.rival.y = ry * self.level_maze1.maze.tile
        self.set_state(ST_LEVEL_MAZE1)

    def begin_maze2(self):
        self.rival.reset_for_level(3)
        self.level_maze2 = LevelRandomMaze(self, variant=2)
        self._apply_skills_to_player(self.level_maze2.player)
        self.level_maze2.player.coins = self.persistent_coins
        opens = self.level_maze2.maze.open_tiles
        rx, ry = opens[0]
        self.rival.x = rx * self.level_maze2.maze.tile
        self.rival.y = ry * self.level_maze2.maze.tile
        self.set_state(ST_LEVEL_MAZE2)

    def begin_plat_cannon(self):
        self.rival.reset_for_level(3)
        self.level_plat_cannon = LevelPlatformCannon(self)
        self._apply_skills_to_player(self.level_plat_cannon.player)
        self.level_plat_cannon.player.coins = self.persistent_coins
        self.set_state(ST_LEVEL_PLAT_CANNON)

    def begin_plat_attract(self):
        self.rival.reset_for_level(4)
        self.level_plat_attract = LevelPlatformAttract(self)
        self._apply_skills_to_player(self.level_plat_attract.player)
        self.level_plat_attract.player.coins = self.persistent_coins
        self.set_state(ST_LEVEL_PLAT_ATTRACT)

    def begin_plat_fence(self):
        self.rival.reset_for_level(4)
        self.level_plat_fence = LevelPlatformFence(self)
        self._apply_skills_to_player(self.level_plat_fence.player)
        self.level_plat_fence.player.coins = self.persistent_coins
        self.set_state(ST_LEVEL_PLAT_FENCE)

    def begin_frozen(self):
        self.rival.reset_for_level(4)
        self.level_frozen = LevelFrozenGarden(self)
        self._apply_skills_to_player(self.level_frozen.player)
        self.level_frozen.player.coins = self.persistent_coins
        self.set_state(ST_LEVEL_FROZEN)

    def begin_love(self):
        self.rival.reset_for_level(5)
        self.level_love = LevelLove(self)
        self._apply_skills_to_player(self.level_love.player)
        self.level_love.player.coins = self.persistent_coins
        self.set_state(ST_LEVEL_LOVE)

    def begin_boss(self):
        self.rival.reset_for_level(4)
        self.rival.set_time_limit(90)  # boss gets 1:30
        self.level_boss = LevelBoss(self)
        self._apply_skills_to_player(self.level_boss.player)
        self.level_boss.player.coins = self.persistent_coins
        self.set_state(ST_LEVEL_BOSS)

    def _current_level_obj(self):
        m = {
            ST_LEVEL_MEADOW: self.level_meadow,
            ST_LEVEL_KIDS: self.level_kids,
            ST_LEVEL_MAZE1: self.level_maze1,
            ST_LEVEL_MAZE2: self.level_maze2,
            ST_LEVEL_PLAT_CANNON: self.level_plat_cannon,
            ST_LEVEL_PLAT_ATTRACT: self.level_plat_attract,
            ST_LEVEL_PLAT_FENCE: self.level_plat_fence,
            ST_LEVEL_FROZEN: self.level_frozen,
            ST_LEVEL_BOSS: self.level_boss,
            ST_LEVEL_LOVE: self.level_love,
        }
        return m.get(self.state, None)

    def _level_state_to_index(self, state):
        for i, (s, _, _) in enumerate(LOBBY_LEVELS):
            if s == state:
                return i
        return -1

    def complete_level(self, next_state, msg):
        idx = self._level_state_to_index(self.state)
        if idx >= 0:
            self.completed_levels.add(idx)
        cur = self._current_level_obj()
        if cur and hasattr(cur, "player"):
            self.persistent_coins = cur.player.coins
        self.complete_msg = msg
        # Compute the ACTUAL next level (the one right after this in the
        # lobby order). The caller's next_state is ignored for sequencing —
        # we always offer the true next level, or None if this was the last.
        if idx >= 0 and idx + 1 < len(LOBBY_LEVELS):
            self.complete_next_state = LOBBY_LEVELS[idx + 1][0]
            self.complete_next_label = LOBBY_LEVELS[idx + 1][1]
        else:
            self.complete_next_state = None
            self.complete_next_label = None
        self.complete_timer = 0  # no auto-advance — wait for player choice
        self.complete_cursor = 0  # 0 = NEXT, 1 = LOBBY
        Sound.play(2, 8)
        self.set_state(ST_LEVEL_COMPLETE)

    def game_over(self, reason):
        self.game_over_screen.set_reason(reason)
        Sound.play(2, 3)
        self.set_state(ST_GAME_OVER)

    def flash_message(self, msg, frames=60):
        self.flash_msg = msg
        self.flash_timer = frames

    # -----------------------------------------------------------------
    # UPDATE
    # -----------------------------------------------------------------
    def update(self):
        self._update_god_input()
        self._update_god_cheats()

        playable_states = (ST_LEVEL_MEADOW, ST_LEVEL_KIDS,
                           ST_LEVEL_MAZE1, ST_LEVEL_MAZE2,
                           ST_LEVEL_PLAT_CANNON, ST_LEVEL_PLAT_ATTRACT,
                           ST_LEVEL_PLAT_FENCE, ST_LEVEL_FROZEN,
                           ST_LEVEL_BOSS, ST_LEVEL_LOVE)
        if self.state in playable_states:
            if pyxel.btnp(pyxel.KEY_P):
                self.prev_state = self.state
                self.set_state(ST_PAUSED)
                return
            cur = self._current_level_obj()
            if cur and hasattr(cur, "player"):
                if pyxel.btnp(pyxel.KEY_1):
                    self.use_potion("speed_potion", cur.player)
                if pyxel.btnp(pyxel.KEY_2):
                    self.use_potion("shield_potion", cur.player)
                if pyxel.btnp(pyxel.KEY_3):
                    self.use_potion("heal_potion", cur.player)
                if pyxel.btnp(pyxel.KEY_4):
                    self.use_potion("jump_potion", cur.player)
        elif self.state == ST_PAUSED:
            if pyxel.btnp(pyxel.KEY_P) or pyxel.btnp(pyxel.KEY_ESCAPE):
                self.state = self.prev_state
            elif pyxel.btnp(pyxel.KEY_L):
                self.goto_lobby()
            return

        if self.flash_timer > 0:
            self.flash_timer -= 1

        s = self.state
        # After any playable level updates its meter, a full bar = fail.
        _playable_for_fail = (ST_LEVEL_MEADOW, ST_LEVEL_KIDS,
                              ST_LEVEL_MAZE1, ST_LEVEL_MAZE2,
                              ST_LEVEL_PLAT_CANNON, ST_LEVEL_PLAT_ATTRACT,
                              ST_LEVEL_PLAT_FENCE, ST_LEVEL_FROZEN,
                              ST_LEVEL_BOSS, ST_LEVEL_LOVE)
        if s == ST_LOADING:
            self.loading_screen.update()
            if self.loading_screen.done():
                self.prologue = Prologue()
                self.set_state(ST_PROLOGUE)
        elif s == ST_PROLOGUE:
            self.prologue.update()
            if self.prologue.done():
                self.goto_title()
        elif s == ST_TITLE:
            self.title_screen.update()
            if self.title_screen.is_done():
                choice = self.title_screen.selected()
                if choice == "START GAME":
                    self.start_new_game()
                elif choice == "CREDITS":
                    self.credits_screen = CreditsScreen()
                    self.set_state(ST_CREDITS)
        elif s == ST_INTRO:
            self.intro_scene.update()
            if self.intro_scene.done():
                self.goto_lobby()
        elif s == ST_LOBBY:
            if self.lobby is None:
                self.lobby = Lobby(self)
            self.lobby.update()
        elif s == ST_SHOP:
            if self.shop is None:
                self.shop = Shop(self)
            self.shop.update()
        elif s == ST_SETTINGS:
            if self.settings_screen is None:
                self.settings_screen = SettingsScreen(self)
            self.settings_screen.update()
        elif s == ST_LEVEL_INTRO:
            if self.level_intro is not None:
                self.level_intro.update()
        elif s == ST_LEVEL_MEADOW:
            self.level_meadow.update()
        elif s == ST_LEVEL_KIDS:
            self.level_kids.update()
        elif s == ST_LEVEL_MAZE1:
            self.level_maze1.update()
        elif s == ST_LEVEL_MAZE2:
            self.level_maze2.update()
        elif s == ST_LEVEL_PLAT_CANNON:
            self.level_plat_cannon.update()
        elif s == ST_LEVEL_PLAT_ATTRACT:
            self.level_plat_attract.update()
        elif s == ST_LEVEL_PLAT_FENCE:
            self.level_plat_fence.update()
        elif s == ST_LEVEL_FROZEN:
            self.level_frozen.update()
        elif s == ST_LEVEL_BOSS:
            self.level_boss.update()
        elif s == ST_LEVEL_LOVE:
            self.level_love.update()

        # Centralized fail check: if the rival bar filled up during this
        # level's update, the player ran out of time -> game over.
        if self.state in _playable_for_fail and self.rival.failed:
            self.rival.failed = False
            self.game_over("THE RIVAL BEAT YOU TO IT")

        if s == ST_LEVEL_COMPLETE:
            has_next = self.complete_next_state is not None
            # Navigate between options (only if there IS a next level)
            if has_next:
                if btnp_any(pyxel.KEY_LEFT, pyxel.KEY_A,
                            pyxel.KEY_UP, pyxel.KEY_W):
                    self.complete_cursor = 0
                    Sound.play(1, 1)
                if btnp_any(pyxel.KEY_RIGHT, pyxel.KEY_D,
                            pyxel.KEY_DOWN, pyxel.KEY_S):
                    self.complete_cursor = 1
                    Sound.play(1, 1)
            else:
                self.complete_cursor = 1  # only lobby available
            # Quick keys still work directly
            if pyxel.btnp(pyxel.KEY_L):
                self.goto_lobby()
            elif pyxel.btnp(pyxel.KEY_N) and has_next:
                self.begin_level_by_state(self.complete_next_state)
            elif btnp_any(pyxel.KEY_RETURN, pyxel.KEY_SPACE):
                if self.complete_cursor == 0 and has_next:
                    self.begin_level_by_state(self.complete_next_state)
                else:
                    self.goto_lobby()
        elif s == ST_WIN:
            if self.win_screen is None:
                self.win_screen = WinScreen()
                self.music.play(MusicManager.TRACK_SONG2)
            self.win_screen.update()
            if self.win_screen.is_done():
                self.credits_screen = CreditsScreen()
                self.set_state(ST_CREDITS)
                self.win_screen = None
        elif s == ST_CREDITS:
            self.credits_screen.update()
            if self.credits_screen.is_done():
                self.goto_title()
        elif s == ST_GAME_OVER:
            self.game_over_screen.update()
            if self.game_over_screen.can_continue():
                self.rival.reset_for_level(1)
                self.goto_lobby()

    # -----------------------------------------------------------------
    # GOD MODE
    # -----------------------------------------------------------------
    def _update_god_input(self):
        if self.god_msg_timer > 0:
            self.god_msg_timer -= 1
        if self.god_stage == 0:
            if pyxel.btnp(pyxel.KEY_G) and not self.god_mode:
                self.god_stage = 1
                self.god_seq_timer = 45
            return
        if self.god_stage == 1:
            self.god_seq_timer -= 1
            if self.god_seq_timer <= 0:
                self.god_stage = 0
                return
            if pyxel.btnp(pyxel.KEY_M):
                self.god_stage = 2
                self.god_code_buffer = ""
                self.god_msg = "ENTER CODE:"
                self.god_msg_timer = 240
            return
        if self.god_stage == 2:
            target = "god"
            keys = {"g": pyxel.KEY_G, "o": pyxel.KEY_O, "d": pyxel.KEY_D}
            for letter, kc in keys.items():
                if pyxel.btnp(kc):
                    expected = target[len(self.god_code_buffer)] \
                        if len(self.god_code_buffer) < len(target) else None
                    if letter == expected:
                        self.god_code_buffer += letter
                        Sound.play(2, 9)
                        if self.god_code_buffer == target:
                            self.god_mode = True
                            self.god_stage = 0
                            self.god_msg = "GOD MODE ON!"
                            self.god_msg_timer = 120
                            Sound.play(2, 7)
                    else:
                        self.god_stage = 0
                        self.god_msg = "WRONG CODE"
                        self.god_msg_timer = 60
                        Sound.play(2, 5)
                    return
            if pyxel.btnp(pyxel.KEY_ESCAPE):
                self.god_stage = 0
                self.god_msg = ""

    def _update_god_cheats(self):
        if not self.god_mode:
            return
        if pyxel.btn(pyxel.KEY_SHIFT) and pyxel.btnp(pyxel.KEY_G):
            self.god_mode = False
            self.god_msg = "GOD MODE OFF"
            self.god_msg_timer = 60
            return
        if pyxel.btnp(pyxel.KEY_N):
            if self.state == ST_LOBBY:
                for i in range(len(LOBBY_LEVELS)):
                    self.completed_levels.add(i)
                self.persistent_coins += 50
                self.god_msg = "ALL LEVELS UNLOCKED + COINS"
                self.god_msg_timer = 80
            elif self.state in (ST_LEVEL_MEADOW, ST_LEVEL_KIDS,
                                 ST_LEVEL_MAZE1, ST_LEVEL_MAZE2,
                                 ST_LEVEL_PLAT_CANNON, ST_LEVEL_PLAT_ATTRACT,
                                 ST_LEVEL_PLAT_FENCE, ST_LEVEL_FROZEN,
                                 ST_LEVEL_BOSS, ST_LEVEL_LOVE):
                self.goto_lobby()
                self.god_msg = "SKIP -> LOBBY"
                self.god_msg_timer = 60
        if pyxel.btnp(pyxel.KEY_B):
            self.skills = {"dash", "double_jump", "special", "max_health"}
            for key in ("speed_potion", "shield_potion", "heal_potion", "jump_potion"):
                self.potions[key] = self.potions.get(key, 0) + 3
            cur = self._current_level_obj()
            if cur and hasattr(cur, "player"):
                self._apply_skills_to_player(cur.player)
            self.god_msg = "ALL SKILLS + POTIONS"
            self.god_msg_timer = 60
        if pyxel.btnp(pyxel.KEY_F):
            self.god_fly = not self.god_fly
            self.god_msg = "FLY ON" if self.god_fly else "FLY OFF"
            self.god_msg_timer = 60
        if pyxel.btnp(pyxel.KEY_K):
            s = self.state
            if s == ST_LEVEL_MEADOW and self.level_meadow:
                self.level_meadow.delivered = self.level_meadow.target_carrots
            elif s == ST_LEVEL_KIDS and self.level_kids:
                self.level_kids.delivered = self.level_kids.target_kids
            elif s == ST_LEVEL_MAZE1 and self.level_maze1:
                self.level_maze1.delivered = self.level_maze1.target_carrots
            elif s == ST_LEVEL_MAZE2 and self.level_maze2:
                self.level_maze2.delivered = self.level_maze2.target_carrots
            elif s == ST_LEVEL_PLAT_CANNON and self.level_plat_cannon:
                self.level_plat_cannon.collected = self.level_plat_cannon.target_carrots
            elif s == ST_LEVEL_PLAT_ATTRACT and self.level_plat_attract:
                self.level_plat_attract.collected = self.level_plat_attract.target_carrots
            elif s == ST_LEVEL_PLAT_FENCE and self.level_plat_fence:
                self.level_plat_fence.cages_broken = self.level_plat_fence.target_cages
            elif s == ST_LEVEL_FROZEN and self.level_frozen:
                self.level_frozen.collected = self.level_frozen.target_carrots
            elif s == ST_LEVEL_BOSS and self.level_boss:
                self.level_boss.boss.hp = 0
                self.level_boss.win_timer = 30
            elif s == ST_LEVEL_LOVE and self.level_love:
                self.level_love.delivered = self.level_love.target_carrots
            self.god_msg = "LEVEL CLEARED"
            self.god_msg_timer = 60
        if pyxel.btnp(pyxel.KEY_C):
            player = self._current_player()
            if player:
                player.carrying_carrots = player.max_carry
                player.coins += 10
                self.persistent_coins += 10
                self.god_msg = "CARROTS + COINS"
                self.god_msg_timer = 60
        player = self._current_player()
        if player and player.alive:
            if player.health < player.max_health and pyxel.frame_count % 30 == 0:
                player.health = player.max_health
            if self.rival.progress > 0.5:
                self.rival.progress = max(0.2, self.rival.progress - 0.005)
            # Sync fly flag onto the player (player.update handles flight)
            player.fly_mode = self.god_fly
        # Turning fly off should clear any lingering flag on all players
        if not self.god_fly:
            cur = self._current_level_obj()
            if cur and hasattr(cur, "player"):
                cur.player.fly_mode = False

    def _current_player(self):
        obj = self._current_level_obj()
        if obj and hasattr(obj, "player"):
            return obj.player
        return None

    # -----------------------------------------------------------------
    # DRAW
    # -----------------------------------------------------------------
    def draw(self):
        pyxel.cls(COL_BLACK)
        s = self.state
        level_objs = {
            ST_LEVEL_MEADOW: self.level_meadow,
            ST_LEVEL_KIDS: self.level_kids,
            ST_LEVEL_MAZE1: self.level_maze1,
            ST_LEVEL_MAZE2: self.level_maze2,
            ST_LEVEL_PLAT_CANNON: self.level_plat_cannon,
            ST_LEVEL_PLAT_ATTRACT: self.level_plat_attract,
            ST_LEVEL_PLAT_FENCE: self.level_plat_fence,
            ST_LEVEL_FROZEN: self.level_frozen,
            ST_LEVEL_BOSS: self.level_boss,
            ST_LEVEL_LOVE: self.level_love,
        }
        if s == ST_LOADING:
            self.loading_screen.draw()
        elif s == ST_PROLOGUE:
            if self.prologue is not None:
                self.prologue.draw()
        elif s == ST_TITLE:
            self.title_screen.draw()
        elif s == ST_INTRO:
            self.intro_scene.draw()
        elif s == ST_LEVEL_INTRO:
            if self.level_intro is not None:
                self.level_intro.draw()
        elif s == ST_LOBBY:
            if self.lobby is None:
                self.lobby = Lobby(self)
            self.lobby.draw()
        elif s == ST_SHOP:
            if self.shop is None:
                self.shop = Shop(self)
            self.shop.draw()
        elif s == ST_SETTINGS:
            if self.settings_screen is None:
                self.settings_screen = SettingsScreen(self)
            self.settings_screen.draw()
        elif s in level_objs:
            obj = level_objs[s]
            if obj:
                obj.draw()
                self._draw_rival_meter()
                self._draw_flash()
                self._draw_potion_hud()
        elif s == ST_LEVEL_COMPLETE:
            prev_obj = level_objs.get(self.prev_state)
            if prev_obj:
                prev_obj.draw()
            self._draw_complete_banner()
        elif s == ST_WIN:
            if self.win_screen is None:
                self.win_screen = WinScreen()
            self.win_screen.draw()
        elif s == ST_CREDITS:
            self.credits_screen.draw()
        elif s == ST_GAME_OVER:
            self.game_over_screen.draw()
        elif s == ST_PAUSED:
            prev_obj = level_objs.get(self.prev_state)
            if prev_obj:
                prev_obj.draw()
            self._draw_pause_overlay()
        self._draw_god_overlay()

    def _draw_potion_hud(self):
        if not self.potions:
            return
        slots = [("speed_potion", "1", COL_LBLUE),
                 ("shield_potion", "2", COL_YELLOW),
                 ("heal_potion", "3", COL_RED),
                 ("jump_potion", "4", COL_LGREEN)]
        px = SCREEN_W - 64
        py = SCREEN_H - 14
        for key, label, col in slots:
            count = self.potions.get(key, 0)
            if count > 0:
                pyxel.rect(px, py, 10, 10, COL_BLACK)
                pyxel.rectb(px, py, 10, 10, col)
                pyxel.text(px + 3, py + 2, label, col)
                pyxel.text(px + 11, py + 2, f"{count}", COL_WHITE)
            px += 16

    def _draw_rival_meter(self):
        self.rival.draw_meter()

    def _draw_flash(self):
        if self.flash_timer > 0 and self.flash_msg:
            w = len(self.flash_msg) * 4 + 12
            x = SCREEN_W // 2 - w // 2
            y = SCREEN_H // 2 - 20
            if self.flash_timer % 8 < 5:
                pyxel.rect(x, y, w, 11, COL_BLACK)
                pyxel.rectb(x, y, w, 11, COL_YELLOW)
                draw_centered(self.flash_msg, y + 3, COL_YELLOW)

    def _draw_complete_banner(self):
        for yy in range(0, SCREEN_H, 2):
            pyxel.rect(0, yy, SCREEN_W, 1, COL_BLACK)
        w = max(len(self.complete_msg) * 8 + 16, 220)
        x = SCREEN_W // 2 - w // 2
        y = SCREEN_H // 2 - 40
        h = 86
        pyxel.rect(x, y, w, h, COL_BLACK)
        pyxel.rectb(x, y, w, h, COL_YELLOW)
        draw_centered(self.complete_msg, y + 8, COL_YELLOW)
        coin_msg = f"COINS: {self.persistent_coins}"
        draw_centered(coin_msg, y + 20, COL_PEACH)

        has_next = self.complete_next_state is not None
        opt_y1 = y + 38
        opt_y2 = y + 54
        blink = (pyxel.frame_count // 8) % 2 == 0

        if has_next:
            # Option 0: NEXT LEVEL
            label = "NEXT: " + (self.complete_next_label or "NEXT LEVEL")
            if self.complete_cursor == 0:
                cw = len(label) * 4 + 14
                pyxel.rectb(SCREEN_W // 2 - cw // 2, opt_y1 - 2, cw, 11,
                            COL_LGREEN if blink else COL_WHITE)
                draw_centered(label, opt_y1, COL_LGREEN)
            else:
                draw_centered(label, opt_y1, COL_WHITE)
            # Option 1: LOBBY
            if self.complete_cursor == 1:
                cw = len("BACK TO LOBBY") * 4 + 14
                pyxel.rectb(SCREEN_W // 2 - cw // 2, opt_y2 - 2, cw, 11,
                            COL_PINK if blink else COL_WHITE)
                draw_centered("BACK TO LOBBY", opt_y2, COL_PINK)
            else:
                draw_centered("BACK TO LOBBY", opt_y2, COL_WHITE)
            draw_centered("ARROWS + ENTER", y + h - 10, COL_DGRAY)
        else:
            # Last level — only lobby (this was the finale path)
            blink2 = (pyxel.frame_count // 8) % 2 == 0
            draw_centered("BACK TO LOBBY", opt_y1 + 8,
                          COL_PINK if blink2 else COL_WHITE)
            draw_centered("ENTER OR L", y + h - 10, COL_DGRAY)

    def _draw_pause_overlay(self):
        for yy in range(0, SCREEN_H, 2):
            pyxel.rect(0, yy, SCREEN_W, 1, COL_BLACK)
        draw_panel(SCREEN_W // 2 - 60, SCREEN_H // 2 - 24, 120, 50,
                   COL_YELLOW, COL_BLACK)
        draw_centered("-- PAUSED --", SCREEN_H // 2 - 14, COL_YELLOW)
        draw_centered("P TO RESUME", SCREEN_H // 2 - 2, COL_WHITE)
        draw_centered("L = LOBBY", SCREEN_H // 2 + 8, COL_PINK)

    def _draw_god_overlay(self):
        if self.god_stage == 2:
            w = 120
            x = SCREEN_W // 2 - w // 2
            y = SCREEN_H // 2 - 16
            pyxel.rect(x, y, w, 32, COL_BLACK)
            pyxel.rectb(x, y, w, 32, COL_YELLOW)
            draw_centered("ENTER CODE", y + 4, COL_YELLOW)
            shown = self.god_code_buffer + "_" * (3 - len(self.god_code_buffer))
            draw_centered(shown, y + 18, COL_PEACH)
        if self.god_mode:
            bx = SCREEN_W - 36
            by = SCREEN_H - 14
            blink = (pyxel.frame_count // 8) % 2 == 0
            col = COL_YELLOW if blink else COL_RED
            pyxel.rect(bx, by, 32, 10, COL_BLACK)
            pyxel.rectb(bx, by, 32, 10, col)
            pyxel.text(bx + 8, by + 2, "GOD", col)
            pyxel.text(bx - 14, by - 8, "N B K C F", COL_DGRAY)
            if self.god_fly:
                pyxel.text(bx - 2, by - 16, "FLY", COL_LGREEN)
        if self.god_msg and self.god_msg_timer > 0:
            w = len(self.god_msg) * 4 + 12
            x = SCREEN_W // 2 - w // 2
            y = 16
            pyxel.rect(x, y, w, 11, COL_BLACK)
            pyxel.rectb(x, y, w, 11, COL_YELLOW)
            draw_centered(self.god_msg, y + 3, COL_YELLOW)


# =====================================================================
# ENTRY POINT
# =====================================================================
HareHurryGame()