# CONTRACT.md — 资产 / 地图 / 代码三方契约（唯一真源）

工作目录：`/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Kimi-K3`
PRD：同目录 `PRD.md`（所有剧情/数值/台词以 PRD 为准，本文件只定义技术接口）。

工具路径：
- Aseprite: `/Applications/Aseprite.app/Contents/MacOS/aseprite`（用法：`aseprite -b --script file.lua`；绘制用 `app.activeImage:drawPixel(x, y, app.pixelColor.rgba(r,g,b,a))`；导出用 `spr:saveAs("out.png")`。已验证可用）
- Tiled 1.12.2: `/Applications/Tiled.app/Contents/MacOS/Tiled`（用法：`Tiled -e script.js` 执行脚本，脚本内用 `var f = new TextFile(path, TextFile.WriteOnly); f.write(xml); f.commit();` 写 .tmx；再用 `Tiled --export-map --embed-tilesets in.tmx out.json` 导出 JSON。已验证可用；注意 `-e` 模式无编辑器，`tiled.open`/`TileMap` 部分 API 不可用，一律走 TextFile 写 XML）

## 1. 目录结构

```
public/assets/
  tileset.png                 # 256×128，16×16 瓦片，16 列 × 8 行 = 128 瓦片
  sprites/player.png          # 64×96：帧 16×24，4 列(行走帧 0-3) × 4 行(方向 0=down 1=up 2=left 3=right)，第 0 帧兼待机
  sprites/npc_afu.png         # 48×24：3 帧横排 = smile / flat / off
  sprites/npc_yuanyuan.png    # 同上
  sprites/npc_dabao.png       # 同上
  sprites/npc_mimi.png        # 同上
  sprites/npc_fuyisheng.png   # 同上
  sprites/npc_guiyeye.png     # 同上
  sprites/npc_bird.png        # 32×24：2 帧 = 闭嘴 / 张嘴
  sprites/extra_squirrel.png  # 32×24：2 帧循环
  sprites/extra_hedgehog.png  # 同上
  sprites/extra_fox.png       # 同上
  sprites/extra_pig.png       # 同上
  sprites/extra_chick.png     # 同上
  portraits/player_smile.png  # 32×32
  portraits/player_real.png   # 32×32（E3 用：微笑消失、疲惫真实的脸）
  portraits/afu.png yuanyuan.png dabao.png mimi.png fuyisheng.png guiyeye.png bird.png  # 各 32×32，smile
  maps/house.json plaza.json bakery.json shop.json clinic.json lake.json field.json well.json fog.json white.json
  maps/*.tmx                  # Tiled 中间产物（保留在仓库）
  maps/tileset.tsx            # 外部瓦片集描述，image source="../tileset.png"
tools/aseprite/*.lua          # 素材生成脚本（保留）
tools/tiled/genmaps.js        # 地图生成脚本（保留）
src/                          # 游戏代码
```

素材规范（PRD §11）：糖果色板（草绿 #7DC95E/#5FB34A、奶油黄 #FFF3D6、蜜糖橙 #F5A623、天空蓝 #8FD3FF、石路 #E8DCC8、湖水 #6EC1E4/#4FA8D8、粉 #F7B7CB、棕 #8B5A2B），白色层独立灰白板（#F5F5F5/#DADADA/#BDBDBD）。无抗锯齿、无渐变。`off` 档与 `smile` 档差异控制在 1–2 像素（如一只眼低 1px 或嘴角多 1px），不许鬼脸。背景透明（alpha=0）。PNG 必须精确尺寸（上面列出）。

## 2. 瓦片 ID 表（tileset.png，id = 行×16+列；地图数据 gid = id+1）

