# 豆豆桌宠宠物包规范 v1.0

> 本文档同时是 AI 生成宠物包的 Prompt 规范参考。  
> 一经发布锁定，不破坏兼容性。后续扩展必须通过新增可选字段实现。

---

## 1. 包结构

宠物包是一个目录，或打包成 `.petpack`（=zip）单文件分发。

```
my_pet/
├── manifest.json         # 必需：元数据 + 动画定义
├── dialogs.json          # 必需：按状态/触发分组的台词
├── sprites/
│   ├── idle.png          # 必需：4 帧 横向 sprite sheet
│   ├── walk.png          # 可选：6 帧，视觉走路效果在帧内完成
│   ├── drag.png          # 可选：2 帧
│   ├── sleep.png         # 可选：4 帧
│   └── talk.png          # 可选：2 帧
├── videos/               # 可选：WebM 动画资源
│   └── special.webm      # 可选：VP9 WebM，适合长动画/大尺寸动画
├── sounds/               # 可选：触发音效
└── preview.png           # 可选：商店/选择器封面 128×128
```

---

## 2. manifest.json schema

```json
{
  "schema_version": "1.0",
  "id": "doudou_default",
  "name": "豆豆",
  "author": "糖盒",
  "version": "1.0.0",
  "description": "圆滚滚的米黄豆子",
  "is_fan_made": false,
  "frame_size": 64,
  "scale": 1,
  "ui_capabilities": {
    "resize_handle": true,
    "resize_handle_anchor": {
      "animation": "idle",
      "source_right": 48,
      "source_bottom": 60
    }
  },
  "animations": {
    "idle":  { "file": "sprites/idle.png",  "frames": 4, "fps": 4 },
    "walk":  { "file": "sprites/walk.png",  "frames": 6, "fps": 8 },
    "drag":  { "file": "sprites/drag.png",  "frames": 2, "fps": 4 },
    "sleep": { "file": "sprites/sleep.png", "frames": 4, "fps": 2 },
    "talk":  { "file": "sprites/talk.png",  "frames": 2, "fps": 6 },
    "special_dance": {
      "file": "videos/special_dance.webm",
      "format": "webm",
      "fps": 30,
      "duration_seconds": 6.0,
      "frame_width": 512,
      "frame_height": 512,
      "anchors": {
        "points": {
          "top": { "x": 256, "y": 42 },
          "bottom": { "x": 256, "y": 472 },
          "left": { "x": 96, "y": 256 },
          "right": { "x": 420, "y": 256 },
          "scale": { "x": 420, "y": 472 }
        },
        "source_ground_bottom_inset": 40
      }
    }
  },
  "behavior_animations": {
    "micro_events": ["micro_yawn"],
    "mouse_chase": ["micro_startled"],
    "double_click": ["rare_angry"],
    "desktop_patrol": ["micro_peek"],
    "rare_events": ["rare_trip"],
    "rare_trip": ["rare_trip"],
    "rare_icon_chase": ["rare_chase"],
    "rare_window_press": ["rare_pressed"],
    "rare_edge_fall": ["rare_fall"]
  },
  "app_reactions": {
    "vscode": "app_vscode",
    "steam": "app_steam"
  },
}
```

### 字段约束

