# 世界包接入规范(World Pack Spec)

> 多世界游戏平台的接入契约。**加一个新世界 = 在 `worlds/` 下丢一个目录,不改任何引擎代码**(开闭原则)。
> 引擎对所有可选项都有回退,世界包只需声明它"与众不同"的部分。
> 校验器:`genesis.world.pack.validate_world(dir)`(空=合规);发现器:`discover_worlds("worlds")`。
> 一切以**中文名**为运行时标识(地点名、人物名)——这些名字就是 `WorldState` 里的键,务必全局唯一且各处一致。

## 1. 目录结构

```
worlds/<world_id>/
├── characters/*.yaml      # * 必需:每个 agent 一份(人物层)
├── places/*.yaml          # * 必需:每个地点一份(世界层·拓扑)
├── world.json             # 可选:世界层·环境语义(死法/枢纽/取景地/密道/开场钟点)
├── items.json             # 可选:道具层(初始设施+物品+可供性 verbs)
├── acts.json              # 可选:剧情层(处境帧 + 凶手行刑幕菜单 + dossier 隐罪档)
├── viz.json               # 可选:2.5D 渲染布局(序章文案、地图坐标、sprite 基路径)
└── beats.json             # 可选:演示节拍(demo)
```

只有 `characters/` 与 `places/` 是硬性必需。缺 `acts.json` → 无剧情导演(stage_directive 留空);
缺 `items.json` → 无初始道具;缺 `world.json` → 环境语义用引擎内置默认。

## 2. 人物层 `characters/*.yaml`

```yaml
kind: character          # 必需
id: vera                 # 必需,文件内唯一(英文短 id)
name: 维拉               # 必需,**运行时标识(全局唯一)**——agent 名、positions/relations 键
age: 28                  # 必需
mbti: ENFJ               # 必需(合法 MBTI)
occupation: 家庭女教师    # 必需
start_location: hall     # 必需,必须是某个 place 的 id
gender: female
backstory: 一句话身世
drive: 终极目的+赌注(永置 prompt 顶端,任务清单只是达成它的手段)
secret: 只有该 agent 自己知道的隐罪/秘密
knows: 该角色"恰好知道"的可行动情报(只进本人 prompt)
role: 凶手               # 含「凶手」二字 = 隐藏凶手(引擎据此开启 is_killer 行刑逻辑)
relationships: { 隆巴德: 互不信任的同类 }   # 名字须在名册内
art: { description: "...", size: 64, view: "low top-down", directions: 4,
       animations: [idle, walk], sprite_path: assets/sprites/characters/<world>/vera }
```

约定:
- **凶手** = `role` 含「凶手」的角色,恰好一名(多名仅取第一名,校验器警告)。凶手的"行刑脚本"在 `acts.json.kill_acts`。
- 平民的**开场探索动力**来自 `acts.json.dossier[name]`(见 §5),引擎自动注入为一条绑定地点的开场目标(`agenda_seed`)。凶手不设自身议程。

## 3. 世界层 · 拓扑 `places/*.yaml`

```yaml
kind: place              # 必需
id: drawing_room         # 必需,文件内唯一(英文短 id)
name: 客厅               # 必需,**运行时标识(全局唯一)**——positions/adjacent/取景地都用它
description: 走进来看到的样子(客观陈设,会进感知)
geography: indoor        # town|city|village|seaside|snow_mountain|forest|plaza|indoor;indoor=室内(停电变黑、天气不影响)
adjacent: [hall, dining] # 邻接 place 的 **id**(校验器要求都存在;移动只能走邻接)
```

## 4. 世界层 · 环境语义 `world.json`(可选)

```json
{
  "start_hour": 21.0,
  "start_place": "门厅",
  "prologue_place": "码头",
  "place_cause": { "断崖": "高处坠落,多处骨折", "海滩": "溺水后被冲上岸" },
  "conspicuous": ["门厅", "客厅"],
  "ceremonial_venues": ["断崖", "礁岩", "柴棚"],
  "secret_passages": [],
  "killer_items": [],
  "kill_device": { "countdown_facility": "soldier_figurines", "state_template": "n{left}",
                   "total": 10, "burn_message": "...", "narr": "...", "signature": null },
  "lore": { "...见下 §4.1..." }
}
```

