import pygame
import math
import random
import sys

# ============================================================
#  UNDERTALE: Sans Judgment — 严格按原版复刻 (增强版 + 性能修复)
#  操作: 方向键/WASD移动 | 蓝心模式: 反重力方向跳跃 | ESC退出
# ============================================================

pygame.init()
pygame.display.set_caption("UNDERTALE: Sans Judgment [Accurate Enhanced]")

W, H = 640, 480
SS = 3

screen = pygame.display.set_mode((W, H))
clock = pygame.time.Clock()
FPS = 60

canvas = pygame.Surface((W * SS, H * SS))


def make_font(size, bold=True):
    for name in ("Consolas", "Courier New", "DejaVu Sans Mono", "monospace"):
        try:
            f = pygame.font.SysFont(name, size, bold=bold)
            if f:
                return f
        except Exception:
            pass
    return pygame.font.Font(None, size)


F_SM = make_font(28)
F_MD = make_font(34)
F_LG = make_font(44)
F_XL = make_font(70)

BLACK  = (0, 0, 0)
WHITE  = (255, 255, 255)
RED    = (255, 0, 0)
BLUE   = (0, 0, 255)
LBLUE  = (0, 160, 255)
ORANGE = (255, 128, 0)
YELLOW = (255, 255, 0)
GREEN  = (0, 255, 0)
CYAN   = (0, 220, 255)
PURPLE = (128, 0, 128)
GRAY   = (128, 128, 128)
DARK   = (20, 20, 20)

BOX = pygame.Rect(38, 175, 565, 130)

SOUL_RED = "red"
SOUL_BLUE = "blue"

# ------------------------------------------------------------
#  战斗常数 —— 与原版一致
# ------------------------------------------------------------
DAMAGE_PER_TICK = 1      # 每次命中 tick 扣 1 HP
KR_PER_TICK     = 1      # 每次命中 tick 累积 1 KR
KR_CAP          = 20     # KR 上限
INV_FRAMES      = 2      # 无敌帧: 仅用于同帧去重, 不阻止连续接触伤害


def s(v):
    return int(v * SS)


def s_rect(r):
    return pygame.Rect(r.x * SS, r.y * SS, r.w * SS, r.h * SS)


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


