270 lines
10 KiB
Python
270 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""Story IR -> 扁平 List[GameEventData] 编译器。
|
||
|
||
产出 events.json(绕过 StorylineMgr.ReplaceStoryEventData,自己做 step/前缀/展开)。
|
||
M4 新增:out_ref 子序列复用(编译前预展开)、选项 skip(押注跳过→直接结算)。
|
||
"""
|
||
from .dictionary import CompileError
|
||
|
||
OUT_REF_SEP = "__" # out_ref 展开前缀分隔符(与节点 id 内的下划线区分)
|
||
|
||
|
||
# ============ out_ref 预展开 ============
|
||
def expand_out_refs(ir):
|
||
"""把 out_ref 节点摊平成普通节点,返回新的 nodes 列表(不改原 ir)。
|
||
|
||
对齐 StorylineMgr.ReplaceStoryEventData 的语义:
|
||
- 子序列每个节点 id 加 {refId}__ 前缀;内部跳转同步加前缀;
|
||
- 子序列出口(next 为空的尾节点)的 next 接到 out_ref 节点的 next;
|
||
- 引用该 out_ref 节点的跳转改写为子序列入口 {refId}__{entry}。
|
||
多个 out_ref 引用同一子序列 → 前缀不同 → id 天然不撞(复用本意)。
|
||
M4 仅支持一层(子序列内不得再含 out_ref)。
|
||
"""
|
||
seqs = {s["id"]: s for s in ir.get("sequences", [])}
|
||
src_nodes = ir.get("nodes", [])
|
||
out_refs = [n for n in src_nodes if n.get("kind") == "out_ref"]
|
||
if not out_refs:
|
||
return list(src_nodes)
|
||
|
||
expanded = [] # 展开出来的子序列节点
|
||
ref_entry = {} # refId -> 子序列入口节点 id(含前缀)
|
||
|
||
for r in out_refs:
|
||
rid, ref = r["id"], r.get("ref")
|
||
seq = seqs.get(ref)
|
||
if seq is None:
|
||
raise CompileError("out_ref 节点 %s 引用了不存在的子序列 %r" % (rid, ref))
|
||
sub = seq.get("nodes", [])
|
||
if not sub:
|
||
raise CompileError("子序列 %r 为空(被 %s 引用)" % (ref, rid))
|
||
sub_ids = {n["id"] for n in sub}
|
||
prefix = rid + OUT_REF_SEP
|
||
ref_entry[rid] = prefix + sub[0]["id"]
|
||
|
||
def remap(t, is_next):
|
||
"""重写子序列内部跳转目标。"""
|
||
if t in sub_ids:
|
||
return prefix + t
|
||
if not t: # 空 next = 子序列出口 -> 接回 caller
|
||
return r.get("next", "") if is_next else t
|
||
return t # 指向子序列外(共享结局等)保持原样
|
||
|
||
for n in sub:
|
||
if n.get("kind") == "out_ref":
|
||
raise CompileError("子序列 %r 含嵌套 out_ref(节点 %s),M4 暂不支持" % (ref, n["id"]))
|
||
c = dict(n)
|
||
c["id"] = prefix + n["id"]
|
||
if "next" in c:
|
||
c["next"] = remap(c.get("next"), True)
|
||
elif n.get("kind") not in ("choice", "choice_once", "random", "fight"):
|
||
# 链尾叶子(无 next 字段)-> 出口接回 caller.next
|
||
c["next"] = r.get("next", "")
|
||
if "options" in c:
|
||
c["options"] = [_remap_opt(o, remap) for o in c["options"]]
|
||
if "branches" in c:
|
||
c["branches"] = [dict(b, goto=remap(b.get("goto"), False)) for b in c["branches"]]
|
||
if c.get("kind") == "fight":
|
||
c["win"] = remap(c.get("win"), False)
|
||
c["lose"] = remap(c.get("lose"), False)
|
||
expanded.append(c)
|
||
|
||
# 主图节点:去掉 out_ref 占位,并把指向它们的跳转改写到子序列入口
|
||
def deref(t):
|
||
return ref_entry.get(t, t)
|
||
|
||
result = []
|
||
for n in src_nodes:
|
||
if n.get("kind") == "out_ref":
|
||
continue
|
||
c = dict(n)
|
||
if "next" in c:
|
||
c["next"] = deref(c.get("next"))
|
||
if "options" in c:
|
||
c["options"] = [dict(o, goto=deref(o.get("goto"))) for o in c["options"]]
|
||
if "branches" in c:
|
||
c["branches"] = [dict(b, goto=deref(b.get("goto"))) for b in c["branches"]]
|
||
if c.get("kind") == "fight":
|
||
c["win"] = deref(c.get("win"))
|
||
c["lose"] = deref(c.get("lose"))
|
||
result.append(c)
|
||
return result + expanded
|
||
|
||
|
||
def _remap_opt(o, remap):
|
||
c = dict(o)
|
||
c["goto"] = remap(o.get("goto"), False)
|
||
if o.get("skip"):
|
||
sk = dict(o["skip"])
|
||
sk["node"] = remap(o["skip"].get("node"), False)
|
||
c["skip"] = sk
|
||
return c
|
||
|
||
|
||
# ============ 编译 ============
|
||
def compile_ir(ir, dic):
|
||
group = ir["id"]
|
||
src_nodes = expand_out_refs(ir)
|
||
nodes = {n["id"]: n for n in src_nodes}
|
||
endings = {e["id"]: e for e in ir.get("endings", [])}
|
||
|
||
def entry(tid):
|
||
"""目标节点的入口行 id(含 group 前缀)。"""
|
||
n = nodes.get(tid)
|
||
if n and n.get("kind") in ("choice", "choice_once", "random"):
|
||
return "%s_%s_o0" % (group, tid)
|
||
return "%s_%s" % (group, tid)
|
||
|
||
def children(tid):
|
||
n = nodes.get(tid)
|
||
if not n:
|
||
return []
|
||
res = []
|
||
if n.get("next"):
|
||
res.append(n["next"])
|
||
res += [o["goto"] for o in n.get("options", [])]
|
||
res += [b["goto"] for b in n.get("branches", [])]
|
||
if n.get("kind") == "fight":
|
||
res += [n.get("win"), n.get("lose")]
|
||
return [r for r in res if r]
|
||
|
||
indeg = {i: 0 for i in list(nodes) + list(endings)}
|
||
for nid in nodes:
|
||
for c in children(nid):
|
||
if c in indeg:
|
||
indeg[c] += 1
|
||
roots = [i for i in nodes if indeg[i] == 0] or list(nodes)[:1]
|
||
|
||
order, seen = [], set()
|
||
|
||
def dfs(tid):
|
||
if tid in seen or tid not in indeg:
|
||
return
|
||
seen.add(tid)
|
||
order.append(tid)
|
||
for c in children(tid):
|
||
dfs(c)
|
||
for r in roots:
|
||
dfs(r)
|
||
for i in list(nodes) + list(endings):
|
||
if i not in seen:
|
||
order.append(i); seen.add(i)
|
||
|
||
rows, step = [], 0
|
||
|
||
def newrow(**kw):
|
||
nonlocal step
|
||
step += 1
|
||
row = {"group": group, "step": step, "type": 0}
|
||
row.update(kw)
|
||
rows.append(row)
|
||
return row
|
||
|
||
for tid in order:
|
||
if tid in endings:
|
||
e = endings[tid]
|
||
rid_str, rac = dic.compile_grants(e.get("grants"))
|
||
status = {"success": 1, "fail": 2, "end": 3}.get(e.get("result", "success"))
|
||
if status is None:
|
||
raise CompileError("结局 %s 的 result 非法: %r(仅 success/fail/end)"
|
||
% (tid, e.get("result")))
|
||
end_id = "%s_%s__end" % (group, tid)
|
||
r = newrow(id="%s_%s" % (group, tid), missionText=e.get("summary", ""),
|
||
nextStepId=end_id)
|
||
if rid_str:
|
||
r["resultRewardIds"] = rid_str
|
||
if rac:
|
||
r["roleActionCode"] = rac
|
||
newrow(id=end_id, gameTaskStatus=status)
|
||
continue
|
||
n = nodes[tid]
|
||
kind = n["kind"]
|
||
if kind == "narration":
|
||
spk = n.get("speaker", "P1")
|
||
newrow(id="%s_%s" % (group, tid), points=[spk], content=n.get("text", ""),
|
||
nextStepId=entry(n["next"]) if n.get("next") else "")
|
||
elif kind == "dialogue":
|
||
newrow(id="%s_%s" % (group, tid), points=[n["speaker"]],
|
||
content=n.get("text", ""),
|
||
nextStepId=entry(n["next"]) if n.get("next") else "")
|
||
elif kind in ("choice", "choice_once"):
|
||
opt_type = 1 if kind == "choice" else 2
|
||
for i, o in enumerate(n.get("options", [])):
|
||
rid_str, rac = dic.compile_grants((o.get("reward") or {}).get("grants"))
|
||
r = newrow(id="%s_%s_o%d" % (group, tid, i), type=opt_type,
|
||
choose=o.get("text", ""), points=["P1"],
|
||
nextStepId=entry(o["goto"]))
|
||
if o.get("condition"):
|
||
r["condition"] = dic.compile_cond(o["condition"])
|
||
if rid_str:
|
||
r["resultRewardIds"] = rid_str
|
||
if rac:
|
||
r["roleActionCode"] = rac
|
||
# skip:押注跳过——选该项时把彩头注入 skip.node 行(runtime 自拼 {group}_)
|
||
if o.get("skip"):
|
||
sk = o["skip"]
|
||
sk_str, _ = dic.compile_grants((sk.get("reward") or {}).get("grants"))
|
||
r["skipNodeId"] = sk["node"]
|
||
r["skipReward"] = sk_str
|
||
elif kind == "random":
|
||
for i, b in enumerate(n.get("branches", [])):
|
||
newrow(id="%s_%s_o%d" % (group, tid, i), type=3,
|
||
weight=b.get("weight", 1), nextStepId=entry(b["goto"]))
|
||
elif kind == "fight":
|
||
newrow(id="%s_%s" % (group, tid),
|
||
points=n.get("camp1") or ["P1"],
|
||
camp2Fighters=n.get("camp2", []),
|
||
fightStatus=[str(n["fight_type"]), entry(n["win"]), entry(n["lose"])])
|
||
elif kind == "move":
|
||
if n.get("mode") == "teleport":
|
||
ms = ["1"]
|
||
elif n.get("mode") == "remove":
|
||
ms = ["2"]
|
||
else:
|
||
ms = [str(n.get("speed", 6)), n.get("ani", "")]
|
||
newrow(id="%s_%s" % (group, tid), points=[n["actor"]],
|
||
movePoint=[n["to"]], moveStatus=ms,
|
||
nextStepId=entry(n["next"]) if n.get("next") else "")
|
||
elif kind == "anim":
|
||
anis = [n["ani"]] + ([str(n["angle"])] if n.get("angle") is not None else [])
|
||
newrow(id="%s_%s" % (group, tid), points=[n["actor"]], anis=anis,
|
||
nextStepId=entry(n["next"]) if n.get("next") else "")
|
||
elif kind == "reward":
|
||
rid_str, rac = dic.compile_grants(n.get("grants"))
|
||
r = newrow(id="%s_%s" % (group, tid),
|
||
nextStepId=entry(n["next"]) if n.get("next") else "")
|
||
if rid_str:
|
||
r["resultRewardIds"] = rid_str
|
||
if rac:
|
||
r["roleActionCode"] = rac
|
||
else:
|
||
raise CompileError("未知节点 kind: %r (节点 %s)" % (kind, tid))
|
||
|
||
ids = [r["id"] for r in rows]
|
||
dup = {x for x in ids if ids.count(x) > 1}
|
||
if dup:
|
||
raise CompileError("行 id 重复: %s" % dup)
|
||
return rows
|
||
|
||
|
||
def extract_texts(ir):
|
||
"""抽取所有玩家可见文本(去重保序),供本地化翻译。
|
||
含 sequences 子序列内文本。"""
|
||
texts, seen = [], set()
|
||
|
||
def add(t):
|
||
if t and t not in seen:
|
||
seen.add(t); texts.append(t)
|
||
|
||
def walk_nodes(node_list):
|
||
for n in node_list:
|
||
add(n.get("text"))
|
||
for o in n.get("options", []):
|
||
add(o.get("text"))
|
||
|
||
walk_nodes(ir.get("nodes", []))
|
||
for s in ir.get("sequences", []):
|
||
walk_nodes(s.get("nodes", []))
|
||
for e in ir.get("endings", []):
|
||
add(e.get("summary"))
|
||
return texts
|