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"


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 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)))
    glow_surf = pygame.Surface((W*SS, H*SS), pygame.SRCALPHA)
    pygame.draw.polygon(glow_surf, (*color, 60), [(p[0], p[1]) for p in pts])
    surf.blit(glow_surf, (0, 0))
    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)
    pygame.draw.circle(surf, WHITE, (s(cx - 3*scale*SS), s(cy - 4*scale*SS)), max(1, s(scale*3)))

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 clamp(v, lo, hi):
    return max(lo, min(v, hi))


# ============================================================
#  灵魂
# ============================================================
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

    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

        # 沿重力垂直方向移动 (gdx==0 时水平, gdy==0 时垂直)
        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

        # 位移
        self.x += self.vx + move_perp * 2.5
        self.y += self.vy

        # 边界碰撞
        r = self.r
        landed = False
        if gdx > 0 and self.x + r >= BOX.right:
            self.x = BOX.right - r; self.vx = 0; landed = True
        elif gdx < 0 and self.x - r <= BOX.left:
            self.x = BOX.left + r; self.vx = 0; landed = True
        if gdy > 0 and self.y + r >= BOX.bottom:
            self.y = BOX.bottom - r; self.vy = 0; landed = True
        elif gdy < 0 and self.y - r <= BOX.top:
            self.y = BOX.top + r; self.vy = 0; landed = True

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

        if landed:
            self.on_ground = True
            self.jump_held = 0

    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) % 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, **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)
        self.lethal = kw.get("lethal", True)
        self.damage = kw.get("damage", 1)
        self.kr_add = kw.get("kr_add", 1.0)
        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 self.lethal 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)
            glow = pygame.Surface((W*SS, H*SS), pygame.SRCALPHA)
            pygame.draw.circle(glow, (*self.color, 50), (cx, cy), r + 3*SS)
            surf.blit(glow, (0, 0))
            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, 200), (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.damage_per_frame = 1
        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)
            glow = pygame.Surface((W*SS, H*SS), pygame.SRCALPHA)
            pygame.draw.circle(glow, (pulse, pulse, pulse, 60), (cx, cy), head_r + 6*SS)
            surf.blit(glow, (0, 0))
            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)

            glow = pygame.Surface((W*SS, H*SS), pygame.SRCALPHA)
            pygame.draw.line(glow, (0, 255, 255, 40), (cx, cy), (ex, ey), w + 12*SS)
            surf.blit(glow, (0, 0))

            glow2 = pygame.Surface((W*SS, H*SS), pygame.SRCALPHA)
            pygame.draw.line(glow2, (0, 200, 255, 80), (cx, cy), (ex, ey), w + 5*SS)
            surf.blit(glow2, (0, 0))

            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,
                                          kr_add=1.0))
                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,
                                          kr_add=1.0))
            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, kr_add=1.0))
            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, kr_add=1.0))
        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, kr_add=1.0))
        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, kr_add=1.0))
            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, kr_add=1.0))
        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, kr_add=1.0))
        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: 蓝白骨头 — 蓝心常驻
# ============================================================
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,
                                  lethal=not is_blue, damage=0 if is_blue else 1,
                                  kr_add=0 if is_blue else 1.0))
        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, kr_add=1.0))
            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, kr_add=1.0))
        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, kr_add=1.0))
            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, kr_add=1.0))
        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, kr_add=1.0))
                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, kr_add=1.0))
            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, kr_add=5.0))
            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, kr_add=1.0))

        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, kr_add=1.0))
                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, kr_add=1.0))
            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, kr_add=5.0))
            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, kr_add=1.0))
            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, kr_add=1.0))
                    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, kr_add=1.0))
                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, kr_add=5.0))
                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, y0 = s(BOX.left), s(BOX.bottom + 30)
    label = F_MD.render("HP", True, WHITE)
    surf.blit(label, (s(BOX.left - 42), y0))

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

    for i in range(min(hp_int, soul.max_hp)):
        pygame.draw.rect(surf, YELLOW, (x0 + i*8*SS, y0, 6*SS, 16*SS))

    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*8*SS, y0, 6*SS, 16*SS))

    val = F_MD.render(f"{hp_int}/{soul.max_hp}", True, GRAY)
    surf.blit(val, (x0 + soul.max_hp*8*SS + 12*SS, y0))

    if kr_int > 0:
        kr_txt = F_MD.render("KR", True, PURPLE)
        surf.blit(kr_txt, (s(BOX.left - 42), y0 + 20*SS))

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
    mercy_offered = False
    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)
            oy = random.randint(-shake, shake)
            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 = 3

            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
                    if turn == 13 and not mercy_offered:
                        mercy_offered = True

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

            for b in bullets:
                if isinstance(b, GasterBlaster):
                    if b.hit_this_frame:
                        soul.hp -= b.damage_per_frame
                        if b.t == b.delay + 1:
                            soul.kr = min(soul.kr + 10, 20)
                        else:
                            soul.kr = min(soul.kr + 1, 20)
                        shake = 6
                        hurt_flash = 4
                elif b.hit_this_frame:
                    soul.hp -= b.damage
                    soul.kr = min(soul.kr + b.kr_add, 20)
                    shake = 8
                    hurt_flash = 6

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

        elif state == "pause":
            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.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

        elif state == "win":
            pass

        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:
                if isinstance(b, GasterBlaster):
                    b.draw(canvas)
                else:
                    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 % 2 == 0:
                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()