| 字段 | 类型 | 必需 | 约束 |
|---|---|---|---|
| `schema_version` | str | ✓ | 固定 `"1.0"` |
| `id` | str | ✓ | `[a-z0-9_]{3,32}`，全局唯一 |
| `name` | str | ✓ | 1-32 字符 |
| `author` | str | ✓ | 1-64 字符 |
| `version` | str | ✓ | SemVer |
| `is_fan_made` | bool | ✓ | 二创/同人作品须显式 `true` |
| `frame_size` | int | ✓ | 单帧像素宽=高，建议 64 |
| `ui_capabilities.resize_handle` | bool | – | 是否允许显示用户拖拽缩放按钮；默认 `true`。设为 `false` 时宿主/API 仍可设置缩放比例 |
| `ui_capabilities.resize_handle_anchor` | obj | – | 缩放按钮锚点；可选扩展。`animation` 指向用于测量的动画名，`source_right`/`source_bottom` 是该动画源帧画布内角色实际占用区域的右/下边缘像素坐标 |
| `animations` | obj | ✓ | 至少包含一个可播放动画；建议提供 `idle` |
| `animations.<n>.format` | str | – | 省略表示 PNG sprite sheet；可选 `"sprite_sheet"`、`"png"`、`"webm"` |
| `animations.<n>.frames` | int | PNG 必需 | sprite sheet 总帧数，WebM 不需要 |
| `animations.<n>.fps` | int | ✓ | PNG 为 1-30；WebM 为 1-60 |
| `animations.<n>.duration_seconds` | number | WebM 必需 | WebM 动画时长，必须大于 0 |
| `animations.<n>.frame_width` | int | 矩形帧/WebM 必需 | 单帧或视频源宽度 |
| `animations.<n>.frame_height` | int | 矩形帧/WebM 必需 | 单帧或视频源高度 |
| `animations.<n>.anchors.points` | obj | – | 每个动画的五点打点：`top`/`bottom`/`left`/`right` 表示角色实际占用区域的上/下/左/右边缘点，`scale` 表示缩放操作空间的位置；每个点都是源帧坐标 `{ "x": int, "y": int }`，坐标范围为 `0..frame_width/frame_height`，右/下边缘可等于画布宽/高 |
| `animations.<n>.anchors.source_ground_bottom_inset` | int | – | 兼容旧运行时的脚底/地面下边距，通常等于 `frame_height - anchors.points.bottom.y` |
| `behavior.walk_speed_px_per_sec` | int | – | 旧字段，仅兼容读取；运行时忽略 |
| `behavior_animations.<group>` | str[] | – | 指向 `animations` 中的选填动画名 |
| `app_reactions.<app>` | str | – | `chrome`/`photoshop`/`vscode`/`steam`/`bilibili` 等应用反应动画名 |

### PNG sprite sheet 约束

- PNG，透明背景
- 横向排列：`width = frame_width × frames`，`height = frame_height`
- 正方形帧可以只填 `frame_size`；矩形帧请填 `frame_width` 和 `frame_height`
- 像素风格请用最近邻缩放（NEAREST），避免抗锯齿模糊

### WebM 动画约束

- WebM 适合长动画、高分辨率动画、帧数很多的动画。
- WebM 文件建议放在 `videos/` 目录，后缀必须是 `.webm`。
- 当前支持目标是 VP9 WebM。需要透明背景时，请使用带 alpha 的 VP9 WebM。
- WebM 动画必须显式填写 `format: "webm"`、`duration_seconds`、`fps`、`frame_width` 和 `frame_height`。
- WebM 不需要 `frames`，客户端不会在导入时解码视频，也不要求运行时安装 ffmpeg。
- 透明 WebM 的实际效果依赖 Windows 上的 Qt Multimedia 后端，发布前需要在目标机器验证。

### PNG/WebM 混用示例

```json
{
  "animations": {
    "idle": {
      "file": "sprites/idle.png",
      "frames": 4,
      "fps": 4
    },
    "rare_show": {
      "file": "videos/rare_show.webm",
      "format": "webm",
      "fps": 30,
      "duration_seconds": 8.0,
      "frame_width": 834,
      "frame_height": 1112,
      "display_width": 96,
      "display_height": 128
    }
  }
}
```

推荐策略：短小常驻动画继续用 PNG sprite sheet；长动画、大尺寸动画、帧数很多的表演动画优先用 WebM。

### 运行时位移约定

位移属于单个 `pet_behavior`，不再依赖某个固定的 `walk` 动画类型。动画负责表现动作，`movement` 负责移动桌宠窗口坐标；未配置 `movement` 的动作仍在当前位置播放。

