'use client';

import { useEffect, useRef, useState } from 'react';

/**
 * 宠物运动层 — 桌宠式物理 + 自主行为(与大脑层正交:大脑管表情,这里管身体在哪)。
 *
 * 机制:
 *  - 重力:拖起松手 / 视窗 resize / 栖息点消失 → 下落,落地小压扁;
 *  - 地板 = 视窗底边(fixed 定位,滚动无关);
 *  - 散步:用户空闲一段时间后沿地板溜达(限"家"附近 ±WANDER,不跑去挡操作);
 *  - 栖息:页面元素标 data-pet-perch(任意高度)→ 跳上去蹲着,每帧跟随元素
 *    rect(滚动/布局变化),元素不可见即摔落;可在平台上散步,**走过边缘就踩空摔下**;
 *  - 蹲守:外部给 watchX(如 AI 生成中的助手窗边)→ 走过去守着。
 *
 * 实现要点:位置每帧直接写 el.style(60fps 不走 React);React 只收 motion/facing
 * 两个低频状态用于切换姿态/朝向。
 */
export type PetMotion = 'rest' | 'walk' | 'fall' | 'jump' | 'drag' | 'swing' | 'fly' | 'climb' | 'magnify';

const GRAVITY = 2600; // px/s²
const WALK_SPEED = 64; // px/s
const FLY_SPEED = 1100; // 超人直线飞行速度 px/s
const CLIMB_SPEED = 115; // 爬下楼下降速度 px/s
const WANDER = 240; // 散步范围:家两侧各 240px
const STROLL_IDLE_MIN_MS = 25_000; // 空闲多久开始想散步
const STROLL_IDLE_MAX_MS = 40_000;
// ── 鼠标互动 ──
const STARTLE_DIST = 90; // 快速掠过判定半径
const STARTLE_SPEED = 1500; // px/s,鼠标速度阈值
const STARTLE_COOLDOWN_MS = 8_000;
const CURIOUS_DIST = 260; // 好奇判定:鼠标在附近
const CURIOUS_DWELL_MS = 2_500; // 鼠标慢下来停留多久触发
const CURIOUS_COOLDOWN_MS = 30_000;
const SEL_WATCH_MS = 12_000; // 选中文字围观限时
const INSPECT_MS = 3_000; // 到达后用放大镜照多久
const SEEK_COOLDOWN_MS = 12_000; // 两次"凑近选区"最小间隔(防每次划选都跑)

/** 栖息面选择器:显式标注 + 一切带边框的元素(Tailwind border* 类 —— 卡片/行/按钮/输入框都算) */
const PERCH_SELECTOR = '[data-pet-perch], [class*="border"]';

export interface LocomotionOpts {
  size: number;
  /** true = 不要自主行动(思考/庆祝/助手窗开着) */
  getBusy: () => boolean;
  /** 想蹲守的地板 x(AI 生成中给助手窗边),null = 无 */
  getWatchX: () => number | null;
  /** 落点持久化(下次进页面从这恢复) */
  persist: (x: number) => void;
}

