9.4 KiB
9.4 KiB
Story IR Schema(v0.2,M4 扩展)
v0.2 变更(M4):新增
sequences/out_ref(子序列复用)、Optionskip(押注跳过)、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. 顶层结构
{
"id": "QY_YYKM", // group 名(唯一,不与现有内置 group 冲突)
"title": "雨夜叩门",
"theme": "正统武侠·道德抉择",
"scale": "标准奇遇", // 仅元信息,不参与编译
"roles": [ Role, ... ],
"stage": Stage,
"sequences": [ Sequence, ... ], // 可选;可复用子序列(被 out_ref 引用)
"nodes": [ Node, ... ],
"endings": [ Ending, ... ]
}
2. Role(语义角色)
{ "slot": "NP1", "name": "神秘剑客", "archetype": "负伤外门高手", "camp": 0 }
slot:编译后写进points/camp2Fighters的语义点位名。P1=玩家固定。archetype:原型标签,编译期→真实 NPC ID(匹配npc_data)。camp:0 中立 / 1 玩家方 / 2 敌方(战斗时用,可省略)。
3. Stage(语义场景)
{ "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)
// 顶层 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(选项)
{ "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(结局)
{ "id": "end_ally", "summary": "结义同盟", "grants": [Grant], "result": "success" }
result:可选,success(默认,→gameTaskStatus1) /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 字段)
{ "kind": "银两", "op": ">=", "value": 500 } // → "V19,2,500" (op: >=→2)
| kind | 真实ID | 说明 |
|---|---|---|
| 银两 | V19 | 门派金钱(RewardItems 特判 schoolMoney) |
Grant(→ resultRewardIds 或行为码)
{ "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(属游戏侧改造,不在编译器范围)。