'use client';

import { useEffect, useRef, useState } from 'react';
import { cn } from '@/lib/utils';
import { emitPetEvent, onPetEvent } from '@/lib/pet-events';
import { usePetBrain } from './pet-brain';
import { usePetLocomotion } from './pet-locomotion';
import { usePetVoice } from './use-pet-voice';
import { PetBody, type PetBodyPose } from './pet-body';

const PET_X_KEY = 'pac-pet-x'; // 只存 x:y 永远由物理决定(地板/栖息点)
const SIZE = 64;

/**
 * PetFab — 助手宠物(Q 版磨牙),桌宠化悬浮钮。
 *
 * 大脑(usePetBrain)管表情,运动层(usePetLocomotion)管位置:
 * 拖起松手有重力,空闲沿底边散步,可跳上 data-pet-perch 元素蹲着,
 * AI 生成中会走到助手窗边守着。点击(非拖)= 开/关助手窗。
 */
export function PetFab({
  open,
  onToggle,
  watchRect,
}: {
  open: boolean;
  onToggle: (fabRect: DOMRect) => void;
  /** 助手窗矩形(开着时),AI 思考中宠物走过去蹲守 */
  watchRect: { x: number; y: number; w: number; h: number } | null;
}) {
  const { pose, bubble, glanceSeq } = usePetBrain();
  const btnRef = useRef<HTMLButtonElement>(null);

  const poseRef = useRef(pose);
  poseRef.current = pose;
  const watchRef = useRef(watchRect);
  watchRef.current = watchRect;

  // ── 刷牙刷字:坐上卡片一会儿 → 掏牙刷把脚下标题区"刷出泡沫",刷完闪✨ ──
  const [brush, setBrush] = useState<{
    rect: { x: number; y: number; w: number; h: number };
    phase: 'scrub' | 'shine';
  } | null>(null);
  const [rinsing, setRinsing] = useState(false); // 刷完 → 漱口吐泡泡
  const [shield, setShield] = useState(false); // 漱完 → 涂氟护盾 buff(12s 青色光罩)
  // ── 蛀牙菌狩猎:刷怪 → 追捕(菌会逃)→ 牙刷敲打 → 爆星 → 庆祝 ──
  const [germAlive, setGermAlive] = useState(false);
  const [whacking, setWhacking] = useState(false); // 敲打中(复用牙刷姿态)
  const [burst, setBurst] = useState<{ x: number; top: number } | null>(null); // 消灭爆星(x + 离地高度)
  const germEl = useRef<HTMLSpanElement>(null);
  // perch = 蛀虫骑在哪个边框上(null = 在地板底边爬)
  const germ = useRef<{
    x: number;
    dir: 1 | -1;
    t0: number;
    state: 'crawl' | 'flee' | 'dying';
    perch: Element | null;
    superman: boolean; // 隐藏小概率:宠物披红披风直线飞过去秒掉它
  } | null>(null);
  const [superPhase, setSuperPhase] = useState<'off' | 'transform' | 'fly'>('off'); // 超人:变身 → 飞
  const superPhaseRef = useRef(superPhase);
  superPhaseRef.current = superPhase;
  const brushingRef = useRef(false);
  brushingRef.current = brush?.phase === 'scrub' || rinsing || whacking;
  const brushCooldownUntil = useRef(0);

  const { motion, facing, landSeq, anchored, rope, api } = usePetLocomotion(btnRef, {
    size: SIZE,
    // 庆祝原地演完;睡着了不梦游;刷字刷到一半不许走
    getBusy: () =>
      poseRef.current === 'celebrate' || poseRef.current === 'sleep' || brushingRef.current,
    // AI 思考中且助手窗开着 → 走到窗左下角守着(放不下就去右边)
    getWatchX: () => {
      const r = watchRef.current;
      if (!r || poseRef.current !== 'think') return null;
      const left = r.x - SIZE - 6;
      return left >= 8 ? left : r.x + r.w + 6;
    },
    persist: (x) => {
      try {
        localStorage.setItem(PET_X_KEY, String(Math.round(x)));
      } catch {
        /* 私密模式等 */
      }
    },
  });

  // 初始登场:恢复上次 x(默认右下),从半空轻轻落下
  useEffect(() => {
    let x = window.innerWidth - SIZE - 16;
    try {
      const raw = localStorage.getItem(PET_X_KEY);
      if (raw) x = Number(raw) || x;
    } catch {
      /* ignore */
    }
    api.place(x, window.innerHeight - SIZE - 120);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // 刷牙调度:坐稳(栖在卡片上)8-18s 后开刷;离开栖息点立即收场
  const startBrush = () => {
    if (Date.now() < brushCooldownUntil.current) return;
    const r = btnRef.current?.getBoundingClientRect();
    if (!r) return;
    brushCooldownUntil.current = Date.now() + 30_000;
    setBrush({
      rect: { x: r.left - 30, y: r.bottom - 4, w: r.width + 60, h: 22 },
      phase: 'scrub',
    });
  };
  const startBrushRef = useRef(startBrush);
  startBrushRef.current = startBrush;
  const sittingNow = anchored && motion === 'rest';
  useEffect(() => {
    if (!sittingNow) {
      setBrush(null);
      return;
    }
    const t = setTimeout(() => startBrushRef.current(), 8_000 + Math.random() * 10_000);
    return () => clearTimeout(t);
  }, [sittingNow]);
  // 刷 3s → 闪亮 0.9s → 漱口 2.2s → 收
  useEffect(() => {
    if (!brush) return;
    const t =
      brush.phase === 'scrub'
        ? setTimeout(() => setBrush((b) => (b ? { ...b, phase: 'shine' } : null)), 3_000)
        : setTimeout(() => {
            setBrush(null);
            setRinsing(true);
          }, 900);
    return () => clearTimeout(t);
  }, [brush]);
  useEffect(() => {
    if (!rinsing) return;
    const t = setTimeout(() => {
      setRinsing(false);
      setShield(true); // 刷牙仪式完成 → 获得涂氟护盾
    }, 2_200);
    return () => clearTimeout(t);
  }, [rinsing]);
  useEffect(() => {
    if (!shield) return;
    const t = setTimeout(() => setShield(false), 12_000);
    return () => clearTimeout(t);
  }, [shield]);
  // dev 后门(pet-lab 一键触发):坐着时立即开刷
  const sittingRef = useRef(sittingNow);
  sittingRef.current = sittingNow;
  useEffect(() => {
    const h = (e: Event) => {
      if ((e as CustomEvent<{ cmd?: string }>).detail?.cmd !== 'brush') return;
      if (sittingRef.current) {
        brushCooldownUntil.current = 0;
        startBrushRef.current();
      }
    };
    window.addEventListener('pac-pet-debug', h);
    return () => window.removeEventListener('pac-pet-debug', h);
  }, []);

  // ── 蛀牙菌:自然刷怪(低频)+ lab 一键 ──
  const motionRef = useRef(motion);
  motionRef.current = motion;
  const anchoredRef = useRef(anchored); // 追捕需知道宠物是否在地板(决定走/跳/飞)
  anchoredRef.current = anchored;

  // ── 牙医组合派生链:检查 → (洗牙/补牙/终止) → (抛光/涂氟/终止) → 收尾;可被交互/蛀虫打断 ──
  const vetTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
  const vetRunning = useRef(false);
  const vetAbort = () => {
    if (vetTimer.current) clearTimeout(vetTimer.current);
    vetTimer.current = null;
    if (vetRunning.current) {
      vetRunning.current = false;
      emitPetEvent({ type: 'director', script: { gesture: 'none' } }); // 丢工具、收额镜、回基线
    }
  };
  const vetAbortRef = useRef(vetAbort);
  vetAbortRef.current = vetAbort;

  // ── LLM 开口说话:页面激活 + 低概率 + 长冷却,观察周围环境说一句(流式进气泡)──
  usePetVoice(btnRef, () => poseRef.current === 'idle' && motionRef.current === 'rest');

  // 牙医组合运行器:大脑 idle 轮盘抽中 vet_combo(或 lab 按钮)→ 跑诊疗派生链
  useEffect(() => {
    // 可继续的前提:在地板/边框静止待命、没在追蛀虫、没在变超人
    const ok = () => motionRef.current === 'rest' && !germ.current && superPhaseRef.current === 'off';
    // 无气泡(诊疗组合只演动作 + 道具,不说话)
    const step = (gesture: string, dur: number, next: () => void) => {
      emitPetEvent({ type: 'director', script: { gesture: gesture as never, ttlMs: dur + 500 } });
      vetTimer.current = setTimeout(() => {
        if (!ok()) {
          vetAbortRef.current();
          return;
        }
        next();
      }, dur);
    };
    const wrap = () => step('shine', 1_300, () => (vetRunning.current = false)); // 摘镜 + 闪亮收尾
    const stage3 = () => {
      const r = Math.random();
      if (r < 0.4) step('vet_polish', 1_800, wrap);
      else if (r < 0.7) {
        setShield(true); // 涂氟 → 获得护盾 buff
        step('vet_fluoride', 1_800, wrap);
      } else wrap(); // 第三动作终止
    };
    const stage2 = () => {
      const r = Math.random();
      if (r < 0.35) step('vet_scale', 2_000, stage3);
      else if (r < 0.65) step('vet_fill', 2_000, stage3);
      else wrap(); // 第二动作终止
    };
    const startVet = () => {
      if (vetRunning.current || !ok() || poseRef.current !== 'idle') return;
      vetRunning.current = true;
      step('vet_exam', 1_600, stage2);
    };
    const unsub = onPetEvent((e) => {
      if (e.type === 'vet_combo') startVet();
    });
    const onDebug = (e: Event) => {
      if ((e as CustomEvent<{ cmd?: string }>).detail?.cmd === 'vet') startVet();
    };
    window.addEventListener('pac-pet-debug', onDebug);
    return () => {
      unsub();
      window.removeEventListener('pac-pet-debug', onDebug);
      vetAbortRef.current();
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
  const spawnGerm = (forceSuper = false) => {
    if (germ.current) return; // 允许宠物坐着/走着时也刷(它会下来/跳上去追)
    vetAbortRef.current(); // 蛀虫出现 → 中断诊疗组合,优先去追
    // 60% 概率骑在某个可见边框上,否则在地板底边
    let perch: Element | null = null;
    if (Math.random() < 0.6) {
      const cands: Element[] = [];
      document.querySelectorAll('[data-pet-perch], [class*="border"]').forEach((p) => {
        const r = p.getBoundingClientRect();
        if (r.width < 60 || r.height < 6) return;
        if (r.left < 0 || r.right > window.innerWidth) return;
        if (r.top < 50 || r.top > window.innerHeight - 90) return; // 太高(跳不上)/太低(≈地板)不选
        cands.push(p);
      });
      perch = cands[Math.floor(Math.random() * cands.length)] ?? null;
    }
    // 算出蛀虫落点(x + 离地高度 yTop),再按"与宠物距离"决定超人概率(越远越可能)
    let gx: number;
    let gyTop: number;
    if (perch) {
      const r = perch.getBoundingClientRect();
      gx = r.left + 6 + Math.random() * Math.max(r.width - 28, 1);
      gyTop = r.top - 22;
    } else {
      const r = btnRef.current?.getBoundingClientRect();
      const baseX = r ? r.left : window.innerWidth / 2;
      const side = Math.random() < 0.5 ? -1 : 1;
      gx = Math.min(Math.max(baseX + side * (200 + Math.random() * 160), 8), window.innerWidth - 30);
      gyTop = window.innerHeight - 22 - 2;
    }
    const pr = btnRef.current?.getBoundingClientRect();
    const dist = pr ? Math.hypot(gx + 11 - (pr.left + SIZE / 2), gyTop + 11 - (pr.top + SIZE / 2)) : 0;
    const superman = forceSuper || Math.random() < Math.min(0.32, 0.04 + dist / 2200); // 距离越远越可能变超人
    germ.current = {
      x: gx,
      dir: Math.random() < 0.5 ? -1 : 1,
      t0: performance.now(),
      state: 'crawl',
      perch,
      superman,
    };
    setGermAlive(true);
  };
  const spawnRef = useRef(spawnGerm);
  spawnRef.current = spawnGerm;
  useEffect(() => {
    const iv = setInterval(() => {
      if (!document.hidden && Math.random() < 0.5) spawnRef.current();
    }, 90_000);
    const h = (e: Event) => {
      const cmd = (e as CustomEvent<{ cmd?: string }>).detail?.cmd;
      if (cmd === 'germ') spawnRef.current(false);
      else if (cmd === 'germ-super') spawnRef.current(true);
    };
    window.addEventListener('pac-pet-debug', h);
    return () => {
      clearInterval(iv);
      window.removeEventListener('pac-pet-debug', h);
    };
  }, []);
  // 狩猎主循环:菌按"地板/边框"两种 lane 爬+逃 + 给运动层发追捕指令(floor 走 / perch 跳上去) + 2D 判定追上/溜走
  useEffect(() => {
    if (!germAlive) return;
    let raf = 0;
    let prev = performance.now();
    let lastCmd = 0;
    const GW = 22; // 菌的渲染宽度
    const tick = (now: number) => {
      raf = requestAnimationFrame(tick);
      const g = germ.current;
      if (!g || document.hidden) return;
      const dt = Math.min((now - prev) / 1000, 0.05);
      prev = now;

      // lane:菌骑的边框还在吗?算出本帧爬行区间 [loX, hiX] 与离地高度 yTop
      let loX = 4;
      let hiX = window.innerWidth - GW - 4;
      let yTop = window.innerHeight - GW - 2; // 地板
      if (g.perch) {
        const rp = (g.perch as HTMLElement).checkVisibility?.() === false ? null : g.perch.getBoundingClientRect();
        if (!rp || rp.width < 40 || !g.perch.isConnected) {
          g.perch = null; // 边框没了 → 掉回地板
        } else {
          loX = rp.left + 2;
          hiX = rp.right - GW - 2;
          yTop = rp.top - GW;
        }
      }

      if (g.state !== 'dying') {
        const pr = btnRef.current?.getBoundingClientRect();
        const petCx = pr ? pr.left + SIZE / 2 : -1e4;
        const petCy = pr ? pr.top + SIZE / 2 : -1e4;
        const gCx = g.x + GW / 2;
        const gCy = yTop + GW / 2;
        const dx = Math.abs(petCx - gCx);
        const dy = Math.abs(petCy - gCy);

        if (g.superman) {
          const TRANSFORM_MS = 950;
          const elapsed = now - g.t0;
          if (elapsed < TRANSFORM_MS) {
            // ① 变身:原地蓄力(用 fly 指令定在自身位置 → 悬停不动、不受重力),不发起攻击
            if (superPhase !== 'transform') setSuperPhase('transform');
            const pr2 = btnRef.current?.getBoundingClientRect();
            if (pr2) {
              window.dispatchEvent(
                new CustomEvent('pac-pet-debug', { detail: { cmd: 'fly', x: pr2.left, y: pr2.top } }),
              );
            }
            if (germEl.current) {
              germEl.current.style.left = `${g.x.toFixed(1)}px`;
              germEl.current.style.top = `${yTop.toFixed(1)}px`;
            }
            return; // 蛀虫此刻冻在原地(g.x 不更新),等变身完
          }
          // ② 飞行:不逃也没用 —— 直线飞过去,每帧锁定目标
          if (superPhase !== 'fly') setSuperPhase('fly');
          window.dispatchEvent(
            new CustomEvent('pac-pet-debug', { detail: { cmd: 'fly', x: gCx - SIZE / 2, y: gCy - SIZE / 2 } }),
          );
          if (dx < 46 && dy < 48) {
            // 撞上即秒杀(无牙刷,披风冲击)
            g.state = 'dying';
            setBurst({ x: g.x, top: yTop });
            germ.current = null;
            setGermAlive(false);
            setTimeout(() => {
              setSuperPhase('off');
              setBurst(null);
            }, 700);
            emitPetEvent({ type: 'director', script: { gesture: 'celebrate', ttlMs: 2_800 } });
          }
          if (germEl.current) {
            germEl.current.style.left = `${g.x.toFixed(1)}px`;
            germEl.current.style.top = `${yTop.toFixed(1)}px`;
          }
          return;
        }

        // 普通追捕:菌被水平逼近就逃窜,平时瞎溜达
        g.state = dx < 150 ? 'flee' : 'crawl';
        if (g.state === 'flee') g.dir = petCx < gCx ? 1 : -1;
        else if (Math.random() < 0.004) g.dir = g.dir === 1 ? -1 : 1;
        g.x = Math.min(Math.max(g.x + (g.state === 'flee' ? 34 : 13) * g.dir * dt, loX), hiX);
        // 追捕指令(节流):地板上走过去;边框上的菌 → 走到正下方后纵身一跃
        if (now - lastCmd > 450) {
          lastCmd = now;
          const onFloorReady = !anchoredRef.current && motionRef.current === 'rest';
          if (g.perch && onFloorReady && dx < 26) {
            window.dispatchEvent(
              new CustomEvent('pac-pet-debug', { detail: { cmd: 'pounce', x: gCx - SIZE / 2, y: yTop } }),
            );
          } else {
            window.dispatchEvent(new CustomEvent('pac-pet-debug', { detail: { cmd: 'hunt', x: gCx - SIZE / 2 } }));
          }
        }
        if (dx < 38 && dy < 44 && motionRef.current !== 'walk') {
          // 同高度且贴近(地板走到 / 纵身跳到)→ 敲打 1.1s → 爆星 → 庆祝
          g.state = 'dying';
          setWhacking(true);
          setTimeout(() => {
            setWhacking(false);
            setBurst({ x: germ.current?.x ?? 0, top: yTop });
            germ.current = null;
            setGermAlive(false);
            setTimeout(() => setBurst(null), 700);
            emitPetEvent({ type: 'director', script: { gesture: 'celebrate', ttlMs: 2_800 } });
          }, 1_100);
        } else if (now - g.t0 > 16_000) {
          germ.current = null;
          setGermAlive(false); // 追不上,菌溜走(无气泡)
        }
      }
      if (germEl.current && germ.current) {
        germEl.current.style.left = `${germ.current.x.toFixed(1)}px`;
        germEl.current.style.top = `${yTop.toFixed(1)}px`;
      }
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [germAlive]);

  // ── 摸摸:悬停 ≥350ms 算"被摸"(眯眼笑 + 爱心),路过不算 ──
  const [cuddled, setCuddled] = useState(false);
  const cuddleTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
  const onHoverEnter = () => {
    if (cuddleTimer.current) clearTimeout(cuddleTimer.current);
    cuddleTimer.current = setTimeout(() => setCuddled(true), 350);
  };
  const onHoverLeave = () => {
    if (cuddleTimer.current) clearTimeout(cuddleTimer.current);
    cuddleTimer.current = null;
    setCuddled(false);
  };
  useEffect(
    () => () => {
      if (cuddleTimer.current) clearTimeout(cuddleTimer.current);
    },
    [],
  );

  // ── 拖拽(4px 阈值区分点击;拖中跟手,松手交还物理) ──
  const drag = useRef<{ startX: number; startY: number; baseX: number; baseY: number; moved: boolean } | null>(null);
  const onPointerDown = (e: React.PointerEvent) => {
    vetAbortRef.current(); // 拖拽/点击 → 中断诊疗组合
    const r = btnRef.current!.getBoundingClientRect();
    drag.current = { startX: e.clientX, startY: e.clientY, baseX: r.left, baseY: r.top, moved: false };
    btnRef.current!.setPointerCapture(e.pointerId);
  };
  const onPointerMove = (e: React.PointerEvent) => {
    const d = drag.current;
    if (!d) return;
    const dx = e.clientX - d.startX;
    const dy = e.clientY - d.startY;
    if (!d.moved) {
      if (Math.hypot(dx, dy) < 4) return;
      d.moved = true;
      api.startDrag();
    }
    api.dragTo(d.baseX + dx, d.baseY + dy);
  };
  const onPointerUp = () => {
    const d = drag.current;
    drag.current = null;
    if (!d) return;
    if (d.moved) {
      api.drop();
    } else {
      const r = btnRef.current?.getBoundingClientRect();
      if (r) onToggle(r);
    }
  };

  // 套绳可视化:rAF 把绳子两端都钉住 —— 锚点端跟随实时鼠标,另一端在宠物头顶(60fps,不走 React)
  const ropeLineRef = useRef<SVGLineElement>(null);
  const ropeAnchorRef = useRef<SVGCircleElement>(null);
  useEffect(() => {
    if (!rope) return;
    const mouse = { x: rope.x, y: rope.y };
    const onMove = (e: PointerEvent) => {
      mouse.x = e.clientX;
      mouse.y = e.clientY;
    };
    window.addEventListener('pointermove', onMove, { passive: true });
    let raf = 0;
    const tick = () => {
      raf = requestAnimationFrame(tick);
      const r = btnRef.current?.getBoundingClientRect();
      const line = ropeLineRef.current;
      if (r && line) {
        line.setAttribute('x1', String(mouse.x));
        line.setAttribute('y1', String(mouse.y));
        line.setAttribute('x2', String(r.left + SIZE / 2));
        line.setAttribute('y2', String(r.top + 8));
        ropeAnchorRef.current?.setAttribute('cx', String(mouse.x));
        ropeAnchorRef.current?.setAttribute('cy', String(mouse.y));
      }
    };
    raf = requestAnimationFrame(tick);
    return () => {
      window.removeEventListener('pointermove', onMove);
      cancelAnimationFrame(raf);
    };
  }, [rope]);

  // 姿态合成:超人披风 > 敲打 > 运动(摔/跳/拖/荡/飞 → 慌张/飞)> 走 > 刷字 > 被摸 > 坐 > 表情
  const bodyPose: PetBodyPose =
    superPhase === 'transform'
      ? 'morph' // 超人变身(原地蓄力 + 披风展开 + 光环)
      : superPhase === 'fly'
        ? 'fly' // 超人飞行(红披风)
        : whacking
        ? 'brush' // 敲蛀牙菌:无论站着还是纵身跃起,都挥牙刷
        : motion === 'fly'
          ? 'fly'
          : motion === 'climb'
            ? 'climb' // 爬楼(背朝外悬挂垫脚)
            : motion === 'magnify'
              ? 'magnify' // 到达选区举放大镜照
              : motion === 'fall' || motion === 'jump' || motion === 'drag' || motion === 'swing'
              ? 'fall'
              : motion === 'walk'
                ? 'walk'
              : brush?.phase === 'scrub'
                ? 'brush'
                : rinsing
                  ? 'rinse'
                  : cuddled && (pose === 'idle' || pose === 'sleep')
                    ? 'happy'
                    : anchored && pose === 'idle'
                      ? 'sit'
                      : pose;

  // 气泡弹向:按当前实际位置(运动层直接写 style,这里读回来)
  const onLeftHalf = (() => {
    const r = btnRef.current?.getBoundingClientRect();
    return r ? r.left + SIZE / 2 < window.innerWidth / 2 : false;
  })();

  return (
    <button
      ref={btnRef}
      type="button"
      title={open ? '收起助手' : '打开助手(可拖动,会自己溜达)'}
      onPointerDown={onPointerDown}
      onPointerMove={onPointerMove}
      onPointerUp={onPointerUp}
      onPointerEnter={onHoverEnter}
      onPointerLeave={onHoverLeave}
      className="fixed z-[61] touch-none select-none"
      style={{ left: -9999, top: -9999 }} // 首帧屏外,place() 后由运动层接管
    >
      {bubble && (
        <span
          className={cn(
            // w-max 让短句贴合内容、max-w 封顶,长句自动换行不溢出气泡框
            'absolute bottom-full mb-1.5 w-max max-w-[230px] whitespace-normal break-words rounded-xl border bg-white px-3 py-1.5 text-xs leading-snug text-slate-700 shadow-lg',
            onLeftHalf ? 'left-0' : 'right-0',
          )}
        >
          {bubble}
        </span>
      )}
      {/* 套绳:锚点(鼠标处)到宠物头顶的一条绳(x2/y2 由 rAF 实时钉) */}
      {rope && (
        <svg
          className="pointer-events-none fixed left-0 top-0 z-[60]"
          style={{ width: '100vw', height: '100vh' }}
        >
          <line ref={ropeLineRef} x1={rope.x} y1={rope.y} x2={rope.x} y2={rope.y} stroke="#d97706" strokeWidth="2" strokeLinecap="round" />
          <circle ref={ropeAnchorRef} cx={rope.x} cy={rope.y} r="3.5" fill="#d97706" />
        </svg>
      )}
      {/* 蛀牙菌:地板或边框上乱爬的紫色小坏蛋(left/top 由 rAF 实时钉;被发现会逃,被追上就被牙刷敲爆) */}
      {germAlive && (
        <span ref={germEl} className="pointer-events-none fixed z-[60]" style={{ top: -9999, left: -9999 }}>
          <svg viewBox="0 0 24 24" width={22} height={22}>
            <g className="pac-germ-bob">
              <g stroke="#7c3aed" strokeWidth="1.6" strokeLinecap="round">
                <path d="M12 3 L12 0.5" />
                <path d="M5 6 L3.2 4.2" />
                <path d="M19 6 L20.8 4.2" />
                <path d="M3.5 13 L1 13" />
                <path d="M20.5 13 L23 13" />
              </g>
              <circle cx="12" cy="13" r="8.5" fill="#a78bfa" stroke="#7c3aed" strokeWidth="1" />
              <circle cx="9" cy="16" r="1.6" fill="#7c3aed" opacity="0.5" />
              <circle cx="15.5" cy="15" r="1.1" fill="#7c3aed" opacity="0.5" />
              <circle cx="9.5" cy="11.5" r="1.4" fill="#1e1b4b" />
              <circle cx="14.5" cy="11.5" r="1.4" fill="#1e1b4b" />
              <path d="M8 9.4 L11 10.4 M16 9.4 L13 10.4" stroke="#1e1b4b" strokeWidth="1" strokeLinecap="round" />
              <path d="M9.5 17.5 Q12 19.5 14.5 17.5" stroke="#1e1b4b" strokeWidth="1.1" fill="none" strokeLinecap="round" />
            </g>
          </svg>
        </span>
      )}
      {/* 消灭爆星(在蛀虫被敲爆的位置) */}
      {burst && (
        <span className="pointer-events-none fixed z-[60]" style={{ left: burst.x - 6, top: burst.top - 4 }}>
          <span className="pac-germ-pop">✶</span>
          <span className="pac-germ-pop" style={{ animationDelay: '0.08s', marginLeft: 6 }}>✦</span>
          <span className="pac-germ-pop" style={{ animationDelay: '0.16s', marginLeft: 6 }}>✶</span>
        </span>
      )}
      {/* 刷字泡沫层:盖在脚下卡片标题区,刷完换闪光(纯视觉覆盖,不动 DOM 文字) */}
      {brush && (
        <span
          className="pointer-events-none fixed z-[60] block"
          style={{ left: brush.rect.x, top: brush.rect.y, width: brush.rect.w, height: brush.rect.h }}
        >
          <style>{FOAM_CSS}</style>
          {brush.phase === 'scrub'
            ? FOAM_DOTS.map((d, i) => (
                <span
                  key={i}
                  className="pac-foam-b"
                  style={{ left: `${d.l}%`, width: d.s, height: d.s, animationDelay: `${d.d}s` }}
                />
              ))
            : SHINE_DOTS.map((d, i) => (
                <span key={i} className="pac-foam-s" style={{ left: `${d.l}%`, animationDelay: `${d.d}s` }}>
                  ✦
                </span>
              ))}
        </span>
      )}
      {/* 朝向翻转放外层 span(svg 根的 transform 留给跳/压扁动画) */}
      <span
        className="block drop-shadow-md transition-transform hover:scale-105"
        style={{ transform: facing === -1 ? 'scaleX(-1)' : undefined }}
      >
        <PetBody
          pose={bodyPose}
          glanceSeq={glanceSeq}
          facing={facing}
          squashSeq={landSeq}
          shield={shield}
          seated={anchored && motion === 'rest'}
          size={SIZE}
        />
      </span>
    </button>
  );
}

// 刷字泡沫/闪光的固定布点(模块级一次性布局,渲染稳定)
const FOAM_DOTS = [
  { l: 6, d: 0, s: 7 },
  { l: 22, d: 0.3, s: 10 },
  { l: 40, d: 0.15, s: 8 },
  { l: 58, d: 0.45, s: 11 },
  { l: 76, d: 0.1, s: 7 },
  { l: 90, d: 0.55, s: 9 },
];
const SHINE_DOTS = [
  { l: 14, d: 0 },
  { l: 48, d: 0.15 },
  { l: 80, d: 0.3 },
];

const FOAM_CSS = `
.pac-foam-b, .pac-foam-s { position: absolute; bottom: 2px; opacity: 0; }
.pac-foam-b { border-radius: 9999px; background: rgba(255,255,255,0.92); border: 1px solid #bae6fd; }
.pac-foam-s { color: #fbbf24; font-size: 13px; line-height: 1; }
.pac-germ-pop { display: inline-block; color: #a78bfa; font-size: 15px; opacity: 0; }
@media (prefers-reduced-motion: no-preference) {
  .pac-foam-b { animation: pacFoamB 1s ease-in-out infinite; }
  .pac-foam-s { animation: pacFoamS 0.85s ease-out forwards; }
  .pac-germ-bob { transform-box: fill-box; transform-origin: center bottom; animation: pacGermBob 0.5s ease-in-out infinite; }
  .pac-germ-pop { animation: pacGermPop 0.6s ease-out forwards; }
}
@keyframes pacFoamB { 0% { opacity: 0; transform: translateY(3px) scale(0.6); } 40% { opacity: 1; } 100% { opacity: 0; transform: translateY(-9px) scale(1.1); } }
@keyframes pacFoamS { 0% { opacity: 0; transform: scale(0.5) rotate(0deg); } 35% { opacity: 1; } 100% { opacity: 0; transform: scale(1.25) rotate(40deg); } }
@keyframes pacGermBob { 0%,100% { transform: scaleY(1); } 50% { transform: scaleY(0.9); } }
@keyframes pacGermPop { 0% { opacity: 1; transform: scale(0.4); } 100% { opacity: 0; transform: scale(1.7) translateY(-10px); } }
`;
