import pygame
import math
import random
import sys

# ============================================================
#  UNDERTALE: Sans Judgment — 中文版 (KR-Only 模式)
#  子弹命中不直接扣血, 只累积 KR
#  KR 随后按照原版机制持续扣除 HP
#  操作: 方向键/WASD移动 | 蓝心模式: 反重力方向跳跃
#  主菜单: ↑↓ 选择 | ENTER 开始 | ESC 退出
#  战斗中: C 跳过剧情 | R 死后重来 | ESC 返回主菜单
# ============================================================

pygame.init()
pygame.display.set_caption("传说之下：Sans 审判 [KR 模式]")

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 _find_cjk_font_path():
    candidates = [
        "microsoftyahei", "msyh", "msyhbd", "simhei", "simsun", "dengxian",
        "pingfangsc", "pingfang", "stheiti", "hiraginosansgb",
        "notosanscjksc", "notosanscjk", "wenquanyimicrohei", "wqymicrohei",
        "wqyzenhei", "droidsansfallback", "arplumingcn",
    ]
    for name in candidates:
        try:
            path = pygame.font.match_font(name, bold=False)
            if path:
                return path
        except Exception:
            pass
    return None


_CJK_FONT_PATH = _find_cjk_font_path()


def make_font(size, bold=True):
    if _CJK_FONT_PATH:
        try:
            f = pygame.font.Font(_CJK_FONT_PATH, size)
            if bold:
                f.set_bold(True)
            return f
        except Exception:
            pass
    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)

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

SOUL_RED = "red"
SOUL_BLUE = "blue"

# ============================================================
#  难度配置
# ============================================================
DIFF_NORMAL = {
    "name": "normal",
    "label": "普通难度",
    "desc": "22 回合 · 标准弹幕 · 2 帧无敌",
    "spawn_scale": 1.0,
    "speed_scale": 1.0,
    "hard": False,
    "kr_cap": 20,
    "inv_frames": 2,
    "visual_hurt": 2,
    "extra_chance": 0.0,
    "jitter": 0.05,
}

DIFF_HARD = {
    "name": "hard",
    "label": "困难难度",
    "desc": "26 回合 · 加密弹幕 · 每帧命中",
    "spawn_scale": 0.62,
    "speed_scale": 1.12,
    "hard": True,
    "kr_cap": 35,
    "inv_frames": 0,
    "visual_hurt": 6,
    "extra_chance": 0.40,
    "jitter": 0.15,
}

DIFF = dict(DIFF_NORMAL)


def set_difficulty(name):
    if name == "hard":
        DIFF.update(DIFF_HARD)
    else:
        DIFF.update(DIFF_NORMAL)


def diff_interval(base):
    return max(1, int(base * DIFF["spawn_scale"]))


def diff_speed(base):
    sp = base * DIFF["speed_scale"]
    if DIFF["hard"]:
        sp *= random.uniform(1 - DIFF["jitter"], 1 + DIFF["jitter"])
    return sp


def extra_roll():
    return random.random() < DIFF["extra_chance"]


# ------------------------------------------------------------
#  战斗常数 —— KR-Only 版本
# ------------------------------------------------------------
DAMAGE_PER_TICK = 0       # 子弹命中不再直接扣 HP
KR_PER_TICK     = 1       # 子弹命中只累积 KR


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))


# ============================================================
#  发光辅助
# ============================================================
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


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)))