- `start_hour` / `start_place`(开局所在地)/ `prologue_place`(序幕登岛地)。
- `place_cause`(地点→客观死法):验尸据此推断,**不被剧本台词污染**。
- `conspicuous`(人来人往的枢纽):凶手绝不在此动手。`ceremonial_venues`(仪式取景地):软导演把猎物往这些僻静处引。`secret_passages`(暗道,只有凶手能走;忠实密室类**留空**)。以上 place 名引擎都会与现实取交集,写错只报 `[W]`、不崩。
- `killer_items`:凶手开局持有的物品 id(如钥匙)。
- `kill_device`:凶案倒计时装置(每死一人推进一个设施 + 可选"签名"物)。`countdown_facility`/`state_template`(`{n}`已死数/`{left}`剩余)/`total`/`signature`。缺则用雾鸦岛默认(胶片盒+残页)。

### 4.1 `lore`(叙事/设定层 —— 每个 agent 共享的世界常识与气质)

**这是让新世界"不串味"的关键**:agent 的共识、说书人、验尸线索、终局、序幕全部从这里取文本。缺则回退雾鸦岛默认。

```json
"lore": {
  "island_name": "士兵岛",
  "areas": [["别墅一层", ["门厅","客厅","..."]], ["室外", ["码头","断崖","..."]]],
  "roster_line": "受 U.N.Owen 之邀上岛的十位客人,彼此素不相识。",
  "hard_rules": "你们困在士兵岛上,暴风雨封海,天亮前无人能离开……",
  "key_mechanics": "①餐桌十个小瓷士兵=倒计时 ②留声机念旧罪 ③各人物证散落岛上 ……",
  "survival_goals": "【今夜共同处境】……①弄清谁是 U.N.Owen ②找离岛/求救 ③自保 ④揪出凶手 ……",
  "ritual_name": "《十个小士兵》童谣",
  "mastermind": "那个把你们骗上岛、自称 U.N.Owen 的主人",
  "novel_title": "《无人生还》", "ghost": null, "narrator_opening": "暴风雨之夜……",
  "prologue_beats": [["留声机自己转了起来……", 0.85, 0]], "prologue_narr": "🎙 审判开始。"
}
```

`ritual_name`/`mastermind` 进验尸线索与终局文案;`novel_title`/`ghost`/`narrator_opening` 进说书人;`prologue_beats` 进序幕过场。

## 5. 剧情层 `acts.json`(可选)

结构:`phases`(按死亡数/事件推进的全员处境帧)+ `kill_acts`(凶手逐幕行刑菜单)+ `dossier`(每人隐罪+物证+物证地点)。完整范例见 `worlds/soldierisland/acts.json` 与 `worlds/ravenisle/acts.json`。

```json
{
  "kill_acts": [
    { "act": 1, "name": "噎毙", "place": ["客厅"], "method": "氰化物",
      "target": "马斯顿", "mirror": "童谣对应句", "cover": "...", "bait": "...", "flags": [] }
  ],
  "dossier": {
    "维拉": { "soft": "软肋", "want": "她想抢先处理掉的定罪物证",
              "evidence_place": "礁岩", "bait_handle": "凶手下饵的话术" }
  }
}
```

- `kill_acts[].target` 必须在角色名册内(`[E]`);`null` = 机动幕。`place` = 取景地(`[W]` 校验)。`flags` 可含 `reluctant`(凶手下不去手)、`flex`(机动)、`finale`、`needs_blackout` 等。
- **`dossier` 是 #3「目标↔地图↔道具」闭环的核心**:`want`+`evidence_place` 既被引擎注入为该角色的开场目标(有去某地的动力),又供凶手 `lure` 下"踩中私欲"的真饵。`evidence_place` 应是真地点(`[W]` 校验),且最好在 `items.json` 里有对应隐藏物品。

## 6. 道具层 `items.json`(可选)

由 `Facility`/`Item` 字段直序列化(可用 `worldstate.dump_seed(ws)` 从代码 seed 机器导出)。