| id | 名称 | id | 名称 |
|---|---|---|---|
| 0 草地 | 1 草+花A | 2 草+花B | 3 深草 |
| 4 土路 | 5 石路 | 6 石路alt | 7 广场砖 |
| 8 湖水A | 9 湖水B | 10 水岸北 | 11 水岸南 |
| 12 水岸西 | 13 水岸东 | 14 花坛粉 | 15 花坛黄 |
| 16 莓果丛(满) | 17 莓果丛(空) | 18 树左上 | 19 树右上 |
| 20 树左下 | 21 树右下 | 22 圆灌木 | 23 树桩 |
| 24 石头 | 25 路标 | 26 栅栏横 | 27 栅栏竖 |
| 28 路灯 | 29 彩带红 | 30 彩带黄 | 31 彩带蓝 |
| 32 奶油墙 | 33 奶油墙+窗 | 34 木板墙 | 35 橙屋顶左 |
| 36 橙屋顶中 | 37 橙屋顶右 | 38 门 | 39 门框顶 |
| 40 窗 | 41 烟囱 | 42 井左上 | 43 井右上 |
| 44 井左下 | 45 井右下 | 46 邮筒 | 47 公告板 |
| 48 木地板A | 49 木地板B | 50 室内墙 | 51 室内墙+窗 |
| 52 床左上 | 53 床右上 | 54 床左下 | 55 床右下 |
| 56 柜台左 | 57 柜台右 | 58 烤炉 | 59 空货架 |
| 60 满货架 | 61 桌子 | 62 椅子 | 63 地毯 |
| 64 日历 | 65 配方纸 | 66 文件纸 | 67 野餐布左上 |
| 68 野餐布右上 | 69 野餐布左下 | 70 野餐布右下 | 71 长椅左 |
| 72 长椅右 | 73 凳子 | 74 柜子 | 75 冰箱 |
| 76 水槽 | 77 盆栽 | 78 木桶 | 79 木箱 |
| 80 雾A | 81 雾B | 82 雾C | 83 白色地板 |
| 84 白色墙 | 85 白墙板 | 86 白门 | 87 白床左上 |
| 88 白床右上 | 89 白床左下 | 90 白床右下 | 91 白柜 |
| 92 白监护仪 | 93 白窗帘 | 94 白地板alt | 95 白天花灯 |
| 96 梯子 | 97 桥横 | 98 桥竖 | 99 蘑菇 |
| 100 花红 | 101 花黄 | 102 花蓝 | 103 蜂蜜罐 |
| 104 面包 | 105 信件 | 106 书 | 107 蜡烛 |
| 108 篮子 | 109 水桶 | 110 扫帚 | 111 蛋糕 |
| 112 红屋顶左 | 113 红屋顶中 | 114 红屋顶右 | 115 蓝屋顶左 |
| 116 蓝屋顶中 | 117 蓝屋顶右 | 118 粉墙 | 119 粉墙+窗 |
| 120 石墙 | 121 石墙+窗 | 122 遮阳篷 | 123 钟 |
| 124 旗 | 125 花环角 | 126 花环中 | 127 空(透明) |

## 3. 地图契约（Tiled 导出 JSON；图层固定三层）

每层地图固定：
- `ground`（瓦片层，无碰撞；必须全部用不透明瓦片——透明装饰瓦片只能放 `obstacles` 层，否则透明处露出黑底。大花 100-102、蘑菇 99、门 38 已在 tileset 里烘焙不透明底，可放 ground）
- `obstacles`（瓦片层，带图层自定义属性 `collides=true`(bool)；代码对该层整层碰撞，但彩带（瓦片 29-31）在代码里排除碰撞、可穿过）
- `objects`（对象层，点对象）

对象约定（name + 自定义属性）：
- `name="spawn"`，属性 `id`(string)：出生点。各图必有 `default`；室内图另有 `from_plaza`；plaza 有 `from_house/from_bakery/from_shop/from_clinic/from_lake/from_field/from_well/from_fog`。
- `name="door"`，属性 `target`(string, 目标地图 key) + `spawn`(string, 目标 spawn id)。双向成对。
- 落点安全：spawn 与任一门判定点的距离（以玩家测量点 y-8 计）必须 >14px（惯例 ≥24px）；运行时另有门武装机制兜底——落到门旁时须先离开感应圈（>18px）才允许触发，且触发即消耗武装（须再次离开 >18px 才可重新触发），防止落点回弹与对话关闭帧的重复触发。
- 2x2 拼块瓦片（树 18-21、井 42-45、床 52-55、野餐布 67-70、白床 87-90）：四个象限占用连续 4 个 id；tileset 生成时把下半两个象限从自然落点（id+16/id+17）复制到 id+2/id+3。
- `name="npc"`，属性 `id`(string)：afu / yuanyuan / dabao / mimi / fuyisheng / guiyeye / bird / squirrel / hedgehog / fox / pig / chick。
- `name="interact"`，属性 `id`(string)：见下表。

10 张地图（尺寸 = 瓦片数；场景 key 同文件名）：