# ============================================================
#  灵魂
# ============================================================
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):
        # --------------------------------------------------
        #  只靠 KR 扣血。
        #  KR 越高, 触发频率越快。每次触发扣 1 HP 并消耗 1 KR。
        # --------------------------------------------------
        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):
        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.x0 = float(x); self.y0 = 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)
        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

        k = self.kind
        if k == "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 k == "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
        elif k == "wave":
            sp = self.opts.get("speed", 5.0)
            ba = self.opts.get("base_angle", 0.0)
            amp = self.opts.get("amp", 20.0)
            freq = self.opts.get("freq", 0.12)
            d = self.t * sp
            off = math.sin(self.t * freq) * amp
            ca, sa = math.cos(ba), math.sin(ba)
            self.x = self.x0 + ca * d - sa * off
            self.y = self.y0 + sa * d + ca * off
        elif k == "spiral":
            cx = self.opts.get("cx", 0.0); cy = self.opts.get("cy", 0.0)
            a0 = self.opts.get("angle0", 0.0)
            av = self.opts.get("av", 0.05)
            sp = self.opts.get("speed", 2.5)
            r = self.t * sp
            a = a0 + self.t * av
            self.x = cx + math.cos(a) * r
            self.y = cy + math.sin(a) * r
            if r > 420:
                self.alive = False
        elif k == "boomerang":
            out_t = self.opts.get("out_time", 60)
            sp = self.opts.get("speed", 6.5)
            if self.t < out_t:
                self.x += self.vx; self.y += self.vy
            else:
                ang = math.atan2(self.y0 - self.y, self.x0 - self.x)
                self.x += math.cos(ang) * sp
                self.y += math.sin(ang) * sp
        elif k == "bounce":
            self.x += self.vx; self.y += self.vy
            r = self.w / 2
            if self.x - r < BOX.left and self.vx < 0:
                self.x = BOX.left + r; self.vx = -self.vx
            elif self.x + r > BOX.right and self.vx > 0:
                self.x = BOX.right - r; self.vx = -self.vx
            if self.y - r < BOX.top and self.vy < 0:
                self.y = BOX.top + r; self.vy = -self.vy
            elif self.y + r > BOX.bottom and self.vy > 0:
                self.y = BOX.bottom - r; self.vy = -self.vy
        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 > self.opts.get("life", 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",
                         "spiral", "boomerang", "bounce"):
            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",
                         "spiral", "boomerang", "bounce"):
            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

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

        if self.t < 10:
            self.head_scale = self.t / 10.0
            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))


# ============================================================
#  Pattern 基类
# ============================================================
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


