# coordinates for items: diary open 32,144 closed 16,144 

import math
import random
import pyxel
from enum import Enum, auto
import json
import os
import time

SCREEN_W = 128
SCREEN_H = 144
PLAYER_SPEED = 1.2
GRAVITY = 0.125
JUMP_SPEED = -2.5
PLATFORM_H = 5
DIARY_CHARS = 24   # max characters per diary line before word-wrap


# ============================================================
#  REWARD TIMER
# ============================================================

class RewardTimer:
    def __init__(self):
        self.time_left = 0
        self.active = False
        self.reward = 0

    def start(self, minutes, reward):
        self.time_left = minutes * 60
        self.reward = reward
        self.active = True

    def update(self, dt):
        if self.active:
            self.time_left -= dt
            if self.time_left <= 0:
                self.time_left = 0
                self.active = False
                return self.reward
        return 0

    def draw(self):
        if self.active:
            mins = int(self.time_left // 60)
            secs = int(self.time_left % 60)
            label = f"TIMER {mins:02d}:{secs:02d}"
            w = len(label) * 4 + 4
            pyxel.rect(2, 31, w + 2, 11, 1)
            pyxel.rectb(2, 31, w + 2, 11, 7)
            pyxel.text(4, 33, label, 10)


# ============================================================
#  PLAYER
# ============================================================

class Player:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.dir = "down"
        self.moving = False

    def update(self, dt, dx, dy):
        self.moving = dx != 0 or dy != 0

        if dx > 0:
            self.dir = "right"
        elif dx < 0:
            self.dir = "left"
        elif dy != 0:
            self.dir = "down"

        self.x += dx * PLAYER_SPEED * 60 * dt
        self.y += dy * PLAYER_SPEED * 60 * dt

        self.x = max(8, min(SCREEN_W - 24, self.x))
        self.y = max(48, min(SCREEN_H - 24, self.y))

    def draw(self):
        frame = (pyxel.frame_count // 8) % 2
        u, v, w, h = 0, 0, 16, 16

        if self.dir == "down":
            u = (16 if frame == 0 else 32) if self.moving else 0

        elif self.dir == "right":
            u = (80 if frame == 0 else 64) if self.moving else 48
            w = -16

        elif self.dir == "left":
            u = (80 if frame == 0 else 64) if self.moving else 48
            w = 16

        pyxel.blt(int(self.x), int(self.y), 0, u, v, w, h, 2)


# ============================================================
#  CAT
# ============================================================

class CatState(Enum):
    IDLE   = auto()
    WALK   = auto()
    FOLLOW = auto()
    SLEEP  = auto()


class Cat:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.state = CatState.SLEEP
        self.timer = random.uniform(5, 12)
        self.walk_target = None
        self.facing_right = True

    def update(self, dt, player):
        self.timer -= dt

        if self.state == CatState.SLEEP:
            if self.timer <= 0:
                self.state = CatState.IDLE
                self.timer = random.uniform(6, 12)
            return

        if self.state == CatState.IDLE:
            if self.timer <= 0:
                if random.random() < 0.4:
                    self.state = CatState.WALK
                    self.timer = random.uniform(4, 8)
                    self.walk_target = (
                        player.x + random.randint(-40, 40),
                        player.y + random.randint(-10, 10),
                    )
                else:
                    self.state = CatState.SLEEP
                    self.timer = random.uniform(8, 16)
            return

        if self.state == CatState.WALK:
            tx, ty = self.walk_target
            dx = tx - self.x
            dy = ty - self.y
            dist = math.hypot(dx, dy)

            if dist > 1:
                self.facing_right = dx > 0
                self.x += dx / dist * 30 * dt
                self.y += dy / dist * 30 * dt

            if dist <= 1 or self.timer <= 0:
                self.state = CatState.IDLE
                self.timer = random.uniform(6, 12)
            return

        if self.state == CatState.FOLLOW:
            if self.timer <= 0:
                self.state = CatState.IDLE
                self.timer = random.uniform(6, 12)
                return

            dx = player.x - self.x
            dy = player.y - self.y
            dist = math.hypot(dx, dy)

            if dist > 10:
                self.facing_right = dx > 0
                self.x += dx / dist * 40 * dt
                self.y += dy / dist * 40 * dt
            return

    def start_follow(self):
        self.state = CatState.FOLLOW
        self.timer = 10

    def is_near(self, px, py):
        return (self.x - px) ** 2 + (self.y - py) ** 2 < 20 ** 2

    def draw(self):
        u, v, w, h = 0, 32, 16, 16

        if self.state == CatState.SLEEP:
            u = 48

        elif self.state == CatState.IDLE:
            blink_phase = (pyxel.frame_count // 6) % 80
            u = 64 if blink_phase < 4 else 0

        elif self.state in (CatState.WALK, CatState.FOLLOW):
            u = 32 if self.facing_right else 16

        pyxel.blt(int(self.x), int(self.y), 0, u, v, w, h, 2)


# ============================================================
#  DREAM LEVELS
# ============================================================

class DreamLevel:
    def __init__(self, level, app):
        self.level = level
        self.app = app
        self.load_level()
        self.reset()

    def load_level(self):
        if self.level == 0:
            self.platforms = [
                (0,  120, 30),
                (35, 105, 20),
                (60,  90, 20),
                (85,  75, 20),
                (105, 60, 20),
            ]
            self.lava_y = 136

        elif self.level == 1:
            self.platforms = [
                (0,   120, 25),
                (25,  105, 20),
                (50,   90, 20),
                (75,   75, 20),
                (100,  60, 20),
            ]
            self.lava_y = 136

        elif self.level == 2:
            self.platforms = [
                (0,   120, 20),
                (30,  100, 20),
                (60,   80, 20),
                (90,   60, 20),
                (108,  40, 20),
            ]
            self.lava_y = 136

    def reset(self):
        self.px = float(self.platforms[0][0] + 4)
        self.py = float(self.platforms[0][1] - 16)
        self.vy = 0.0
        self.on_ground = False
        self.facing_right = True
        self.won = False
        self.won_timer = 0.0
        self.deaths = 0

    def update(self, dt):
        if self.won:
            self.won_timer -= dt
            return self.won_timer <= 0

        moving_right = pyxel.btn(pyxel.KEY_RIGHT) or pyxel.btn(pyxel.KEY_D)
        moving_left  = pyxel.btn(pyxel.KEY_LEFT)  or pyxel.btn(pyxel.KEY_A)
        jump = pyxel.btnp(pyxel.KEY_Z) or pyxel.btnp(pyxel.KEY_UP)

        dx = int(moving_right) - int(moving_left)
        if dx > 0:
            self.facing_right = True
        elif dx < 0:
            self.facing_right = False

        self.px += dx * 1.3
        self.px = max(0, min(SCREEN_W - 16, self.px))

        if jump and self.on_ground:
            self.vy = JUMP_SPEED

        self.vy = min(self.vy + GRAVITY, 6)
        self.py += self.vy

        self.on_ground = False
        if self.vy >= 0:
            for i, (plx, ply, plw) in enumerate(self.platforms):
                feet = self.py + 16
                if (plx <= self.px + 12 and self.px + 4 <= plx + plw and
                        ply <= feet <= ply + PLATFORM_H + self.vy + 2):
                    self.py = ply - 16
                    self.vy = 0
                    self.on_ground = True
                    if i == len(self.platforms) - 1:
                        self.won = True
                        self.won_timer = 2.5
                        pyxel.play(3, 6)  # dream win sound
                    break

        if self.py > self.lava_y:
            self.deaths += 1
            pyxel.play(3, 7)  # lava death sound
            self.reset()

        return False

    def draw(self):
        theme = self.app.setting_theme

        if theme == 0:
            bg = 2;  star1, star2 = 7, 6;   cloud = 13; lava1, lava2 = 8, 9
        elif theme == 1:
            bg = 9;  star1, star2 = 10, 7;  cloud = 8;  lava1, lava2 = 2, 3
        else:
            bg = 1;  star1, star2 = 12, 11; cloud = 5;  lava1, lava2 = 6, 7

        pyxel.cls(bg)

        for sx, sy in [
            (random.randint(0, SCREEN_W), random.randint(0, 160))
            for _ in range(50)
        ]:
            pyxel.pset(sx, sy, star1)

        for i, cx in enumerate([15, 55, 95]):
            cy = 40 + int(math.sin((pyxel.frame_count / 40) + i * 2) * 3)
            pyxel.rect(cx, cy, 36, 10, cloud)
            pyxel.rect(cx + 6, cy - 5, 20, 8, cloud)

        pyxel.text(2, 2, "DREAMLAND", 7)
        if self.deaths > 0:
            pyxel.text(80, 2, f"Falls:{self.deaths}", 8)

        gx, gy = self.platforms[-1][0], self.platforms[-1][1]
        pyxel.text(gx + 4, gy - 9, "GOAL!", 10)

        for i, (plx, ply, plw) in enumerate(self.platforms):
            top_col  = 11 if i == len(self.platforms) - 1 else 7
            fill_col = 3  if i < len(self.platforms) - 1 else 11
            pyxel.rect(plx, ply, plw, PLATFORM_H, fill_col)
            pyxel.rect(plx, ply, plw, 1, top_col)

        lava_col = lava1 if (pyxel.frame_count // 12) % 2 == 0 else lava2
        pyxel.rect(0, self.lava_y, SCREEN_W, SCREEN_H - self.lava_y, lava_col)

        moving = (pyxel.btn(pyxel.KEY_RIGHT) or pyxel.btn(pyxel.KEY_D) or
                  pyxel.btn(pyxel.KEY_LEFT)  or pyxel.btn(pyxel.KEY_A))
        frame = (pyxel.frame_count // 8) % 2
        su = (80 if frame == 0 else 64) if moving else 48
        sw = 16 if self.facing_right else -16
        pyxel.blt(int(self.px), int(self.py), 0, su, 0, sw, 16, 2)

        if self.won:
            pyxel.rect(14, 100, 100, 28, 1)
            pyxel.rectb(14, 100, 100, 28, 10)
            pyxel.text(22, 107, "YOU ESCAPED!", 10)
            pyxel.text(22, 117, "Waking up...", 7)


# ============================================================
#  HELPERS
# ============================================================

def word_wrap(text, max_chars):
    """Split a paragraph string into display lines of at most max_chars width."""
    result = []
    for paragraph in text.split("\n"):
        if paragraph == "":
            result.append("")          # preserve blank lines between paragraphs
            continue
        words = paragraph.split(" ")
        line  = ""
        for word in words:
            if line == "":
                line = word
            elif len(line) + 1 + len(word) <= max_chars:
                line += " " + word
            else:
                result.append(line)
                line = word
        if line:
            result.append(line)
    return result


# ============================================================
#  MAIN APP
# ============================================================

class App:
    def __init__(self):
        pyxel.init(SCREEN_W, SCREEN_H, title="Midnight Corner", fps=60)
        pyxel.load("res.pyxres")

        # ---- Music ----
        pyxel.sound(0).set("c3e3g3e3 d3f3a3f3 c3e3g3b3 a3g3e3c3", "t", "6", "n", 30)
        pyxel.sound(1).set("c2c2g2g2 a2a2e2e2 f2f2c2c2 g2g2c2c2", "p", "4", "n", 30)

        # ---- Sound effects (Pyxel octave range: 0-4) ----
        pyxel.sound(2).set("c2c2c2b1 c2c2c2b1", "n", "33333333", "f", 20)  # cat purr
        pyxel.sound(3).set("g3c4e4",             "t", "765",      "n", 10)  # diary open
        pyxel.sound(4).set("c4d4",               "t", "76",       "n",  5)  # page turn
        pyxel.sound(5).set("c4g3e3c3",           "t", "7654",     "f", 30)  # enter dream
        pyxel.sound(6).set("c4e4g4a4",           "t", "7777",     "n", 12)  # dream win
        pyxel.sound(7).set("c4a3f3c3",           "p", "7654",     "f", 18)  # lava death
        pyxel.sound(8).set("c4g4a4",             "t", "777",      "n",  8)  # coin
        pyxel.sound(9).set("e4g4a4",             "t", "777",      "n", 10)  # purchase
        pyxel.sound(10).set("g3f3",              "p", "55",       "n", 10)  # can't afford
        pyxel.sound(11).set("c4e4g4a4e4",        "t", "77777",    "n", 18)  # timer done

        pyxel.play(0, 0, loop=True)
        pyxel.play(1, 1, loop=True)

        self.state = "title"
        self.menu_index = 0
        self.timer_index = 0
        self.settings_index = 0
        self.fade = 1.0

        self.setting_music    = True
        self.setting_lighting = True
        self.setting_speed    = 1
        self.setting_theme    = 0

        self.player = Player(60, 100)
        self.cat    = Cat(90, 100)

        self.coins = 0
        self.message = ""
        self.message_timer = 0

        self.shop_open       = False
        self.timer_menu_open = False

        self.reward_timer = RewardTimer()

        self.current_dream_level = 0
        self.dream = DreamLevel(self.current_dream_level, self)

        self.diary_open     = False
        self.diary_page     = 0
        self.diary_scroll   = 0
        self.diary_unlocked = 1
        self.timer_end_time = 0.0

        self.diary_pages = [
            "June 3rd\n\nToday I noticed a boy in the bookstore...\n\nHe kept pretending to look at the shelves, but every few minutes he would glance over at me and then immediately look away.\n\nI do not think he realized I could see him.\n\nHe looked terrified.\n\nIt was adorable.",

            "June 18th\n\nIt rained today.\n\nWe got trapped under the bakery awning together for almost ten minutes.\n\nFor most of that time he seemed to be fighting a very serious battle with his own thoughts.\n\nThen he finally said, 'I like your book.'\n\nI smiled and told him he could borrow it when I finished.\n\nHis name is Oliver.",

            "September 1st\n\nThe market was crowded today.\n\nI took Oliver's hand so we would not get separated.\n\nHe went completely quiet.\n\nFor a second I worried I had done something wrong.\n\nThen I looked over and realized he was trying very hard not to smile.\n\nI think that was the moment I knew.",

            "March 21st\n\nOliver proposed tonight.\n\nHe had prepared a whole speech, but halfway through he forgot every word.\n\nThen he started apologizing for forgetting the speech.\n\nThen I started laughing.\n\nThen he thought I was laughing at him.\n\nThen I cried.\n\nThen he cried.\n\nAnd somehow it was the most perfect moment of my life.\n\nIf anyone ever finds this diary in the attic, I hope they know this house was once very, very loved."
        ]

        self.load_game()

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

    # ------------------------------------------------------------------ UPDATE

    def update(self):
        dt = 1 / 60

        if self.state == "title":
            self.update_title(dt)
            return

        if self.state == "settings":
            self.update_settings(dt)
            return

        if self.diary_open:
            self.update_diary()
            return

        if self.state == "dream":
            done = self.dream.update(dt)
            if done:
                if self.dream.won:
                    self.coins += 5
                    self.message = "+5 coins! Sweet dreams!"
                    self.message_timer = 3
                    self.current_dream_level = (self.current_dream_level + 1) % 3
                self.state = "game"
            return

        dx = int(pyxel.btn(pyxel.KEY_RIGHT) or pyxel.btn(pyxel.KEY_D)) - \
             int(pyxel.btn(pyxel.KEY_LEFT)  or pyxel.btn(pyxel.KEY_A))
        dy = int(pyxel.btn(pyxel.KEY_DOWN)  or pyxel.btn(pyxel.KEY_S)) - \
             int(pyxel.btn(pyxel.KEY_UP)    or pyxel.btn(pyxel.KEY_W))

        speed_mult = [0.6, 1.0, 1.6][self.setting_speed]
        self.player.update(dt * speed_mult, dx, dy)
        self.cat.update(dt, self.player)

        if self.reward_timer.active:
            remaining = self.timer_end_time - time.time()
            if remaining <= 0:
                self.reward_timer.active    = False
                self.reward_timer.time_left = 0
                reward = self.reward_timer.reward
                self.coins += reward
                self.diary_unlocked = min(len(self.diary_pages),
                                         self.diary_unlocked + 1)
                self.message = (
                    f"+{reward} coins! "
                    "A diary page was discovered."
                )
                self.message_timer = 3
                pyxel.play(3, 11)
                self.save_game()
            else:
                self.reward_timer.time_left = remaining

        if pyxel.btnp(pyxel.KEY_T) and not self.reward_timer.active:
            if self.shop_open:
                self.shop_open = False
            self.timer_menu_open = not self.timer_menu_open
            self.timer_index = 0

        if self.timer_menu_open:
            self.update_timer_menu()
            return

        if pyxel.btnp(pyxel.KEY_SPACE):
            if self.shop_open:
                self.shop_open = False
            else:
                self.handle_interact()

        if self.shop_open:
            self.update_shop()

        if self.message_timer > 0:
            self.message_timer -= dt
            if self.message_timer <= 0:
                self.message = ""

    # ------------------------------------------------------------------ TITLE

    def update_title(self, dt):
        if self.fade > 0:
            self.fade = max(0, self.fade - dt * 0.5)

        if pyxel.btnp(pyxel.KEY_UP):
            self.menu_index = max(0, self.menu_index - 1)
        if pyxel.btnp(pyxel.KEY_DOWN):
            self.menu_index = min(1, self.menu_index + 1)

        if pyxel.btnp(pyxel.KEY_SPACE) or pyxel.btnp(pyxel.KEY_RETURN):
            if self.menu_index == 0:
                self.state = "game"
            else:
                self.settings_index = 0
                self.state = "settings"

    # ------------------------------------------------------------------ TIMER MENU

    def update_timer_menu(self):
        options = [1, 5, 10, 15, 25, 45]
        if pyxel.btnp(pyxel.KEY_UP):
            self.timer_index = max(0, self.timer_index - 1)
        if pyxel.btnp(pyxel.KEY_DOWN):
            self.timer_index = min(len(options) - 1, self.timer_index + 1)
        if pyxel.btnp(pyxel.KEY_SPACE) or pyxel.btnp(pyxel.KEY_RETURN):
            self.start_timer(options[self.timer_index])
            self.timer_menu_open = False

    def start_timer(self, minutes):
        reward = max(1, minutes // 3)
        self.reward_timer.start(minutes, reward)
        self.timer_end_time = time.time() + minutes * 60
        self.message = f"Timer: {minutes} min -- good luck!"
        self.message_timer = 3
        self.save_game()

    # ------------------------------------------------------------------ INTERACT

    def handle_interact(self):
        px, py = self.player.x, self.player.y

        if 96 < px < 112 and 32 < py < 80:
            self.diary_open   = True
            self.diary_page   = 0
            self.diary_scroll = 0
            pyxel.play(3, 3)
            return

        if 64 < px < 95 and 32 < py < 80:
            self.message = random.choice([
                "Fascinating, distant galaxies...",
                "Cat in Paris solving a murder...",
                "Done! 64 books read this year!",
                "oooh plotwist",
                "I KNEW IT!",
                "WAIT WHAT!? NO! HOW? WHY???",
                "They better not. And they did",
                "*throws book at wall*",
                "Art of coding without AI...",
            ])
            self.message_timer = 3
            if random.random() < 0.6:
                self.coins += 1
                pyxel.play(3, 8)
            return

        if 32 < px < 64 and 32 < py < 64:
            self.shop_open = True
            self.message = "Shop opened."
            self.message_timer = 2
            return

        if 0 < px < 32 and 32 < py < 100:
            self.enter_dream()
            return

        if self.cat.is_near(px, py):
            self.message = "The cat purrs softly."
            self.message_timer = 2
            pyxel.play(3, 2)
            if random.random() < 0.5:
                self.cat.start_follow()
            return

    def enter_dream(self):
        pyxel.play(3, 5)
        self.dream = DreamLevel(self.current_dream_level, self)
        self.dream.reset()
        self.state = "dream"
        self.message = ""

    # ------------------------------------------------------------------ SHOP

    def update_shop(self):
        if pyxel.btnp(pyxel.KEY_1):
            self.try_buy("Rug", 5)
        if pyxel.btnp(pyxel.KEY_2):
            self.try_buy("Fairy Lights", 8)
        if pyxel.btnp(pyxel.KEY_3):
            self.try_buy("Poster", 6)

    def try_buy(self, item, price):
        if self.coins < price:
            self.message = f"Need {price} coins!"
            self.message_timer = 2
            pyxel.play(3, 10)
            return
        self.coins -= price
        self.message = f"Bought {item}!"
        self.message_timer = 2
        pyxel.play(3, 9)

    # ------------------------------------------------------------------ DIARY

    def update_diary(self):
        # ---- page turn: LEFT / RIGHT ----
        if pyxel.btnp(pyxel.KEY_RIGHT):
            new_page = min(self.diary_unlocked - 1, self.diary_page + 1)
            if new_page != self.diary_page:
                self.diary_page   = new_page
                self.diary_scroll = 0
                pyxel.play(3, 4)

        if pyxel.btnp(pyxel.KEY_LEFT):
            new_page = max(0, self.diary_page - 1)
            if new_page != self.diary_page:
                self.diary_page   = new_page
                self.diary_scroll = 0
                pyxel.play(3, 4)

        # ---- scroll: UP / DOWN  (hold to repeat: 10 frame delay, every 3 frames) ----
        lines      = word_wrap(self.diary_pages[self.diary_page], DIARY_CHARS)
        visible    = 10
        max_scroll = max(0, len(lines) - visible)

        if pyxel.btnp(pyxel.KEY_DOWN, 10, 3):
            self.diary_scroll = min(max_scroll, self.diary_scroll + 1)
        if pyxel.btnp(pyxel.KEY_UP, 10, 3):
            self.diary_scroll = max(0, self.diary_scroll - 1)

        # ---- close ----
        if pyxel.btnp(pyxel.KEY_Q):
            self.diary_open   = False
            self.diary_scroll = 0

    # ------------------------------------------------------------------ DRAW

    def draw(self):
        skip_lighting = self.shop_open or self.timer_menu_open

        if self.state == "title":
            self.draw_title()
            return

        if self.state == "settings":
            self.draw_settings()
            return

        if self.state == "dream":
            self.dream.draw()
            return

        pyxel.cls(0)
        pyxel.bltm(0, 0, 0, 0, 0, SCREEN_W, SCREEN_H)

        if not skip_lighting and self.setting_lighting:
            self.draw_lighting()

        self.cat.draw()
        self.player.draw()
        self.draw_hud()
        self.reward_timer.draw()

        if self.diary_open:
            self.draw_diary()

        if self.shop_open:
            self.draw_shop()

        if self.timer_menu_open:
            self.draw_timer_menu()

    # ------------------------------------------------------------------ SETTINGS

    def update_settings(self, dt):
        num_options = 4

        if pyxel.btnp(pyxel.KEY_UP):
            self.settings_index = max(0, self.settings_index - 1)
        if pyxel.btnp(pyxel.KEY_DOWN):
            self.settings_index = min(num_options - 1, self.settings_index + 1)

        if pyxel.btnp(pyxel.KEY_SPACE) or pyxel.btnp(pyxel.KEY_RETURN):
            if self.settings_index == 0:
                self.setting_music = not self.setting_music
                if self.setting_music:
                    pyxel.play(0, 0, loop=True)
                    pyxel.play(1, 1, loop=True)
                else:
                    pyxel.stop()
            elif self.settings_index == 1:
                self.setting_lighting = not self.setting_lighting
            elif self.settings_index == 2:
                self.setting_speed = (self.setting_speed + 1) % 3
            elif self.settings_index == 3:
                self.setting_theme = (self.setting_theme + 1) % 3

        if pyxel.btnp(pyxel.KEY_Q) or pyxel.btnp(pyxel.KEY_BACKSPACE):
            self.state = "title"

    def draw_settings(self):
        pyxel.cls(0)
        pyxel.text(SCREEN_W // 2 - 16, 40, "SETTINGS", 10)
        pyxel.line(20, 55, SCREEN_W - 20, 55, 5)

        speed_labels = ["SLOW", "NORMAL", "FAST"]
        theme_labels = ["Classic", "Sunset", "Ocean"]

        options = [
            ("Music",    "ON" if self.setting_music    else "OFF"),
            ("Lighting", "ON" if self.setting_lighting else "OFF"),
            ("Speed",    speed_labels[self.setting_speed]),
            ("Theme",    theme_labels[self.setting_theme]),
        ]

        for i, (label, value) in enumerate(options):
            y = 55 + i * 16
            is_sel  = i == self.settings_index
            row_col = 10 if is_sel else 7
            prefix  = "> " if is_sel else "  "
            val_col = 11 if value in ("ON", "NORMAL", "Classic") else 8
            pyxel.text(32, y, prefix + label, row_col)
            pyxel.text(90, y, value, val_col)

        pyxel.line(20, SCREEN_H - 30, SCREEN_W - 20, SCREEN_H - 30, 5)
        pyxel.text(20, SCREEN_H - 20, "SPC=toggle  Q=back", 5)

    # ------------------------------------------------------------------ TITLE

    def draw_title(self):
        pyxel.cls(0)
        title = "MIDNIGHT CORNER"
        pyxel.text(SCREEN_W // 2 - len(title) * 2, 50, title, 7)

        options = ["START", "SETTINGS"]
        for i, text in enumerate(options):
            color  = 10 if i == self.menu_index else 7
            prefix = "> " if i == self.menu_index else "  "
            pyxel.text(SCREEN_W // 2 - 20, 80 + i * 14, prefix + text, color)

        if self.fade > 0:
            pyxel.rect(0, 0, SCREEN_W, SCREEN_H, 0)

    # ------------------------------------------------------------------ LIGHTING

    def draw_lighting(self):
        pyxel.rect(0, 0, SCREEN_W, SCREEN_H, 1)
        cx, cy = 95, 70
        for r, col in [(70, 5), (48, 6), (28, 7)]:
            pyxel.circ(cx, cy, r, col)
        wx, wy = 60, 60
        for r, col in [(56, 2), (36, 3)]:
            pyxel.circ(wx, wy, r, col)
        pyxel.bltm(0, 0, 0, 0, 0, SCREEN_W, SCREEN_H)

    # ------------------------------------------------------------------ HUD

    def draw_hud(self):
        pyxel.blt(4, 4, 0, 0, 128, 8, 8, 2)
        pyxel.text(14, 5, str(self.coins), 10)
        pyxel.text(4, 14, "SPACE: act  T: timer", 7)
        pyxel.text(4, 22, "Bed = Dream!", 6)

        if self.message:
            w = len(self.message) * 4
            x = SCREEN_W // 2 - w // 2
            pyxel.rect(x - 2, 2, w + 4, 12, 1)
            pyxel.text(x, 4, self.message, 7)

    # ------------------------------------------------------------------ DIARY UI

    def draw_diary(self):
        x, y, w, h = 6, 20, 116, 110
        pyxel.rect(x, y, w, h, 1)
        pyxel.rectb(x, y, w, h, 7)

        all_lines  = word_wrap(self.diary_pages[self.diary_page], DIARY_CHARS)
        visible    = 10
        start      = self.diary_scroll
        end        = start + visible

        ty = y + 8
        for line in all_lines[start:end]:
            pyxel.text(x + 6, ty, line, 7)
            ty += 8

        # scroll indicators
        if self.diary_scroll > 0:
            pyxel.text(x + w - 16, y + 4, "^ UP", 6)
        if end < len(all_lines):
            pyxel.text(x + w - 19, y + h - 22, "v DN", 6)

        nav = f"< pg {self.diary_page + 1}/{self.diary_unlocked} >  Q=close"
        pyxel.text(x + 4, y + h - 12, nav, 5)

    # ------------------------------------------------------------------ SHOP UI

    def draw_shop(self):
        x, y, w, h = 10, 30, 108, 70
        pyxel.rect(x, y, w, h, 1)
        pyxel.rectb(x, y, w, h, 7)
        pyxel.text(x + 10, y + 6,  "SHOP", 10)
        pyxel.text(x + 10, y + 24, "1) Rug          (5)", 7)
        pyxel.text(x + 10, y + 38, "2) Fairy Lights (8)", 7)
        pyxel.text(x + 10, y + 52, "3) Poster       (6)", 7)
        pyxel.text(x + 10, y + 60, "SPACE close", 5)

    # ------------------------------------------------------------------ TIMER MENU UI

    def draw_timer_menu(self):
        options = ["1 min", "5 min", "10 min", "15 min", "25 min", "45 min"]
        x, y, w, h = 8, 20, 112, 120
        pyxel.rect(x, y, w, h, 1)
        pyxel.rectb(x, y, w, h, 7)
        pyxel.text(x + 16, y + 10, "STUDY TIMER", 10)

        for i, label in enumerate(options):
            color  = 10 if i == self.timer_index else 7
            prefix = "> " if i == self.timer_index else "  "
            pyxel.text(x + 16, y + 30 + i * 16, prefix + label, color)

        pyxel.text(x + 64, y + 16, "T=close", 5)

    # ------------------------------------------------------------------ SAVE / LOAD

    def save_game(self):
        data = {
            "coins":               self.coins,
            "diary_unlocked":      self.diary_unlocked,
            "current_dream_level": self.current_dream_level,
            "timer_active":        self.reward_timer.active,
            "timer_end_time":      self.timer_end_time,
        }
        with open("save.json", "w") as f:
            json.dump(data, f)

    def load_game(self):
        if not os.path.exists("save.json"):
            return
        with open("save.json", "r") as f:
            data = json.load(f)

        self.coins               = data.get("coins", 0)
        self.diary_unlocked      = data.get("diary_unlocked", 1)
        self.current_dream_level = data.get("current_dream_level", 0)
        self.timer_end_time      = data.get("timer_end_time", 0.0)

        if data.get("timer_active", False):
            remaining = self.timer_end_time - time.time()
            if remaining > 0:
                self.reward_timer.time_left = remaining
                self.reward_timer.reward    = data.get("timer_reward", 1)
                self.reward_timer.active    = True


# ============================================================
#  ENTRY POINT
# ============================================================

if __name__ == "__main__":
    App()