```json
{
  "facilities": [
    { "id": "switchboard", "name": "总电闸", "place": "发电机房", "state": "intact",
      "desc": {"intact": "...", "smashed": "..."}, "hint": "用『砸电闸』制造停电",
      "verbs": { "砸": {"feedback": "...", "event": "...", "scope": "global",
                        "salience": 0.9, "set_state": "smashed", "reveal_item": null} } }
  ],
  "items": [
    { "id": "diary", "name": "阮青的日记", "place": "青之房", "hidden": true,
      "hint": "拿到=指证铁证;藏/毁=湮灭证据" }
  ]
}
```

- **可供性(use 动作)= 声明式 `verbs` 注册表,引擎不含任何世界专属分支**。`{动词子串: {效果...}}`,动词按子串匹配玩家的 `use` 参数。新世界把"加油/砸闸/点灯/开门/敲钟/撬箱"等全写进 verbs,**零引擎代码**。完整效果字段:
  - 前置:`requires_state`(本设施须为某状态)、`requires_held_item`(操作者须持某物 id);不满足 → `feedback_fail`。
  - 状态:`set_state`(置本设施状态)、`reveal_item`(揭出隐藏物)、`set_power`(true/false 切全岛电)、`connect`(["A","B"] 打通暗门/通路)。
  - 燃料计数:`fuel_to`(集满后置的状态)+`consume_held_match`(消耗一件名字含此子串的手持物)+`fuel_need`(需要几件);`feedback` 可用 `{count}`/`{need}`,集满追加 `feedback_full`、未满追加 `feedback_more`。
  - 世界副作用:`advance_ferry_h`(把渡船提前 N 小时来接,如点灯求救)。
  - 广播:`feedback`(给操作者)、`event`+`scope`(place/global)+`salience`、`narr`(上帝视角时间轴)。
  - 范例(雾鸦岛已全部迁成数据):砸电闸=`{set_state:smashed,set_power:false,...}`;点灯塔=`{requires_state:fueled,set_state:lit,advance_ferry_h:2}`;钥匙开门=`{requires_held_item:master_key,set_state:open,connect:[...]}`。见 `worlds/ravenisle/items.json`。

> **引擎与世界无关的保证**:`runtime/engine.py`、`bulletin.py`、`narrator.py`、`devices.py` 不再含任何具体世界的地名/剧情/机关分支——全部经 `world.json`/`items.json`/`acts.json`/`viz.json` 注入,缺则回退雾鸦岛默认。新世界 = 一个目录的数据 + 资产,**不改一行引擎**。
- `item.hidden:true` = 需 `search` 才现身;`item.place:""` = 初始被持有/不在场。`reveal_item` 须指向已定义物品(`[W]`)。

## 7. 校验与发现

```python
from genesis.world.pack import validate_world, discover_worlds, describe_world
validate_world("worlds/soldierisland")   # -> []=合规;[E]…阻断加载;[W]…建议修
discover_worlds("worlds")                # -> ["ravenisle", "soldierisland", ...]
describe_world("worlds/soldierisland")   # -> {id, roster, places, has_acts, valid, issues}
```

平台加载世界前应 `validate_world`,有 `[E]` 即拒绝。当前选择世界用环境变量 `GENESIS_WORLD=<id>`(见 `server/live_server.py`);平台化的"用户选世界"在此基础上扩展(按 `discover_worlds` 列单 → 每会话指定 world_id)。

## 8. 新世界 checklist

1. 建 `worlds/<id>/`,写 `places/*.yaml`(拓扑)、`characters/*.yaml`(含一名 `role:凶手`)。
2. 写 `acts.json`:`dossier` 给每位平民配 `want`+`evidence_place`(#3 动力);`kill_acts` 排凶手行刑序列。
3. 写 `items.json`:每个 `evidence_place` 放一件对应隐藏物品;每个地点配能"动手"的 `verbs`(让处处可探索)。
4. 写 `world.json`:`place_cause`/`conspicuous`/`ceremonial_venues`/`secret_passages`/`start_hour`。
5. `validate_world` 跑到无 `[E]`;占位资产先跑通逻辑;最后补 `viz.json` + 美术。

关联:[[ravenisle]] 是参考实现;[[soldierisland-faithful]] 是首个按本规范从零接入的世界;复刻方法论见 `docs/world-game-playbook.md`。
