# Pyxel Studio

import pyxel
from pyxel import *

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

WIDTH = 256
HEIGHT = 256
FPS = 60

LANES = [60, 100, 140, 180]
KEYS = [KEY_D, KEY_F, KEY_J, KEY_K]

HIT_LINE = 210
NOTE_SPEED = 120  # pixels per second

HIT_WINDOW = {
    "perfect": 0.05,
    "great": 0.1,
    "good": 0.18
}

# ---------------- NOTE ----------------

class Note:
    def __init__(self, lane, time, length=0):
        self.lane = lane
        self.time = time
        self.length = length

        self.hit = False
        self.holding = False
        self.completed = False

# ---------------- GAME ----------------
class XOIntro:
    def __init__(self):
        pyxel.init(160, 120, title="XO Exact Intro")

        pyxel.sound(0).set(
            notes=(
                "c#4 d#4 f4 g#4 a#4 g#4 f4 d#4 c#4 r "
                "c4 d#4 g4 a#4 g4 d#4 c4 r "
                "c#4 d#4 f4 g#4 a#4 g#4 f4 d#4 c#4 r "
                "c4 d#4 g4 a#4 g4 d#4 c4 r "
                "c#4 d#4 f4 g#4 a#4 g#4 f4 d#4 "
                "c4 d#4 g4 a#4 g4 d#4 c4 r "
                "f4 g#4 c5 d#5 c5 g#4 f4 r "
                "d#4 g4 a#4 g4 d#4 c4 r r"
            ),
            tones="t",
            volumes="7",
            effects="n",
            speed=30
        )



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

    def update(self):
        if pyxel.btnp(pyxel.KEY_SPACE):
            pyxel.play(0, 0)
            pyxel.play(1, 1)

    def draw(self):
        pyxel.cls(7)
        pyxel.text(30, 50, "XO - Exact Intro", 7)
        pyxel.text(20, 70, "Press SPACE to play", 6)

