Files
story-edit-web/IR_SCHEMA.md

158 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Story IR Schemav0.2M4 扩展)
> v0.2 变更M4新增 `sequences`/`out_ref`子序列复用、Option `skip`(押注跳过)、
> `stage.point_set`(点位集引用 + 坐标校验);条件/奖励词典外置到 `ir_dictionary.json`。
> 校验+编译内核收敛进 `ir_core/` 包CLI 与 M5 Web 后端共用)。
Story IR 是"讲故事"与"编译成配置"两段之间的唯一交接棒。它只描述**语义层**
(语义角色 / 语义场景 / 叙事节点),把真实 ID、prefab 点位、`step` 号、`nextStepId`
前缀拼接全部留给编译器。
配套:
- 样张:`tools/event_authoring/samples/yuye_koumen.ir.json`
- 降级目标:`Assets/Scripts/Base/Config/Data/GameEventData.cs`
- 设计依据:`docs/plans/2026-06-03-event-config-automation-design.md`
---
## 1. 顶层结构
```jsonc
{
"id": "QY_YYKM", // group 名(唯一,不与现有内置 group 冲突)
"title": "雨夜叩门",
"theme": "正统武侠·道德抉择",
"scale": "标准奇遇", // 仅元信息,不参与编译
"roles": [ Role, ... ],
"stage": Stage,
"sequences": [ Sequence, ... ], // 可选;可复用子序列(被 out_ref 引用)
"nodes": [ Node, ... ],
"endings": [ Ending, ... ]
}
```
## 2. Role语义角色
```jsonc
{ "slot": "NP1", "name": "神秘剑客", "archetype": "负伤外门高手", "camp": 0 }
```
- `slot`:编译后写进 `points`/`camp2Fighters` 的语义点位名。`P1`=玩家固定。
- `archetype`:原型标签,编译期→真实 NPC ID匹配 `npc_data`)。
- `camp`0 中立 / 1 玩家方 / 2 敌方(战斗时用,可省略)。
## 3. Stage语义场景
```jsonc
{ "type": "门派入口·夜", "reuse_hint": "K3_A", "point_set": "QY_YYKM" }
```
- `type`:场景语义;编译器据此 + 所需角色数从点位库挑结构匹配的 prefab。
- `reuse_hint`:可选,显式指定复用哪个现有 prefab仅作 BDTree host
- `point_set`:可选,引用的点位集名(默认 = `id`)。对应 `Assets/StreamingAssets/Story/PointSets/{point_set}.points.json`M1 取点工具产出。编译期校验IR 里实际站位的点位名speaker/actor/move.to/fight 阵营/camera必须都在该点位集中存在点位集文件不存在则降级为警告`--strict-points` 下视为错)。
## 4. Node叙事节点
通用字段:`id`(组内唯一)、`kind`。各 `kind` 字段如下:
| kind | 字段 | → GameEventData |
|---|---|---|
| `narration` | `speaker?`, `text`, `next` | type0挂角色(默认 P1)用气泡实景呈现,**不走全屏黑幕**`content`=text |
| `dialogue` | `speaker`, `text`, `camera?`, `next` | type0`points`=[speaker]`content``cameraPoint` |
| `move` | `actor`, `to`, `mode?`(walk/teleport/remove), `speed?`, `ani?`, `next` | type0`points`/`movePoint`/`moveStatus` |
| `anim` | `actor`, `ani`, `angle?`, `next` | type0`anis`=[ani,angle] |
| `choice` | `options`:[Option] | type1每 option 一行 |
| `choice_once` | `options`:[Option] | type2编译器保证有恒满足兜底项 |
| `random` | `branches`:[{`weight`,`goto`}] | type3`weight`+`nextStepId` |
| `fight` | `fight_type`(1 击倒/2 死斗), `camp2`:[slot], `camp1?`:[slot], `win`, `lose` | type0`camp2Fighters`,`fightStatus`=[type,win,lose] |
| `reward` | `grants`:[Grant], `next` | type0`resultRewardIds``roleActionCode` |
| `out_ref` | `ref`(子序列 id), `next` | 编译前预展开成普通节点(见 §4.1);本节点不直接产行 |
### 4.1 Sequence / out_ref子序列复用对应 `OutRefStoryNodeData`
```jsonc
// 顶层 sequences可复用子序列
{ "id": "seq_cheer", "nodes": [ {Node}, ... ] }
// 引用它的节点
{ "id": "ref_a", "kind": "out_ref", "ref": "seq_cheer", "next": "end_win" }
```
- **语义**:把一段公共流程(庆功/过场/结算)抽成子序列,多处 `out_ref` 复用,避免重复手写。
- **编译**(对齐 `StorylineMgr.ReplaceStoryEventData` 的展开):编译前把每个 `out_ref` 节点 `r` 摊平——
克隆 `ref` 子序列的节点id 加 `{r.id}__` 前缀,内部跳转同步加前缀;子序列出口(`next` 为空的尾节点)
`next` 接到 `r.next`;指向 `r` 的跳转改写为子序列入口 `{r.id}__{首节点}`
- **多处复用不撞 id**:不同 `out_ref` 节点前缀不同 → 各自一份独立行。
- **限制**M4 仅支持**一层**(子序列内不得再含 `out_ref`),违者校验报错;子序列内跳转目标须落在子序列内或共享结局。
### Option选项
```jsonc
{ "text": "收留他", "condition": Condition?, "reward": {"grants":[Grant]}?,
"skip": { "node": "end_bet", "reward": {"grants":[Grant]} }?, "goto": "end_ally" }
```
- `text``choose``condition``condition``reward``resultRewardIds`(选项被选后自身发的奖励/扣费),`goto``nextStepId`
- **`skip`(押注跳过,对应 `skipNodeId`+`skipReward`**:选该项时**把 `skip.reward` 注入 `skip.node` 那行的奖励**,常配合 `goto` 越过中间战斗直达结算行(押对了→不打了直接领彩头)。
- 编译:选项行写 `skipNodeId = skip.node`(裸节点 id运行时 `ChooseTaskUI` 自动拼 `{group}_` 查行)、`skipReward = compile(skip.reward)`
- 注意:运行时是**覆盖** `skip.node` 行的 `resultRewardIds`(非追加)→ 让 `skip.node` 的自带 grants 为空、彩头全由 `skip.reward` 给。
- 校验:`skip.node` 须可解析;`skip.reward.grants` 非空且合法。
## 5. Ending结局
```jsonc
{ "id": "end_ally", "summary": "结义同盟", "grants": [Grant], "result": "success" }
```
- `result`:可选,`success`(默认,→`gameTaskStatus`1) / `fail`(→2) / `end`(→3)。决定收尾分支。
- 编译为**两行**:① 结算行(`missionText`=summary + `resultRewardIds`/`roleActionCode``gameTaskStatus`=0
`nextStepId`→终结行);② 裸终结行(仅 `gameTaskStatus`)。
- **为何拆两行**`EventRefreshStepAction``RefreshStep()` 返回 false即下一步 `gameTaskStatus>0`)时
返回 `Failure`,会中断该步 Sequence——若把奖励与 `gameTaskStatus` 放同一行,发奖励/入门的 Action 被跳过。
终结行触发 `DirectStoryEnd`(复位镜头 `otherTarget=null` / 恢复玩家控制 / 结束剧情)。与可视化编辑器
及内置奇遇 `Qiyu/QY10_A.json`(末节点 `taskStatus:1`、内容为空、奖励在前置节点)一致。
## 6. Condition / Grant 词典(外置 `ir_dictionary.json`
> M4 起词典外置到 `tools/event_authoring/ir_dictionary.json`,编译器(`ir_core`)与 M5 Web 前端共读。
> 新增**查表类** kind 只改该 JSON、零代码需引擎特例的 kind 标 `"engine": true`。
> 编译器对未登记 kind **报错不静默**。下表为当前登记项(随推进扩充)。
### Condition→ `condition` 字段)
```jsonc
{ "kind": "银两", "op": ">=", "value": 500 } // → "V19,2,500" (op: >=→2)
```
| kind | 真实ID | 说明 |
|---|---|---|
| 银两 | V19 | 门派金钱(`RewardItems` 特判 schoolMoney |
### Grant→ `resultRewardIds` 或行为码)
```jsonc
{ "kind": "银两", "value": 200 } // → "V19,200"(正=给,负=扣)
{ "kind": "道具", "item": "P6", "value": 3 } // → "P6,3"
{ "kind": "友好度", "target": "NP1", "value": 30 } // → "NP1,V15,30"
{ "kind": "入门", "target": "NP1" } // → roleActionCode "JoinToPlayerSch=NP1"
```
| kind | 真实ID/机制 |
|---|---|
| 银两 | V19 |
| 道具 | 由 `item` 指定drop_item_data ID |
| 友好度 | V15`target` NPC slot |
| 入门 | roleActionCode `JoinToPlayerSch`NPC 加入玩家门派) |
> 词典随 MVP 推进扩充;编译器对未登记的 kind 报错,不静默产出。
## 7. 编译器降级不变量(与 StorylineMgr 现有逻辑一致)
- 自动生成 `step`(按节点拓扑序)。
- 自动给 `id`/`nextStepId`/`fightStatus[1..2]``{group}_` 前缀。
- 所有跳转目标(`next`/`goto`/`win`/`lose`/`branches[].goto`)必须可解析到某节点或结局,否则编译失败。
- 每个 `choice`/`choice_once` 至少一个无 `condition`(或恒满足)的兜底项,否则编译失败。
## 8. 多语言 / 本地化
游戏本地化走 `SGameText`(重写 `Text.text` setter。剧情对话气泡 `TaskDialogUI.contentText`
已是 `SGameText`,链路可用。规则:
- **文本一律用简体中文撰写**`content`/`choose`/`summary`),作为 canonical key
- **简体↔繁体**`SGameText` 字符级机械转换,自动覆盖,无需额外工作。
- **韩文(及未来语言)**`SGameText` 用中文整句作 key 查 `StreamingAssets/i18n/ko.tsv`
- 编译器附带产出 `{group}.i18n.tsv`(中文 key 列 + 空韩文列),翻译填好后合并进 `ko.tsv`
新剧情文本即纳入本地化,不会成为漏网。
- **占位符**:需动态插入角色名时用游戏既有约定(如 `{NP1}``{A5,NP1}`),编译器原样保留。
注意:含占位的整句**韩文查表会 miss**(拿到的是替换后的完整串)→ 简繁不受影响,韩文 fallback 中文。
这是项目级已知限制(待 `SGameText.SetFormat` 模板方案解决),故重要台词尽量少用占位。
- **运行时前提**:剧情文本的显示组件必须是 `SGameText`。对话气泡已满足;黑屏旁白/选项 UI
若发现是原生 `Text`,需在游戏侧补挂 `SGameText`(属游戏侧改造,不在编译器范围)。