待机互动新增 `wander`（闲逛）判定。闲逛可以绑定任意动画，并通过 `movement.mode` 选择：

- `random_wander`：随机闲逛，运行时在安全桌面范围内生成 1-3 段短距离移动。
- `straight_wander`：直线闲逛，按方向移动一段距离。

待机互动里除 `idle` 外，其他判定都可以给每个 `pet_behavior` 配置移动方式：

```json
{
  "slot": 1,
  "animation": "window_top_patrol",
  "weight": 100,
  "cooldown_seconds": 60,
  "movement": {
    "enabled": true,
    "mode": "directed",
    "direction": "right",
    "speed": 5
  }
}
```

- `enabled`：是否启用运行时位移。
- `mode`：`directed`、`random_wander`、`straight_wander`。旧包中的 `auto` / `manual` 会兼容为 `directed`。
- `direction`：`up`、`down`、`left`、`right`、`random`。
- `speed`：创作者填写的移动速度数值，运行时按统一倍率换算为像素/秒。
- `distance_px`：可选直线移动距离。
- `min_distance_px` / `max_distance_px`：可选随机闲逛距离范围。

例如 `window_top_edge` 被触发时，如果选中的行为配置了 `direction: "right"`、`speed: 5`，运行时会先把桌宠移动到目标窗口上边缘的最左侧，然后一边播放该行为动画，一边以速度 5 向右移动到可用边界。动画名不需要叫 `walk`。

### 行为动画约定

- `micro_*`：微事件动画，例如打哈欠、发呆、玩手机。
- `rare_*`：稀有事件动画，例如摔倒、睡着、边缘掉落后的恢复表现。
- `app_*`：应用识别动画，例如 `app_vscode`、`app_steam`。
- 可绑定的行为组包括 `micro_events`、`mouse_chase`、`double_click`、`desktop_patrol`、`rare_events`、`rare_trip`、`rare_icon_chase`、`rare_window_press`、`rare_edge_fall`。
- 这些动画都是可选项。缺失时客户端会回退到通用轻量反馈，不应导致宠物包加载失败。
---

## 3. dialogs.json schema

```json
{
  "schema_version": "1.0",
  "groups": {
    "idle":   ["在干嘛呀？", "今天天气不错", "我有点饿了"],
    "walk":   ["散散步～", "出发！", "去哪里好呢"],
    "drag":   ["哎呀！", "放我下来啦", "嗯～"],
    "sleep":  ["zzz...", "好困..."],
    "click":  ["你好！", "戳我做什么"],
    "pomodoro_done": ["休息一下吧！"]
  }
}
```

每个数组建议 5-10 条，每条 ≤15 字。客户端随机抽取。

---

## 4. AI 生成 Prompt 模板（精简版）

```
你是豆豆桌宠的宠物包元数据生成助手。
请严格按豆豆桌宠宠物包 v1.0 规范，生成 manifest.json 与 dialogs.json。

# 用户描述
角色名称: {name}
外貌:    {appearance}
性格:    {personality}
说话风格: {tone}
动作:    {animations}

# 输出要求
1. 严格输出两个代码块: ```json (manifest) 和 ```json (dialogs)
2. id 用英文小写下划线，3-32 字符，独一无二
3. 每个动作分组的台词至少 5 句，最多 10 句，单句 ≤15 字
4. 必须至少包含一个可播放动画；建议包含 idle，walk/drag 可选
5. 不要输出 sprite 文件本身（用户自己提供）
6. 不要输出任何额外解释文字
```

完整规范摘要会嵌入到客户端 AI 向导生成的 prompt 中。

---

## 5. 校验失败时的错误格式

```
❌ 加载失败：sprites/walk.png 找不到（manifest 要求 6 帧 × 64px）

💡 提示：把这段错误信息和 manifest.json 一起发给 AI，
     让它帮你修正路径或重新生成 manifest。
[复制错误信息]
```

错误必须包含：缺失项、期望值、修复提示。