class Game:

    def __init__(self):

        init(WIDTH, HEIGHT, title="Rhythm Master", fps=FPS)

        pyxel.sounds[0].set(
            "c#3 d#3 f3 g#3 a#3 g#3 f3 d#3 c#3 r "
            "c3 d#3 g3 a#3 g3 d#3 c3 r "
            "c#3 d#3 f3 g#3 a#3 g#3 f3 d#3 c#3 r "
            "c3 d#3 g3 a#3 g3 d#3 c3 r "
            "c#3 d#3 f3 g#3 a#3 g#3 f3 d#3 "
            "c3 d#3 g3 a#3 g3 d#3 c3 r "
            "f3 g#3 c4 d#4 c4 g#3 f3 r "
            "d#3 g3 a#3 g3 d#3 c3 r r",
            "t", "7", "n", 30
        )



        self.state = "menu"
        self.level_index = 0

        self.time = 0
        self.notes = []
        self.active_notes = []

        self.combo = 0
        self.max_combo = 0
        self.score = 0
        self.hp = 100

        self.judge_text = ""
        self.judge_timer = 0

        self.total_hits = 0
        self.total_accuracy = 0

        self.result_index = 0

        self.load_levels()

        run(self.update, self.draw)

    # ---------------- LEVELS ----------------

    def load_levels(self):

        self.level_names = ["easy", "hard", "master"]

        self.levels = {


            "easy": [
                (1.0,0),(1.8,1),(2.6,2),(3.4,3),
                (4.5,0,1.2),
                (6.5,2),(7.3,1),(8.1,3),(8.9,0)
                ],

            "hard": [
                (1.0,0),(1.6,1),(2.2,2),(3.0,3),
                (3.8,1,1.0),
                (5.5,3),(6.0,2),(6.5,1),
                (7.2,0,1.3)
                ],

                "master": [
                (0.8,0),(1.3,1),(1.8,2),(2.3,3),
                (3.0,0,1.2),
                (4.5,2),(5.0,3),(5.5,1),
                (6.2,1,1.4),
                (8.0,3),(8.5,2)
                ]
        }

    def start_level(self):

        level_name = self.level_names[self.level_index]

        self.notes = []
        self.active_notes = []

        for n in self.levels[level_name]:
            if len(n) == 3:
                self.notes.append(Note(n[1], n[0], n[2]))
            else:
                self.notes.append(Note(n[1], n[0]))

        self.time = 0
        self.combo = 0
        self.max_combo = 0
        self.score = 0
        self.hp = 100

        self.total_hits = 0
        self.total_accuracy = 0

        self.state = "game"

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

    # ---------------- LOGIC ----------------

    def get_time(self):
        return self.time / FPS

    def get_note_y(self, note, current_time):
        return HIT_LINE - (note.time - current_time) * NOTE_SPEED

    # ✅ FIXED FUNCTION
    def spawn_notes(self, current_time):

        spawn_offset = HIT_LINE / NOTE_SPEED

        for note in self.notes:

            if note in self.active_notes:
                continue

            # spawn earlier so notes fall correctly
            if note.time - spawn_offset <= current_time:
                self.active_notes.append(note)

    def judge(self, lane, current_time):

        best_note = None
        best_diff = 999

        for note in self.active_notes:

            if note.lane != lane or note.hit:
                continue

            diff = abs(note.time - current_time)

            if diff < best_diff:
                best_diff = diff
                best_note = note

        if best_note:

            if best_diff < HIT_WINDOW["perfect"]:
                result = "PERFECT"
                points = 300
                acc = 1.0

            elif best_diff < HIT_WINDOW["great"]:
                result = "GREAT"
                points = 200
                acc = 0.7

            elif best_diff < HIT_WINDOW["good"]:
                result = "GOOD"
                points = 100
                acc = 0.4

            else:
                result = "MISS"
                points = 0
                acc = 0

            self.judge_text = result
            self.judge_timer = 20

            self.total_hits += 1
            self.total_accuracy += acc

            if result != "MISS":
                self.combo += 1
                self.score += points
                best_note.hit = True

                if best_note.length > 0:
                    best_note.holding = True

            else:
                self.combo = 0
                self.hp -= 5

            return

        self.combo = 0
        self.hp -= 5
        self.judge_text = "MISS"

    def update_long_notes(self, current_time):

        for note in self.active_notes:

            if note.holding:

                if btn(KEYS[note.lane]):

                    end_time = note.time + note.length

                    if current_time >= end_time:

                        diff = abs(current_time - end_time)

                        if diff < HIT_WINDOW["perfect"]:
                            self.judge_text = "PERFECT"
                            self.score += 300
                        elif diff < HIT_WINDOW["good"]:
                            self.judge_text = "GOOD"
                            self.score += 150
                        else:
                            self.judge_text = "MISS"
                            self.combo = 0

                        note.holding = False
                        note.completed = True
                        self.combo += 1

                else:
                    self.combo = 0
                    self.hp -= 10
                    note.holding = False
                    note.completed = True

    def update(self):

        if self.state == "menu":

            if btnp(KEY_UP):
                self.level_index = (self.level_index - 1) % 3

            if btnp(KEY_DOWN):
                self.level_index = (self.level_index + 1) % 3

            if btnp(KEY_SPACE):
                self.start_level()

        elif self.state == "game":

            self.time += 1
            current_time = self.get_time()

            self.spawn_notes(current_time)

            for note in self.active_notes:

                y = self.get_note_y(note, current_time)

                if y > HEIGHT and not note.hit:
                    note.hit = True
                    self.combo = 0
                    self.hp -= 10

            for i,key in enumerate(KEYS):
                if btnp(key):
                    self.judge(i, current_time)

            self.update_long_notes(current_time)

            self.max_combo = max(self.max_combo, self.combo)

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

            if self.hp <= 0:
                pyxel.stop(0)
                pyxel.stop(1)
                self.state = "fail"

            if current_time > 10:
                pyxel.stop(0)
                pyxel.stop(1)
                self.state = "clear"

        else:

            if btnp(KEY_UP):
                self.result_index = (self.result_index - 1) % 3

            if btnp(KEY_DOWN):
                self.result_index = (self.result_index + 1) % 3

            if btnp(KEY_SPACE):
                self.state = "menu"

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

    def draw_lanes(self):

        for x in LANES:
            rect(x-10,0,20,HEIGHT,13)

        line(0,HIT_LINE,WIDTH,HIT_LINE,9)

    def draw_notes(self, current_time):

        for note in self.active_notes:

            if note.completed:
                continue

            x = LANES[note.lane]
            y = self.get_note_y(note, current_time)

            if note.length > 0:
                length_px = note.length * NOTE_SPEED
                rect(x-4, y-length_px, 8, length_px, 9)

            rect(x-6, y, 12, 8, 14)

    def draw_ui(self):

        acc = 0
        if self.total_hits > 0:
            acc = (self.total_accuracy / self.total_hits) * 100

        text(10,10,f"SCORE {self.score}",13)
        text(10,20,f"{acc:.1f}%",7)
        text(200,10,f"{self.combo}",9)

        rect(20,30,200,6,1)
        rect(20,30,self.hp*2,6,6)

        if self.judge_timer > 0:
            text(110,180,self.judge_text,12)

    def draw_menu(self):

        text(90,60,"RHYTHM MASTER",12)
        text(90,110,"SELECT LEVEL",14)

        for i,name in enumerate(self.level_names):
            c = 9 if i == self.level_index else 13
            text(110,140 + i*15,name,c)

        text(70,210,"UP/DOWN + SPACE",14)

    def get_rank(self, acc):

        if acc > 95:
            return "S"
        elif acc > 85:
            return "A"
        elif acc > 70:
            return "B"
        else:
            return "C"

    def draw_result(self):

        acc = 0
        if self.total_hits > 0:
            acc = (self.total_accuracy / self.total_hits) * 100

        rank = self.get_rank(acc)

        text(110,80,rank,6)
        text(90,110,f"SCORE {self.score}",9 if self.result_index == 0 else 13)
        text(90,130,f"COMBO {self.max_combo}",9 if self.result_index == 1 else 13)
        text(90,150,f"{acc:.1f}%",9 if self.result_index == 2 else 13)

        text(70,200,"SPACE -> MENU",14)

    def draw(self):

        cls(7)

        if self.state == "menu":
            self.draw_menu()

        elif self.state == "game":

            current_time = self.get_time()

            self.draw_lanes()
            self.draw_notes(current_time)
            self.draw_ui()

        else:
            self.draw_result()
Game()