export function usePetLocomotion(
  elRef: React.RefObject<HTMLElement | null>,
  { size, getBusy, getWatchX, persist }: LocomotionOpts,
) {
  const [motion, setMotion] = useState<PetMotion>('rest');
  const [facing, setFacing] = useState<1 | -1>(1);
  /** 落地信号(自增):身体收到后播一次压扁回弹 */
  const [landSeq, setLandSeq] = useState(0);
  /** 是否栖在元素上(true 时静止姿态用"坐在边沿晃腿") */
  const [anchored, setAnchored] = useState(false);
  /** 套绳锚点(荡飞中绳子另一端的视窗坐标;null = 没在荡) */
  const [rope, setRope] = useState<{ x: number; y: number } | null>(null);

  // 全部物理状态进 ref(rAF 闭环,不触发渲染)
  const s = useRef({
    x: 0,
    y: 0,
    vx: 0,
    vy: 0,
    motion: 'rest' as PetMotion,
    facing: 1 as 1 | -1,
    homeX: 0, // "家":最近一次用户放置/落地点,散步以此为锚
    walkTargetX: 0,
    /** 走到目标后要跳的栖息点(没有 = 纯散步) */
    jumpTo: null as { el: Element; offsetX: number } | null,
    /** 当前栖息的元素(rest 时非 null = 蹲在元素上) */
    anchor: null as { el: Element; offsetX: number } | null,
    lastActiveAt: Date.now(),
    nextStrollAt: Date.now() + STROLL_IDLE_MIN_MS,
    // 鼠标互动:光标位置/速度 + 惊吓/好奇冷却
    cur: { x: -1e4, y: -1e4, t: 0, lastFastAt: 0 },
    // 转圈手势识别(画圈套绳):累计转角 + 路径范围 + 冷却
    circle: { prevH: null as number | null, accum: 0, t0: 0, len: 0, minX: 0, maxX: 0, minY: 0, maxY: 0, lastAt: 0 },
    startleUntil: 0,
    curiousUntil: 0,
    // 拖拽测速(松手抛掷用)
    dragLast: null as { x: number; t: number } | null,
    // 选中文字围观:用"走 → 爬楼 → 放大镜照 → 爬下楼回地板"接近选区
    seek: null as { x: number; y: number; until: number; face: 1 | -1 } | null,
    seekStage: '' as '' | 'walk' | 'up' | 'inspect' | 'down',
    seekCooldownUntil: 0,
    climbTargetY: 0, // 爬行(上/下)的目标高度
    magnifyUntil: 0,
    // 蛀虫追捕:huntJump=true 时空中段不被中间层边框接住,一路落到地板(防卡中间层);
    // fly=超人直线飞向目标(无视重力/墙/边框)
    huntJump: false,
    flyX: 0,
    flyY: 0,
    flyCmdAt: 0,
    // 套绳荡飞(钟摆:θ 从锚点垂直向下量起)
    rope: null as { ax: number; ay: number; L: number; theta: number; omega: number; cross: number; t0: number } | null,
  });

  // 对外命令(拖拽接入)
  const api = useRef({
    startDrag() {
      const c = s.current;
      c.motion = 'drag';
      c.anchor = null;
      c.vx = 0;
      c.vy = 0;
      c.dragLast = null;
      c.rope = null; // 荡到一半被抓住 → 绳收掉
      setRope(null);
    },
    dragTo(x: number, y: number) {
      const c = s.current;
      // 估算拖拽横速(指数平滑),松手时变成抛掷初速
      const now = performance.now();
      const nx = clampX(x, size);
      if (c.dragLast) {
        const dtm = (now - c.dragLast.t) / 1000;
        if (dtm > 0.001) c.vx = 0.7 * c.vx + 0.3 * ((nx - c.x) / dtm);
      }
      c.dragLast = { x: nx, t: now };
      c.x = nx;
      c.y = Math.min(Math.max(y, 4), floorY(size));
    },
    /** 松手 → 带惯性抛出(重力下落 + 横速保留;已在地板且没甩 → 原地安家) */
    drop() {
      const c = s.current;
      c.homeX = c.x;
      c.vx = Math.max(-900, Math.min(900, c.vx));
      // 最近 120ms 内没在动 → 不算甩,清横速
      if (!c.dragLast || performance.now() - c.dragLast.t > 120) c.vx = 0;
      if (c.y >= floorY(size) - 1 && Math.abs(c.vx) < 60) {
        c.motion = 'rest';
        c.vx = 0;
        persist(c.x);
      } else {
        c.motion = 'fall';
        c.vy = 0;
      }
    },
    /** 初始放置(restore):放到指定 x,从其 y 落下(进页面的小开场) */
    place(x: number, y: number) {
      const c = s.current;
      c.x = clampX(x, size);
      c.y = Math.min(Math.max(y, 4), floorY(size));
      c.homeX = c.x;
      c.motion = c.y >= floorY(size) - 1 ? 'rest' : 'fall';
    },
  });

  useEffect(() => {
    const el = elRef.current;
    if (!el) return;
    const c = s.current;

    const apply = () => {
      el.style.left = `${c.x.toFixed(1)}px`;
      el.style.top = `${c.y.toFixed(1)}px`;
    };

    const sync = () => {
      setMotion(c.motion);
      setFacing(c.facing);
      setAnchored(!!c.anchor);
    };

    const land = () => {
      c.y = floorY(size);
      c.vy = 0;
      c.vx = 0;
      c.motion = 'rest';
      c.homeX = c.x;
      c.huntJump = false; // 落地即解除"穿透中间层"
      setLandSeq((n) => n + 1);
      persist(c.x);
      sync();
    };

    /** 栖息面快照(150ms TTL):广选择器 + 下落每帧检测必须缓存,否则全 DOM 测矩形会卡。 */
    let surfCache: { t: number; list: { el: Element; left: number; right: number; top: number }[] } | null = null;
    const surfaces = () => {
      const now = performance.now();
      if (surfCache && now - surfCache.t < 150) return surfCache.list;
      const list: { el: Element; left: number; right: number; top: number }[] = [];
      document.querySelectorAll(PERCH_SELECTOR).forEach((p) => {
        if (el.contains(p)) return; // 宠物自己身上的(气泡/泡沫层)不算
        // visibility:hidden / display:none 的不算(如收起的助手窗,rect 仍有尺寸)
        if ((p as HTMLElement).checkVisibility && !(p as HTMLElement).checkVisibility()) return;
        const r = p.getBoundingClientRect();
        if (r.width < size + 8 || r.height < 4) return;
        // 水平在视窗内才算(抽屉平移出屏的元素 rect 仍在,不滤会跳到屏幕外)
        if (r.left < 0 || r.right > window.innerWidth) return;
        if (r.top - size < 4) return; // 站上去头要在视窗内
        list.push({ el: p, left: r.left, right: r.right, top: r.top });
      });
      surfCache = { t: now, list };
      return list;
    };

    /** 找栖息点(散步目的地):从可见面里随机挑一个。 */
    const findPerch = (): { el: Element; offsetX: number } | null => {
      const list = surfaces();
      const pick = list[Math.floor(Math.random() * list.length)];
      if (!pick) return null;
      return { el: pick.el, offsetX: 8 + Math.random() * Math.max(pick.right - pick.left - size - 16, 1) };
    };

    /** 下落穿面检测:本帧脚底从 prevBottom 落到 newBottom,途中跨过某栖息面顶边
     *  且横向重心在面内 → 接住(返回最高的那个面)。 */
    const surfaceUnder = (prevBottom: number, newBottom: number): { el: Element; offsetX: number } | null => {
      let best: { el: Element; left: number; top: number } | null = null;
      const cx = c.x + size / 2;
      for (const sf of surfaces()) {
        if (cx < sf.left || cx > sf.right) continue;
        if (sf.top < prevBottom - 2 || sf.top > newBottom) continue; // 本帧没跨过其顶边
        if (!best || sf.top < best.top) best = sf;
      }
      if (!best) return null;
      const b = best as { el: Element; left: number; top: number };
      return { el: b.el, offsetX: Math.max(4, c.x - b.left) };
    };

    const scheduleStroll = () => {
      c.nextStrollAt =
        Date.now() + STROLL_IDLE_MIN_MS + Math.random() * (STROLL_IDLE_MAX_MS - STROLL_IDLE_MIN_MS);
    };

    const startWalk = (targetX: number, jumpTo: typeof c.jumpTo = null, keepAnchor = false) => {
      c.walkTargetX = clampX(targetX, size);
      c.jumpTo = jumpTo;
      if (!keepAnchor) c.anchor = null; // keepAnchor = 在栖息平台上走(可能走到边缘踩空)
      c.motion = 'walk';
      c.facing = c.walkTargetX >= c.x ? 1 : -1;
      sync();
    };

    /** 套绳荡飞:绳子套在锚点(默认 = 当前鼠标位置),钟摆荡 1.5 个来回后松手甩飞。 */
    /** 套绳就绪:鼠标在视窗内、且离宠物足够远(锚点可在任意方位 —— 上/下/侧都行)。 */
    const swingReady = () =>
      c.cur.t > 0 &&
      c.cur.x >= 0 &&
      c.cur.x <= window.innerWidth &&
      c.cur.y >= 0 &&
      c.cur.y <= window.innerHeight &&
      Math.hypot(c.cur.x - (c.x + size / 2), c.cur.y - (c.y + size / 2)) > 110;

    const startSwing = (anchor?: { x: number; y: number }) => {
      const pcx = c.x + size / 2;
      const pcy = c.y + size / 2;
      const ax = anchor?.x ?? c.cur.x;
      const ay = anchor?.y ?? c.cur.y;
      // 锚点(鼠标)只需在视窗内 —— 可在宠物上方/下方/侧方任意位置(宠物会绕锚点甩荡)
      if (ax < 0 || ax > window.innerWidth || ay < 0 || ay > window.innerHeight) return;
      const L = Math.hypot(pcx - ax, pcy - ay);
      if (L < 50) return;
      const dir = ax < window.innerWidth / 2 ? 1 : -1; // 往屏幕开阔侧荡
      c.rope = {
        ax,
        ay,
        L,
        theta: Math.atan2(pcx - ax, pcy - ay),
        omega: dir * 1.5 * Math.sqrt(GRAVITY / L),
        cross: 0,
        t0: performance.now(),
      };
      c.anchor = null;
      c.jumpTo = null;
      c.motion = 'swing';
      setRope({ x: ax, y: ay });
      sync();
    };

    /** 爬楼:背朝外、悬挂垫脚,匀速爬到目标高度(默认地板;接近选区时爬到选区高度)。 */
    const startClimb = (targetY: number = floorY(size)) => {
      c.anchor = null;
      c.jumpTo = null;
      c.vx = 0;
      c.vy = 0;
      c.climbTargetY = targetY;
      c.motion = 'climb';
      sync();
    };

    const onActivity = () => {
      c.lastActiveAt = Date.now();
    };
    window.addEventListener('pointerdown', onActivity, { passive: true });
    window.addEventListener('keydown', onActivity, { passive: true });
    window.addEventListener('wheel', onActivity, { passive: true });

    // 鼠标互动:测速 + 惊吓(快速掠过身边 → 吓得往旁边蹦一下)
    const onCursor = (e: PointerEvent) => {
      const now = Date.now();
      const dtm = now - c.cur.t;
      if (dtm > 0) {
        const speed = (Math.hypot(e.clientX - c.cur.x, e.clientY - c.cur.y) / dtm) * 1000;
        if (speed > STARTLE_SPEED) {
          c.cur.lastFastAt = now;
          const dist = Math.hypot(e.clientX - (c.x + size / 2), e.clientY - (c.y + size / 2));
          if (
            dist < STARTLE_DIST &&
            now > c.startleUntil &&
            (c.motion === 'rest' || c.motion === 'walk') &&
            !getBusy()
          ) {
            c.startleUntil = now + STARTLE_COOLDOWN_MS;
            c.anchor = null; // 在栖息台上被吓到 → 直接蹦下来
            c.jumpTo = null;
            c.facing = e.clientX < c.x + size / 2 ? 1 : -1; // 背对鼠标方向逃
            c.vx = 150 * c.facing;
            c.vy = -460;
            c.motion = 'jump';
            sync();
          }
        }
      }

      // 画圈手势 → 套绳荡飞:累计转角超过约一圈(且路径成环、范围合理)即触发
      const cr = c.circle;
      const mdx = e.clientX - c.cur.x;
      const mdy = e.clientY - c.cur.y;
      const seg = Math.hypot(mdx, mdy);
      if (seg > 2.5) {
        if (cr.prevH === null || now - cr.t0 > 1_400) {
          cr.accum = 0; // 窗口重置(停顿/超时)
          cr.len = 0;
          cr.t0 = now;
          cr.minX = cr.maxX = e.clientX;
          cr.minY = cr.maxY = e.clientY;
          cr.prevH = null;
        }
        const h = Math.atan2(mdy, mdx);
        if (cr.prevH !== null) {
          let d = h - cr.prevH;
          while (d > Math.PI) d -= 2 * Math.PI;
          while (d < -Math.PI) d += 2 * Math.PI;
          cr.accum += d;
          cr.len += seg;
        }
        cr.prevH = h;
        cr.minX = Math.min(cr.minX, e.clientX);
        cr.maxX = Math.max(cr.maxX, e.clientX);
        cr.minY = Math.min(cr.minY, e.clientY);
        cr.maxY = Math.max(cr.maxY, e.clientY);
        const diag = Math.hypot(cr.maxX - cr.minX, cr.maxY - cr.minY);
        if (
          Math.abs(cr.accum) > 5.8 && // ≈ 330°
          cr.len > 140 &&
          diag > 30 &&
          diag < 650 &&
          now - cr.lastAt > 6_000 &&
          c.motion === 'rest' &&
          !getBusy()
        ) {
          cr.lastAt = now;
          cr.accum = 0;
          cr.prevH = null;
          startSwing({ x: e.clientX, y: e.clientY }); // 锚点 = 画圈处
        }
      }

      c.cur.x = e.clientX;
      c.cur.y = e.clientY;
      c.cur.t = now;
    };
    window.addEventListener('pointermove', onCursor, { passive: true });

    // 选中文字围观:划选 → 走到选区正下方仰头看(限时;选区取消不再追)
    let selTimer: ReturnType<typeof setTimeout> | undefined;
    const onSelChange = () => {
      clearTimeout(selTimer);
      selTimer = setTimeout(() => {
        const sel = document.getSelection();
        if (!sel || sel.isCollapsed || sel.rangeCount === 0) {
          if (c.seekStage === '') c.seek = null;
          return;
        }
        const r = sel.getRangeAt(0).getBoundingClientRect();
        if (!r || r.width < 8 || r.bottom < 4 || r.top > window.innerHeight - 4) {
          if (c.seekStage === '') c.seek = null;
          return;
        }
        // 停在文字左侧(放不下就去右侧),面朝文字;高度对齐文字中线
        let ix = r.left - size - 2;
        let face: 1 | -1 = 1;
        if (ix < 4) {
          ix = r.right + 2;
          face = -1;
        }
        const iy = Math.min(Math.max(r.top + r.height / 2 - size / 2, 4), floorY(size));
        if (c.seekStage === '') c.seek = { x: clampX(ix, size), y: iy, until: Date.now() + SEL_WATCH_MS, face };
      }, 500);
    };
    document.addEventListener('selectionchange', onSelChange);

    // 内部命令通道:pet-lab 一键触发 + 蛀虫追捕等编排模块驱动(生产业务代码不派发)
    const onDebug = (e: Event) => {
      const detail = (e as CustomEvent<{ cmd?: string; x?: number; y?: number; perch?: Element | null }>).detail;
      const cmd = detail?.cmd;
      // 超人直飞(fly):每帧刷新目标,无视一切;持续到 widget 停发(蛀虫已灭)→ fly case 超时落地
      if (cmd === 'fly') {
        c.flyX = typeof detail?.x === 'number' ? detail.x : c.x;
        c.flyY = typeof detail?.y === 'number' ? detail.y : c.y;
        c.flyCmdAt = performance.now();
        if (c.motion !== 'fly') {
          c.anchor = null;
          c.jumpTo = null;
          c.rope = null;
          c.motion = 'fly';
          sync();
        }
        return;
      }
      // 地板追捕(hunt):走到蛀虫正下方;若正蹲在边框上 → 跳下来(穿透中间层直落地板)
      if (cmd === 'hunt') {
        if (getBusy() || c.motion === 'fly') return;
        const x = typeof detail?.x === 'number' ? detail.x : c.x;
        if (c.anchor && c.motion === 'rest') {
          c.anchor = null;
          c.motion = 'fall';
          c.vy = 0;
          c.huntJump = true; // 直落地板,不卡中间层
          sync();
        } else if (c.motion === 'rest' && !c.anchor && Math.abs(x - c.x) > 8) {
          startWalk(x);
        }
        return;
      }
      // 纵身一跃(pounce):站在蛀虫正下方的地板 → 跳起去敲边框上的它(不落在边框、不卡层)
      if (cmd === 'pounce') {
        if (getBusy() || c.motion !== 'rest' || c.anchor) return;
        const px = typeof detail?.x === 'number' ? detail.x : c.x;
        const ty = typeof detail?.y === 'number' ? detail.y : c.y; // 蛀虫顶边
        const apex = ty - 8; // 跳到略高于蛀虫,让宠物在它高度有停留
        c.motion = 'jump';
        c.jumpTo = null;
        c.huntJump = true;
        c.vy = -Math.sqrt(2 * GRAVITY * Math.max(c.y - apex, 30));
        c.vx = (px - c.x) * 1.4;
        c.facing = c.vx >= 0 ? 1 : -1;
        sync();
        return;
      }
      // 爬下楼(lab):需正蹲在边框上
      if (cmd === 'climb') {
        if (c.anchor && c.motion === 'rest' && !getBusy()) startClimb();
        return;
      }
      // 套绳荡飞:站着/坐高处都能起(从边框上也能甩荡;鼠标可在低处)
      if (cmd === 'swing') {
        if (getBusy() || c.motion === 'swing' || c.motion === 'fly') return;
        const fallback = {
          x: clampX(c.x + (Math.random() < 0.5 ? -140 : 140), size) + size / 2,
          y: Math.max(c.y - 240, 30),
        };
        startSwing(swingReady() ? undefined : fallback);
        return;
      }
      if (c.motion !== 'rest' || c.anchor || getBusy()) return; // lab 指令:只在地板待命时响应
      if (cmd === 'perch') {
        const p = findPerch();
        if (p) {
          const r = p.el.getBoundingClientRect();
          startWalk(r.left + p.offsetX, p);
        }
      } else if (cmd === 'stroll') {
        startWalk(c.homeX + (Math.random() * 2 - 1) * WANDER);
      }
    };
    window.addEventListener('pac-pet-debug', onDebug);

    const onResize = () => {
      c.x = clampX(c.x, size);
      if (c.motion === 'rest' && !c.anchor && c.y < floorY(size) - 1) {
        c.motion = 'fall'; // 地板下移了 → 摔下去
        sync();
      }
      c.y = Math.min(c.y, floorY(size));
    };
    window.addEventListener('resize', onResize);

    let raf = 0;
    let prev = performance.now();
    const loop = (now: number) => {
      raf = requestAnimationFrame(loop);
      const dt = Math.min((now - prev) / 1000, 0.05); // tab 切回的大 dt 截断
      prev = now;
      if (document.hidden) return;

      // 选区接近被打断(拖拽/惊吓/荡绳/被追…任何非 seek 运动)→ 放弃这次接近
      if (c.seekStage && c.motion !== 'walk' && c.motion !== 'climb' && c.motion !== 'magnify') {
        c.seek = null;
        c.seekStage = '';
      }

      switch (c.motion) {
        case 'drag':
          break;
        case 'fall': {
          const prevBottom = c.y + size;
          c.vy += GRAVITY * dt;
          c.y += c.vy * dt;
          // 抛掷惯性:横向位移 + 空气阻尼 + 撞墙反弹
          if (c.vx !== 0) {
            c.x += c.vx * dt;
            c.vx *= Math.max(0, 1 - 1.8 * dt);
            const minX = 4;
            const maxX = window.innerWidth - size - 4;
            if (c.x <= minX || c.x >= maxX) {
              c.x = c.x <= minX ? minX : maxX;
              c.vx = -c.vx * 0.45; // 弹墙损耗
              c.facing = c.vx >= 0 ? 1 : -1;
              sync();
            }
          }
          // 下落途中穿过卡片顶边 → 接住,落在边框上(不直接掉到底);追捕空中段穿透中间层直落地板
          if (c.vy > 0 && !c.huntJump) {
            const surf = surfaceUnder(prevBottom, c.y + size);
            if (surf) {
              const r = surf.el.getBoundingClientRect();
              c.y = r.top - size;
              c.x = clampX(r.left + surf.offsetX, size);
              c.vx = 0;
              c.vy = 0;
              c.anchor = surf;
              c.motion = 'rest';
              setLandSeq((n) => n + 1);
              sync();
              break;
            }
          }
          if (c.y >= floorY(size)) land();
          break;
        }
        case 'walk': {
          c.x += WALK_SPEED * c.facing * dt;
          // 撞到屏幕边缘 → 撞一下(压扁)、回头往回走
          const minX = 4;
          const maxX = window.innerWidth - size - 4;
          if (!c.anchor && (c.x <= minX || c.x >= maxX)) {
            c.x = c.x <= minX ? minX : maxX;
            setLandSeq((n) => n + 1); // 复用压扁动画当"撞墙"反馈
            c.facing = c.x <= minX ? 1 : -1;
            c.walkTargetX = clampX(c.x + c.facing * (80 + Math.random() * 160), size);
            sync();
            break;
          }
          // 在栖息平台上走:y 贴平台顶,重心越过边缘 → 踩空摔落
          if (c.anchor) {
            const r = c.anchor.el.getBoundingClientRect();
            const gone = !c.anchor.el.isConnected || r.width === 0;
            const overEdge = c.x + size / 2 < r.left || c.x + size / 2 > r.right;
            if (gone || overEdge) {
              c.anchor = null;
              c.motion = 'fall';
              c.vy = 0;
              sync();
              break;
            }
            c.y = r.top - size;
          }
          const arrived = c.facing === 1 ? c.x >= c.walkTargetX : c.x <= c.walkTargetX;
          if (getBusy()) {
            // 思考/庆祝来了 → 原地停下演
            c.motion = 'rest';
            sync();
          } else if (arrived) {
            c.x = c.walkTargetX;
            if (c.jumpTo) {
              const r = c.jumpTo.el.getBoundingClientRect();
              const standY = r.top - size;
              c.motion = 'jump';
              c.vy = -Math.sqrt(2 * GRAVITY * Math.max(c.y - standY + 12, 20));
              // 横速按飞行时间(到顶点)算,高跳低跳都同步到位
              const flight = Math.abs(c.vy) / GRAVITY;
              c.vx = (r.left + c.jumpTo.offsetX - c.x) / Math.max(flight, 0.2);
              sync();
            } else if (c.seekStage === 'walk' && c.seek) {
              // 走到选区正下方 → 爬楼上去(到选区高度)
              c.seekStage = 'up';
              startClimb(c.seek.y);
            } else {
              c.motion = 'rest';
              c.homeX = c.x;
              sync();
              scheduleStroll();
            }
          }
          break;
        }
        case 'jump': {
          const prevBottomJ = c.y + size;
          c.vy += GRAVITY * dt;
          c.x += c.vx * dt;
          c.y += c.vy * dt;
          // 无目标跳(惊吓蹦开)下落段同样可被卡片顶边接住
          if (!c.jumpTo && c.vy > 0 && !c.huntJump) {
            const surf = surfaceUnder(prevBottomJ, c.y + size);
            if (surf) {
              const r = surf.el.getBoundingClientRect();
              c.y = r.top - size;
              c.x = clampX(r.left + surf.offsetX, size);
              c.vx = 0;
              c.vy = 0;
              c.anchor = surf;
              c.motion = 'rest';
              setLandSeq((n) => n + 1);
              sync();
              break;
            }
          }
          const t = c.jumpTo;
          if (t) {
            const r = t.el.getBoundingClientRect();
            const standY = r.top - size;
            if (c.vy > 0 && c.y >= standY) {
              // 落上栖息点
              c.x = clampX(r.left + t.offsetX, size);
              c.y = standY;
              c.vx = 0;
              c.vy = 0;
              c.anchor = t;
              c.jumpTo = null;
              c.motion = 'rest';
              setLandSeq((n) => n + 1);
              sync();
            }
          }
          if (c.y >= floorY(size)) land(); // 没接住 → 落地
          break;
        }
        case 'swing': {
          const ro = c.rope;
          if (!ro) {
            c.motion = 'fall';
            sync();
            break;
          }
          // ① 绳子一端永远钉在鼠标:锚点每帧跟随光标(在视窗内才更新,鼠标出界则保持上一位置)
          if (c.cur.t > 0 && c.cur.x >= 0 && c.cur.x <= window.innerWidth && c.cur.y >= 0) {
            ro.ax = c.cur.x;
            ro.ay = c.cur.y;
          }
          // 钟摆积分 + 轻阻尼(摆角 θ 从锚点正下方量起;锚点平移则整条摆跟着平移)
          ro.omega += -(GRAVITY / ro.L) * Math.sin(ro.theta) * dt;
          ro.omega *= Math.max(0, 1 - 0.12 * dt);
          const prevTheta = ro.theta;
          ro.theta += ro.omega * dt;
          if (Math.sign(prevTheta) !== Math.sign(ro.theta)) ro.cross++; // 过最低点计数
          // ② 有绳期间不碰壁:位置纯由"锚点 + 摆"决定,不夹边界、不撞墙反弹
          c.x = ro.ax + ro.L * Math.sin(ro.theta) - size / 2;
          c.y = ro.ay + ro.L * Math.cos(ro.theta) - size / 2;
          c.facing = ro.omega >= 0 ? 1 : -1;
          if (c.y >= floorY(size)) {
            c.rope = null;
            setRope(null);
            land(); // 绳太长蹭到地 → 直接落地
            break;
          }
          // 松绳条件:荡够(过底 3 次)或超时;松手后才进 fall → ③ 此时边界/边框才生效
          if (ro.cross >= 3 || performance.now() - ro.t0 > 4_500) {
            c.vx = Math.max(-820, Math.min(820, ro.L * ro.omega * Math.cos(ro.theta)));
            c.vy = Math.max(-720, Math.min(500, -ro.L * ro.omega * Math.sin(ro.theta)));
            c.rope = null;
            setRope(null);
            c.motion = 'fall';
            c.facing = c.vx >= 0 ? 1 : -1;
            c.x = clampX(c.x, size); // 落地态接管前,把位置夹回视窗(可能荡出界了)
            sync();
          }
          break;
        }
        case 'climb': {
          // 背朝外、悬挂垫脚,匀速爬向 climbTargetY(可上可下;x 不动,穿过中间楼层)
          const cdy = c.climbTargetY - c.y;
          c.y += Math.sign(cdy) * Math.min(CLIMB_SPEED * dt, Math.abs(cdy));
          if (Math.abs(c.y - c.climbTargetY) < 1) {
            if (c.seekStage === 'up') {
              // 爬到选区高度 → 掏放大镜照
              c.seekStage = 'inspect';
              c.motion = 'magnify';
              c.magnifyUntil = now + INSPECT_MS;
              if (c.seek) c.facing = c.seek.face;
              sync();
            } else if (c.seekStage === 'down') {
              c.seek = null;
              c.seekStage = '';
              land();
            } else {
              land(); // 普通爬下楼到地板
            }
          }
          break;
        }
        case 'magnify': {
          // 悬在选区旁,用放大镜照(不动);看够了 / 选区过期 → 爬下楼回地板
          if (c.seek) c.facing = c.seek.face;
          if (now > c.magnifyUntil || !c.seek || Date.now() > c.seek.until) {
            c.seekStage = 'down';
            startClimb(floorY(size));
          }
          break;
        }
        case 'fly': {
          // 超人:直线匀速飞向目标(无视重力/墙/边框);命中由 widget 的 2D 判定触发,
          // 一段时间收不到新目标(蛀虫已灭)→ 转 fall 一路落回地板
          if (performance.now() - c.flyCmdAt > 450) {
            c.motion = 'fall';
            c.huntJump = true; // 落回地板途中不被中间层接住
            c.vy = 0;
            sync();
            break;
          }
          const dx = c.flyX - c.x;
          const dy = c.flyY - c.y;
          const d = Math.hypot(dx, dy) || 1;
          const step = FLY_SPEED * dt;
          c.x += (dx / d) * Math.min(step, d);
          c.y += (dy / d) * Math.min(step, d);
          c.facing = dx >= 0 ? 1 : -1;
          break;
        }
        case 'rest': {
          if (c.anchor) {
            // 蹲在元素上:跟随 rect;元素没了/出视野 → 摔落
            const r = c.anchor.el.getBoundingClientRect();
            const gone =
              !c.anchor.el.isConnected ||
              r.width === 0 ||
              r.top - size < -size ||
              r.top > floorY(size) + size ||
              r.right < size || // 横向滑出视窗(如抽屉收起的平移)也算没了
              r.left > window.innerWidth - size;
            if (gone) {
              c.anchor = null;
              c.motion = 'fall';
              c.vy = 0;
              sync();
              break;
            }
            // 蹲够了 / 要去蹲守 AI → 走向平台边缘,自然踩空下来
            const wantDown = Date.now() > c.nextStrollAt || getWatchX() !== null;
            if (wantDown && !getBusy()) {
              // 在边框上想去别的楼层时,优先甩绳荡过去(概率提高)
              if (getWatchX() === null && Math.random() < 0.4 && swingReady()) {
                startSwing();
                scheduleStroll();
                break;
              }
              // 一定概率"爬下楼":背朝外、悬挂垫脚,慢慢爬回地板(非地板态专属)
              if (getWatchX() === null && Math.random() < 0.45) {
                startClimb();
                scheduleStroll();
                break;
              }
              const watchX = getWatchX();
              const center = c.x + size / 2;
              // 候选目标(越过边缘 24px);clamp 后重心仍在平台外才"走得出去"
              // (平台贴视窗边时该侧出不去,如左栏贴左缘的"加载更多")
              const tl = r.left - size / 2 - 24;
              const tr = r.right - size / 2 + 24;
              const canL = clampX(tl, size) + size / 2 < r.left;
              const canR = clampX(tr, size) + size / 2 > r.right;
              const preferL = watchX !== null ? watchX < center : center - r.left < r.right - center;
              if (canL || canR) {
                const goLeft = canL && (preferL || !canR);
                startWalk(goLeft ? tl : tr, null, true);
              } else {
                // 平台横跨整个视窗,没边可走 → 原地跳下兜底
                c.anchor = null;
                c.motion = 'fall';
                c.vy = 0;
                sync();
              }
              scheduleStroll();
              break;
            }
            c.x = clampX(r.left + c.anchor.offsetX, size);
            c.y = r.top - size;
            break;
          }
          // 蹲守点(AI 生成中守助手窗)优先于散步
          const watchX = getWatchX();
          if (watchX !== null && Math.abs(watchX - c.x) > 12 && !getBusy()) {
            startWalk(watchX);
            break;
          }
          // 选中文字 → 凑近选区:走到下方 → 爬楼上去 → 放大镜照 → 爬下楼回(地板待命时才起)
          if (
            c.seek &&
            c.seekStage === '' &&
            !c.anchor &&
            Date.now() < c.seek.until &&
            Date.now() > c.seekCooldownUntil &&
            getWatchX() === null &&
            !getBusy()
          ) {
            c.seekCooldownUntil = Date.now() + SEEK_COOLDOWN_MS;
            c.seekStage = 'walk';
            startWalk(c.seek.x);
            break;
          }
          // 好奇:鼠标在附近慢下来停了一会 → 凑过去看看(地板上才凑,带冷却防黏人)
          {
            const now = Date.now();
            const dx = c.cur.x - (c.x + size / 2);
            const near =
              Math.abs(dx) < CURIOUS_DIST && Math.abs(c.cur.y - (c.y + size / 2)) < CURIOUS_DIST;
            const dwelling = c.cur.t > 0 && now - c.cur.lastFastAt > CURIOUS_DWELL_MS && now - c.cur.t < CURIOUS_DWELL_MS;
            if (near && dwelling && Math.abs(dx) > 90 && now > c.curiousUntil && !getBusy()) {
              c.curiousUntil = now + CURIOUS_COOLDOWN_MS;
              // 凑到鼠标侧边 ~52px 处(不顶到光标正下方,留出看的距离)
              startWalk(c.cur.x - size / 2 - Math.sign(dx) * 52);
              break;
            }
          }
          // 用户空闲 → 散步/跳栖息点
          const idleMs = Date.now() - c.lastActiveAt;
          if (idleMs > STROLL_IDLE_MIN_MS && Date.now() > c.nextStrollAt && !getBusy() && watchX === null) {
            const roll = Math.random();
            // 偶尔表演套绳荡飞:鼠标在视窗任意位置(上/下/侧)都行
            if (roll < 0.15 && swingReady()) {
              startSwing();
              scheduleStroll();
              break;
            }
            const perch = roll < 0.7 ? findPerch() : null;
            if (perch) {
              const r = perch.el.getBoundingClientRect();
              startWalk(r.left + perch.offsetX, perch);
            } else {
              startWalk(c.homeX + (Math.random() * 2 - 1) * WANDER);
            }
            scheduleStroll();
          }
          break;
        }
      }
      apply();
    };
    raf = requestAnimationFrame(loop);

    return () => {
      cancelAnimationFrame(raf);
      window.removeEventListener('pointerdown', onActivity);
      window.removeEventListener('keydown', onActivity);
      window.removeEventListener('wheel', onActivity);
      window.removeEventListener('pointermove', onCursor);
      window.removeEventListener('pac-pet-debug', onDebug);
      window.removeEventListener('resize', onResize);
      document.removeEventListener('selectionchange', onSelChange);
      clearTimeout(selTimer);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [elRef, size]);

  return { motion, facing, landSeq, anchored, rope, api: api.current };
}

function floorY(size: number): number {
  return window.innerHeight - size - 2;
}
function clampX(x: number, size: number): number {
  return Math.min(Math.max(x, 4), window.innerWidth - size - 4);
}
