# -*- 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