| key | 文件 | 尺寸 | 必填对象 |
|---|---|---|---|
| house | maps/house.json | 12×8 | spawn(default,from_plaza)、door→plaza、interact:bed、interact:calendar |
| plaza | maps/plaza.json | 30×20 | spawn(default,from_house,from_bakery,from_shop,from_clinic,from_lake,from_field,from_well,from_fog)、door×8(house/bakery/shop/clinic/lake/field/well/fog)、npc:afu(公告板旁)、npc:bird(树上)、npc:squirrel、npc:fox、interact:noticeboard、interact:flowerbed_1/2/3 |
| bakery | maps/bakery.json | 15×10 | spawn(default,from_plaza)、door→plaza、npc:yuanyuan、interact:counter_bread、interact:recipe_paper |
| shop | maps/shop.json | 15×10 | spawn(default,from_plaza)、door→plaza、npc:mimi、npc:hedgehog、interact:shelf_count |
| clinic | maps/clinic.json | 12×8 | spawn(default,from_plaza)、door→plaza、npc:fuyisheng、interact:doctor_desk |
| lake | maps/lake.json | 24×16 | spawn(default,from_plaza)、door→plaza、npc:guiyeye(长椅旁)、npc:pig、interact:picnic_mat |
| field | maps/field.json | 20×14 | spawn(default,from_plaza)、door→plaza、npc:chick、interact:berry_1..5、interact:water_point |
| well | maps/well.json | 14×10 | spawn(default,from_plaza)、door→plaza、interact:ancient_well |
| fog | maps/fog.json | 20×8 | spawn(default,from_plaza)、door→plaza、interact:fog_wall |
| white | maps/white.json | 20×14 | spawn(default)；上半 20×6 走廊 + 左下 10×8 病房，内部连通；白色层瓦片(83–95) |

地图要点（PRD §10）：plaza 为枢纽，含公告板(47)、花坛(14/15)、双子鸟树(18-21)、庆典空地；室外场景用草地/石路/栅栏/房屋装饰铺满，lake 有大片湖水(8-13)与长椅(71/72)，field 有莓果丛(16)，well 有古井(42-45)，fog 边缘铺雾(80-82)，white 全用白色层瓦片（走廊+病房，病房内放白床 87-90 与白监护仪 92）。室内场景：墙(50/51)围边、木地板(48/49)、家具按 PRD §10 要点摆放。house 必须有床(52-55)与日历(64)；bakery 有柜台(56/57)、烤炉(58)、配方纸(65)；shop 有货架(59/60)；clinic 有桌子(61)+文件纸(66)。

## 4. 代码架构契约（Phaser 3 + TS + Vite）

- `src/main.ts`：Phaser config，逻辑 320×180，`pixelArt:true`，`scale:{ mode: Phaser.Scale.NONE, autoCenter: CENTER_BOTH, parent:'game' }` + window resize 监听做整数倍放大（canvas style 宽高 = 320k×180k，k = max(1, floor(min(winW/320, winH/180)))）。
- 场景 key：`BootScene / TitleScene / HouseScene / PlazaScene / BakeryScene / ShopScene / ClinicScene / LakeScene / FieldScene / WellScene / FogScene / WhiteScene / UIScene / DreamScene / EndingScene`。
- 地图场景共用基类 `MapScene`（`src/scenes/MapScene.ts`）：子类只提供 `mapKey` 与少量钩子。基类负责：tilemap 加载（`this.load.tilemapTiledJSON`，tileset 名 `candy` 对应 `assets/tileset.png`）、player（96 px/s、四向动画 walk-down/up/left/right + 待机帧、obstacles 层碰撞、相机跟随+钳制）、≤24px 出「按 E」提示（交 UIScene 显示）、door 切换（fade out/in + 目标 spawn）、NPC/群演实例化与表情档切换、注册 CorruptionDirector 的视听参数（相机 postFX：ColorMatrix 饱和度 / Vignette 晕影 / 负片单帧）。
- `CorruptionDirector`（`src/core/CorruptionDirector.ts`）：普通 TS 单例 + `Phaser.Events.EventEmitter`。状态：playerName、startTime(ms)、day(1-7)、happiness(0-100)、awareness(0-100)、clues(Set)、tasksDone(Set)、refusedTasks(Set)、escapedCelebrations、stage 派生值。事件名：`happiness` `awareness` `stage` `day` `clue` `task` `celebration` `ending`。是全部状态唯一真源；提供 debug 用 setter。
- 对话：`UIScene` 底部通栏框（说话者名 + 头像 + 文本，E 推进；选项三选：温柔/迟疑/否定）；文本腐蚀在渲染层按 §6.5 表替换（原文案干净）。头像用 portraits/。
- 音频：`src/audio/AudioEngine.ts`，WebAudio 程序合成（无音频文件）。
- Debug：`src/debug/DebugPanel.ts`，反引号键开关的 DOM 面板（默认隐藏、无 UI 痕迹）：设幸福/觉知/日(1-7)/传送场景/置齐线索/强制结局 E1-E4。
- 数值与台词逐字逐数实现 PRD §5–§9。

## 5. 验收相关硬约束

- `npm run build`（tsc --noEmit + vite build）必须零错误；`npm run preview` 可玩。
- 无 localStorage、无外部网络请求、无外部音频/字体文件。
- 控制台零报错。
