(function (root, factory) {
  const api = factory();
  if (typeof module === "object" && module.exports) {
    module.exports = api;
  }
  root.PetPackCore = api;
})(typeof globalThis !== "undefined" ? globalThis : window, function () {
  const DEFAULT_BODY_ANIMATIONS = [];
  const MOOD_GROUPS = ["click", "drag", "idle", "talk"];
  const INTERACTION_TYPES = [
    {
      key: "idle_interaction",
      label: "待机",
      judgements: [
        ["idle", "待机"],
        ["wander", "闲逛"],
        ["taskbar_top_edge", "任务栏上边缘"],
        ["desktop_top_edge", "桌面上边缘"],
        ["desktop_left_edge", "桌面左边缘"],
        ["desktop_right_edge", "桌面右边缘"],
        ["window_top_edge", "窗口上边缘"],
        ["window_bottom_edge", "窗口下边缘"],
        ["window_left_edge", "窗口左边缘"],
        ["window_right_edge", "窗口右边缘"],
      ],
    },
    {
      key: "mouse_interaction",
      label: "鼠标",
      judgements: [
        ["mouse_near", "鼠标靠近"],
        ["single_click", "单击"],
        ["double_click", "双击"],
        ["rapid_click", "快速连点"],
        ["drag", "拖拽"],
      ],
    },
    {
      key: "application_interaction",
      label: "应用",
      judgements: [
        ["chrome", "Chrome"],
        ["photoshop", "Photoshop"],
        ["steam", "Steam"],
        ["vscode", "VSCode"],
        ["bilibili", "Bilibili"],
      ],
    },
  ];
  INTERACTION_TYPES.push({
    key: "teleport_interaction",
    label: "消失出现",
    judgements: [
      ["disappear", "消失"],
      ["appear", "出现"],
    ],
  });
  INTERACTION_TYPES.push({
    key: "pomodoro_interaction",
    label: "番茄钟",
    judgements: [
      ["pomodoro_start", "开始"],
      ["pomodoro_running", "进行中"],
      ["pomodoro_finish", "完成"],
    ],
  });
  INTERACTION_TYPES.push({
    key: "rest_mode_interaction",
    label: "休息模式",
    judgements: [
      ["rest_mode_enter", "进入"],
      ["rest_mode_running", "休息中"],
      ["rest_mode_exit", "退出"],
    ],
  });
  INTERACTION_TYPES.push({
    key: "water_reminder_interaction",
    label: "喝水提醒",
    judgements: [
      ["water_reminder_due", "提醒"],
      ["water_reminder_snoozed", "延后"],
      ["water_reminder_completed", "完成"],
    ],
  });
  INTERACTION_TYPES.push({
    key: "sedentary_reminder_interaction",
    label: "久坐提醒",
    judgements: [
      ["sedentary_reminder_due", "提醒"],
      ["sedentary_rest_started", "开始休息"],
      ["sedentary_rest_completed", "回到工作"],
    ],
  });
  const WIDGET_INTERACTION_CARDS = [
    {
      key: "pomodoro",
      typeKey: "pomodoro_interaction",
      label: "番茄钟",
      description: "开始、专注中、完成时播放的阶段动画。",
    },
    {
      key: "rest_mode",
      typeKey: "rest_mode_interaction",
      label: "休息模式",
      description: "进入休息、休息中、退出休息时播放的阶段动画。",
    },
    {
      key: "water_reminder",
      typeKey: "water_reminder_interaction",
      label: "喝水提醒",
      description: "到点提醒、用户延后、确认喝水时播放的习惯动画。",
    },
    {
      key: "sedentary_reminder",
      typeKey: "sedentary_reminder_interaction",
      label: "久坐提醒",
      description: "久坐提醒、开始休息、回到工作时播放的习惯动画。",
    },
  ];
  const BEHAVIOR_GROUPS = INTERACTION_TYPES
    .filter((type) => type.key !== "application_interaction")
    .flatMap((type) => type.judgements.map(([key]) => key));
  const APP_REACTION_KEYS = ["chrome", "photoshop", "vscode", "steam", "bilibili"];
  const SCENE_APP_CARDS = [
    { key: "vscode", label: "VSCode", iconLabel: "VS", group: "application", app: "vscode" },
    { key: "bilibili", label: "Bilibili", iconLabel: "Bi", group: "application", app: "bilibili" },
    { key: "photoshop", label: "Photoshop", iconLabel: "Ps", group: "application", app: "photoshop" },
    { key: "steam", label: "Steam", iconLabel: "St", group: "application", app: "steam" },
    { key: "chrome", label: "Chrome", iconLabel: "Ch", group: "application", app: "chrome" },
    { key: "camera_on", label: "摄像头开启", iconLabel: "Cam", group: "application", app_category: "meeting" },
    { key: "voice_chat", label: "语音聊天", iconLabel: "Vo", group: "application", app_category: "meeting" },
    { key: "game", label: "游戏", iconLabel: "Game", group: "application", app_category: "game" },
  ];
  const SCENE_APP_EVENTS = [
    { key: "start", label: "启动", trigger: "app_enter" },
    { key: "duration", label: "使用时长", trigger: "app_duration" },
    { key: "close", label: "关闭", trigger: "app_leave" },
  ];
  const LOOP_SEGMENTS = ["intro", "loop", "outro"];
  const LOOP_SEGMENT_LABELS = {
    intro: "渐入动画",
    loop: "循环动画",
    outro: "渐出动画",
  };
  const ORDINARY_MAX_PET_BEHAVIORS = 3;
  const DEVELOPER_MAX_PET_BEHAVIORS = 10;
  const FREE_FRAME_SIZE_LIMIT = 128;
  const LOOP_DURATION_LIMITS = { min: 0, max: 86400 };
  const INTERACTION_RULE_BINDINGS = [
    ["idle_interaction", "idle", "idle", 180],
    ["idle_interaction", "wander", "wander", 60],
    ["mouse_interaction", "mouse_near", "mouse_near", 8],
    ["mouse_interaction", "single_click", "single_click", 8],
    ["mouse_interaction", "double_click", "double_click", 20],
    ["mouse_interaction", "rapid_click", "rapid_click", 12],
    ["mouse_interaction", "drag", "drag", 2],
    ["idle_interaction", "window_top_edge", "window_top_edge", 120],
    ["idle_interaction", "window_bottom_edge", "window_bottom_edge", 120],
    ["idle_interaction", "window_left_edge", "window_left_edge", 120],
    ["idle_interaction", "window_right_edge", "window_right_edge", 120],
    ["idle_interaction", "taskbar_top_edge", "taskbar_top_edge", 120],
    ["idle_interaction", "desktop_top_edge", "desktop_top_edge", 120],
    ["idle_interaction", "desktop_left_edge", "desktop_left_edge", 120],
    ["idle_interaction", "desktop_right_edge", "desktop_right_edge", 120],
  ];
  const APP_INTERACTION_DEFAULT_COOLDOWN = 600;
  const TELEPORT_INTERACTION_TYPE = "teleport_interaction";
  const MOUSE_INTERACTION_DEFAULT_OFF_TYPES = new Set([
    "application_interaction",
    TELEPORT_INTERACTION_TYPE,
    "pomodoro_interaction",
    "rest_mode_interaction",
  ]);
  const COOLDOWN_LIMITS = { min: 0, max: 86400 };
  const MOVEMENT_SPEED_LIMITS = { min: 1, max: 50 };
  const MOVEMENT_DIRECTIONS = ["up", "down", "left", "right", "random"];
  const MOVEMENT_MODES = ["directed", "random_wander", "straight_wander"];
  const ANIMATION_ANCHOR_POINT_KEYS = ["top", "bottom", "left", "right", "scale"];

  // Judgement keys that can produce substantial apparent or runtime motion.
  // Mirrors client/pack/interaction_rules.py MOTION_JUDGEMENT_KEYS so DIY
  // warnings + the rhythm simulator agree with the runtime engine.
  const MOTION_JUDGEMENT_KEYS = new Set([
    "walk", "wander",
    "taskbar_top_edge", "desktop_top_edge", "desktop_left_edge", "desktop_right_edge",
    "window_top_edge", "window_bottom_edge", "window_left_edge", "window_right_edge",
  ]);

  // Pacing presets surfaced by the DIY "活跃度" slider. Each maps to the
  // manifest.behavior_tempo knobs the runtime reads (idle dwell between active
  // behaviours + shared cooldown across all motion judgements).
  const BEHAVIOR_TEMPO_PRESETS = {
    calm: { label: "安静陪伴", idle_dwell_seconds: [18, 40], motion_cooldown_seconds: 90 },
    lively: { label: "日常活泼", idle_dwell_seconds: [8, 20], motion_cooldown_seconds: 45 },
    drama: { label: "戏精表演", idle_dwell_seconds: [3, 9], motion_cooldown_seconds: 18 },
  };
  const BEHAVIOR_TEMPO_PROFILES = ["calm", "lively", "drama"];
  const DEFAULT_BEHAVIOR_TEMPO_PROFILE = "lively";
  const OFFICIAL_TRIGGER_CONDITION_TYPES = [
    { key: "time_window", label: "时间窗口" },
    { key: "duration", label: "持续状态" },
    { key: "frequency", label: "事件频率" },
    { key: "app_match", label: "应用匹配" },
    { key: "activity_pattern", label: "行为模式" },
    { key: "history_trend", label: "历史趋势" },
    { key: "pet_relationship", label: "宠物关系" },
  ];
  const OFFICIAL_SCENE_REGISTRY = [
    {
      id: "late_night_work",
      label: "凌晨还在工作",
      group: "workload",
      conditionTypes: ["time_window", "app_match", "duration"],
      defaults: {
        time_window: { start: "23:00", end: "03:00", days: "any" },
        app_match: { mode: "foreground", categories: ["coding", "document", "design"] },
        duration: { state: "active_matching_app", minutes: 5 },
        cooldown_seconds: 1800,
      },
    },
    {
      id: "long_no_rest",
      label: "长时间没休息",
      group: "workload",
      conditionTypes: ["duration", "activity_pattern"],
      defaults: {
        duration: { state: "no_rest", minutes: 90 },
        activity_pattern: { pattern: "active_without_rest", intensity: "normal" },
        cooldown_seconds: 3600,
      },
    },
    {
      id: "window_switching",
      label: "连续切窗口",
      group: "attention",
      conditionTypes: ["frequency", "activity_pattern"],
      defaults: {
        frequency: { event: "window_switch", window_minutes: 3, min_count: 12 },
        activity_pattern: { pattern: "frequent_switching", intensity: "normal" },
        cooldown_seconds: 1200,
      },
    },
    {
      id: "high_alt_tab",
      label: "高频 Alt+Tab",
      group: "attention",
      conditionTypes: ["frequency"],
      defaults: {
        frequency: { event: "alt_tab", window_minutes: 2, min_count: 8 },
        cooldown_seconds: 1200,
      },
    },
    {
      id: "vscode_focus",
      label: "一直在 VSCode",
      group: "application",
      conditionTypes: ["app_match", "duration"],
      defaults: {
        app_match: { mode: "foreground", apps: ["vscode"], categories: ["coding"] },
        duration: { state: "same_app", minutes: 30 },
        cooldown_seconds: 1800,
      },
    },
    {
      id: "prd_focus",
      label: "一直在 PRD",
      group: "application",
      conditionTypes: ["app_match", "duration"],
      defaults: {
        app_match: { mode: "foreground", categories: ["document"] },
        duration: { state: "same_app", minutes: 30 },
        cooldown_seconds: 1800,
      },
    },
    {
      id: "deep_coding",
      label: "沉浸编码",
      group: "attention",
      conditionTypes: ["app_match", "activity_pattern", "duration"],
      defaults: {
        app_match: { mode: "foreground", categories: ["coding"] },
        activity_pattern: { pattern: "deep_focus", intensity: "normal" },
        duration: { state: "focused", minutes: 25 },
        cooldown_seconds: 1800,
      },
    },
    {
      id: "deep_video_editing",
      label: "沉浸剪视频",
      group: "attention",
      conditionTypes: ["app_match", "activity_pattern", "duration"],
      defaults: {
        app_match: { mode: "foreground", categories: ["video_editing"] },
        activity_pattern: { pattern: "deep_focus", intensity: "normal" },
        duration: { state: "focused", minutes: 25 },
        cooldown_seconds: 1800,
      },
    },
    {
      id: "gaming",
      label: "玩游戏",
      group: "application",
      conditionTypes: ["app_match", "activity_pattern", "duration"],
      defaults: {
        app_match: { mode: "foreground", categories: ["game"] },
        activity_pattern: { pattern: "gaming", intensity: "normal" },
        duration: { state: "same_app", minutes: 10 },
        cooldown_seconds: 1800,
      },
    },
    {
      id: "slacking",
      label: "摸鱼",
      group: "application",
      conditionTypes: ["time_window", "app_match", "activity_pattern", "duration"],
      defaults: {
        time_window: { days: "workday" },
        app_match: { mode: "foreground", categories: ["entertainment", "video", "social", "game"] },
        activity_pattern: { pattern: "slacking", intensity: "normal" },
        duration: { state: "same_category", minutes: 10 },
        cooldown_seconds: 1800,
      },
    },
    {
      id: "meeting",
      label: "开会",
      group: "application",
      conditionTypes: ["app_match", "activity_pattern", "duration"],
      defaults: {
        app_match: { mode: "foreground", categories: ["meeting"] },
        activity_pattern: { pattern: "meeting", intensity: "normal" },
        duration: { state: "meeting", minutes: 5 },
        cooldown_seconds: 1800,
      },
    },
    {
      id: "yesterday_late_sleep",
      label: "你昨天熬夜了",
      group: "workload",
      conditionTypes: ["time_window", "history_trend"],
      defaults: {
        time_window: { start: "00:00", end: "03:00", period: "yesterday" },
        history_trend: { metric: "late_active_days", period_days: 1, min_count: 1 },
        duration: { state: "active", minutes: 30 },
        cooldown_seconds: 43200,
      },
    },
    {
      id: "frequent_blender",
      label: "你最近经常打开 Blender",
      group: "application",
      conditionTypes: ["app_match", "frequency", "history_trend"],
      defaults: {
        app_match: { mode: "opened_recently", apps: ["blender"], categories: ["design"] },
        frequency: { event: "app_open", window_minutes: 10080, min_count: 5 },
        history_trend: { metric: "app_open_days", period_days: 7, min_count: 5 },
        cooldown_seconds: 43200,
      },
    },
    {
      id: "three_day_overtime",
      label: "你连续三天加班",
      group: "workload",
      conditionTypes: ["time_window", "duration", "history_trend"],
      defaults: {
        time_window: { start: "21:00", end: "03:00", days: "workday" },
        app_match: { mode: "foreground", categories: ["coding", "document", "design"] },
        duration: { state: "late_work", minutes: 60 },
        history_trend: { metric: "overtime_days", period_days: 3, min_count: 3, consecutive_days: 3 },
        cooldown_seconds: 86400,
      },
    },
    {
      id: "long_no_interaction",
      label: "你最近很久没互动",
      group: "relationship",
      conditionTypes: ["pet_relationship", "history_trend"],
      defaults: {
        pet_relationship: { metric: "interaction_gap", pet_id: "active_pet", days: 3 },
        history_trend: { metric: "recent_interactions", period_days: 3, min_count: 0 },
        cooldown_seconds: 43200,
      },
    },
  ];
  const OFFICIAL_SCENE_TYPE_KEY = "official_scene_interaction";

  function defaultJudgementCooldown(typeKey, judgementKey) {
    if (typeKey === TELEPORT_INTERACTION_TYPE) return 0;
    if (typeKey === "application_interaction") return APP_INTERACTION_DEFAULT_COOLDOWN;
    for (const [type, judgement, , cooldown] of INTERACTION_RULE_BINDINGS) {
      if (type === typeKey && judgement === judgementKey) return cooldown;
    }
    return 60;
  }
  const DEVELOPER_MAX_JUDGEMENTS = {
    application_interaction: ["chrome", "photoshop", "steam", "vscode", "bilibili"],
    idle_interaction: [
      "window_bottom_edge",
      "window_left_edge",
      "window_right_edge",
      "desktop_top_edge",
      "desktop_left_edge",
      "desktop_right_edge",
    ],
  };
  const LEGACY_SPATIAL_TYPES = new Set(["window_interaction", "desktop_interaction"]);

  function canonicalInteractionType(typeKey, judgementKey) {
    if (LEGACY_SPATIAL_TYPES.has(typeKey)) return "idle_interaction";
    return typeKey;
  }

  function slugify(value) {
    return String(value || "")
      .trim()
      .toLowerCase()
      .normalize("NFKD")
      .replace(/[\u0300-\u036f]/g, "")
      .replace(/[^a-z0-9]+/g, "_")
      .replace(/^_+|_+$/g, "")
      .replace(/_+/g, "_");
  }

  function buildPackId(name, author, version) {
    const namePart = slugify(name);
    const authorPart = slugify(author);
    const versionPart = slugify(version).replace(/\./g, "_") || "1_0_0";
    if (!namePart && !authorPart) {
      return `pet_${shortHash(`${name}|${author}`)}_${versionPart}`.slice(0, 32);
    }
    const id = [namePart, authorPart, versionPart]
      .filter(Boolean)
      .join("_")
      .slice(0, 32)
      .replace(/^_+|_+$/g, "");
    return /^[a-z0-9_]{3,32}$/.test(id) ? id : "pet_pack_1_0_0";
  }

  function shortHash(value) {
    let hash = 2166136261;
    for (const char of String(value || "")) {
      hash ^= char.codePointAt(0);
      hash = Math.imul(hash, 16777619) >>> 0;
    }
    return hash.toString(36).slice(0, 6);
  }

  function orderAnimationEntries(animations) {
    const ordered = [];
    const seen = new Set();
    const add = (animation, type) => {
      if (!animation || seen.has(animation.key)) return;
      ordered.push({ animation, type });
      seen.add(animation.key);
    };

    for (const spec of DEFAULT_BODY_ANIMATIONS) {
      add(animations?.[spec.key], "body");
    }
    for (const animation of Object.values(animations || {})) {
      if (animation.key?.startsWith("mood_")) add(animation, "mood");
    }
    for (const animation of Object.values(animations || {})) {
      if (/^(micro_|rare_|app_)/.test(animation.key || "")) add(animation, "behavior");
    }
    for (const animation of Object.values(animations || {})) {
      add(animation, "custom");
    }
    return ordered;
  }

  function validateMetadata(metadata) {
    const errors = [];
    const warnings = [];
    const name = String(metadata.name || "").trim();
    const author = String(metadata.author || "").trim();
    const version = String(metadata.version || "").trim();
    const description = String(metadata.description || "").trim();

    if (!name) errors.push("宠物名称为必填项。");
    if (name.length > 32) errors.push("宠物名称不能超过 32 个字符。");
    if (!author) errors.push("作者为必填项。");
    if (author.length > 64) errors.push("作者不能超过 64 个字符。");
    if (!/^\d+\.\d+\.\d+$/.test(version)) {
      errors.push("版本号必须使用 x.y.z 格式，例如 1.0.0。");
    }
    if (!description) warnings.push("未填写描述，导出的宠物包仍可使用。");

    const id = buildPackId(name, author, version);
    if (!/^[a-z0-9_]{3,32}$/.test(id)) {
      errors.push("自动生成的包 ID 不合法，请调整宠物名称、作者或版本。");
    }
    return { errors, warnings, id };
  }

  function inferFrameSize(frames) {
    const validFrames = (frames || []).filter(Boolean);
    const errors = [];
    if (!validFrames.length) {
      return { frameSize: null, errors: ["没有可用帧。"] };
    }
    const first = validFrames[0];
    const positive = validFrames.every((frame) => frame.width > 0 && frame.height > 0);
    const same = validFrames.every((frame) => frame.width === first.width && frame.height === first.height);
    if (!positive || !same) {
      errors.push("所有动画帧必须使用相同的宽高尺寸。");
    }
    return {
      frameSize: errors.length ? null : Math.max(first.width, first.height),
      frameWidth: errors.length ? null : first.width,
      frameHeight: errors.length ? null : first.height,
      errors,
    };
  }

  function inferSheet(sheet) {
    if (!sheet || !sheet.width || !sheet.height) {
      return { frameSize: null, frames: 0, errors: ["sprite sheet 无法读取尺寸。"] };
    }
    if (sheet.height <= 0 || sheet.width % sheet.height !== 0) {
      return { frameSize: null, frames: 0, errors: ["sprite sheet 宽度必须是高度的整数倍。"] };
    }
    return {
      frameSize: sheet.height,
      frameWidth: sheet.height,
      frameHeight: sheet.height,
      frames: sheet.width / sheet.height,
      errors: [],
    };
  }

  function isLoopAnimation(animation) {
    return Boolean(animation && (animation.kind === "loop" || animation.type === "loop"));
  }

  function animationSegment(animation, segment) {
    return animation?.segments?.[segment] || { frames: [], sheet: null };
  }

  function animationSegmentFrameCount(segment) {
    if (!segment) return 0;
    if (isVideoAnimationAsset(segment)) return animationHasAsset(segment) ? 1 : 0;
    if (segment.frames && segment.frames.length) return segment.frames.length;
    if (segment.sheet) return inferSheet(segment.sheet).frames;
    return 0;
  }

  function animationAssetType(animation) {
    const type = String(animation?.assetType || "").trim().toLowerCase();
    if (type === "webm") return "webm";
    if (animation?.video) return "webm";
    if (animation?.sheet) return "sprite_sheet";
    return "png_frames";
  }

  function isVideoAnimationAsset(animation) {
    return animationAssetType(animation) === "webm";
  }

  function videoMetadata(animation) {
    return animation?.video || null;
  }

  function importedPackSignatureFromManifest(manifest) {
    const signature = manifest?.ownership_signature;
    return typeof signature === "string" && signature.trim() ? signature.trim() : null;
  }

  function validateImportedPackOwnership(project) {
    if (!project?.importedPackId) return "";
    const signature = project?.importedPackSignature;
    if (typeof signature !== "string" || !signature.trim()) {
      return "Imported petpack needs a server ownership signature before re-export.";
    }
    return "";
  }

  function animationHasAsset(animation) {
    if (!animation) return false;
    if (isVideoAnimationAsset(animation)) {
      const video = videoMetadata(animation);
      return Boolean(video && video.width > 0 && video.height > 0 && Number(video.durationSeconds) > 0);
    }
    return animationFrameCount(animation) > 0;
  }

  function animationFrameCount(animation) {
    if (!animation) return 0;
    if (isVideoAnimationAsset(animation)) return animationHasAsset(animation) ? 1 : 0;
    if (isLoopAnimation(animation)) {
      return LOOP_SEGMENTS.reduce(
        (sum, segment) => sum + animationSegmentFrameCount(animationSegment(animation, segment)),
        0,
      );
    }
    return animationSegmentFrameCount(animation);
  }

  function animationSegmentFrameSize(segment) {
    if (!segment) return { frameSize: null, errors: ["没有上传动画素材。"] };
    if (isVideoAnimationAsset(segment)) return animationFrameSize(segment);
    if (segment.frames && segment.frames.length) return inferFrameSize(segment.frames);
    if (segment.sheet) {
      const sheetResult = inferSheet(segment.sheet);
      return {
        frameSize: sheetResult.frameSize,
        frameWidth: sheetResult.frameWidth,
        frameHeight: sheetResult.frameHeight,
        errors: sheetResult.errors,
      };
    }
    return { frameSize: null, errors: ["没有上传动画素材。"] };
  }

  function animationFrameSize(animation) {
    if (!animation) return { frameSize: null, errors: ["动画为空。"] };
    if (isVideoAnimationAsset(animation)) {
      const video = videoMetadata(animation);
      const errors = [];
      if (!video) errors.push("没有上传 WebM 视频。");
      if (video && !String(video.name || "").toLowerCase().endsWith(".webm")) {
        errors.push("WebM 文件后缀必须是 .webm。");
      }
      if (!video || !(video.width > 0) || !(video.height > 0)) {
        errors.push("WebM 无法读取有效宽高。");
      }
      if (!video || !(Number(video.durationSeconds) > 0)) {
        errors.push("WebM duration 必须大于 0。");
      }
      return {
        frameSize: errors.length ? null : Math.max(video.width, video.height),
        frameWidth: errors.length ? null : video.width,
        frameHeight: errors.length ? null : video.height,
        durationSeconds: errors.length ? null : Number(video.durationSeconds),
        errors,
      };
    }
    if (isLoopAnimation(animation)) return animationSegmentFrameSize(animationSegment(animation, "loop"));
    return animationSegmentFrameSize(animation);
  }

  function validateAnimation(animation, allFrameSizes) {
    const errors = [];
    const warnings = [];
    const key = animation.key;
    if (isVideoAnimationAsset(animation)) {
      const sizeResult = animationFrameSize(animation);
      errors.push(...sizeResult.errors.map((message) => `${key}: ${message}`));
      if (sizeResult.frameWidth && sizeResult.frameHeight) {
        allFrameSizes.push({
          key,
          frameSize: sizeResult.frameSize,
          frameWidth: sizeResult.frameWidth,
          frameHeight: sizeResult.frameHeight,
        });
      }
      if (!Number.isInteger(animation.fps) || animation.fps < 1 || animation.fps > 60) {
        errors.push(`${key}: WebM fps 必须是 1-60 的整数。`);
      }
      return { errors, warnings };
    }
    const hasAsset = animationHasAsset(animation);
    if (!hasAsset) {
      if (animation.required) errors.push(`缺少必传动画 ${key}。`);
      return { errors, warnings };
    }
    if (isLoopAnimation(animation)) {
      const hasVideoSegment = LOOP_SEGMENTS.some((segmentName) => isVideoAnimationAsset(animationSegment(animation, segmentName)));
      for (const segmentName of LOOP_SEGMENTS) {
        const segment = animationSegment(animation, segmentName);
        const segmentCount = animationSegmentFrameCount(segment);
        if (!segmentCount) {
          errors.push(`${key}.${segmentName}: 缺少${LOOP_SEGMENT_LABELS[segmentName]}素材。`);
          continue;
        }
        const segmentSize = animationSegmentFrameSize(segment);
        errors.push(...segmentSize.errors.map((message) => `${key}.${segmentName}: ${message}`));
        if (segmentSize.frameWidth && segmentSize.frameHeight) {
          allFrameSizes.push({
            key: `${key}.${segmentName}`,
            frameSize: segmentSize.frameSize,
            frameWidth: segmentSize.frameWidth,
            frameHeight: segmentSize.frameHeight,
          });
        }
      }
      if (
        animation.loopDurationSeconds !== null
        && animation.loopDurationSeconds !== undefined
        && animation.loopDurationSeconds !== ""
      ) {
        const duration = Number(animation.loopDurationSeconds);
        if (
          !Number.isFinite(duration)
          || duration < LOOP_DURATION_LIMITS.min
          || duration > LOOP_DURATION_LIMITS.max
        ) {
          errors.push(`${key}: loop duration must be ${LOOP_DURATION_LIMITS.min}-${LOOP_DURATION_LIMITS.max} seconds, or infinite.`);
        }
      }
      const fpsMax = hasVideoSegment ? 60 : 30;
      if (!Number.isInteger(animation.fps) || animation.fps < 1 || animation.fps > fpsMax) {
        errors.push(`${key}: fps must be an integer from 1 to ${fpsMax}.`);
      }
      return { errors, warnings };
    }
    const sizeResult = animationFrameSize(animation);
    errors.push(...sizeResult.errors.map((message) => `${key}: ${message}`));
    if (sizeResult.frameWidth && sizeResult.frameHeight) {
      allFrameSizes.push({
        key,
        frameSize: sizeResult.frameSize,
        frameWidth: sizeResult.frameWidth,
        frameHeight: sizeResult.frameHeight,
      });
    }
    if (!Number.isInteger(animation.fps) || animation.fps < 1 || animation.fps > 30) {
      errors.push(`${key}: fps 必须是 1-30 的整数。`);
    }
    return { errors, warnings };
  }

  function isMotionJudgementKey(judgementKey) {
    return MOTION_JUDGEMENT_KEYS.has(String(judgementKey || ""));
  }

  // Resolve the project's pacing knobs into the manifest.behavior_tempo shape.
  // Accepts either a preset key string or an object {profile, idle_dwell_seconds,
  // motion_cooldown_seconds}; explicit numeric overrides win over the preset.
  function resolveBehaviorTempo(project) {
    const raw = (project && project.behaviorTempo) || {};
    const profileKey = typeof raw === "string" ? raw : raw.profile;
    const profile = BEHAVIOR_TEMPO_PRESETS[profileKey] ? profileKey : DEFAULT_BEHAVIOR_TEMPO_PROFILE;
    const preset = BEHAVIOR_TEMPO_PRESETS[profile];
    const dwellRaw = (typeof raw === "object" && Array.isArray(raw.idle_dwell_seconds) && raw.idle_dwell_seconds.length === 2)
      ? raw.idle_dwell_seconds
      : preset.idle_dwell_seconds;
    let lo = Math.max(0, Math.round(Number(dwellRaw[0]) || 0));
    let hi = Math.max(0, Math.round(Number(dwellRaw[1]) || 0));
    if (hi < lo) { const tmp = lo; lo = hi; hi = tmp; }
    const motionRaw = (typeof raw === "object" && Number.isFinite(Number(raw.motion_cooldown_seconds)))
      ? Number(raw.motion_cooldown_seconds)
      : preset.motion_cooldown_seconds;
    return {
      profile,
      idle_dwell_seconds: [lo, hi],
      motion_cooldown_seconds: Math.max(0, Math.round(motionRaw)),
    };
  }

  function behaviorTempoFromManifest(manifest) {
    const tempo = (manifest && manifest.behavior_tempo) || {};
    const dwell = Array.isArray(tempo.idle_dwell_seconds) && tempo.idle_dwell_seconds.length === 2
      ? tempo.idle_dwell_seconds.map((v) => Math.max(0, Number(v) || 0))
      : BEHAVIOR_TEMPO_PRESETS[DEFAULT_BEHAVIOR_TEMPO_PROFILE].idle_dwell_seconds.slice();
    let [lo, hi] = dwell;
    if (hi < lo) { const tmp = lo; lo = hi; hi = tmp; }
    const motion = Number.isFinite(Number(tempo.motion_cooldown_seconds))
      ? Math.max(0, Number(tempo.motion_cooldown_seconds))
      : BEHAVIOR_TEMPO_PRESETS[DEFAULT_BEHAVIOR_TEMPO_PROFILE].motion_cooldown_seconds;
    return { idle_dwell_seconds: [lo, hi], motion_cooldown_seconds: motion };
  }

  // Approximate one play-through length (seconds) of a built manifest animation.
  function manifestAnimationDurationSeconds(manifest, animationKey, fallback = 2.0) {
    const anim = (manifest.animations || {})[animationKey];
    if (!anim || typeof anim !== "object") return fallback;
    if (anim.type === "loop" || anim.loop === true && anim.segments) {
      const loopDur = Number(anim.loop_duration_seconds);
      if (Number.isFinite(loopDur) && loopDur > 0) return loopDur;
      return fallback;
    }
    if (Number.isFinite(Number(anim.duration_seconds)) && Number(anim.duration_seconds) > 0) {
      return Number(anim.duration_seconds);
    }
    const frames = Number(anim.frames);
    const fps = Number(anim.fps) || 8;
    if (Number.isFinite(frames) && frames > 0 && fps > 0) return frames / fps;
    return fallback;
  }

  // Tiny deterministic RNG (mulberry32) so previews/tests are reproducible.
  function makeTempoRng(seed) {
    let a = (Number(seed) || 1) >>> 0;
    return function next() {
      a |= 0; a = (a + 0x6d2b79f5) | 0;
      let t = Math.imul(a ^ (a >>> 15), 1 | a);
      t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
      return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
    };
  }

  // Collect the judgements the runtime would autonomously evaluate at animation
  // end (mirrors iter_all_judgements + filter_eligible + context gating):
  // skips official-scene / teleport / feature / application types.
  function collectSimulationJudgements(manifest, { isProUser = false, typeSwitches = {} } = {}) {
    const SKIP_TYPES = new Set([
      "official_scene_interaction", "teleport_interaction", "pomodoro_interaction",
      "rest_mode_interaction", "water_reminder_interaction", "sedentary_reminder_interaction",
      "application_interaction",
    ]);
    const maxSlot = isProUser ? DEVELOPER_MAX_PET_BEHAVIORS : ORDINARY_MAX_PET_BEHAVIORS;
    const types = (manifest.interaction_rules && manifest.interaction_rules.types) || {};
    const out = [];
    for (const [typeKey, typeSpec] of Object.entries(types)) {
      if (SKIP_TYPES.has(typeKey)) continue;
      if (typeSwitches[typeKey] === false) continue;
      const judgements = (typeSpec && typeSpec.judgements) || {};
      for (const [judgementKey, spec] of Object.entries(judgements)) {
        const behaviors = (Array.isArray(spec.pet_behaviors) ? spec.pet_behaviors : [])
          .filter((b) => b && typeof b === "object" && typeof b.animation === "string"
            && Number.isInteger(b.slot) && b.slot >= 1 && b.slot <= maxSlot);
        if (!behaviors.length) continue;
        const weight = behaviors.reduce((sum, b) => sum + (Number(b.weight) > 0 ? Number(b.weight) : 0), 0);
        out.push({
          typeKey,
          judgementKey,
          fullKey: `${typeKey}.${judgementKey}`,
          isIdleDefault: typeKey === "idle_interaction" && judgementKey === "idle",
          isMotion: isMotionJudgementKey(judgementKey),
          behaviors,
          weight: weight > 0 ? weight : behaviors.length,
        });
      }
    }
    return out;
  }

  function pickWeighted(items, weightOf, rng) {
    const total = items.reduce((s, it) => s + Math.max(0, weightOf(it)), 0);
    if (total <= 0) return items[Math.min(items.length - 1, Math.floor(rng() * items.length))];
    let threshold = rng() * total;
    for (const it of items) {
      threshold -= Math.max(0, weightOf(it));
      if (threshold <= 0) return it;
    }
    return items[items.length - 1];
  }

  // Replay the runtime decision loop (dwell gate → prefer active, fall back to
  // idle → per-judgement + shared-motion cooldown) over `durationSeconds` of
  // simulated idle time. Returns the event timeline + summary stats so the DIY
  // preview can show "what the pet does over 5 minutes".
  function simulateBehaviorTimeline(manifest, options = {}) {
    const { durationSeconds = 300, seed = 1, isProUser = false, typeSwitches = {} } = options;
    const tempo = behaviorTempoFromManifest(manifest);
    const candidates = collectSimulationJudgements(manifest, { isProUser, typeSwitches });
    const idleCandidate = candidates.find((c) => c.isIdleDefault) || null;
    const active = candidates.filter((c) => !c.isIdleDefault);
    const rng = makeTempoRng(seed);
    const MOTION_KEY = "__global_motion__";
    const cooldownUntil = Object.create(null);

    const playIdle = (t) => {
      const beh = idleCandidate
        ? pickWeighted(idleCandidate.behaviors, (b) => Number(b.weight) || 1, rng)
        : null;
      const animation = beh ? beh.animation : "idle";
      return { t, kind: "idle", typeKey: "idle_interaction", judgementKey: "idle", animation,
        duration: manifestAnimationDurationSeconds(manifest, animation) };
    };

    const events = [];
    let t = 0;
    let nextActiveAt = 0;
    let guard = 0;
    while (t < durationSeconds && guard++ < 200000) {
      let ev;
      if (t < nextActiveAt) {
        ev = playIdle(t);
      } else {
        let pool = active.filter((c) => t >= (cooldownUntil[c.fullKey] || 0)
          && (!c.isMotion || t >= (cooldownUntil[MOTION_KEY] || 0)));
        let fired = null;
        while (pool.length) {
          const c = pickWeighted(pool, (x) => x.weight, rng);
          const beh = pickWeighted(c.behaviors, (b) => Number(b.weight) || 1, rng);
          if (!beh) { pool = pool.filter((x) => x !== c); continue; }
          const cd = Math.max(0, Number(beh.cooldown_seconds) || 0);
          cooldownUntil[c.fullKey] = t + cd;
          if (c.isMotion && tempo.motion_cooldown_seconds > 0) {
            cooldownUntil[MOTION_KEY] = t + tempo.motion_cooldown_seconds;
          }
          fired = { t, kind: c.isMotion ? "motion" : "active", typeKey: c.typeKey,
            judgementKey: c.judgementKey, animation: beh.animation,
            duration: manifestAnimationDurationSeconds(manifest, beh.animation) };
          const [lo, hi] = tempo.idle_dwell_seconds;
          nextActiveAt = t + (hi > lo ? lo + rng() * (hi - lo) : lo);
          break;
        }
        ev = fired || playIdle(t);
      }
      events.push(ev);
      t += Math.max(0.2, ev.duration || 2.0);
    }

    const motionCount = events.filter((e) => e.kind === "motion").length;
    const activeCount = events.filter((e) => e.kind === "active" || e.kind === "motion").length;
    const minutes = durationSeconds / 60;
    return {
      tempo,
      events,
      stats: {
        durationSeconds,
        totalEvents: events.length,
        idleCount: events.filter((e) => e.kind === "idle").length,
        activeCount,
        motionCount,
        motionPerMinute: minutes > 0 ? motionCount / minutes : 0,
        activePerMinute: minutes > 0 ? activeCount / minutes : 0,
      },
    };
  }

  // Static "rhythm health check": count motion judgements + resolved tempo and
  // return human-readable warnings for the export step (Step 6 体检告警).
  function assessBehaviorTempo(project, manifest) {
    const builtManifest = manifest || buildManifest(project);
    const tempo = behaviorTempoFromManifest(builtManifest);
    const candidates = collectSimulationJudgements(builtManifest, {
      isProUser: !!project?.account?.developerMax,
    });
    const motionJudgements = candidates.filter((c) => c.isMotion);
    const hasIdle = candidates.some((c) => c.isIdleDefault);
    const warnings = [];
    const [dwellLo, dwellHi] = tempo.idle_dwell_seconds;
    const avgDwell = (dwellLo + dwellHi) / 2;

    if (avgDwell < 6) {
      warnings.push(`主动行为间隔偏短（约 ${avgDwell.toFixed(0)} 秒一次），宠物可能显得躁动。建议把"活跃度"调向"安静"一侧，或增大静止驻留时间。`);
    }
    if (motionJudgements.length >= 4 && tempo.motion_cooldown_seconds < 30) {
      warnings.push(`绑定了 ${motionJudgements.length} 个位移类动画，且位移全局冷却仅 ${tempo.motion_cooldown_seconds} 秒，宠物可能频繁在桌面移动。建议提高位移冷却或减少位移类绑定。`);
    }
    if (!hasIdle) {
      warnings.push("尚未为待机（idle）绑定动画。待机是宠物 80% 时间的默认状态，强烈建议至少绑定 1 个安静的待机动画。");
    }
    return {
      tempo,
      profile: resolveBehaviorTempo(project).profile,
      motionJudgementCount: motionJudgements.length,
      hasIdle,
      warnings,
    };
  }

  // Pure anchor-stability analysis. Given per-frame alpha bounding boxes
  // ({x,y,w,h}, top-left origin), measure how far the character's bottom-centre
  // anchor drifts across frames. The DIY upload UI extracts the boxes from the
  // decoded frames (pixels live only in the browser) and feeds them here; the
  // result powers the "锚点漂移 / 尾帧未归位" hints that prevent the runtime
  // teleport-on-switch the matting skill warns about.
  function analyzeAnchorStability(boxes) {
    const valid = (Array.isArray(boxes) ? boxes : []).filter(
      (b) => b && Number.isFinite(b.w) && Number.isFinite(b.h) && b.w > 0 && b.h > 0,
    );
    if (!valid.length) return { measured: false, frames: 0 };
    const anchorX = (b) => b.x + b.w / 2;
    const bottomY = (b) => b.y + b.h;
    const refX = anchorX(valid[0]);
    const refBottom = bottomY(valid[0]);
    let maxDriftX = 0;
    let maxDriftBottom = 0;
    for (const b of valid) {
      maxDriftX = Math.max(maxDriftX, Math.abs(anchorX(b) - refX));
      maxDriftBottom = Math.max(maxDriftBottom, Math.abs(bottomY(b) - refBottom));
    }
    const last = valid[valid.length - 1];
    return {
      measured: true,
      frames: valid.length,
      refAnchorX: refX,
      refBottomY: refBottom,
      maxDriftX,
      maxDriftBottom,
      tailDriftX: Math.abs(anchorX(last) - refX),
      tailDriftBottom: Math.abs(bottomY(last) - refBottom),
    };
  }

  // Turn an analyzeAnchorStability() result into human-readable warnings.
  // Motion-bound animations are expected to travel mid-clip but MUST return to
  // the start anchor on the last frame; non-motion animations should not drift
  // at all. tolerancePx defaults to 4% of the canvas width (min 8px).
  function anchorStabilityWarnings(analysis, options = {}) {
    if (!analysis || !analysis.measured) return [];
    const { isMotion = false, canvasWidth = 0, label = "动画" } = options;
    const tol = Math.max(8, Math.round((Number(canvasWidth) || 0) * 0.04));
    const out = [];
    if (isMotion) {
      if (analysis.tailDriftX > tol) {
        out.push(`位移动画「${label}」最后一帧角色没有回到起始位置（水平偏移约 ${Math.round(analysis.tailDriftX)}px），切换到其它动画时会出现位置跳变。建议让收尾帧把角色走回画面初始锚点。`);
      }
    } else if (analysis.maxDriftX > tol) {
      out.push(`动画「${label}」角色在帧之间水平漂移约 ${Math.round(analysis.maxDriftX)}px。作为原地动作，建议把角色锚点固定在统一位置，否则循环/切换时会抖动。`);
    }
    return out;
  }

  function validateProject(project) {
    const errors = [];
    const warnings = [];
    const account = project.account || {};
    const importedOwnershipError = validateImportedPackOwnership(project);
    if (importedOwnershipError) errors.push(importedOwnershipError);
    const metadata = validateMetadata(project.metadata || {});
    errors.push(...metadata.errors);
    warnings.push(...metadata.warnings);

    const animations = project.animations || {};
    const allFrameSizes = [];
    if (!Object.values(animations).some((animation) => animationHasAsset(animation))) {
      errors.push("至少需要上传 1 个动画。");
    }
    for (const animation of Object.values(animations)) {
      if (!animation) continue;
      const result = validateAnimation(animation, allFrameSizes);
      errors.push(...result.errors);
      warnings.push(...result.warnings);
    }
    const primaryDimensions = allFrameSizes[0] || {};
    const oversizedDimensions = allFrameSizes.find((entry) => (
      entry.frameWidth > FREE_FRAME_SIZE_LIMIT || entry.frameHeight > FREE_FRAME_SIZE_LIMIT
    ));
    if (!account.loggedIn && oversizedDimensions) {
      errors.push(`未登录用户上传图片分辨率最高支持 ${FREE_FRAME_SIZE_LIMIT}x${FREE_FRAME_SIZE_LIMIT}；超过请先登录。`);
    }

    const moodBindings = project.moodBindings || {};
    for (const [group, names] of Object.entries(moodBindings)) {
      for (const name of names || []) {
        const animation = animations[name];
        if (!animation || !animationHasAsset(animation)) {
          errors.push(`心情触发 ${group} 绑定了无效动画 ${name}。`);
        }
      }
    }
    const behaviorBindings = project.behaviorBindings || {};
    for (const [group, names] of Object.entries(behaviorBindings)) {
      for (const name of names || []) {
        const animation = animations[name];
        if (!animation || !animationHasAsset(animation)) {
          errors.push(`行为触发 ${group} 绑定了无效动画 ${name}。`);
        }
      }
    }
    const appReactions = project.appReactions || {};
    for (const [appName, animationNames] of Object.entries(appReactions)) {
      for (const animationName of normalizeAnimationList(animationNames)) {
        const animation = animations[animationName];
        if (!animation || !animationHasAsset(animation)) {
          errors.push(`应用识别 ${appName} 绑定了无效动画 ${animationName}。`);
        }
      }
    }

    // 节奏体检：基于解析后的 behavior_tempo + 位移类绑定给出非阻塞告警。
    try {
      const tempoAssessment = assessBehaviorTempo(project);
      warnings.push(...tempoAssessment.warnings);
    } catch (err) {
      /* tempo assessment is advisory only — never block export on it */
    }

    return {
      errors: [...new Set(errors)],
      warnings: [...new Set(warnings)],
      frameSize: primaryDimensions.frameSize || null,
      frameWidth: primaryDimensions.frameWidth || null,
      frameHeight: primaryDimensions.frameHeight || null,
      frameDimensions: allFrameSizes,
      id: metadata.id,
    };
  }

  function buildAnimationSegmentSpec(key, segmentName, segment, fps) {
    const frames = animationSegmentFrameCount(segment);
    if (!frames) return null;
    const sizeResult = animationSegmentFrameSize(segment);
    if (isVideoAnimationAsset(segment)) {
      if (sizeResult.errors.length) return null;
      const video = videoMetadata(segment);
      return {
        file: `videos/${key}_${segmentName}.webm`,
        format: "webm",
        fps: fps || 30,
        loop: segmentName === "loop",
        duration_seconds: Number(video.durationSeconds),
        frame_width: video.width,
        frame_height: video.height,
      };
    }
    const spec = {
      file: `sprites/${key}_${segmentName}.png`,
      frames,
      fps: fps || 8,
      loop: segmentName === "loop",
    };
    if (!sizeResult.errors.length && sizeResult.frameWidth && sizeResult.frameHeight) {
      spec.frame_size = sizeResult.frameSize;
      spec.frame_width = sizeResult.frameWidth;
      spec.frame_height = sizeResult.frameHeight;
    }
    return spec;
  }

  function coerceLoopDuration(value) {
    if (value === null || value === undefined || value === "") return null;
    const number = Number(value);
    if (!Number.isFinite(number)) return null;
    return Math.max(LOOP_DURATION_LIMITS.min, Math.min(LOOP_DURATION_LIMITS.max, Math.round(number)));
  }

  function normalizeResizeHandleAnchor(anchor, animations) {
    if (!anchor || typeof anchor !== "object") return null;
    const animation = typeof anchor.animation === "string" && animations?.[anchor.animation]
      ? anchor.animation
      : "";
    if (!animation) return null;
    const sourceRight = Math.round(Number(anchor.source_right));
    const sourceBottom = Math.round(Number(anchor.source_bottom));
    if (!Number.isFinite(sourceRight) || !Number.isFinite(sourceBottom)) return null;
    if (sourceRight < 0 || sourceBottom < 0) return null;
    return {
      animation,
      source_right: sourceRight,
      source_bottom: sourceBottom,
    };
  }

  function clampAnchorCoordinate(value, max) {
    const number = Math.round(Number(value));
    if (!Number.isFinite(number)) return null;
    const upper = Math.max(0, Math.round(Number(max) || 0));
    return Math.max(0, Math.min(upper, number));
  }

  function normalizeAnchorPoint(point, frameWidth, frameHeight) {
    if (!point || typeof point !== "object") return null;
    const x = clampAnchorCoordinate(point.x, frameWidth);
    const y = clampAnchorCoordinate(point.y, frameHeight);
    if (x === null || y === null) return null;
    return { x, y };
  }

  function normalizeAnimationAnchors(anchors, frameWidth, frameHeight) {
    if (!anchors || typeof anchors !== "object") return null;
    const normalized = {};
    const points = {};
    for (const key of ANIMATION_ANCHOR_POINT_KEYS) {
      const point = normalizeAnchorPoint(anchors.points?.[key], frameWidth, frameHeight);
      if (point) points[key] = point;
    }
    if (ANIMATION_ANCHOR_POINT_KEYS.every((key) => points[key])) {
      normalized.points = points;
    }

    const rawInset = anchors.source_ground_bottom_inset;
    const inset = Math.round(Number(rawInset));
    if (Number.isFinite(inset) && inset >= 0) {
      normalized.source_ground_bottom_inset = Math.max(0, Math.min(Math.round(Number(frameHeight) || inset), inset));
    } else if (points.bottom) {
      normalized.source_ground_bottom_inset = Math.max(0, Math.round((Number(frameHeight) || points.bottom.y) - points.bottom.y));
    }
    return Object.keys(normalized).length ? normalized : null;
  }

  function attachAnimationAnchors(spec, animation, frameWidth, frameHeight) {
    const anchors = normalizeAnimationAnchors(animation?.anchors, frameWidth, frameHeight);
    if (anchors) spec.anchors = anchors;
    return spec;
  }

  function buildManifest(project) {
    const metadata = project.metadata || {};
    const animations = {};
    const inferredDimensions = [];
    for (const [key, animation] of Object.entries(project.animations || {})) {
      if (!animationHasAsset(animation)) continue;
      if (isVideoAnimationAsset(animation)) {
        const sizeResult = animationFrameSize(animation);
        if (sizeResult.errors.length) continue;
        const video = videoMetadata(animation);
        animations[key] = {
          file: `videos/${key}.webm`,
          format: "webm",
          fps: animation.fps || 30,
          duration_seconds: Number(video.durationSeconds),
          frame_width: video.width,
          frame_height: video.height,
        };
        attachAnimationAnchors(animations[key], animation, video.width, video.height);
        inferredDimensions.push({
          key,
          frameSize: Math.max(video.width, video.height),
          frameWidth: video.width,
          frameHeight: video.height,
        });
        continue;
      }
      const frames = animationFrameCount(animation);
      if (isLoopAnimation(animation)) {
        const segments = {};
        for (const segmentName of LOOP_SEGMENTS) {
          const segmentSpec = buildAnimationSegmentSpec(
            key,
            segmentName,
            animationSegment(animation, segmentName),
            animation.fps,
          );
          if (segmentSpec) segments[segmentName] = segmentSpec;
        }
        if (!segments.intro || !segments.loop || !segments.outro) continue;
        const loopSpec = segments.loop;
        animations[key] = {
          file: loopSpec.file,
          fps: animation.fps || 8,
          loop: true,
          type: "loop",
          loop_duration_seconds: coerceLoopDuration(animation.loopDurationSeconds),
          segments,
        };
        if (loopSpec.format) animations[key].format = loopSpec.format;
        if (loopSpec.frames) animations[key].frames = loopSpec.frames;
        if (loopSpec.duration_seconds) animations[key].duration_seconds = loopSpec.duration_seconds;
        if (loopSpec.frame_width) animations[key].frame_width = loopSpec.frame_width;
        if (loopSpec.frame_height) animations[key].frame_height = loopSpec.frame_height;
        if (loopSpec.frame_width && loopSpec.frame_height) {
          inferredDimensions.push({
            key,
            frameSize: loopSpec.frame_size,
            frameWidth: loopSpec.frame_width,
            frameHeight: loopSpec.frame_height,
          });
        }
        attachAnimationAnchors(
          animations[key],
          animation,
          loopSpec.frame_width || project.frameWidth || project.frameSize || 128,
          loopSpec.frame_height || project.frameHeight || project.frameSize || 128,
        );
        continue;
      }
      const sizeResult = animationFrameSize(animation);
      const spec = {
        file: `sprites/${key}.png`,
        frames,
        fps: animation.fps || 8,
        loop: animation.loop !== false,
      };
      if (!sizeResult.errors.length && sizeResult.frameWidth && sizeResult.frameHeight) {
        spec.frame_size = sizeResult.frameSize;
        spec.frame_width = sizeResult.frameWidth;
        spec.frame_height = sizeResult.frameHeight;
        attachAnimationAnchors(spec, animation, sizeResult.frameWidth, sizeResult.frameHeight);
        inferredDimensions.push({
          key,
          frameSize: sizeResult.frameSize,
          frameWidth: sizeResult.frameWidth,
          frameHeight: sizeResult.frameHeight,
        });
      }
      animations[key] = spec;
    }
    const primaryDimensions = inferredDimensions[0] || {};
    const frameSize = project.frameSize || primaryDimensions.frameSize || 128;
    const frameWidth = project.frameWidth || primaryDimensions.frameWidth || frameSize;
    const frameHeight = project.frameHeight || primaryDimensions.frameHeight || frameSize;
    const moodAnimations = {};
    for (const group of MOOD_GROUPS) {
      const names = (project.moodBindings || {})[group] || [];
      const valid = names.filter((name) => animations[name]);
      if (valid.length) moodAnimations[group] = valid;
    }
    const behaviorAnimations = {};
    for (const group of BEHAVIOR_GROUPS) {
      const owningType = INTERACTION_TYPES.find((type) => type.judgements.some(([key]) => key === group));
      if (owningType && isDeveloperMaxJudgement(owningType.key, group) && !project.account?.developerMax) continue;
      const names = (project.behaviorBindings || {})[group] || [];
      const valid = names.filter((name) => animations[name]);
      if (valid.length) behaviorAnimations[group] = valid;
    }
    const appReactions = {};
    for (const [appName, animationName] of Object.entries(project.appReactions || {})) {
      if (isDeveloperMaxJudgement("application_interaction", appName) && !project.account?.developerMax) continue;
      const valid = normalizeAnimationList(animationName).filter((name) => animations[name]);
      if (valid.length) appReactions[appName] = valid[0];
    }
    const interactionRules = buildInteractionRules({ ...project, animations });
    const officialSceneInteraction = buildOfficialSceneInteraction({ ...project, animations });
    const manifest = {
      schema_version: "1.0",
      id: buildPackId(metadata.name, metadata.author, metadata.version),
      name: String(metadata.name || "").trim(),
      author: String(metadata.author || "").trim(),
      version: String(metadata.version || "1.0.0").trim(),
      description: String(metadata.description || "").trim(),
      is_fan_made: true,
      frame_size: frameSize,
      frame_width: frameWidth,
      frame_height: frameHeight,
      display_size: project.displaySize || 128,
      scale: 1,
      entitlement: project.account?.developerMax
        ? {
            tier: "max",
            badge: "MAX创作者",
            behavior_slots: DEVELOPER_MAX_PET_BEHAVIORS,
            priority_support: true,
          }
        : {
            tier: project.account?.loggedIn ? "free_logged_in" : "guest",
            behavior_slots: ORDINARY_MAX_PET_BEHAVIORS,
          },
      animations,
      behavior: {
        idle_to_walk_seconds: [8, 20],
        walk_speed_px_per_sec: 30,
        sleep_after_idle_seconds: 120,
        window_behavior_chance: 0.28,
      },
      // Live pacing knob consumed by the runtime engine (idle dwell between
      // active behaviours + shared cooldown across motion judgements).
      behavior_tempo: resolveBehaviorTempo(project),
    };
    const resizeHandleAnchor = normalizeResizeHandleAnchor(project.resizeHandleAnchor, animations);
    if (resizeHandleAnchor) {
      manifest.ui_capabilities = {
        resize_handle: true,
        resize_handle_anchor: resizeHandleAnchor,
      };
    }
    if (project.account?.developerMax) {
      manifest.creator_entitlement = {
        level: "developer_max",
        badge: "MAX创作者",
        behavior_slots: DEVELOPER_MAX_PET_BEHAVIORS,
      };
    }
    if (Object.keys(moodAnimations).length) manifest.mood_animations = moodAnimations;
    if (Object.keys(behaviorAnimations).length) manifest.behavior_animations = behaviorAnimations;
    if (Object.keys(appReactions).length) manifest.app_reactions = appReactions;
    const processBehaviorRules = normalizeProcessBehaviorRules(project.processBehaviorRules, animations);
    if (processBehaviorRules.length) manifest.process_behavior_rules = processBehaviorRules;
    if (officialSceneInteraction) {
      const rules = interactionRules || { version: 2, types: {} };
      rules.version = Math.max(3, Number(rules.version) || 2);
      rules.types[OFFICIAL_SCENE_TYPE_KEY] = officialSceneInteraction;
      manifest.interaction_rules = rules;
    } else if (interactionRules) {
      manifest.interaction_rules = interactionRules;
    }
    if (typeof project.ownershipSignature === "string" && project.ownershipSignature.trim()) {
      manifest.ownership_signature = project.ownershipSignature.trim();
    }
    return manifest;
  }

  function buildInteractionRules(project) {
    const animations = project.animations || {};
    const rules = { version: 2, types: {} };
    const behaviorBindings = project.behaviorBindings || {};
    const maxPetBehaviors = project.account?.developerMax
      ? DEVELOPER_MAX_PET_BEHAVIORS
      : ORDINARY_MAX_PET_BEHAVIORS;

    const interactionConfig = project.interactionConfig || {};
    const handledJudgements = new Set();
    for (const [typeKey, judgementKey, bindingKey, cooldownSeconds] of INTERACTION_RULE_BINDINGS) {
      if (isDeveloperMaxJudgement(typeKey, judgementKey) && !project.account?.developerMax) continue;
      handledJudgements.add(`${typeKey}.${judgementKey}`);
      const judgementConfig = interactionConfig[typeKey]?.judgements?.[judgementKey] || {};
      const behaviors = petBehaviorsFromNames(
        behaviorBindings[bindingKey],
        animations,
        maxPetBehaviors,
        judgementConfig.weights,
        {},
        {
          cooldowns: judgementConfig.cooldowns,
          fallbackCooldown: cooldownSeconds,
          includeCooldown: true,
          mouseInteractions: judgementConfig.mouseInteractions,
          movement: supportsBehaviorMovement(typeKey, judgementKey) ? judgementConfig.movement : null,
          defaultMovementMode: defaultMovementModeForJudgement(typeKey, judgementKey),
          typeKey,
        },
      );
      if (behaviors.length) {
        addJudgement(
          rules,
          typeKey,
          judgementKey,
          behaviors,
        );
      }
    }

    for (const type of INTERACTION_TYPES) {
      if (type.key === "application_interaction" || type.key === "teleport_interaction") continue;
      for (const [judgementKey] of type.judgements) {
        if (handledJudgements.has(`${type.key}.${judgementKey}`)) continue;
        if (isDeveloperMaxJudgement(type.key, judgementKey) && !project.account?.developerMax) continue;
        const judgementConfig = interactionConfig[type.key]?.judgements?.[judgementKey] || {};
        const behaviors = petBehaviorsFromNames(
          behaviorBindings[judgementKey],
          animations,
          maxPetBehaviors,
          judgementConfig.weights,
          {},
          {
            cooldowns: judgementConfig.cooldowns,
            fallbackCooldown: defaultJudgementCooldown(type.key, judgementKey),
            includeCooldown: true,
            mouseInteractions: judgementConfig.mouseInteractions,
            movement: supportsBehaviorMovement(type.key, judgementKey) ? judgementConfig.movement : null,
            defaultMovementMode: defaultMovementModeForJudgement(type.key, judgementKey),
            typeKey: type.key,
          },
        );
        if (behaviors.length) {
          addJudgement(rules, type.key, judgementKey, behaviors);
        }
      }
    }

    for (const [appName, names] of Object.entries(project.appReactions || {})) {
      if (isDeveloperMaxJudgement("application_interaction", appName) && !project.account?.developerMax) continue;
      const judgementConfig = interactionConfig.application_interaction?.judgements?.[appName] || {};
      const behaviors = petBehaviorsFromNames(
        names,
        animations,
        maxPetBehaviors,
        judgementConfig.weights,
        {},
        {
          cooldowns: judgementConfig.cooldowns,
          fallbackCooldown: APP_INTERACTION_DEFAULT_COOLDOWN,
          includeCooldown: true,
          leaveWake: judgementConfig.leaveWake,
          includeLeaveWake: true,
          mouseInteractions: judgementConfig.mouseInteractions,
          typeKey: "application_interaction",
        },
      );
      if (behaviors.length) {
        addJudgement(
          rules,
          "application_interaction",
          appName,
          behaviors,
        );
      }
    }

    for (const judgementKey of ["disappear", "appear"]) {
      const judgementConfig = interactionConfig.teleport_interaction?.judgements?.[judgementKey] || {};
      const behaviors = petBehaviorsFromNames(
        behaviorBindings[judgementKey],
        animations,
        maxPetBehaviors,
        judgementConfig.weights,
        judgementConfig.distances,
        { mouseInteractions: judgementConfig.mouseInteractions, typeKey: "teleport_interaction" },
      );
      if (behaviors.length) {
        addJudgement(rules, "teleport_interaction", judgementKey, behaviors);
      }
    }

    return Object.keys(rules.types).length ? rules : null;
  }

  function normalizeProcessBehaviorRules(rules, animations) {
    return (Array.isArray(rules) ? rules : [])
      .filter((rule) => rule && typeof rule === "object")
      .map((rule, index) => {
        const animation = String(rule.animation || "");
        if (!animation || !animations[animation]) return null;
        const owner = rule.owner && typeof rule.owner === "object" ? rule.owner : {};
        const trigger = normalizeProcessTrigger(rule.trigger);
        if (!trigger) return null;
        const normalizedOwner = {
          type: "pet_behavior",
          interaction_type: String(owner.interaction_type || owner.interactionType || ""),
          judgement: String(owner.judgement || ""),
          slot: Math.max(1, Math.round(Number(owner.slot) || 1)),
        };
        if (!normalizedOwner.interaction_type || !normalizedOwner.judgement) return null;
        const normalized = {
          id: String(rule.id || `process_rule_${index + 1}`),
          owner: normalizedOwner,
          trigger,
          animation,
        };
        const name = String(rule.name || "").trim();
        if (name) normalized.name = name;
        return normalized;
      })
      .filter(Boolean);
  }

  function normalizeProcessTrigger(trigger) {
    if (!trigger || typeof trigger !== "object") return { type: "pomodoro", event: "running" };
    const legacyTypeMap = {
      app_open: "app_enter",
      app_foreground: "app_duration",
    };
    const type = legacyTypeMap[String(trigger.type || "pomodoro")] || String(trigger.type || "pomodoro");
    if (type === "pomodoro") {
      const event = ["start", "running", "finish"].includes(trigger.event) ? trigger.event : "running";
      return { type, event };
    }
    if (type === "app_duration" || type === "app_enter" || type === "app_leave") {
      const appCategory = ["coding", "game", "video"].includes(trigger.app_category)
        ? trigger.app_category
        : (["coding", "game", "video"].includes(trigger.category) ? trigger.category : "coding");
      const normalized = { type, app_category: appCategory };
      const timeRange = trigger.time_range || trigger.timeRange;
      if (timeRange && typeof timeRange === "object") {
        normalized.time_range = {
          start: String(timeRange.start || "00:00"),
          end: String(timeRange.end || "23:59"),
        };
      }
      if (type === "app_duration") {
        const minutes = Math.max(1, Math.round(Number(trigger.duration_minutes || trigger.durationMinutes) || 30));
        normalized.duration_minutes = minutes;
      }
      return normalized;
    }
    if (type === "manual_test") return { type };
    return { type: "pomodoro", event: "running" };
  }

  function officialSceneById(sceneId) {
    return OFFICIAL_SCENE_REGISTRY.find((scene) => scene.id === sceneId) || null;
  }

  function cloneJson(value) {
    return JSON.parse(JSON.stringify(value));
  }

  function mergeConditionDefaults(defaults, overrides) {
    const out = cloneJson(defaults || {});
    for (const [key, value] of Object.entries(overrides || {})) {
      if (value && typeof value === "object" && !Array.isArray(value) && out[key] && typeof out[key] === "object" && !Array.isArray(out[key])) {
        out[key] = { ...out[key], ...value };
      } else {
        out[key] = value;
      }
    }
    return out;
  }

  function normalizeOfficialPetBehaviors(behaviors, animations, maxCount) {
    const seen = new Set();
    return (Array.isArray(behaviors) ? behaviors : [])
      .filter((behavior) => behavior && typeof behavior === "object")
      .filter((behavior) => typeof behavior.animation === "string" && animations[behavior.animation])
      .slice(0, maxCount)
      .map((behavior, index) => {
        const normalized = {
          slot: index + 1,
          animation: behavior.animation,
          weight: 100,
        };
        if (seen.has(normalized.animation)) return null;
        seen.add(normalized.animation);
        return normalized;
      })
      .filter(Boolean);
  }

  function normalizeOfficialSceneRule(rule, animations, maxPetBehaviors) {
    if (!rule || typeof rule !== "object") return null;
    const sceneId = String(rule.scene_id || rule.sceneId || "").trim();
    const scene = officialSceneById(sceneId);
    if (!scene) return null;
    const defaults = scene.defaults || {};
    const conditionDefaults = Object.fromEntries(
      (scene.conditionTypes || [])
        .filter((key) => defaults[key] !== undefined)
        .map((key) => [key, defaults[key]]),
    );
    const petBehaviors = normalizeOfficialPetBehaviors(
      rule.pet_behaviors,
      animations,
      maxPetBehaviors,
    );
    if (!petBehaviors.length) return null;
    return {
      scene_id: scene.id,
      label: scene.label,
      group: scene.group,
      condition_types: [...scene.conditionTypes],
      conditions: mergeConditionDefaults(conditionDefaults, {}),
      cooldown_seconds: coerceCooldown(defaults.cooldown_seconds, defaults.cooldown_seconds ?? 0),
      pet_behaviors: petBehaviors,
    };
  }

  function buildOfficialSceneInteraction(project) {
    const animations = project.animations || {};
    const maxPetBehaviors = project.account?.developerMax
      ? DEVELOPER_MAX_PET_BEHAVIORS
      : ORDINARY_MAX_PET_BEHAVIORS;
    const judgements = {};
    for (const rule of project.officialSceneRules || []) {
      const normalized = normalizeOfficialSceneRule(rule, animations, maxPetBehaviors);
      if (!normalized) continue;
      judgements[normalized.scene_id] = {
        label: normalized.label,
        official_scene: {
          scene_id: normalized.scene_id,
          group: normalized.group,
          condition_types: normalized.condition_types,
          conditions: normalized.conditions,
        },
        cooldown_seconds: normalized.cooldown_seconds,
        pet_behaviors: normalized.pet_behaviors,
      };
    }
    if (!Object.keys(judgements).length) return null;
    return { label: "官方场景", judgements };
  }

  function interactionTypeLabel(typeKey) {
    return INTERACTION_TYPES.find((type) => type.key === typeKey)?.label || typeKey;
  }

  function isDeveloperMaxJudgement(typeKey, judgementKey) {
    return (DEVELOPER_MAX_JUDGEMENTS[typeKey] || []).includes(judgementKey);
  }

  function isRequiredJudgement(typeKey, judgementKey) {
    return typeKey === "idle_interaction" && judgementKey === "idle";
  }

  function supportsBehaviorMovement(typeKey, judgementKey) {
    return typeKey === "idle_interaction" && judgementKey !== "idle";
  }

  function defaultMovementModeForJudgement(typeKey, judgementKey) {
    return typeKey === "idle_interaction" && judgementKey === "wander"
      ? "random_wander"
      : "directed";
  }

  function developerMaxJudgementCount(type) {
    return type.judgements.filter(([judgementKey]) => isDeveloperMaxJudgement(type.key, judgementKey)).length;
  }

  function judgementLabel(typeKey, judgementKey) {
    const type = INTERACTION_TYPES.find((item) => item.key === typeKey);
    return type?.judgements.find(([key]) => key === judgementKey)?.[1] || judgementKey;
  }

  function addJudgement(rules, typeKey, judgementKey, petBehaviors) {
    if (!rules.types[typeKey]) {
      rules.types[typeKey] = { label: interactionTypeLabel(typeKey), judgements: {} };
    }
    const judgement = {
      label: judgementLabel(typeKey, judgementKey),
      pet_behaviors: petBehaviors,
    };
    rules.types[typeKey].judgements[judgementKey] = judgement;
  }

  function defaultMouseInteractionEnabled(typeKey) {
    return !MOUSE_INTERACTION_DEFAULT_OFF_TYPES.has(typeKey);
  }

  function petBehaviorsFromNames(names, animations, maxCount, weights = {}, distances = {}, options = {}) {
    return normalizeAnimationList(names)
      .filter((name) => animations[name])
      .slice(0, maxCount)
      .map((name, index) => {
        const behavior = {
          slot: index + 1,
          animation: name,
          weight: Math.max(1, Math.round(Number(weights[name]) || (100 / Math.min(maxCount, normalizeAnimationList(names).length || 1)))),
        };
        if (options.includeCooldown) {
          behavior.cooldown_seconds = coerceCooldown(options.cooldowns?.[name], options.fallbackCooldown || 0);
        }
        if (distances[name] === "near" || distances[name] === "far") {
          behavior.distance = distances[name];
        }
        const movement = normalizeMovementConfig(
          options.movement?.[name],
          options.defaultMovementMode || "directed",
        );
        if (movement) behavior.movement = movement;
        if (
          options.includeLeaveWake
          && options.leaveWake?.[name]
          && isLoopAnimation(animations[name])
        ) {
          behavior.exit_on_app_leave = true;
        }
        const mouseInteraction = options.mouseInteractions?.[name];
        if (mouseInteraction === false || (mouseInteraction !== true && !defaultMouseInteractionEnabled(options.typeKey))) {
          behavior.ignore_mouse_interaction = true;
        }
        return behavior;
      });
  }

  function normalizeMovementConfig(config, fallbackMode = "directed") {
    if (!config || typeof config !== "object" || !config.enabled) return null;
    const rawMode = String(config.mode || fallbackMode || "directed");
    const mode = rawMode === "auto" || rawMode === "manual"
      ? "directed"
      : (MOVEMENT_MODES.includes(rawMode) ? rawMode : fallbackMode);
    const direction = MOVEMENT_DIRECTIONS.includes(String(config.direction))
      ? String(config.direction)
      : "random";
    const speedSource = config.speed !== undefined
      ? config.speed
      : (config.speed_px_per_sec !== undefined ? Number(config.speed_px_per_sec) / 20 : undefined);
    const movement = {
      enabled: true,
      mode: MOVEMENT_MODES.includes(mode) ? mode : "directed",
      direction,
      speed: coerceMovementSpeed(speedSource),
    };
    for (const field of ["distance_px", "min_distance_px", "max_distance_px"]) {
      const number = Number(config[field]);
      if (Number.isFinite(number) && number > 0) movement[field] = Math.round(number);
    }
    return movement;
  }

  function coerceMovementSpeed(value) {
    const number = Number(value);
    if (!Number.isFinite(number)) return 5;
    return Math.max(
      MOVEMENT_SPEED_LIMITS.min,
      Math.min(MOVEMENT_SPEED_LIMITS.max, Math.round(number)),
    );
  }

  function coerceCooldown(value, fallback) {
    if (value === null || value === undefined || value === "") return fallback;
    const number = Number(value);
    if (!Number.isFinite(number)) return fallback;
    return Math.max(COOLDOWN_LIMITS.min, Math.min(COOLDOWN_LIMITS.max, Math.round(number)));
  }

  function normalizeAnimationList(value) {
    if (Array.isArray(value)) return value.filter((name) => typeof name === "string" && name);
    return typeof value === "string" && value ? [value] : [];
  }

  function textBytes(text) {
    if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(text);
    return Uint8Array.from(Buffer.from(text, "utf8"));
  }

  function utf8FromBytes(bytes) {
    const data = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes || []);
    if (typeof TextDecoder !== "undefined") return new TextDecoder().decode(data);
    return Buffer.from(data).toString("utf8");
  }

  function crc32(bytes) {
    let crc = ~0;
    for (let i = 0; i < bytes.length; i += 1) {
      crc ^= bytes[i];
      for (let j = 0; j < 8; j += 1) {
        crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
      }
    }
    return ~crc >>> 0;
  }

  function concatBytes(parts) {
    const total = parts.reduce((sum, part) => sum + part.length, 0);
    const out = new Uint8Array(total);
    let offset = 0;
    for (const part of parts) {
      out.set(part, offset);
      offset += part.length;
    }
    return out;
  }

  function u16(value) {
    return new Uint8Array([value & 255, (value >>> 8) & 255]);
  }

  function u32(value) {
    return new Uint8Array([
      value & 255,
      (value >>> 8) & 255,
      (value >>> 16) & 255,
      (value >>> 24) & 255,
    ]);
  }

  function createStoredZip(entries) {
    const localParts = [];
    const centralParts = [];
    let offset = 0;
    for (const entry of entries) {
      const pathBytes = textBytes(entry.path.replace(/\\/g, "/"));
      const data = entry.data instanceof Uint8Array ? entry.data : new Uint8Array(entry.data);
      const crc = crc32(data);
      const local = concatBytes([
        u32(0x04034b50),
        u16(20),
        u16(0x0800),
        u16(0),
        u16(0),
        u16(0),
        u32(crc),
        u32(data.length),
        u32(data.length),
        u16(pathBytes.length),
        u16(0),
        pathBytes,
        data,
      ]);
      localParts.push(local);
      centralParts.push(concatBytes([
        u32(0x02014b50),
        u16(20),
        u16(20),
        u16(0x0800),
        u16(0),
        u16(0),
        u16(0),
        u32(crc),
        u32(data.length),
        u32(data.length),
        u16(pathBytes.length),
        u16(0),
        u16(0),
        u16(0),
        u16(0),
        u32(0),
        u32(offset),
        pathBytes,
      ]));
      offset += local.length;
    }
    const central = concatBytes(centralParts);
    const local = concatBytes(localParts);
    const end = concatBytes([
      u32(0x06054b50),
      u16(0),
      u16(0),
      u16(entries.length),
      u16(entries.length),
      u32(central.length),
      u32(local.length),
      u16(0),
    ]);
    return concatBytes([local, central, end]);
  }

  function readU16(bytes, offset) {
    return bytes[offset] | (bytes[offset + 1] << 8);
  }

  function readU32(bytes, offset) {
    return (
      bytes[offset]
      | (bytes[offset + 1] << 8)
      | (bytes[offset + 2] << 16)
      | (bytes[offset + 3] << 24)
    ) >>> 0;
  }

  function findEndOfCentralDirectory(bytes) {
    const minOffset = Math.max(0, bytes.length - 0xffff - 22);
    for (let offset = bytes.length - 22; offset >= minOffset; offset -= 1) {
      if (readU32(bytes, offset) === 0x06054b50) return offset;
    }
    return -1;
  }

  function normalizeZipPath(path) {
    return String(path || "").replace(/\\/g, "/").replace(/^\/+/, "");
  }

  function readStoredZip(bytes) {
    const data = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes || []);
    const endOffset = findEndOfCentralDirectory(data);
    if (endOffset < 0) throw new Error("Invalid .petpack: missing zip directory.");

    const totalEntries = readU16(data, endOffset + 10);
    let offset = readU32(data, endOffset + 16);
    const entries = {};
    for (let index = 0; index < totalEntries; index += 1) {
      if (readU32(data, offset) !== 0x02014b50) {
        throw new Error("Invalid .petpack: broken zip directory.");
      }
      const method = readU16(data, offset + 10);
      const compressedSize = readU32(data, offset + 20);
      const uncompressedSize = readU32(data, offset + 24);
      const nameLength = readU16(data, offset + 28);
      const extraLength = readU16(data, offset + 30);
      const commentLength = readU16(data, offset + 32);
      const localOffset = readU32(data, offset + 42);
      const name = normalizeZipPath(utf8FromBytes(data.slice(offset + 46, offset + 46 + nameLength)));
      if (method !== 0) {
        throw new Error(`Unsupported compressed zip entry: ${name || "(unnamed)"}.`);
      }
      if (compressedSize !== uncompressedSize) {
        throw new Error(`Invalid stored zip entry size: ${name || "(unnamed)"}.`);
      }
      if (readU32(data, localOffset) !== 0x04034b50) {
        throw new Error(`Invalid local zip header: ${name || "(unnamed)"}.`);
      }
      const localNameLength = readU16(data, localOffset + 26);
      const localExtraLength = readU16(data, localOffset + 28);
      const dataOffset = localOffset + 30 + localNameLength + localExtraLength;
      entries[name] = data.slice(dataOffset, dataOffset + compressedSize);
      offset += 46 + nameLength + extraLength + commentLength;
    }
    return entries;
  }

  function findZipEntryPath(entries, path) {
    const wanted = normalizeZipPath(path);
    if (entries[wanted]) return wanted;
    const suffix = `/${wanted}`;
    return Object.keys(entries || {}).find((entryPath) => normalizeZipPath(entryPath).endsWith(suffix)) || "";
  }

  function manifestFrameDimensions(manifest, spec = {}) {
    const frameSize = manifest.frame_size || 128;
    const frameWidth = manifest.frame_width || frameSize;
    const frameHeight = manifest.frame_height || frameSize;
    const specFrameSize = spec.frame_size || frameSize;
    return {
      frameWidth: spec.frame_width || (spec.frame_size ? specFrameSize : frameWidth),
      frameHeight: spec.frame_height || (spec.frame_size ? specFrameSize : frameHeight),
    };
  }

  function importedAnimationSegment(manifest, spec = {}) {
    const dimensions = manifestFrameDimensions(manifest, spec);
    if (String(spec.format || "").toLowerCase() === "webm") {
      return {
        assetType: "webm",
        frames: [],
        sheet: null,
        video: {
          name: normalizeZipPath(spec.file || "").split("/").pop() || "animation.webm",
          width: Number(spec.frame_width) || dimensions.frameWidth,
          height: Number(spec.frame_height) || dimensions.frameHeight,
          durationSeconds: Number(spec.duration_seconds) || 0,
          importFile: normalizeZipPath(spec.file || ""),
        },
        importFile: normalizeZipPath(spec.file || ""),
        anchors: normalizeAnimationAnchors(spec.anchors, dimensions.frameWidth, dimensions.frameHeight),
      };
    }
    return {
      frames: [],
      sheet: null,
      importFile: normalizeZipPath(spec.file || ""),
      importFrameCount: Number(spec.frames) || 0,
      importFrameWidth: dimensions.frameWidth,
      importFrameHeight: dimensions.frameHeight,
      anchors: normalizeAnimationAnchors(spec.anchors, dimensions.frameWidth, dimensions.frameHeight),
    };
  }

  function importedAnimation(manifest, key, spec = {}) {
    const isWebm = spec.type !== "loop" && String(spec.format || "").toLowerCase() === "webm";
    const animation = {
      key,
      required: false,
      fps: Number(spec.fps) || 8,
      loop: spec.loop !== false,
      kind: spec.type === "loop" ? "loop" : "sequence",
      loopDurationSeconds: spec.type === "loop" ? (spec.loop_duration_seconds ?? null) : 10,
      assetType: isWebm ? "webm" : "png_frames",
      frames: [],
      sheet: null,
      video: isWebm
        ? {
            name: normalizeZipPath(spec.file || "").split("/").pop() || `${key}.webm`,
            width: Number(spec.frame_width) || manifestFrameDimensions(manifest, spec).frameWidth,
            height: Number(spec.frame_height) || manifestFrameDimensions(manifest, spec).frameHeight,
            durationSeconds: Number(spec.duration_seconds) || 0,
            importFile: normalizeZipPath(spec.file || ""),
          }
        : null,
      importFile: normalizeZipPath(spec.file || ""),
      importFrameCount: Number(spec.frames) || 0,
      importFrameWidth: manifestFrameDimensions(manifest, spec).frameWidth,
      importFrameHeight: manifestFrameDimensions(manifest, spec).frameHeight,
      anchors: normalizeAnimationAnchors(spec.anchors, manifestFrameDimensions(manifest, spec).frameWidth, manifestFrameDimensions(manifest, spec).frameHeight),
    };
    if (spec.type === "loop") {
      animation.segments = {};
      for (const segmentName of LOOP_SEGMENTS) {
        animation.segments[segmentName] = importedAnimationSegment(
          manifest,
          spec.segments?.[segmentName] || {},
        );
      }
    }
    return animation;
  }

  function mergeUnique(left, right) {
    return [...new Set([...(left || []), ...normalizeAnimationList(right)])];
  }

  function emptyInteractionConfig() {
    return Object.fromEntries(INTERACTION_TYPES.map((type) => [
      type.key,
      {
        judgements: Object.fromEntries(type.judgements.map(([judgementKey]) => [
          judgementKey,
          {
            weights: {},
            cooldowns: {},
            distances: {},
            movement: {},
            mouseInteractions: {},
          },
        ])),
      },
    ]));
  }

  function ensureImportedJudgementConfig(config, typeKey, judgementKey) {
    if (!config[typeKey]) config[typeKey] = { judgements: {} };
    if (!config[typeKey].judgements) config[typeKey].judgements = {};
    if (!config[typeKey].judgements[judgementKey]) {
      config[typeKey].judgements[judgementKey] = {
        weights: {},
        cooldowns: {},
        distances: {},
        movement: {},
        mouseInteractions: {},
      };
    }
    return config[typeKey].judgements[judgementKey];
  }

  function officialSceneRulesFromManifest(manifest, animationKeys) {
    const officialType = manifest.interaction_rules?.types?.[OFFICIAL_SCENE_TYPE_KEY];
    const judgements = officialType?.judgements;
    if (!judgements || typeof judgements !== "object") return [];
    const out = [];
    for (const [judgementKey, judgement] of Object.entries(judgements)) {
      if (!judgement || typeof judgement !== "object") continue;
      const sceneId = String(judgement.official_scene?.scene_id || judgementKey || "").trim();
      const scene = officialSceneById(sceneId);
      if (!scene) continue;
      const petBehaviors = (Array.isArray(judgement.pet_behaviors) ? judgement.pet_behaviors : [])
        .filter((behavior) => behavior && typeof behavior === "object")
        .filter((behavior) => typeof behavior.animation === "string" && animationKeys.has(behavior.animation))
        .sort((a, b) => (a.slot || 0) - (b.slot || 0))
        .map((behavior, index) => {
          const normalized = {
            slot: index + 1,
            animation: behavior.animation,
            weight: Math.max(1, Math.round(Number(behavior.weight) || 100)),
          };
          if (typeof behavior.dialogue === "string" && behavior.dialogue.trim()) {
            normalized.dialogue = behavior.dialogue.trim();
          }
          return normalized;
        });
      if (!petBehaviors.length) continue;
      const normalized = normalizeOfficialSceneRule(
        {
          scene_id: scene.id,
          conditions: judgement.official_scene?.conditions || {},
          cooldown_seconds: judgement.cooldown_seconds,
          pet_behaviors: petBehaviors,
        },
        Object.fromEntries([...animationKeys].map((key) => [key, true])),
        DEVELOPER_MAX_PET_BEHAVIORS,
      );
      if (normalized) out.push(normalized);
    }
    return out;
  }

  function draftFromImportedManifest(manifest) {
    const animations = Object.fromEntries(Object.entries(manifest.animations || {}).map(([key, spec]) => [
      key,
      importedAnimation(manifest, key, spec),
    ]));
    const animationKeys = new Set(Object.keys(animations));
    const moodBindings = Object.fromEntries(MOOD_GROUPS.map((group) => [group, []]));
    const behaviorBindings = Object.fromEntries(BEHAVIOR_GROUPS.map((group) => [group, []]));
    const appReactions = Object.fromEntries(APP_REACTION_KEYS.map((key) => [key, ""]));
    const interactionConfig = emptyInteractionConfig();

    for (const [group, names] of Object.entries(manifest.mood_animations || {})) {
      moodBindings[group] = mergeUnique(moodBindings[group], names).filter((name) => animationKeys.has(name));
    }
    for (const [group, names] of Object.entries(manifest.behavior_animations || {})) {
      behaviorBindings[group] = mergeUnique(behaviorBindings[group], names).filter((name) => animationKeys.has(name));
    }
    for (const [appName, names] of Object.entries(manifest.app_reactions || {})) {
      appReactions[appName] = mergeUnique(normalizeAnimationList(appReactions[appName]), names)
        .filter((name) => animationKeys.has(name));
    }

    const types = manifest.interaction_rules?.types || {};
    for (const [typeKey, typeSpec] of Object.entries(types)) {
      if (typeKey === OFFICIAL_SCENE_TYPE_KEY) continue;
      for (const [judgementKey, judgement] of Object.entries(typeSpec?.judgements || {})) {
        const behaviors = Array.isArray(judgement.pet_behaviors)
          ? [...judgement.pet_behaviors].sort((a, b) => (a.slot || 0) - (b.slot || 0))
          : [];
        const names = behaviors.map((behavior) => behavior.animation).filter((name) => animationKeys.has(name));
        const canonicalTypeKey = canonicalInteractionType(typeKey, judgementKey);
        if (canonicalTypeKey === "application_interaction") {
          appReactions[judgementKey] = mergeUnique(normalizeAnimationList(appReactions[judgementKey]), names);
        } else {
          behaviorBindings[judgementKey] = mergeUnique(behaviorBindings[judgementKey], names);
        }
        const config = ensureImportedJudgementConfig(interactionConfig, canonicalTypeKey, judgementKey);
        const legacyCooldown = typeof judgement.cooldown_seconds === "number"
          ? judgement.cooldown_seconds
          : null;
        for (const behavior of behaviors) {
          const animation = behavior.animation;
          if (!animation || !animationKeys.has(animation)) continue;
          if (typeof behavior.weight === "number") config.weights[animation] = behavior.weight;
          if (typeof behavior.cooldown_seconds === "number") {
            config.cooldowns = config.cooldowns || {};
            config.cooldowns[animation] = behavior.cooldown_seconds;
          } else if (legacyCooldown !== null) {
            config.cooldowns = config.cooldowns || {};
            config.cooldowns[animation] = legacyCooldown;
          }
          if (behavior.distance === "near" || behavior.distance === "far") {
            config.distances[animation] = behavior.distance;
          }
          const movement = normalizeMovementConfig(behavior.movement);
          if (movement) {
            config.movement = config.movement || {};
            config.movement[animation] = movement;
          }
          if (behavior.exit_on_app_leave) {
            config.leaveWake = config.leaveWake || {};
            config.leaveWake[animation] = true;
          }
          if (behavior.ignore_mouse_interaction) {
            config.mouseInteractions = config.mouseInteractions || {};
            config.mouseInteractions[animation] = false;
          }
        }
      }
    }

    return {
      metadata: {
        name: String(manifest.name || ""),
        author: String(manifest.author || ""),
        version: String(manifest.version || "1.0.0"),
        description: String(manifest.description || ""),
      },
      animations,
      moodBindings,
      behaviorBindings,
      appReactions,
      interactionConfig,
      officialSceneRules: officialSceneRulesFromManifest(manifest, animationKeys),
      processBehaviorRules: normalizeProcessBehaviorRules(manifest.process_behavior_rules, animations),
      behaviorTempo: resolveBehaviorTempo({ behaviorTempo: manifest.behavior_tempo }),
      importedPackId: manifest.id || "",
      importedPackSignature: importedPackSignatureFromManifest(manifest),
    };
  }

  return {
    DEFAULT_BODY_ANIMATIONS,
    MOOD_GROUPS,
    BEHAVIOR_GROUPS,
    APP_REACTION_KEYS,
    WIDGET_INTERACTION_CARDS,
    SCENE_APP_CARDS,
    SCENE_APP_EVENTS,
    INTERACTION_TYPES,
    OFFICIAL_TRIGGER_CONDITION_TYPES,
    OFFICIAL_SCENE_REGISTRY,
    DEVELOPER_MAX_JUDGEMENTS,
    ORDINARY_MAX_PET_BEHAVIORS,
    DEVELOPER_MAX_PET_BEHAVIORS,
    FREE_FRAME_SIZE_LIMIT,
    LOOP_SEGMENTS,
    LOOP_SEGMENT_LABELS,
    LOOP_DURATION_LIMITS,
    COOLDOWN_LIMITS,
    MOVEMENT_SPEED_LIMITS,
    MOVEMENT_DIRECTIONS,
    MOVEMENT_MODES,
    ANIMATION_ANCHOR_POINT_KEYS,
    MOTION_JUDGEMENT_KEYS,
    BEHAVIOR_TEMPO_PRESETS,
    BEHAVIOR_TEMPO_PROFILES,
    DEFAULT_BEHAVIOR_TEMPO_PROFILE,
    isMotionJudgementKey,
    resolveBehaviorTempo,
    behaviorTempoFromManifest,
    assessBehaviorTempo,
    simulateBehaviorTimeline,
    analyzeAnchorStability,
    anchorStabilityWarnings,
    defaultMouseInteractionEnabled,
    defaultJudgementCooldown,
    buildPackId,
    orderAnimationEntries,
    validateMetadata,
    inferFrameSize,
    inferSheet,
    isLoopAnimation,
    animationSegment,
    animationSegmentFrameCount,
    animationFrameCount,
    animationHasAsset,
    isVideoAnimationAsset,
    importedPackSignatureFromManifest,
    validateImportedPackOwnership,
    validateProject,
    buildManifest,
    buildInteractionRules,
    isDeveloperMaxJudgement,
    isRequiredJudgement,
    supportsBehaviorMovement,
    defaultMovementModeForJudgement,
    developerMaxJudgementCount,
    textBytes,
    utf8FromBytes,
    createStoredZip,
    readStoredZip,
    findZipEntryPath,
    draftFromImportedManifest,
  };
});