# ============================================================
#  攻击模式
# ============================================================
class StrongestAttack(Pattern):
    def __init__(self):
        super().__init__(460)
        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 % diff_interval(10) == 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,
                                          diff_speed(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,
                                          diff_speed(7.0), 0, 18,
                                          BOX.bottom - gap_y - gap_h / 2, "rect", WHITE))
                if DIFF["hard"] and extra_roll():
                    side = random.choice(["left", "right"])
                    sx = BOX.left - 25 if side == "left" else BOX.right + 25
                    vx = diff_speed(7.0) if side == "left" else -diff_speed(7.0)
                    gy2 = random.uniform(BOX.top + 40, BOX.bottom - 40)
                    if BOX.top < gy2 - gap_h / 2:
                        bullets.append(Bullet(sx, (BOX.top + gy2 - gap_h / 2) / 2, vx, 0, 18,
                                              gy2 - gap_h / 2 - BOX.top, "rect", WHITE))
                    if BOX.bottom > gy2 + gap_h / 2:
                        bullets.append(Bullet(sx, (gy2 + gap_h / 2 + BOX.bottom) / 2, vx, 0, 18,
                                              BOX.bottom - gy2 - gap_h / 2, "rect", WHITE))
            if self.seg_t > 140:
                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 == 160:
                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 DIFF["hard"] and self.seg_t == 220:
                bullets.append(GasterBlaster(BOX.centerx, BOX.top - 40, math.pi / 2, delay=25, active=20))
                bullets.append(GasterBlaster(BOX.centerx, BOX.bottom + 40, -math.pi / 2, delay=25, active=20))
            if self.seg_t > 310:
                self.done = True

        return not self.done


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
        self.next_t = 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
        base = 24 if self.hard else 32
        interval = diff_interval(base)
        if self.timer >= self.next_t and self.spawned < self.lines:
            self.spawned += 1
            self.next_t = self.timer + interval + random.randint(-2, 2)
            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 = diff_speed(6.5) if side == "left" else -diff_speed(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 extra_roll():
                side2 = "left" if side == "right" else "right"
                sx2 = BOX.left - 25 if side2 == "left" else BOX.right + 25
                vx2 = diff_speed(6.0) if side2 == "left" else -diff_speed(6.0)
                gy2 = random.uniform(BOX.top + 40, BOX.bottom - 40)
                if BOX.top < gy2 - gap_h / 2:
                    bullets.append(Bullet(sx2, (BOX.top + gy2 - gap_h / 2) / 2, vx2, 0, 16,
                                          gy2 - gap_h / 2 - BOX.top, "rect", WHITE))
                if BOX.bottom > gy2 + gap_h / 2:
                    bullets.append(Bullet(sx2, (gy2 + gap_h / 2 + BOX.bottom) / 2, vx2, 0, 16,
                                          BOX.bottom - gy2 - gap_h / 2, "rect", WHITE))
        if self.t > self.dur: self.done = True
        return not self.done


class SmallBones(Pattern):
    def __init__(self):
        super().__init__(440)
        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
        iv = diff_interval(18)
        if self.timer % iv == 0 and self.spawned < 18:
            self.spawned += 1
            side = random.choice(["left", "right"])
            sx = BOX.left - 25 if side == "left" else BOX.right + 25
            vx = diff_speed(6.0) if side == "left" else -diff_speed(6.0)
            y = BOX.bottom - 25
            bullets.append(Bullet(sx, y, vx, 0, 16, 40, "rect", WHITE))
            if extra_roll():
                bullets.append(Bullet(sx, BOX.bottom - 60, vx, 0, 16, 25, "rect", WHITE))
        if self.t > self.dur: self.done = True
        return not self.done


class BoneGaps(Pattern):
    def __init__(self):
        super().__init__(500)
        self.timer = 0; self.spawned = 0
        self.next_t = 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 = diff_interval(22)
        if self.timer >= self.next_t and self.spawned < 14:
            self.spawned += 1
            self.next_t = self.timer + interval + random.randint(-3, 3)
            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 = diff_speed(6.0) if side == "left" else -diff_speed(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


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
        iv = diff_interval(14)
        if self.timer % iv == 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 extra_roll():
                x2 = random.uniform(BOX.left + 10, BOX.right - 10)
                h2 = random.uniform(25, 60)
                bullets.append(Bullet(x2, BOX.bottom - h2 / 2, 0, 0, 14, h2, "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)


class BlueWhiteBones(Pattern):
    def __init__(self, total=8):
        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
        iv = diff_interval(32)
        if self.timer % iv == 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 = diff_speed(5.5) if side == "left" else -diff_speed(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


class RandomBones(Pattern):
    def __init__(self):
        super().__init__(480)
        self.timer = 0; self.spawned = 0
        self.next_t = 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 >= self.next_t and self.spawned < 14:
            self.spawned += 1
            self.next_t = self.timer + diff_interval(random.randint(16, 32))
            speed = diff_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


class SineBones(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
        iv = diff_interval(28)
        if self.timer % iv == 0 and self.spawned < 12:
            self.spawned += 1
            side = random.choice(["left", "right"])
            sx = BOX.left - 25 if side == "left" else BOX.right + 25
            ba = 0.0 if side == "left" else math.pi
            y = random.uniform(BOX.top + 30, BOX.bottom - 30)
            bullets.append(Bullet(sx, y, 0, 0, 14, 60, "wave", WHITE,
                                  speed=diff_speed(6.0), base_angle=ba,
                                  amp=20, freq=0.10, life=250))
        if self.t > self.dur: self.done = True
        return not self.done


class SpiralBullets(Pattern):
    def __init__(self):
        super().__init__(420)
        self.timer = 0
        self.arms = 4

    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.bottom - 25
        if self.timer > 20 and self.timer % diff_interval(4) == 0:
            a0 = self.timer * 0.075
            for i in range(self.arms):
                a = a0 + i * math.tau / self.arms
                bullets.append(Bullet(BOX.centerx, BOX.centery, 0, 0, 11, 11,
                                      "spiral", CYAN,
                                      cx=BOX.centerx, cy=BOX.centery,
                                      angle0=a, av=0.013, speed=diff_speed(2.4), life=220))
        if self.t > self.dur: self.done = True
        return not self.done


class BouncingBones(Pattern):
    def __init__(self):
        super().__init__(500)
        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
        iv = diff_interval(46)
        if self.timer % iv == 0 and self.spawned < 8:
            self.spawned += 1
            x = random.uniform(BOX.left + 45, BOX.right - 45)
            y = random.uniform(BOX.top + 45, BOX.bottom - 45)
            a = random.uniform(0, math.tau)
            sp = diff_speed(random.uniform(3.5, 5.0))
            bullets.append(Bullet(x, y, math.cos(a) * sp, math.sin(a) * sp,
                                  16, 16, "bounce", WHITE, life=280))
        if self.t > self.dur: self.done = True
        return not self.done


class HomingSwarm(Pattern):
    def __init__(self):
        super().__init__(500)
        self.timer = 0

    def update(self, bullets, soul):
        self.t += 1; self.timer += 1
        if self.t == 1:
            soul.set_red_mode()
            soul.x = BOX.centerx
            soul.y = BOX.centery
        iv = diff_interval(50)
        if self.timer % iv == 0 and self.timer <= 360:
            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 = diff_speed(random.uniform(2.4, 3.1))
                bullets.append(Bullet(x, y, math.cos(ang) * sp, math.sin(ang) * sp,
                                      12, 12, "homing", CYAN, speed=sp,
                                      turn=random.uniform(0.016, 0.026),
                                      life=240))
        if self.t > self.dur: self.done = True
        return not self.done


class RainAttack(Pattern):
    def __init__(self):
        super().__init__(460)
        self.timer = 0

    def update(self, bullets, soul):
        self.t += 1; self.timer += 1
        if self.t == 1:
            soul.set_red_mode()
            soul.x = BOX.centerx
            soul.y = BOX.centery
        iv = diff_interval(11)
        if self.timer % iv == 0:
            x = random.uniform(BOX.left + 12, BOX.right - 12)
            bullets.append(Bullet(x, BOX.top - 20, 0, diff_speed(random.uniform(4.5, 6.5)),
                                  11, 11, "circle", YELLOW, life=200))
        if self.timer % 90 == 0:
            for _ in range(4):
                x = random.uniform(BOX.left + 12, BOX.right - 12)
                bullets.append(Bullet(x, BOX.top - 20,
                                      random.uniform(-1.6, 1.6),
                                      diff_speed(random.uniform(5.0, 7.0)),
                                      10, 10, "circle", ORANGE, life=220))
        if self.t > self.dur: self.done = True
        return not self.done


class PortalStrike(Pattern):
    def __init__(self):
        super().__init__(460)
        self.timer = 0

    def update(self, bullets, soul):
        self.t += 1; self.timer += 1
        if self.t == 1:
            soul.set_red_mode()
            soul.x = BOX.centerx
            soul.y = BOX.centery
        iv = diff_interval(40)
        if self.timer % iv == 0:
            px = random.uniform(BOX.left + 40, BOX.right - 40)
            py = random.uniform(BOX.top + 40, BOX.bottom - 40)
            arms = random.choice([6, 7, 8])
            base = random.uniform(0, math.tau)
            for i in range(arms):
                a = base + i * math.tau / arms
                sp = diff_speed(random.uniform(3.2, 4.6))
                bullets.append(Bullet(px, py, math.cos(a) * sp, math.sin(a) * sp,
                                      12, 12, "circle", PURPLE, life=200))
        if self.t > self.dur: self.done = True
        return not self.done


class BoomerangBones(Pattern):
    def __init__(self):
        super().__init__(520)
        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
        iv = diff_interval(48)
        if self.timer % iv == 0 and self.spawned < 6:
            self.spawned += 1
            side = random.choice(["left", "right"])
            sx = BOX.left - 25 if side == "left" else BOX.right + 25
            vx = diff_speed(6.5) if side == "left" else -diff_speed(6.5)
            y = random.uniform(BOX.top + 30, BOX.bottom - 30)
            bullets.append(Bullet(sx, y, vx, 0, 14, 60, "boomerang", ORANGE,
                                  out_time=70, speed=diff_speed(7.0), life=400))
        if self.t > self.dur: self.done = True
        return not self.done


class Crossfire(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
        iv = diff_interval(22)
        if self.timer % iv == 0 and self.spawned < 14:
            self.spawned += 1
            gap_y = random.uniform(BOX.top + 45, BOX.bottom - 45)
            gap_h = 60
            side = random.choice(["left", "right"])
            sx = BOX.left - 25 if side == "left" else BOX.right + 25
            vx = diff_speed(7.0) if side == "left" else -diff_speed(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))
            gap_x = random.uniform(BOX.left + 45, BOX.right - 45)
            gap_w = 60
            side2 = random.choice(["top", "bottom"])
            sy = BOX.top - 25 if side2 == "top" else BOX.bottom + 25
            vy = diff_speed(6.5) if side2 == "top" else -diff_speed(6.5)
            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, 14, "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, 14, "rect", WHITE))
        if self.t > self.dur: self.done = True
        return not self.done


class Spiral(Pattern):
    def __init__(self):
        super().__init__(540)
        self.timer = 0
        self.angle = 0.0

    def update(self, bullets, soul):
        self.t += 1; self.timer += 1
        if self.t == 1:
            soul.set_red_mode()
            soul.x = BOX.left + 40
            soul.y = BOX.centery
        iv = diff_interval(4)
        if self.timer % iv == 0:
            self.angle += 0.42
            cx, cy = BOX.centerx, BOX.centery
            for i in range(3):
                a = self.angle + i * math.tau / 3
                sp = diff_speed(3.2)
                bullets.append(Bullet(cx, cy, math.cos(a) * sp, math.sin(a) * sp,
                                      10, 10, "circle", CYAN))
        if self.timer % 60 == 0:
            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 = diff_speed(random.uniform(3.0, 4.0))
                bullets.append(Bullet(x, y, math.cos(ang) * sp, math.sin(ang) * sp,
                                      10, 10, "homing", YELLOW, speed=sp, turn=0.024))
        if self.t > self.dur: self.done = True
        return not self.done


class TripleBlaster(Pattern):
    def __init__(self):
        super().__init__(420)
        self.timer = 0

    def update(self, bullets, soul):
        self.t += 1; self.timer += 1
        if self.t == 1:
            soul.set_red_mode()
            soul.x = BOX.centerx
            soul.y = BOX.centery
        if self.timer == 25:
            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))
            bullets.append(GasterBlaster(BOX.centerx, BOX.top - 30, math.pi / 2, delay=20, active=16))
        if self.timer == 110:
            bullets.append(GasterBlaster(BOX.centerx, BOX.bottom + 30, -math.pi / 2, delay=20, active=16))
            bullets.append(GasterBlaster(BOX.left - 30, BOX.top - 30,
                math.atan2(BOX.centery - (BOX.top - 30), BOX.centerx - (BOX.left - 30)),
                delay=20, active=16))
            bullets.append(GasterBlaster(BOX.right + 30, BOX.bottom + 30,
                math.atan2(BOX.centery - (BOX.bottom + 30), BOX.centerx - (BOX.right + 30)),
                delay=20, active=16))
        if self.timer == 200:
            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=20, active=16))
        if self.timer == 290:
            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.timer > 360: self.done = True
        return not self.done


class GasterArray(Pattern):
    def __init__(self):
        super().__init__(390)
        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 > 22:
            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 DIFF["hard"]:
                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.wave == 2 and self.timer > 210:
            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


class VerticalBones(Pattern):
    def __init__(self, force_red=False):
        super().__init__(440)
        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

        iv = diff_interval(22)
        if self.timer % iv == 0 and self.spawned < 14:
            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 = diff_speed(6.0) if side == "top" else -diff_speed(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


class BulletHell(Pattern):
    def __init__(self):
        super().__init__(560)
        self.timer = 0

    def update(self, bullets, soul):
        self.t += 1; self.timer += 1
        if self.t == 1:
            soul.set_red_mode()
            soul.x = BOX.centerx
            soul.y = BOX.centery
        if self.timer % diff_interval(4) == 0:
            for _ in range(2):
                x = random.uniform(BOX.left + 8, BOX.right - 8)
                bullets.append(Bullet(x, BOX.top - 18, 0, diff_speed(random.uniform(5.0, 7.5)),
                                      10, 10, "circle", YELLOW))
        if self.timer % diff_interval(35) == 0:
            for _ in range(3):
                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 = diff_speed(random.uniform(2.8, 3.8))
                bullets.append(Bullet(x, y, math.cos(ang) * sp, math.sin(ang) * sp,
                                      10, 10, "homing", CYAN, speed=sp, turn=0.024))
        if self.timer % 65 == 0:
            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.t > self.dur: self.done = True
        return not self.done


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

        switch_iv = 60 if DIFF["hard"] else 90
        if self.mode_switch_timer >= switch_iv:
            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()

        iv = diff_interval(15)
        if self.timer % iv == 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 = diff_speed(6.8) if side == "left" else -diff_speed(6.8)
                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 = diff_speed(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, diff_speed(random.uniform(5.5, 8.0)),
                                          10, 10, "circle", YELLOW))

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


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 % diff_interval(10) == 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 = diff_speed(7.0) if side == "left" else -diff_speed(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 % diff_interval(13) == 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 = diff_speed(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 % diff_interval(17) == 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 % diff_interval(35) == 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 % diff_interval(10) == 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 = diff_speed(7.5) if side == "left" else -diff_speed(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 = diff_speed(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 build_turn_patterns():
    if DIFF["hard"]:
        return [
            StrongestAttack,
            lambda: BoneLines(22),
            SmallBones,
            BoneGaps,
            Platforms,
            lambda: BlueWhiteBones(16),
            SineBones,
            SpiralBullets,
            BouncingBones,
            Crossfire,
            lambda: BoneLines(22, True),
            RandomBones,
            Spiral,
            TripleBlaster,
            HomingSwarm,
            RainAttack,
            PortalStrike,
            GasterArray,
            GasterArray,
            lambda: VerticalBones(force_red=True),
            lambda: VerticalBones(force_red=True),
            BoomerangBones,
            BulletHell,
            ChaosMix,
            ChaosMix,
            FinalAttack,
        ]
    else:
        return [
            StrongestAttack,
            lambda: BoneLines(16),
            SmallBones,
            BoneGaps,
            Platforms,
            lambda: BlueWhiteBones(8),
            SineBones,
            SpiralBullets,
            BouncingBones,
            lambda: BoneLines(16, True),
            RandomBones,
            HomingSwarm,
            RainAttack,
            PortalStrike,
            GasterArray,
            lambda: VerticalBones(force_red=True),
            BoomerangBones,
            RandomBones,
            ChaosMix,
            ChaosMix,
            ChaosMix,
            FinalAttack,
        ]


# ============================================================
#  绘制辅助
# ============================================================
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

    # 黄色: 现有 HP
    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 扣除的部分
    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))

    # HP 数值
    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))

    # KR 数值 (仅当 > 0 时显示)
    if kr_int > 0:
        kr_txt = F_SM.render(f"KR {kr_int}", True, PURPLE)
        kr_x = val_x + val.get_width() + 6 * SS
        surf.blit(kr_txt, (kr_x, y0))


# ============================================================
#  主菜单
# ============================================================
def draw_menu(canvas, idx):
    title1 = F_XL.render("传说之下", True, WHITE)
    title2 = F_LG.render("SANS 审判", True, RED)
    canvas.blit(title1, (W * SS // 2 - title1.get_width() // 2, s(45)))
    canvas.blit(title2, (W * SS // 2 - title2.get_width() // 2, s(125)))

    sub = F_SM.render("— 选择难度 —", True, GRAY)
    canvas.blit(sub, (W * SS // 2 - sub.get_width() // 2, s(200)))

    options = [
        ("普通难度", "22 回合 · 标准弹幕 · 只造成 KR"),
        ("困难难度", "26 回合 · 加密弹幕 · 每帧命中 · KR 上限 35"),
    ]
    for i, (name, desc) in enumerate(options):
        y = 250 + i * 68
        selected = (i == idx)
        col = YELLOW if selected else WHITE
        prefix = "▶ " if selected else "   "
        name_s = F_MD.render(prefix + name, True, col)
        canvas.blit(name_s, (W * SS // 2 - name_s.get_width() // 2, s(y)))
        desc_col = (220, 220, 100) if selected else GRAY
        desc_s = F_SM.render(desc, True, desc_col)
        canvas.blit(desc_s, (W * SS // 2 - desc_s.get_width() // 2, s(y + 36)))

    hint = F_SM.render("↑↓ 选择    ENTER 开始    ESC 退出", True, GRAY)
    canvas.blit(hint, (W * SS // 2 - hint.get_width() // 2, s(420)))

    mode = F_SM.render("【KR 模式】子弹不直接扣血, 只累积 KR", True, PURPLE)
    canvas.blit(mode, (W * SS // 2 - mode.get_width() // 2, s(450)))


# ============================================================
#  剧情文本
# ============================================================
OPENING = [
    ("外面的天气真不错。", 90),
    ("鸟儿在歌唱，花儿在绽放……", 90),
    ("在这样的日子里，像你这样的孩子……", 100),
    ("就该在地狱里焚烧。", 120),
]


# ============================================================
#  游戏状态
# ============================================================
class GameState:
    def __init__(self):
        self.reset_menu()

    def reset_menu(self):
        self.state = "menu"
        self.state_t = 0
        self.menu_index = 0
        self.difficulty = "normal"
        self.soul = Soul(BOX.centerx, BOX.centery)
        self.bullets = []
        self.platforms_draw = None
        self.pattern = None
        self.turn = 0
        self.turn_patterns = []
        self.total_turns = 0
        self.line_i = 0
        self.shake = 0
        self.hurt_flash = 0
        self.screen_flicker = 0
        self.box_offset_x = 0
        self.sans_sleeping = False

    def start_battle(self, difficulty):
        set_difficulty(difficulty)
        self.difficulty = difficulty
        self.soul = Soul(BOX.centerx, BOX.centery)
        self.bullets = []
        self.platforms_draw = None
        self.turn_patterns = build_turn_patterns()
        self.total_turns = len(self.turn_patterns)
        self.state = "opening"
        self.state_t = 0
        self.line_i = 0
        self.turn = 0
        self.pattern = None
        self.shake = 0
        self.hurt_flash = 0
        self.screen_flicker = 0
        self.box_offset_x = 0
        self.sans_sleeping = False

    def skip_opening(self):
        self.state = "battle"
        self.state_t = 0
        self.turn = 0
        self.pattern = self.turn_patterns[0]()
        if isinstance(self.pattern, Platforms):
            self.platforms_draw = self.pattern


# ============================================================
#  战斗渲染 (共用)
# ============================================================
def _draw_battle(canvas, gs):
    draw_rect = BOX.move(gs.box_offset_x, 0)
    pygame.draw.rect(canvas, WHITE, s_rect(draw_rect), 4)
    draw_hp(canvas, gs.soul)

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

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

    for b in gs.bullets:
        b.draw(canvas)

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

    mode_txt = F_SM.render(
        f"[{'蓝心' if gs.soul.mode == SOUL_BLUE else '红心'}模式]",
        True, BLUE if gs.soul.mode == SOUL_BLUE else RED)
    canvas.blit(mode_txt, (s(BOX.left), s(BOX.bottom + 54)))


# ============================================================
#  主循环
# ============================================================
def main():
    gs = GameState()

    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:
                if e.key == pygame.K_ESCAPE:
                    if gs.state == "menu":
                        pygame.quit(); return
                    else:
                        gs.reset_menu()
                elif gs.state == "menu":
                    if e.key == pygame.K_UP:
                        gs.menu_index = (gs.menu_index - 1) % 2
                    elif e.key == pygame.K_DOWN:
                        gs.menu_index = (gs.menu_index + 1) % 2
                    elif e.key in (pygame.K_RETURN, pygame.K_KP_ENTER):
                        gs.start_battle("hard" if gs.menu_index == 1 else "normal")
                elif gs.state == "opening":
                    if e.key == pygame.K_c:
                        gs.skip_opening()
                elif gs.state == "lose":
                    if e.key == pygame.K_r:
                        gs.start_battle(gs.difficulty)
                elif gs.state == "win":
                    if e.key == pygame.K_r:
                        gs.reset_menu()

        canvas.fill(BLACK)
        gs.state_t += 1

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

        # ==================================================
        #  UPDATE + 绘制
        # ==================================================
        if gs.state == "menu":
            draw_menu(canvas, gs.menu_index)

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

            draw_box(canvas)
            skip_hint = F_SM.render("按 C 跳过剧情", True, GRAY)
            canvas.blit(skip_hint, (W * SS // 2 - skip_hint.get_width() // 2, s(420)))

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

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

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

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

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

            # --------------------------------------------------
            #  KR-Only 伤害判定:
            #  子弹命中不直接扣 HP, 只累积 KR
            #  （DAMAGE_PER_TICK = 0 时, 下面那行 HP 减法实际无效果）
            # --------------------------------------------------
            can_be_hit = (DIFF["inv_frames"] == 0) or (gs.soul.inv <= 0)
            if can_be_hit:
                hit_any = False
                for b in gs.bullets:
                    if b.hit_this_frame:
                        hit_any = True
                        break
                if hit_any:
                    # 直接伤害 = 0, 只累积 KR
                    gs.soul.hp -= DAMAGE_PER_TICK
                    gs.soul.kr = min(gs.soul.kr + KR_PER_TICK, DIFF["kr_cap"])
                    gs.soul.inv = max(DIFF["inv_frames"], DIFF["visual_hurt"])
                    # 视觉反馈 (画面震动 + 红闪)
                    gs.shake = max(gs.shake, 5)
                    gs.hurt_flash = max(gs.hurt_flash, 4)

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

            _draw_battle(canvas, gs)

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

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

            if gs.state_t < 600:
                if keys[pygame.K_LEFT]:
                    gs.soul.x = BOX.centerx
                    gs.soul.y = BOX.centery

            if gs.state_t > 400:
                gs.sans_sleeping = True

            if gs.sans_sleeping:
                if keys[pygame.K_RIGHT]:
                    gs.box_offset_x += 3.0
                elif keys[pygame.K_LEFT]:
                    gs.box_offset_x -= 3.0
                gs.box_offset_x = clamp(gs.box_offset_x, 0, 400)
                if gs.box_offset_x > 350:
                    gs.state = "kill"
                    gs.state_t = 0

            _draw_battle(canvas, gs)

            if gs.state == "special":
                if gs.state_t < 600 and not gs.sans_sleeping:
                    draw_text_center(canvas, "* Sans 正在拖延时间……", F_MD, WHITE, BOX.top - 30)
                if gs.sans_sleeping:
                    draw_text_center(canvas, "* Sans 睡着了。", F_MD, WHITE, BOX.top - 30)
                    draw_text_center(canvas, "* 按住 → 把弹幕面板推到 [攻击] 按钮。",
                                     F_SM, GRAY, BOX.bottom + 80)

        elif gs.state == "kill":
            draw_box(canvas)
            draw_hp(canvas, gs.soul)
            if gs.state_t < 60:
                draw_text_center(canvas, "* 你攻击了 Sans。", F_MD, WHITE, H // 2 - 20)
            elif gs.state_t < 120:
                draw_text_center(canvas, "* Sans 倒下了。", F_MD, WHITE, H // 2 - 20)
            elif gs.state_t < 300:
                draw_text_center(canvas, "* 他虚弱地笑了笑……", F_MD, GRAY, H // 2 - 30)
                draw_text_center(canvas, "*「papyrus，你想要点什么吗？」", F_SM, GRAY, H // 2 + 20)
            else:
                gs.state = "win"
                gs.state_t = 0

        elif gs.state == "lose":
            draw_text_center(canvas, "游戏结束", F_XL, RED, H // 2 - 80)
            draw_text_center(canvas, "* 你倒下了……", F_MD, WHITE, H // 2 - 5)
            draw_text_center(canvas, f"难度: {DIFF['label']}", F_SM, GRAY, H // 2 + 45)
            draw_text_center(canvas, "R 重来    ESC 返回主菜单", F_SM, GRAY, H // 2 + 100)

        elif gs.state == "win":
            draw_text_center(canvas, "你 赢 了！", F_XL, YELLOW, H // 2 - 100)
            draw_text_center(canvas, "* 你击败了 Sans。", F_MD, WHITE, H // 2 - 30)
            draw_text_center(canvas, f"难度: {DIFF['label']}", F_SM, GRAY, H // 2 + 20)
            draw_text_center(canvas, "R 返回主菜单    ESC 退出", F_SM, GRAY, H // 2 + 90)

        # ---- 全屏效果 ----
        if gs.screen_flicker > 0:
            gs.screen_flicker -= 1
            if gs.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 gs.hurt_flash > 0:
            gs.hurt_flash -= 1
            overlay = pygame.Surface((W * SS, H * SS), pygame.SRCALPHA)
            overlay.fill((255, 0, 0, min(90, gs.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()