# ============================================================
#  局部发光辅助 —— 性能关键, 绝不分配全屏 Surface
# ============================================================
def blit_glow_circle(surf, cx, cy, r, color, alpha, pad=None):
    if pad is None:
        pad = max(2, r // 3)
    size = (r + pad) * 2
    if size <= 0:
        return
    glow = pygame.Surface((size, size), pygame.SRCALPHA)
    pygame.draw.circle(glow, (*color, alpha), (size // 2, size // 2), r + pad)
    surf.blit(glow, (cx - size // 2, cy - size // 2))


def blit_glow_line(surf, x1, y1, x2, y2, width, color, alpha):
    pad = width
    minx = int(min(x1, x2)) - pad
    miny = int(min(y1, y2)) - pad
    maxx = int(max(x1, x2)) + pad
    maxy = int(max(y1, y2)) + pad
    w = maxx - minx
    h = maxy - miny
    if w <= 0 or h <= 0:
        return
    glow = pygame.Surface((w, h), pygame.SRCALPHA)
    pygame.draw.line(glow, (*color, alpha),
                     (x1 - minx, y1 - miny),
                     (x2 - minx, y2 - miny),
                     width)
    surf.blit(glow, (minx, miny))


def draw_heart(surf, cx, cy, scale, color):
    pts = []
    for i in range(96):
        t = i / 96.0 * math.tau
        x = 16 * math.sin(t) ** 3
        y = 13 * math.cos(t) - 5 * math.cos(2 * t) - 2 * math.cos(3 * t) - math.cos(4 * t)
        pts.append((s(cx + x * scale), s(cy - y * scale)))

    xs = [p[0] for p in pts]
    ys = [p[1] for p in pts]
    minx, maxx = min(xs), max(xs)
    miny, maxy = min(ys), max(ys)
    pad = 6 * SS
    w = maxx - minx + pad * 2
    h = maxy - miny + pad * 2
    if w > 0 and h > 0:
        glow = pygame.Surface((w, h), pygame.SRCALPHA)
        local_pts = [(p[0] - minx + pad, p[1] - miny + pad) for p in pts]
        pygame.draw.polygon(glow, (*color, 60), local_pts)
        surf.blit(glow, (minx - pad, miny - pad))

    pygame.draw.polygon(surf, color, pts)
    hi = (min(255, color[0] + 80), min(255, color[1] + 80), min(255, color[2] + 80))
    inner = [(p[0] - 5 * SS, p[1] - 5 * SS) for p in pts]
    pygame.draw.polygon(surf, hi, inner)

    hl_x = s(cx - 3 * scale)
    hl_y = s(cy - 4 * scale)
    hl_r = max(1, s(scale * 3))
    pygame.draw.circle(surf, WHITE, (hl_x, hl_y), hl_r)


def circle_rect_hit(cx, cy, r, rect):
    nx = max(rect.left, min(cx, rect.right))
    ny = max(rect.top, min(cy, rect.bottom))
    return (cx - nx) ** 2 + (cy - ny) ** 2 < r * r


# ============================================================
#  灵魂
# ============================================================
class Soul:
    def __init__(self, x, y):
        self.x = float(x)
        self.y = float(y)
        self.r = 7
        self.hp = 99
        self.max_hp = 99
        self.inv = 0
        self.kr = 0.0
        self.mode = SOUL_RED

        self.grav_dir = (0, 1)
        self.grav_speed = 0.48
        self.vy = 0.0
        self.vx = 0.0
        self.on_ground = True
        self.jump_held = 0
        self.jump_power = 8.5
        self.max_jump_hold = 22

        self.platforms = []

    def set_blue_mode(self, grav_dir=(0, 1)):
        if self.mode == SOUL_BLUE and self.grav_dir == grav_dir:
            return
        self.mode = SOUL_BLUE
        self.grav_dir = grav_dir
        self.vy = 0.0
        self.vx = 0.0
        self.on_ground = False
        self.jump_held = 0

    def set_red_mode(self):
        if self.mode == SOUL_RED:
            return
        self.mode = SOUL_RED
        self.vy = 0.0
        self.vx = 0.0
        self.on_ground = True
        self.jump_held = 0

    def update_kr(self):
        if self.inv > 0:
            self.inv -= 1
        if self.kr > 0:
            kr_int = int(self.kr)
            if kr_int >= 40:   rate = 1
            elif kr_int >= 30: rate = 2
            elif kr_int >= 20: rate = 5
            elif kr_int >= 10: rate = 15
            else:              rate = 30
            if random.random() < 1.0 / rate:
                self.hp -= 1
                self.kr = max(0.0, self.kr - 1.0)
                if self.hp < 0:
                    self.hp = 0

    def update(self, keys):
        self.update_kr()
        if self.mode == SOUL_BLUE:
            self._update_blue(keys)
        else:
            self._update_red(keys)

    def _update_blue(self, keys):
        gdx, gdy = self.grav_dir

        move_perp = 0
        if gdx != 0:
            if keys[pygame.K_UP] or keys[pygame.K_w]: move_perp -= 1
            if keys[pygame.K_DOWN] or keys[pygame.K_s]: move_perp += 1
        else:
            if keys[pygame.K_LEFT] or keys[pygame.K_a]: move_perp -= 1
            if keys[pygame.K_RIGHT] or keys[pygame.K_d]: move_perp += 1

        jump_input = False
        if gdx != 0:
            if gdx > 0 and (keys[pygame.K_LEFT] or keys[pygame.K_a]): jump_input = True
            elif gdx < 0 and (keys[pygame.K_RIGHT] or keys[pygame.K_d]): jump_input = True
        if gdy != 0:
            if gdy > 0 and (keys[pygame.K_UP] or keys[pygame.K_w]): jump_input = True
            elif gdy < 0 and (keys[pygame.K_DOWN] or keys[pygame.K_s]): jump_input = True

        if jump_input:
            if self.on_ground:
                base = self.jump_power * 0.65
                if gdy > 0: self.vy = -base
                elif gdy < 0: self.vy = base
                if gdx > 0: self.vx = -base
                elif gdx < 0: self.vx = base
                self.on_ground = False
                self.jump_held = 1
            elif self.jump_held < self.max_jump_hold:
                self.jump_held += 1
                boost = self.jump_power * 0.06
                if gdy > 0: self.vy -= boost
                elif gdy < 0: self.vy += boost
                if gdx > 0: self.vx -= boost
                elif gdx < 0: self.vx += boost
        else:
            self.jump_held = 0

        if gdx != 0: self.vx += gdx * self.grav_speed
        if gdy != 0: self.vy += gdy * self.grav_speed

        if gdx != 0:
            self.x += self.vx
            self.y += self.vy + move_perp * 2.5
        else:
            self.x += self.vx + move_perp * 2.5
            self.y += self.vy

        r = self.r

        if gdx > 0 and self.x + r >= BOX.right:
            self.x = BOX.right - r; self.vx = 0
        elif gdx < 0 and self.x - r <= BOX.left:
            self.x = BOX.left + r; self.vx = 0
        if gdy > 0 and self.y + r >= BOX.bottom:
            self.y = BOX.bottom - r; self.vy = 0
        elif gdy < 0 and self.y - r <= BOX.top:
            self.y = BOX.top + r; self.vy = 0

        if gdy > 0 and self.vy > 0:
            for p in self.platforms:
                if p.left <= self.x <= p.right:
                    prev_bottom = self.y + r - self.vy
                    if prev_bottom <= p.top and self.y + r >= p.top:
                        self.y = p.top - r
                        self.vy = 0
                        break

        self.x = clamp(self.x, BOX.left + r, BOX.right - r)
        self.y = clamp(self.y, BOX.top + r, BOX.bottom - r)

        on_ground = False
        if gdy > 0 and self.y + r >= BOX.bottom - 0.5: on_ground = True
        if gdy < 0 and self.y - r <= BOX.top + 0.5: on_ground = True
        if gdx > 0 and self.x + r >= BOX.right - 0.5: on_ground = True
        if gdx < 0 and self.x - r <= BOX.left + 0.5: on_ground = True
        if not on_ground and gdy > 0:
            for p in self.platforms:
                if p.left <= self.x <= p.right and abs((self.y + r) - p.top) < 1.0:
                    on_ground = True
                    break
        self.on_ground = on_ground
        if not on_ground:
            self.jump_held = 0 if not jump_input else self.jump_held

    def _update_red(self, keys):
        dx = dy = 0.0
        if keys[pygame.K_LEFT] or keys[pygame.K_a]: dx -= 1
        if keys[pygame.K_RIGHT] or keys[pygame.K_d]: dx += 1
        if keys[pygame.K_UP] or keys[pygame.K_w]: dy -= 1
        if keys[pygame.K_DOWN] or keys[pygame.K_s]: dy += 1
        if dx and dy:
            k = 1.0 / math.sqrt(2); dx *= k; dy *= k
        self.x += dx * 3.2
        self.y += dy * 3.2
        r = self.r
        self.x = clamp(self.x, BOX.left + r, BOX.right - r)
        self.y = clamp(self.y, BOX.top + r, BOX.bottom - r)

    def draw(self, surf):
        # 原版闪烁节奏: 每 2 帧切换一次可见性
        if self.inv > 0 and (self.inv % 2 == 0):
            return
        color = BLUE if self.mode == SOUL_BLUE else RED
        draw_heart(surf, self.x, self.y, 0.6, color)


# ============================================================
#  子弹基类
# ============================================================
class Bullet:
    def __init__(self, x, y, vx=0.0, vy=0.0, w=16, h=16,
                 kind="rect", color=WHITE, safe=False, **kw):
        self.x = float(x); self.y = float(y)
        self.vx = float(vx); self.vy = float(vy)
        self.w = w; self.h = h
        self.kind = kind; self.color = color
        self.opts = kw
        self.alive = True; self.t = 0
        self.angle = kw.get("angle", 0.0)
        # 所有非 safe 子弹统一每帧 1 点伤害
        self.safe = safe
        self.hit_this_frame = False

    def get_rect(self):
        return pygame.Rect(int(self.x - self.w / 2), int(self.y - self.h / 2),
                           int(self.w), int(self.h))

    def update(self, soul):
        self.t += 1
        self.hit_this_frame = False

        if self.kind == "homing":
            speed = self.opts.get("speed", 3.0)
            turn = self.opts.get("turn", 0.025)
            tgt = math.atan2(soul.y - self.y, soul.x - self.x)
            cur = math.atan2(self.vy, self.vx)
            d = (tgt - cur + math.pi) % math.tau - math.pi
            d = max(-turn, min(turn, d)); cur += d
            self.vx = math.cos(cur) * speed; self.vy = math.sin(cur) * speed
            self.x += self.vx; self.y += self.vy
        elif self.kind == "orbit":
            self.angle += self.opts.get("av", 0.02)
            r = self.opts.get("orbit_r", 100)
            cx = self.opts.get("cx", 0); cy = self.opts.get("cy", 0)
            self.x = cx + math.cos(self.angle) * r
            self.y = cy + math.sin(self.angle) * r
        else:
            self.x += self.vx; self.y += self.vy

        m = 300
        if self.x < -m or self.x > W + m or self.y < -m or self.y > H + m:
            self.alive = False
        if self.t > 2500:
            self.alive = False

        if (not self.safe) and self._hits(soul):
            self.hit_this_frame = True

    def _hits(self, soul):
        if self.kind in ("circle", "homing", "orbit"):
            r = self.w / 2
            return (soul.x - self.x) ** 2 + (soul.y - self.y) ** 2 < (r + soul.r) ** 2
        r = self.get_rect()
        return circle_rect_hit(soul.x, soul.y, soul.r, r)

    def draw(self, surf):
        if self.kind in ("circle", "homing", "orbit"):
            cx, cy = s(self.x), s(self.y)
            r = s(self.w / 2)
            blit_glow_circle(surf, cx, cy, r + 3 * SS, self.color, 50)
            pygame.draw.circle(surf, self.color, (cx, cy), r)
            if r > 4 * SS:
                pygame.draw.circle(surf, WHITE, (cx - r // 3, cy - r // 3), max(SS, r // 3))
                pygame.draw.circle(surf, self.color, (cx, cy), r, max(SS, 2 * SS))
        else:
            r = s_rect(self.get_rect())
            rad = min(r.w, r.h) // 2
            if self.color == LBLUE:
                tmp = pygame.Surface((r.w, r.h), pygame.SRCALPHA)
                pygame.draw.rect(tmp, (*LBLUE, 170), (0, 0, r.w, r.h), border_radius=rad)
                pygame.draw.rect(tmp, (200, 240, 255, 100),
                                 (4 * SS, 4 * SS, r.w - 8 * SS, max(2, r.h // 4)),
                                 border_radius=2)
                surf.blit(tmp, r.topleft)
            else:
                pygame.draw.rect(surf, self.color, r, border_radius=rad)
                pygame.draw.rect(surf, (255, 255, 255),
                                 (r.x, r.y, r.w, 2 * SS), border_radius=rad)
                shadow = pygame.Surface((r.w, 3 * SS), pygame.SRCALPHA)
                shadow.fill((0, 0, 0, 60))
                surf.blit(shadow, (r.x, r.bottom - 3 * SS))
                pygame.draw.rect(surf, (230, 230, 230),
                                 (r.x, r.y + 2 * SS, 2 * SS, r.h - 4 * SS),
                                 border_radius=rad)


# ============================================================
#  龙骨炮
# ============================================================
class GasterBlaster:
    def __init__(self, x, y, angle, delay=30, active=18):
        self.x = float(x); self.y = float(y)
        self.angle = angle
        self.delay = delay
        self.active = active
        self.t = 0
        self.alive = True
        self.hit_this_frame = False
        self.beam_length = 400
        self.beam_width = 26
        self.head_scale = 1.0
        self.head_rot = 0.0

    def update(self, soul):
        self.t += 1
        self.hit_this_frame = False

        if self.t < 10:
            self.head_scale = self.t / 10.0
            self.head_rot = (1.0 - self.t / 10.0) * 0.8
            return

        if self.t < self.delay:
            return

        if self.t <= self.delay + self.active:
            if self._beam_hits(soul):
                self.hit_this_frame = True
            return

        if self.t > self.delay + self.active + 10:
            self.alive = False

    def _beam_hits(self, soul):
        ex = self.x + math.cos(self.angle) * self.beam_length
        ey = self.y + math.sin(self.angle) * self.beam_length
        dx, dy = ex - self.x, ey - self.y
        length = math.hypot(dx, dy)
        if length == 0: return False
        ux, uy = dx / length, dy / length
        proj = (soul.x - self.x) * ux + (soul.y - self.y) * uy
        proj = max(0, min(proj, length))
        cx = self.x + ux * proj
        cy = self.y + uy * proj
        return math.hypot(soul.x - cx, soul.y - cy) < (self.beam_width / 2 + soul.r)

    def draw(self, surf):
        cx, cy = s(self.x), s(self.y)
        head_r = int(16 * SS * self.head_scale)

        if self.t < self.delay:
            pulse = int(abs(math.sin(self.t * 0.15)) * 40 + 60)
            blit_glow_circle(surf, cx, cy, head_r, (pulse, pulse, pulse), 60)
            pygame.draw.circle(surf, (pulse, pulse, pulse), (cx, cy), head_r)
            pygame.draw.circle(surf, (200, 200, 200), (cx, cy), head_r, 2 * SS)
            ring_r = head_r + int(abs(math.sin(self.t * 0.2)) * 8 * SS)
            pygame.draw.circle(surf, (150, 150, 150), (cx, cy), ring_r, 1 * SS)

        elif self.t <= self.delay + self.active:
            ex = cx + int(math.cos(self.angle) * self.beam_length * SS)
            ey = cy + int(math.sin(self.angle) * self.beam_length * SS)
            w = int(self.beam_width * SS)

            blit_glow_line(surf, cx, cy, ex, ey, w + 12 * SS, (0, 255, 255), 40)
            blit_glow_line(surf, cx, cy, ex, ey, w + 5 * SS, (0, 200, 255), 80)

            pygame.draw.line(surf, (0, 220, 255), (cx, cy), (ex, ey), w)
            pygame.draw.line(surf, (255, 255, 255), (cx, cy), (ex, ey), max(2, w // 3))

            pygame.draw.circle(surf, (0, 180, 220), (cx, cy), int(18 * SS))
            pygame.draw.circle(surf, (0, 255, 255), (cx, cy), int(16 * SS))
            pygame.draw.circle(surf, (255, 255, 255), (cx - 4 * SS, cy - 4 * SS), int(6 * SS))
            eye_x = cx - int(math.cos(self.angle + 0.5) * 6 * SS)
            eye_y = cy - int(math.sin(self.angle + 0.5) * 6 * SS)
            pygame.draw.circle(surf, (0, 100, 180), (eye_x, eye_y), int(3 * SS))
            pygame.draw.circle(surf, WHITE, (eye_x - SS, eye_y - SS), int(1.5 * SS))


# ============================================================
#  攻击模式基类
# ============================================================
class Pattern:
    def __init__(self, dur):
        self.dur = dur; self.t = 0; self.done = False

    def update(self, bullets, soul):
        self.t += 1
        if self.t >= self.dur: self.done = True
        return not self.done


# ============================================================
#  攻击1: 最强攻击
# ============================================================
class StrongestAttack(Pattern):
    def __init__(self):
        super().__init__(420)
        self.seg = 0
        self.seg_t = 0
        self.timer = 0
        self.blue_set = False

    def update(self, bullets, soul):
        self.t += 1; self.seg_t += 1

        if self.seg == 0:
            if not self.blue_set:
                soul.set_blue_mode((0, 1))
                soul.x = BOX.centerx
                soul.y = BOX.top + 20
                self.blue_set = True

            if self.seg_t == 15:
                soul.vy = 14.0

            if self.seg_t > 90:
                self.seg = 1; self.seg_t = 0
                soul.set_red_mode()
                soul.x = BOX.centerx
                soul.y = BOX.centery

        elif self.seg == 1:
            self.timer += 1
            if self.timer % 12 == 0:
                gap_y = random.uniform(BOX.top + 40, BOX.bottom - 40)
                gap_h = 50
                if BOX.top < gap_y - gap_h / 2:
                    bullets.append(Bullet(BOX.left - 25, (BOX.top + gap_y - gap_h / 2) / 2,
                                          7.0, 0, 18, gap_y - gap_h / 2 - BOX.top,
                                          "rect", WHITE))
                if BOX.bottom > gap_y + gap_h / 2:
                    bullets.append(Bullet(BOX.left - 25, (gap_y + gap_h / 2 + BOX.bottom) / 2,
                                          7.0, 0, 18, BOX.bottom - gap_y - gap_h / 2,
                                          "rect", WHITE))
            if self.seg_t > 130:
                self.seg = 2; self.seg_t = 0; self.timer = 0

        elif self.seg == 2:
            self.timer += 1
            if self.seg_t == 5:
                bullets.append(GasterBlaster(BOX.left - 35, BOX.centery, 0, delay=25, active=18))
                bullets.append(GasterBlaster(BOX.right + 35, BOX.centery, math.pi, delay=25, active=18))
            if self.seg_t == 80:
                for cx, cy, ang in (
                    (BOX.left - 25, BOX.top - 25,
                     math.atan2(BOX.centery - (BOX.top - 25), BOX.centerx - (BOX.left - 25))),
                    (BOX.right + 25, BOX.top - 25,
                     math.atan2(BOX.centery - (BOX.top - 25), BOX.centerx - (BOX.right + 25))),
                    (BOX.left - 25, BOX.bottom + 25,
                     math.atan2(BOX.centery - (BOX.bottom + 25), BOX.centerx - (BOX.left - 25))),
                    (BOX.right + 25, BOX.bottom + 25,
                     math.atan2(BOX.centery - (BOX.bottom + 25), BOX.centerx - (BOX.right + 25))),
                ):
                    bullets.append(GasterBlaster(cx, cy, ang, delay=25, active=18))
            if self.seg_t == 155:
                bullets.append(GasterBlaster(BOX.left - 35, BOX.centery, 0, delay=25, active=18))
                bullets.append(GasterBlaster(BOX.right + 35, BOX.centery, math.pi, delay=25, active=18))
            if self.seg_t == 230:
                bullets.append(GasterBlaster(BOX.left - 40, BOX.centery, 0, delay=28, active=22))
                bullets.append(GasterBlaster(BOX.right + 40, BOX.centery, math.pi, delay=28, active=22))
            if self.seg_t > 310:
                self.done = True

        return not self.done


# ============================================================
#  攻击2: 骨头线 — 蓝心常驻
# ============================================================
class BoneLines(Pattern):
    def __init__(self, lines=16, hard=False):
        super().__init__(lines * 32 + 80)
        self.lines = lines; self.hard = hard
        self.spawned = 0; self.timer = 0

    def update(self, bullets, soul):
        self.t += 1; self.timer += 1
        if self.t == 1:
            soul.set_blue_mode((0, 1))
            soul.x = BOX.centerx
            soul.y = BOX.centery
        interval = 24 if self.hard else 32
        if self.timer % interval == 0 and self.spawned < self.lines:
            self.spawned += 1
            gap_y = random.uniform(BOX.top + 40, BOX.bottom - 40)
            gap_h = 55 if not self.hard else 48
            side = random.choice(["left", "right"])
            sx = BOX.left - 25 if side == "left" else BOX.right + 25
            vx = 6.5 if side == "left" else -6.5
            if BOX.top < gap_y - gap_h / 2:
                bullets.append(Bullet(sx, (BOX.top + gap_y - gap_h / 2) / 2, vx, 0, 16,
                                      gap_y - gap_h / 2 - BOX.top, "rect", WHITE))
            if BOX.bottom > gap_y + gap_h / 2:
                bullets.append(Bullet(sx, (gap_y + gap_h / 2 + BOX.bottom) / 2, vx, 0, 16,
                                      BOX.bottom - gap_y - gap_h / 2, "rect", WHITE))
        if self.t > self.dur: self.done = True
        return not self.done


# ============================================================
#  攻击3: 白骨头小跳
# ============================================================
class SmallBones(Pattern):
    def __init__(self):
        super().__init__(400)
        self.timer = 0; self.spawned = 0

    def update(self, bullets, soul):
        self.t += 1; self.timer += 1
        if self.t == 1:
            soul.set_blue_mode((0, 1))
            soul.x = BOX.centerx
            soul.y = BOX.centery
        if self.timer % 22 == 0 and self.spawned < 12:
            self.spawned += 1
            side = random.choice(["left", "right"])
            sx = BOX.left - 25 if side == "left" else BOX.right + 25
            vx = 5.5 if side == "left" else -5.5
            y = BOX.bottom - 25
            bullets.append(Bullet(sx, y, vx, 0, 16, 40, "rect", WHITE))
        if self.t > self.dur: self.done = True
        return not self.done


# ============================================================
#  攻击4: 骨头间隙跳跃
# ============================================================
class BoneGaps(Pattern):
    def __init__(self):
        super().__init__(450)
        self.timer = 0; self.spawned = 0

    def update(self, bullets, soul):
        self.t += 1; self.timer += 1
        if self.t == 1:
            soul.set_blue_mode((0, 1))
            soul.x = BOX.centerx
            soul.y = BOX.centery
        if self.timer % 28 == 0 and self.spawned < 10:
            self.spawned += 1
            gap_y = random.uniform(BOX.top + 35, BOX.bottom - 35)
            gap_h = 55
            side = random.choice(["left", "right"])
            sx = BOX.left - 25 if side == "left" else BOX.right + 25
            vx = 6.0 if side == "left" else -6.0
            if BOX.top < gap_y - gap_h / 2:
                bullets.append(Bullet(sx, (BOX.top + gap_y - gap_h / 2) / 2, vx, 0, 16,
                                      gap_y - gap_h / 2 - BOX.top, "rect", WHITE))
            if BOX.bottom > gap_y + gap_h / 2:
                bullets.append(Bullet(sx, (gap_y + gap_h / 2 + BOX.bottom) / 2, vx, 0, 16,
                                      BOX.bottom - gap_y - gap_h / 2, "rect", WHITE))
        if self.t > self.dur: self.done = True
        return not self.done


# ============================================================
#  攻击5: 平台跳跃
# ============================================================
class Platforms(Pattern):
    def __init__(self):
        super().__init__(550)
        self.platforms = []
        self.timer = 0; self.spawned = 0
        px = BOX.left + 60
        py = BOX.bottom - 35
        for i in range(3):
            self.platforms.append(pygame.Rect(px, py, 50, 8))
            px += 100; py -= 35

    def update(self, bullets, soul):
        self.t += 1; self.timer += 1
        if self.t == 1:
            soul.set_blue_mode((0, 1))
            soul.x = BOX.centerx
            soul.y = BOX.centery
        if self.timer % 16 == 0:
            self.spawned += 1
            x = random.uniform(BOX.left + 10, BOX.right - 10)
            h = random.uniform(25, 70)
            bullets.append(Bullet(x, BOX.bottom - h / 2, 0, 0, 14, h, "rect", WHITE))
        if self.t > self.dur: self.done = True
        return not self.done

    def draw_platforms(self, surf):
        for p in self.platforms:
            r = s_rect(p)
            pygame.draw.rect(surf, GREEN, r, border_radius=2)
            pygame.draw.rect(surf, (150, 255, 150), (r.x, r.y, r.w, 2 * SS), border_radius=2)


# ============================================================
#  攻击6-7: 蓝白骨头
#  蓝骨 = 原版"移动时安全"的机制 → 这里近似为 safe=True
# ============================================================
class BlueWhiteBones(Pattern):
    def __init__(self, total=12):
        super().__init__(total * 40 + 60)
        self.total = total; self.timer = 0; self.spawned = 0

    def update(self, bullets, soul):
        self.t += 1; self.timer += 1
        if self.t == 1:
            soul.set_blue_mode((0, 1))
            soul.x = BOX.centerx
            soul.y = BOX.centery
        if self.timer % 40 == 0 and self.spawned < self.total:
            self.spawned += 1
            is_blue = self.spawned % 2 == 1
            col = LBLUE if is_blue else WHITE
            h = 35 if is_blue else 50
            side = "left" if self.spawned % 4 in (1, 2) else "right"
            sx = BOX.left - 25 if side == "left" else BOX.right + 25
            vx = 5.5 if side == "left" else -5.5
            y = BOX.bottom - h / 2 - 5
            bullets.append(Bullet(sx, y, vx, 0, 14, h, "rect", col, safe=is_blue))
        if self.t > self.dur: self.done = True
        return not self.done


# ============================================================
#  攻击8-9: 随机骨头线
# ============================================================
class RandomBones(Pattern):
    def __init__(self):
        super().__init__(480)
        self.timer = 0; self.spawned = 0

    def update(self, bullets, soul):
        self.t += 1; self.timer += 1
        if self.t == 1:
            soul.set_blue_mode((0, 1))
            soul.x = BOX.centerx
            soul.y = BOX.centery
        if self.timer % random.randint(18, 40) == 0 and self.spawned < 14:
            self.spawned += 1
            speed = random.uniform(5.0, 8.5)
            gap_y = random.uniform(BOX.top + 35, BOX.bottom - 35)
            gap_h = random.uniform(40, 70)
            side = random.choice(["left", "right"])
            sx = BOX.left - 25 if side == "left" else BOX.right + 25
            vx = speed if side == "left" else -speed
            if BOX.top < gap_y - gap_h / 2:
                bullets.append(Bullet(sx, (BOX.top + gap_y - gap_h / 2) / 2, vx, 0, 14,
                                      gap_y - gap_h / 2 - BOX.top, "rect", WHITE))
            if BOX.bottom > gap_y + gap_h / 2:
                bullets.append(Bullet(sx, (gap_y + gap_h / 2 + BOX.bottom) / 2, vx, 0, 14,
                                      BOX.bottom - gap_y - gap_h / 2, "rect", WHITE))
        if self.t > self.dur: self.done = True
        return not self.done


# ============================================================
#  攻击10-11: 龙骨炮阵列
# ============================================================
class GasterArray(Pattern):
    def __init__(self):
        super().__init__(380)
        self.timer = 0; self.wave = 0

    def update(self, bullets, soul):
        self.t += 1; self.timer += 1
        if self.t == 1:
            soul.set_blue_mode((0, 1))
            soul.x = BOX.centerx
            soul.y = BOX.centery
        if self.wave == 0 and self.timer > 25:
            self.wave = 1
            for cx, cy, ang in (
                (BOX.left - 25, BOX.top - 25,
                 math.atan2(BOX.centery - (BOX.top - 25), BOX.centerx - (BOX.left - 25))),
                (BOX.right + 25, BOX.top - 25,
                 math.atan2(BOX.centery - (BOX.top - 25), BOX.centerx - (BOX.right + 25))),
                (BOX.left - 25, BOX.bottom + 25,
                 math.atan2(BOX.centery - (BOX.bottom + 25), BOX.centerx - (BOX.left - 25))),
                (BOX.right + 25, BOX.bottom + 25,
                 math.atan2(BOX.centery - (BOX.bottom + 25), BOX.centerx - (BOX.right + 25))),
            ):
                bullets.append(GasterBlaster(cx, cy, ang, delay=22, active=18))
        if self.wave == 1 and self.timer > 110:
            self.wave = 2
            bullets.append(GasterBlaster(BOX.left - 30, BOX.centery, 0, delay=22, active=18))
            bullets.append(GasterBlaster(BOX.right + 30, BOX.centery, math.pi, delay=22, active=18))
        if self.wave == 2 and self.timer > 200:
            self.wave = 3
            bullets.append(GasterBlaster(BOX.centerx, BOX.top - 30, math.pi / 2, delay=22, active=18))
            bullets.append(GasterBlaster(BOX.centerx, BOX.bottom + 30, -math.pi / 2, delay=22, active=18))
        if self.timer > 310: self.done = True
        return not self.done


# ============================================================
#  攻击12-13: 垂直骨头线
# ============================================================
class VerticalBones(Pattern):
    def __init__(self, force_red=False):
        super().__init__(420)
        self.timer = 0; self.spawned = 0
        self.force_red = force_red
        self.switched = False

    def update(self, bullets, soul):
        self.t += 1; self.timer += 1

        if self.t == 1:
            soul.set_blue_mode((0, 1))
            soul.x = BOX.centerx
            soul.y = BOX.centery

        if self.force_red and not self.switched and self.timer > 200:
            soul.set_red_mode()
            soul.x = BOX.centerx
            soul.y = BOX.centery
            self.switched = True

        if self.timer % 26 == 0 and self.spawned < 10:
            self.spawned += 1
            gap_x = random.uniform(BOX.left + 45, BOX.right - 45)
            gap_w = 55
            side = random.choice(["top", "bottom"])
            sy = BOX.top - 25 if side == "top" else BOX.bottom + 25
            vy = 6.0 if side == "top" else -6.0
            if BOX.left < gap_x - gap_w / 2:
                bullets.append(Bullet((BOX.left + gap_x - gap_w / 2) / 2, sy, 0, vy,
                                      gap_x - gap_w / 2 - BOX.left, 16, "rect", WHITE))
            if BOX.right > gap_x + gap_w / 2:
                bullets.append(Bullet((gap_x + gap_w / 2 + BOX.right) / 2, sy, 0, vy,
                                      BOX.right - gap_x - gap_w / 2, 16, "rect", WHITE))
        if self.t > self.dur: self.done = True
        return not self.done


# ============================================================
#  攻击14-21: 混沌混合
# ============================================================
class ChaosMix(Pattern):
    def __init__(self):
        super().__init__(650)
        self.timer = 0
        self.mode_switch_timer = 0

    def update(self, bullets, soul):
        self.t += 1; self.timer += 1
        self.mode_switch_timer += 1

        if self.t == 1:
            soul.set_red_mode()
            soul.x = BOX.centerx
            soul.y = BOX.centery

        if self.mode_switch_timer >= 90:
            self.mode_switch_timer = 0
            if random.random() < 0.5:
                soul.set_blue_mode((0, 1) if random.random() < 0.5 else (0, -1))
            else:
                soul.set_red_mode()

        if self.timer % 20 == 0:
            r = random.random()
            if r < 0.25:
                gap_y = random.uniform(BOX.top + 40, BOX.bottom - 40)
                gap_h = random.uniform(40, 65)
                side = random.choice(["left", "right"])
                sx = BOX.left - 25 if side == "left" else BOX.right + 25
                vx = 6.5 if side == "left" else -6.5
                if BOX.top < gap_y - gap_h / 2:
                    bullets.append(Bullet(sx, (BOX.top + gap_y - gap_h / 2) / 2, vx, 0, 14,
                                          gap_y - gap_h / 2 - BOX.top, "rect", WHITE))
                if BOX.bottom > gap_y + gap_h / 2:
                    bullets.append(Bullet(sx, (gap_y + gap_h / 2 + BOX.bottom) / 2, vx, 0, 14,
                                          BOX.bottom - gap_y - gap_h / 2, "rect", WHITE))
            elif r < 0.50:
                side = random.randrange(4)
                if side == 0: x, y = random.uniform(BOX.left, BOX.right), BOX.top - 30
                elif side == 1: x, y = BOX.right + 30, random.uniform(BOX.top, BOX.bottom)
                elif side == 2: x, y = random.uniform(BOX.left, BOX.right), BOX.bottom + 30
                else: x, y = BOX.left - 30, random.uniform(BOX.top, BOX.bottom)
                ang = math.atan2(soul.y - y, soul.x - x)
                sp = random.uniform(2.8, 3.8)
                bullets.append(Bullet(x, y, math.cos(ang) * sp, math.sin(ang) * sp,
                                      12, 12, "homing", CYAN, speed=sp, turn=0.024))
            elif r < 0.75:
                if random.random() < 0.5:
                    bullets.append(GasterBlaster(BOX.left - 28, BOX.centery, 0, delay=20, active=15))
                    bullets.append(GasterBlaster(BOX.right + 28, BOX.centery, math.pi, delay=20, active=15))
                else:
                    bullets.append(GasterBlaster(BOX.centerx, BOX.top - 28, math.pi / 2, delay=20, active=15))
                    bullets.append(GasterBlaster(BOX.centerx, BOX.bottom + 28, -math.pi / 2, delay=20, active=15))
            else:
                for _ in range(3):
                    x = random.uniform(BOX.left + 10, BOX.right - 10)
                    bullets.append(Bullet(x, BOX.top - 18, 0, random.uniform(5.5, 8.0),
                                          10, 10, "circle", YELLOW))

        if self.t > self.dur: self.done = True
        return not self.done


# ============================================================
#  攻击22: 最终攻击
# ============================================================
class FinalAttack(Pattern):
    def __init__(self):
        super().__init__(2400)
        self.timer = 0; self.phase = 0; self.phase_t = 0

    def update(self, bullets, soul):
        self.t += 1; self.timer += 1; self.phase_t += 1

        if self.t == 1:
            soul.set_red_mode()
            soul.x = BOX.centerx
            soul.y = BOX.centery

        if self.phase == 0:
            if self.timer % 12 == 0:
                gap_y = random.uniform(BOX.top + 40, BOX.bottom - 40)
                gap_h = random.uniform(40, 65)
                side = random.choice(["left", "right"])
                sx = BOX.left - 25 if side == "left" else BOX.right + 25
                vx = 7.0 if side == "left" else -7.0
                if BOX.top < gap_y - gap_h / 2:
                    bullets.append(Bullet(sx, (BOX.top + gap_y - gap_h / 2) / 2, vx, 0, 14,
                                          gap_y - gap_h / 2 - BOX.top, "rect", WHITE))
                if BOX.bottom > gap_y + gap_h / 2:
                    bullets.append(Bullet(sx, (gap_y + gap_h / 2 + BOX.bottom) / 2, vx, 0, 14,
                                          BOX.bottom - gap_y - gap_h / 2, "rect", WHITE))
            if self.phase_t > 600: self.phase = 1; self.phase_t = 0

        elif self.phase == 1:
            if self.timer % 15 == 0:
                r = random.random()
                if r < 0.5:
                    bullets.append(GasterBlaster(BOX.left - 28, BOX.centery, 0, delay=18, active=14))
                    bullets.append(GasterBlaster(BOX.right + 28, BOX.centery, math.pi, delay=18, active=14))
                else:
                    for _ in range(2):
                        side = random.randrange(4)
                        if side == 0: x, y = random.uniform(BOX.left, BOX.right), BOX.top - 30
                        elif side == 1: x, y = BOX.right + 30, random.uniform(BOX.top, BOX.bottom)
                        elif side == 2: x, y = random.uniform(BOX.left, BOX.right), BOX.bottom + 30
                        else: x, y = BOX.left - 30, random.uniform(BOX.top, BOX.bottom)
                        ang = math.atan2(soul.y - y, soul.x - x)
                        sp = random.uniform(3.0, 4.0)
                        bullets.append(Bullet(x, y, math.cos(ang) * sp, math.sin(ang) * sp,
                                              11, 11, "homing", CYAN, speed=sp, turn=0.022))
            if self.phase_t > 600: self.phase = 2; self.phase_t = 0

        elif self.phase == 2:
            if self.timer % 20 == 0:
                cx, cy = BOX.centerx, BOX.centery
                for rr in range(30, 200, 15):
                    base = random.random() * math.tau
                    bullets.append(Bullet(0, 0, 0, 0, 10, 10, "orbit", WHITE,
                                          cx=cx, cy=cy, orbit_r=rr, av=0.022, angle=base))
            if self.timer % 40 == 0:
                bullets.append(GasterBlaster(BOX.left - 30, BOX.centery, 0, delay=20, active=16))
                bullets.append(GasterBlaster(BOX.right + 30, BOX.centery, math.pi, delay=20, active=16))
            if self.phase_t > 600: self.phase = 3; self.phase_t = 0

        elif self.phase == 3:
            if self.timer % 12 == 0:
                r = random.random()
                if r < 0.33:
                    gap_y = random.uniform(BOX.top + 35, BOX.bottom - 35)
                    gap_h = random.uniform(35, 60)
                    side = random.choice(["left", "right"])
                    sx = BOX.left - 25 if side == "left" else BOX.right + 25
                    vx = 7.5 if side == "left" else -7.5
                    if BOX.top < gap_y - gap_h / 2:
                        bullets.append(Bullet(sx, (BOX.top + gap_y - gap_h / 2) / 2, vx, 0, 13,
                                              gap_y - gap_h / 2 - BOX.top, "rect", WHITE))
                    if BOX.bottom > gap_y + gap_h / 2:
                        bullets.append(Bullet(sx, (gap_y + gap_h / 2 + BOX.bottom) / 2, vx, 0, 13,
                                              BOX.bottom - gap_y - gap_h / 2, "rect", WHITE))
                elif r < 0.66:
                    for _ in range(2):
                        side = random.randrange(4)
                        if side == 0: x, y = random.uniform(BOX.left, BOX.right), BOX.top - 30
                        elif side == 1: x, y = BOX.right + 30, random.uniform(BOX.top, BOX.bottom)
                        elif side == 2: x, y = random.uniform(BOX.left, BOX.right), BOX.bottom + 30
                        else: x, y = BOX.left - 30, random.uniform(BOX.top, BOX.bottom)
                        ang = math.atan2(soul.y - y, soul.x - x)
                        sp = random.uniform(3.2, 4.2)
                        bullets.append(Bullet(x, y, math.cos(ang) * sp, math.sin(ang) * sp,
                                              10, 10, "homing", CYAN, speed=sp, turn=0.020))
                else:
                    bullets.append(GasterBlaster(BOX.left - 28, BOX.centery, 0, delay=18, active=14))
                    bullets.append(GasterBlaster(BOX.right + 28, BOX.centery, math.pi, delay=18, active=14))
            if self.phase_t > 600: self.done = True

        return not self.done


# ============================================================
#  绘制辅助
# ============================================================
def draw_box(surf):
    pygame.draw.rect(surf, WHITE, s_rect(BOX), 4)


def draw_hp(surf, soul):
    x0 = s(BOX.left)
    y0 = s(BOX.bottom + 26)

    label = F_SM.render("HP", True, WHITE)
    surf.blit(label, (x0, y0 - 26 * SS))

    hp_int = int(soul.hp)
    kr_int = int(soul.kr)

    bar_w   = 3 * SS
    bar_gap = 5 * SS
    bar_h   = 14 * SS

    for i in range(min(hp_int, soul.max_hp)):
        pygame.draw.rect(surf, YELLOW, (x0 + i * bar_gap, y0, bar_w, bar_h))

    kr_start = max(0, hp_int - kr_int)
    for i in range(kr_start, min(hp_int, soul.max_hp)):
        pygame.draw.rect(surf, PURPLE, (x0 + i * bar_gap, y0, bar_w, bar_h))

    val = F_SM.render(f"{hp_int}/{soul.max_hp}", True, GRAY)
    val_x = x0 + soul.max_hp * bar_gap + 6 * SS
    surf.blit(val, (val_x, y0))


def draw_text_center(surf, text, font, color, y):
    t = font.render(text, True, color)
    surf.blit(t, (W * SS // 2 - t.get_width() // 2, s(y)))


OPENING = [
    ("it's a beautiful day outside.", 90),
    ("birds are singing, flowers are blooming...", 90),
    ("on days like these, kids like you...", 100),
    ("Should be burning in hell.", 120),
]


# ============================================================
#  主游戏
# ============================================================
def main():
    soul = Soul(BOX.centerx, BOX.centery)
    bullets = []
    platforms_draw = None

    state = "opening"
    state_t = 0
    line_i = 0
    turn = 0
    pattern = None
    shake = 0
    hurt_flash = 0
    screen_flicker = 0
    box_offset_x = 0
    sans_sleeping = False

    turn_patterns = [
        StrongestAttack,
        lambda: BoneLines(16),
        SmallBones,
        BoneGaps,
        Platforms,
        lambda: BlueWhiteBones(8),
        lambda: BlueWhiteBones(8),
        lambda: BoneLines(16, True),
        RandomBones,
        RandomBones,
        GasterArray,
        GasterArray,
        lambda: VerticalBones(force_red=True),
        lambda: VerticalBones(force_red=True),
        ChaosMix,
        ChaosMix,
        ChaosMix,
        ChaosMix,
        ChaosMix,
        ChaosMix,
        ChaosMix,
        FinalAttack,
    ]

    while True:
        clock.tick(FPS)
        keys = pygame.key.get_pressed()

        for e in pygame.event.get():
            if e.type == pygame.QUIT:
                pygame.quit(); return
            if e.type == pygame.KEYDOWN and e.key == pygame.K_ESCAPE:
                pygame.quit(); return

        canvas.fill(BLACK)
        state_t += 1

        ox = oy = 0
        if shake > 0:
            ox = random.randint(-shake, shake) * SS
            oy = random.randint(-shake, shake) * SS
            shake -= 1

        # ---- 状态机 ----
        if state == "opening":
            if line_i < len(OPENING):
                text, delay = OPENING[line_i]
                draw_text_center(canvas, text, F_MD, WHITE, H // 2 - 30)
                if state_t > delay:
                    line_i += 1; state_t = 0
            else:
                state = "battle"; state_t = 0
                turn = 0
                pattern = turn_patterns[0]()
                if isinstance(pattern, Platforms): platforms_draw = pattern

        elif state == "battle":
            if turn >= 11 and random.random() < 0.02:
                screen_flicker = 6

            soul.platforms = platforms_draw.platforms if isinstance(platforms_draw, Platforms) else []

            soul.update(keys)
            if isinstance(pattern, Platforms): platforms_draw = pattern

            if pattern is not None:
                alive = pattern.update(bullets, soul)
                if not alive:
                    bullets = []; pattern = None
                    turn += 1
                    state = "pause"; state_t = 0
                    platforms_draw = None

            for b in bullets:
                b.update(soul)
            bullets = [b for b in bullets if b.alive]

            # --------------------------------------------------
            #  伤害处理 —— 原版机制:
            #  1. 每次命中 tick 固定扣 1 HP
            #  2. 每次命中 tick 固定累积 1 KR
            #  3. 无敌帧 = 2, 仅用于阻止"同一帧被两颗子弹各扣一次"
            #     物理上连续接触时, 大约每 2 帧扣 1 点 → 与原版手感一致
            # --------------------------------------------------
            for b in bullets:
                if not b.hit_this_frame or soul.inv > 0:
                    continue
                # 蓝骨 safe 已在 update 里过滤, 这里一定是有害子弹
                soul.hp -= DAMAGE_PER_TICK
                soul.kr = min(soul.kr + KR_PER_TICK, KR_CAP)
                soul.inv = INV_FRAMES
                shake = max(shake, 6)
                hurt_flash = max(hurt_flash, 5)

            if soul.hp <= 0:
                state = "lose"; state_t = 0

        elif state == "pause":
            soul.platforms = []
            soul.update(keys)
            for b in bullets:
                b.update(soul)
            bullets = [b for b in bullets if b.alive]
            if state_t > 45:
                if turn >= len(turn_patterns):
                    state = "special"; state_t = 0
                    sans_sleeping = False
                    box_offset_x = 0
                else:
                    state = "battle"; state_t = 0
                    pattern = turn_patterns[turn]()
                    if isinstance(pattern, Platforms): platforms_draw = pattern

        elif state == "special":
            soul.platforms = []
            soul.update(keys)
            for b in bullets:
                b.update(soul)
            bullets = [b for b in bullets if b.alive]

            if state_t < 600:
                draw_text_center(canvas, "* Sans 正在拖延时间...", F_MD, WHITE, BOX.top - 30)
                if keys[pygame.K_LEFT]:
                    soul.x = BOX.centerx
                    soul.y = BOX.centery

            if state_t > 400:
                sans_sleeping = True

            if sans_sleeping:
                draw_text_center(canvas, "* Sans 睡着了。", F_MD, WHITE, BOX.top - 30)
                draw_text_center(canvas, "* 拖动弹幕面板到 [FIGHT] 按钮。", F_SM, GRAY, BOX.bottom + 50)
                if keys[pygame.K_RIGHT]: box_offset_x += 3.0
                elif keys[pygame.K_LEFT]: box_offset_x -= 3.0
                box_offset_x = clamp(box_offset_x, 0, 400)
                if box_offset_x > 350:
                    state = "kill"; state_t = 0

        elif state == "kill":
            if state_t < 60:
                draw_text_center(canvas, "* 你攻击了 Sans。", F_MD, WHITE, H // 2 - 20)
            elif state_t < 120:
                draw_text_center(canvas, "* Sans 倒下了。", F_MD, WHITE, H // 2 - 20)
            elif state_t < 300:
                draw_text_center(canvas, "* 他虚弱地笑了笑...", F_MD, GRAY, H // 2 - 20)
                draw_text_center(canvas, "* \"papyrus, do you want anything?\"", F_SM, GRAY, H // 2 + 20)
            else:
                state = "win"; state_t = 0

        elif state == "lose":
            if state_t > 150:
                pygame.quit(); return

        # ---- 渲染 ----
        if state in ("battle", "pause", "special"):
            draw_rect = BOX.move(box_offset_x, 0)
            pygame.draw.rect(canvas, WHITE, s_rect(draw_rect), 4)
            draw_hp(canvas, soul)

            if turn < 22:
                name = f"TURN {turn + 1}/22"
                t = F_SM.render(name, True, GRAY)
                canvas.blit(t, (s(BOX.left), s(BOX.top - 30)))

            if platforms_draw is not None:
                platforms_draw.draw_platforms(canvas)

            for b in bullets:
                b.draw(canvas)

            if not sans_sleeping or state_t < 400:
                soul.draw(canvas)

            mode_txt = F_MD.render(
                f"[{'BLUE' if soul.mode == SOUL_BLUE else 'RED'} MODE]",
                True, BLUE if soul.mode == SOUL_BLUE else RED)
            canvas.blit(mode_txt, (s(BOX.left), s(BOX.bottom + 54)))

        elif state == "opening":
            draw_box(canvas)

        elif state == "kill":
            draw_box(canvas)
            draw_hp(canvas, soul)

        elif state == "lose":
            draw_text_center(canvas, "GAME OVER", F_XL, RED, H // 2 - 60)
            draw_text_center(canvas, "* 你倒下了...", F_MD, WHITE, H // 2 + 20)
            draw_text_center(canvas, "按 ESC 退出", F_SM, GRAY, H // 2 + 80)

        elif state == "win":
            draw_text_center(canvas, "YOU WIN!", F_XL, YELLOW, H // 2 - 80)
            draw_text_center(canvas, "* 你击败了 Sans。", F_MD, WHITE, H // 2 - 10)
            draw_text_center(canvas, "* 屠杀路线完成。", F_MD, GRAY, H // 2 + 30)
            draw_text_center(canvas, "按 ESC 退出", F_SM, GRAY, H // 2 + 100)

        if screen_flicker > 0:
            screen_flicker -= 1
            if screen_flicker % 4 < 2:
                overlay = pygame.Surface((W * SS, H * SS), pygame.SRCALPHA)
                overlay.fill((255, 255, 255, 30))
                canvas.blit(overlay, (0, 0))

        if hurt_flash > 0:
            hurt_flash -= 1
            overlay = pygame.Surface((W * SS, H * SS), pygame.SRCALPHA)
            overlay.fill((255, 0, 0, min(90, hurt_flash * 12)))
            canvas.blit(overlay, (0, 0))

        screen.fill(BLACK)
        scaled = pygame.transform.smoothscale(canvas, (W, H))
        screen.blit(scaled, (ox, oy))
        pygame.display.flip()


if __name__ == "__main__":
    main()