init: 剧情事件协作 Web 编辑器独立仓(从 SGame/tools/event_authoring 拆出)
This commit is contained in:
159
samples/bishi_yazhu.events.json
Normal file
159
samples/bishi_yazhu.events.json
Normal file
@ -0,0 +1,159 @@
|
||||
[
|
||||
{
|
||||
"group": "QY_BSYZ",
|
||||
"step": 1,
|
||||
"type": 0,
|
||||
"id": "QY_BSYZ_n1",
|
||||
"points": [
|
||||
"P1"
|
||||
],
|
||||
"content": "市集擂台前人声鼎沸,一名精壮武师立于台上,无人敢应战。",
|
||||
"nextStepId": "QY_BSYZ_n2"
|
||||
},
|
||||
{
|
||||
"group": "QY_BSYZ",
|
||||
"step": 2,
|
||||
"type": 0,
|
||||
"id": "QY_BSYZ_n2",
|
||||
"points": [
|
||||
"NP2"
|
||||
],
|
||||
"content": "押注了押注了!是亲自下场,还是押这位高手赢?",
|
||||
"nextStepId": "QY_BSYZ_n3_o0"
|
||||
},
|
||||
{
|
||||
"group": "QY_BSYZ",
|
||||
"step": 3,
|
||||
"type": 1,
|
||||
"id": "QY_BSYZ_n3_o0",
|
||||
"choose": "亲自下场会会他",
|
||||
"points": [
|
||||
"P1"
|
||||
],
|
||||
"nextStepId": "QY_BSYZ_fight1"
|
||||
},
|
||||
{
|
||||
"group": "QY_BSYZ",
|
||||
"step": 4,
|
||||
"type": 1,
|
||||
"id": "QY_BSYZ_n3_o1",
|
||||
"choose": "押这位高手赢(押注50两)",
|
||||
"points": [
|
||||
"P1"
|
||||
],
|
||||
"nextStepId": "QY_BSYZ_ref_cheer_b__c1",
|
||||
"skipNodeId": "end_bet_win",
|
||||
"skipReward": "V19,300"
|
||||
},
|
||||
{
|
||||
"group": "QY_BSYZ",
|
||||
"step": 5,
|
||||
"type": 1,
|
||||
"id": "QY_BSYZ_n3_o2",
|
||||
"choose": "看个热闹就走",
|
||||
"points": [
|
||||
"P1"
|
||||
],
|
||||
"nextStepId": "QY_BSYZ_end_leave"
|
||||
},
|
||||
{
|
||||
"group": "QY_BSYZ",
|
||||
"step": 6,
|
||||
"type": 0,
|
||||
"id": "QY_BSYZ_fight1",
|
||||
"points": [
|
||||
"P1"
|
||||
],
|
||||
"camp2Fighters": [
|
||||
"NP1"
|
||||
],
|
||||
"fightStatus": [
|
||||
"1",
|
||||
"QY_BSYZ_ref_cheer_a__c1",
|
||||
"QY_BSYZ_end_fight_lose"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "QY_BSYZ",
|
||||
"step": 7,
|
||||
"type": 0,
|
||||
"id": "QY_BSYZ_ref_cheer_a__c1",
|
||||
"points": [
|
||||
"NP2"
|
||||
],
|
||||
"content": "好身手!满堂喝彩!",
|
||||
"nextStepId": "QY_BSYZ_end_fight_win"
|
||||
},
|
||||
{
|
||||
"group": "QY_BSYZ",
|
||||
"step": 8,
|
||||
"type": 0,
|
||||
"id": "QY_BSYZ_end_fight_win",
|
||||
"missionText": "技压群雄",
|
||||
"nextStepId": "QY_BSYZ_end_fight_win__end",
|
||||
"resultRewardIds": "V19,200;NP1,V15,20"
|
||||
},
|
||||
{
|
||||
"group": "QY_BSYZ",
|
||||
"step": 9,
|
||||
"type": 0,
|
||||
"id": "QY_BSYZ_end_fight_win__end",
|
||||
"gameTaskStatus": 1
|
||||
},
|
||||
{
|
||||
"group": "QY_BSYZ",
|
||||
"step": 10,
|
||||
"type": 0,
|
||||
"id": "QY_BSYZ_end_fight_lose",
|
||||
"missionText": "败下阵来",
|
||||
"nextStepId": "QY_BSYZ_end_fight_lose__end"
|
||||
},
|
||||
{
|
||||
"group": "QY_BSYZ",
|
||||
"step": 11,
|
||||
"type": 0,
|
||||
"id": "QY_BSYZ_end_fight_lose__end",
|
||||
"gameTaskStatus": 2
|
||||
},
|
||||
{
|
||||
"group": "QY_BSYZ",
|
||||
"step": 12,
|
||||
"type": 0,
|
||||
"id": "QY_BSYZ_ref_cheer_b__c1",
|
||||
"points": [
|
||||
"NP2"
|
||||
],
|
||||
"content": "好身手!满堂喝彩!",
|
||||
"nextStepId": "QY_BSYZ_end_bet_win"
|
||||
},
|
||||
{
|
||||
"group": "QY_BSYZ",
|
||||
"step": 13,
|
||||
"type": 0,
|
||||
"id": "QY_BSYZ_end_bet_win",
|
||||
"missionText": "押对了,坐收彩头",
|
||||
"nextStepId": "QY_BSYZ_end_bet_win__end"
|
||||
},
|
||||
{
|
||||
"group": "QY_BSYZ",
|
||||
"step": 14,
|
||||
"type": 0,
|
||||
"id": "QY_BSYZ_end_bet_win__end",
|
||||
"gameTaskStatus": 1
|
||||
},
|
||||
{
|
||||
"group": "QY_BSYZ",
|
||||
"step": 15,
|
||||
"type": 0,
|
||||
"id": "QY_BSYZ_end_leave",
|
||||
"missionText": "看罢热闹离场",
|
||||
"nextStepId": "QY_BSYZ_end_leave__end"
|
||||
},
|
||||
{
|
||||
"group": "QY_BSYZ",
|
||||
"step": 16,
|
||||
"type": 0,
|
||||
"id": "QY_BSYZ_end_leave__end",
|
||||
"gameTaskStatus": 1
|
||||
}
|
||||
]
|
||||
11
samples/bishi_yazhu.i18n.tsv
Normal file
11
samples/bishi_yazhu.i18n.tsv
Normal file
@ -0,0 +1,11 @@
|
||||
# 简体中文(key,勿改) 韩文(待译;繁体无需填,SGameText 自动转换)
|
||||
市集擂台前人声鼎沸,一名精壮武师立于台上,无人敢应战。
|
||||
押注了押注了!是亲自下场,还是押这位高手赢?
|
||||
亲自下场会会他
|
||||
押这位高手赢(押注50两)
|
||||
看个热闹就走
|
||||
好身手!满堂喝彩!
|
||||
技压群雄
|
||||
败下阵来
|
||||
押对了,坐收彩头
|
||||
看罢热闹离场
|
||||
|
67
samples/bishi_yazhu.ir.json
Normal file
67
samples/bishi_yazhu.ir.json
Normal file
@ -0,0 +1,67 @@
|
||||
{
|
||||
"id": "QY_BSYZ",
|
||||
"title": "比武押注",
|
||||
"theme": "市井江湖·赌性博弈",
|
||||
"scale": "标准奇遇",
|
||||
"roles": [
|
||||
{ "slot": "P1", "name": "路过的你", "archetype": "玩家", "camp": 1 },
|
||||
{ "slot": "NP1", "name": "擂台高手", "archetype": "市井武师", "camp": 2 },
|
||||
{ "slot": "NP2", "name": "赌坊庄家", "archetype": "市井掮客", "camp": 0 }
|
||||
],
|
||||
"stage": { "type": "市集擂台·昼", "point_set": "QY_BSYZ", "reuse_hint": "K3_A" },
|
||||
"sequences": [
|
||||
{
|
||||
"id": "seq_cheer",
|
||||
"nodes": [
|
||||
{ "id": "c1", "kind": "dialogue", "speaker": "NP2",
|
||||
"text": "好身手!满堂喝彩!" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "n1", "kind": "narration",
|
||||
"text": "市集擂台前人声鼎沸,一名精壮武师立于台上,无人敢应战。",
|
||||
"next": "n2"
|
||||
},
|
||||
{
|
||||
"id": "n2", "kind": "dialogue", "speaker": "NP2",
|
||||
"text": "押注了押注了!是亲自下场,还是押这位高手赢?",
|
||||
"next": "n3"
|
||||
},
|
||||
{
|
||||
"id": "n3", "kind": "choice",
|
||||
"options": [
|
||||
{ "text": "亲自下场会会他", "goto": "fight1" },
|
||||
{
|
||||
"text": "押这位高手赢(押注50两)",
|
||||
"skip": { "node": "end_bet_win", "reward": { "grants": [ { "kind": "银两", "value": 300 } ] } },
|
||||
"goto": "ref_cheer_b"
|
||||
},
|
||||
{ "text": "看个热闹就走", "goto": "end_leave" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "fight1", "kind": "fight",
|
||||
"fight_type": 1,
|
||||
"camp1": [ "P1" ],
|
||||
"camp2": [ "NP1" ],
|
||||
"win": "ref_cheer_a",
|
||||
"lose": "end_fight_lose"
|
||||
},
|
||||
{ "id": "ref_cheer_a", "kind": "out_ref", "ref": "seq_cheer", "next": "end_fight_win" },
|
||||
{ "id": "ref_cheer_b", "kind": "out_ref", "ref": "seq_cheer", "next": "end_bet_win" }
|
||||
],
|
||||
"endings": [
|
||||
{
|
||||
"id": "end_fight_win", "summary": "技压群雄",
|
||||
"grants": [
|
||||
{ "kind": "银两", "value": 200 },
|
||||
{ "kind": "友好度", "target": "NP1", "value": 20 }
|
||||
]
|
||||
},
|
||||
{ "id": "end_fight_lose", "summary": "败下阵来", "grants": [], "result": "fail" },
|
||||
{ "id": "end_bet_win", "summary": "押对了,坐收彩头", "grants": [] },
|
||||
{ "id": "end_leave", "summary": "看罢热闹离场", "grants": [] }
|
||||
]
|
||||
}
|
||||
151
samples/yuye_koumen.events.json
Normal file
151
samples/yuye_koumen.events.json
Normal file
@ -0,0 +1,151 @@
|
||||
[
|
||||
{
|
||||
"group": "QY_YYKM",
|
||||
"step": 1,
|
||||
"type": 0,
|
||||
"id": "QY_YYKM_n1",
|
||||
"points": [
|
||||
"P1"
|
||||
],
|
||||
"content": "暴雨倾盆,山门外的灯笼在风里摇晃。一阵急促的叩门声,盖过了雷声。",
|
||||
"nextStepId": "QY_YYKM_n2"
|
||||
},
|
||||
{
|
||||
"group": "QY_YYKM",
|
||||
"step": 2,
|
||||
"type": 0,
|
||||
"id": "QY_YYKM_n2",
|
||||
"points": [
|
||||
"NP1"
|
||||
],
|
||||
"content": "在下途经贵派,身负旧伤,恳请借宿一晚,天明即走。",
|
||||
"nextStepId": "QY_YYKM_n3"
|
||||
},
|
||||
{
|
||||
"group": "QY_YYKM",
|
||||
"step": 3,
|
||||
"type": 0,
|
||||
"id": "QY_YYKM_n3",
|
||||
"points": [
|
||||
"P1"
|
||||
],
|
||||
"content": "(这位侠客腰间的铁牌……分明是近日劫掠商队的黑风寨样式。)",
|
||||
"nextStepId": "QY_YYKM_n4_o0"
|
||||
},
|
||||
{
|
||||
"group": "QY_YYKM",
|
||||
"step": 4,
|
||||
"type": 1,
|
||||
"id": "QY_YYKM_n4_o0",
|
||||
"choose": "江湖救急,先收留他",
|
||||
"points": [
|
||||
"P1"
|
||||
],
|
||||
"nextStepId": "QY_YYKM_end_ally"
|
||||
},
|
||||
{
|
||||
"group": "QY_YYKM",
|
||||
"step": 5,
|
||||
"type": 1,
|
||||
"id": "QY_YYKM_n4_o1",
|
||||
"choose": "不动声色,擒下他交予掌门",
|
||||
"points": [
|
||||
"P1"
|
||||
],
|
||||
"nextStepId": "QY_YYKM_fight1"
|
||||
},
|
||||
{
|
||||
"group": "QY_YYKM",
|
||||
"step": 6,
|
||||
"type": 1,
|
||||
"id": "QY_YYKM_n4_o2",
|
||||
"choose": "赠些盘缠,请他即刻离去",
|
||||
"points": [
|
||||
"P1"
|
||||
],
|
||||
"nextStepId": "QY_YYKM_end_pay",
|
||||
"condition": "V19,2,500",
|
||||
"resultRewardIds": "V19,-500"
|
||||
},
|
||||
{
|
||||
"group": "QY_YYKM",
|
||||
"step": 7,
|
||||
"type": 0,
|
||||
"id": "QY_YYKM_end_ally",
|
||||
"missionText": "结义同盟",
|
||||
"nextStepId": "QY_YYKM_end_ally__end",
|
||||
"resultRewardIds": "NP1,V15,30",
|
||||
"roleActionCode": "JoinToPlayerSch=NP1"
|
||||
},
|
||||
{
|
||||
"group": "QY_YYKM",
|
||||
"step": 8,
|
||||
"type": 0,
|
||||
"id": "QY_YYKM_end_ally__end",
|
||||
"gameTaskStatus": 1
|
||||
},
|
||||
{
|
||||
"group": "QY_YYKM",
|
||||
"step": 9,
|
||||
"type": 0,
|
||||
"id": "QY_YYKM_fight1",
|
||||
"points": [
|
||||
"P1"
|
||||
],
|
||||
"camp2Fighters": [
|
||||
"NP1"
|
||||
],
|
||||
"fightStatus": [
|
||||
"1",
|
||||
"QY_YYKM_end_subdue",
|
||||
"QY_YYKM_end_lose"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "QY_YYKM",
|
||||
"step": 10,
|
||||
"type": 0,
|
||||
"id": "QY_YYKM_end_subdue",
|
||||
"missionText": "擒贼献掌门",
|
||||
"nextStepId": "QY_YYKM_end_subdue__end",
|
||||
"resultRewardIds": "V19,200;P6,1"
|
||||
},
|
||||
{
|
||||
"group": "QY_YYKM",
|
||||
"step": 11,
|
||||
"type": 0,
|
||||
"id": "QY_YYKM_end_subdue__end",
|
||||
"gameTaskStatus": 1
|
||||
},
|
||||
{
|
||||
"group": "QY_YYKM",
|
||||
"step": 12,
|
||||
"type": 0,
|
||||
"id": "QY_YYKM_end_lose",
|
||||
"missionText": "技不如人",
|
||||
"nextStepId": "QY_YYKM_end_lose__end"
|
||||
},
|
||||
{
|
||||
"group": "QY_YYKM",
|
||||
"step": 13,
|
||||
"type": 0,
|
||||
"id": "QY_YYKM_end_lose__end",
|
||||
"gameTaskStatus": 2
|
||||
},
|
||||
{
|
||||
"group": "QY_YYKM",
|
||||
"step": 14,
|
||||
"type": 0,
|
||||
"id": "QY_YYKM_end_pay",
|
||||
"missionText": "破财消灾",
|
||||
"nextStepId": "QY_YYKM_end_pay__end",
|
||||
"resultRewardIds": "NP1,V15,10"
|
||||
},
|
||||
{
|
||||
"group": "QY_YYKM",
|
||||
"step": 15,
|
||||
"type": 0,
|
||||
"id": "QY_YYKM_end_pay__end",
|
||||
"gameTaskStatus": 1
|
||||
}
|
||||
]
|
||||
248
samples/yuye_koumen.html
Normal file
248
samples/yuye_koumen.html
Normal file
@ -0,0 +1,248 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>雨夜叩门 · Story 面板</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin:0; font-family:"Microsoft YaHei","PingFang SC",sans-serif;
|
||||
background:#161310; color:#e8e0d4; }
|
||||
header { padding:14px 22px; background:#1f1a15; border-bottom:1px solid #3a322a; }
|
||||
header h1 { margin:0; font-size:20px; color:#e6c878; }
|
||||
header .meta { margin-top:4px; font-size:13px; color:#9a8f7e; }
|
||||
header .roles { margin-top:8px; display:flex; flex-wrap:wrap; gap:8px; }
|
||||
.role { font-size:12px; padding:3px 9px; border-radius:11px;
|
||||
background:#2a241d; border:1px solid #4a4030; }
|
||||
.role b { color:#e6c878; }
|
||||
#wrap { display:flex; height:calc(100vh - 86px); }
|
||||
#graph { position:relative; flex:1; overflow:auto; padding:30px; }
|
||||
#svg { position:absolute; top:0; left:0; pointer-events:none; }
|
||||
#layers { position:relative; z-index:2; margin:0 auto; }
|
||||
.node { background:#262019; border:1.5px solid #4a4030; border-radius:9px;
|
||||
padding:9px 12px; width:188px; cursor:pointer; transition:.15s;
|
||||
box-shadow:0 2px 6px rgba(0,0,0,.4); }
|
||||
.node:hover { border-color:#e6c878; transform:translateY(-2px); }
|
||||
.node.sel { border-color:#e6c878; box-shadow:0 0 0 2px rgba(230,200,120,.4); }
|
||||
.node .k { font-size:11px; color:#b89a5a; font-weight:bold; }
|
||||
.node .t { font-size:13px; margin-top:3px; line-height:1.4; color:#ddd3c2; }
|
||||
.node.kind-ending { background:#3a2a17; border-color:#e0a850;
|
||||
box-shadow:0 0 0 1px rgba(224,168,80,.35), 0 2px 9px rgba(0,0,0,.45); }
|
||||
.node.kind-ending .k { color:#f2c463; }
|
||||
.node.kind-ending .t { color:#eccf95; }
|
||||
.node.kind-ending .rw { font-size:11.5px; color:#c9a86a; margin-top:4px;
|
||||
border-top:1px dashed #6a5630; padding-top:4px; }
|
||||
.node.kind-fight { border-color:#7a4a4a; background:#2a1c1c; }
|
||||
.node.kind-fight .k { color:#d87878; }
|
||||
/* 选择节点:流程图判定框,蓝色横六边形(菱形感) */
|
||||
.node.kind-choice, .node.kind-choice_once {
|
||||
background:#1d2840; border:none; padding-left:26px; padding-right:26px;
|
||||
clip-path: polygon(16px 0, calc(100% - 16px) 0, 100% 50%, calc(100% - 16px) 100%, 16px 100%, 0 50%); }
|
||||
.node.kind-choice .k, .node.kind-choice_once .k { color:#9ec0f0; }
|
||||
.node.kind-choice .t, .node.kind-choice_once .t { color:#cdddf0; }
|
||||
#side { width:340px; background:#1c1813; border-left:1px solid #3a322a;
|
||||
overflow:auto; padding:0; }
|
||||
#detail { padding:16px; }
|
||||
#detail h3 { margin:0 0 8px; color:#e6c878; font-size:16px; }
|
||||
#detail .row { margin:7px 0; font-size:13px; line-height:1.6; }
|
||||
#detail .lab { color:#9a8f7e; }
|
||||
#detail .quote { background:#241f18; border-left:3px solid #b89a5a;
|
||||
padding:8px 10px; border-radius:4px; color:#e8dfce; }
|
||||
#detail .opt { background:#221d16; border:1px solid #3a322a; border-radius:6px;
|
||||
padding:7px 9px; margin:6px 0; font-size:12.5px; }
|
||||
#detail .opt .arrow { color:#7aa0d8; }
|
||||
.sec-title { padding:12px 16px; font-size:13px; color:#9a8f7e;
|
||||
border-bottom:1px solid #3a322a; background:#19150f; letter-spacing:1px; }
|
||||
#rewards { padding:12px 16px; font-size:12.5px; }
|
||||
#rewards .rw { margin:6px 0; padding:6px 9px; background:#221d16; border-radius:5px; }
|
||||
#rewards .rw b { color:#e6c878; }
|
||||
.toolbar { padding:10px 16px; border-top:1px solid #3a322a; }
|
||||
.toolbar button { background:#3a3024; color:#e6c878; border:1px solid #5a4a32;
|
||||
padding:6px 14px; border-radius:5px; cursor:pointer; font-size:13px; }
|
||||
.toolbar button:hover { background:#4a3d2c; }
|
||||
.empty { color:#6a6256; font-size:13px; padding:20px 16px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1 id="h-title"></h1>
|
||||
<div class="meta" id="h-meta"></div>
|
||||
<div class="roles" id="h-roles"></div>
|
||||
</header>
|
||||
<div id="wrap">
|
||||
<div id="graph"><svg id="svg"></svg><div id="layers"></div></div>
|
||||
<div id="side">
|
||||
<div class="sec-title">节点详情</div>
|
||||
<div id="detail"><div class="empty">点击左侧任意节点查看台词 / 角色 / 奖励</div></div>
|
||||
<div class="sec-title">奖励总览</div>
|
||||
<div id="rewards"></div>
|
||||
<div class="toolbar"><button onclick="exportIR()">导出 IR (JSON)</button></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const IR = {"id": "QY_YYKM", "title": "雨夜叩门", "theme": "正统武侠·道德抉择", "scale": "标准奇遇", "roles": [{"slot": "P1", "name": "值夜弟子", "archetype": "玩家", "camp": 1}, {"slot": "NP1", "name": "神秘剑客", "archetype": "负伤外门高手", "camp": 0}, {"slot": "NP2", "name": "掌门", "archetype": "本门掌门", "camp": 0}], "stage": {"type": "门派入口·夜", "reuse_hint": "K3_A"}, "nodes": [{"id": "n1", "kind": "narration", "text": "暴雨倾盆,山门外的灯笼在风里摇晃。一阵急促的叩门声,盖过了雷声。", "next": "n2"}, {"id": "n2", "kind": "dialogue", "speaker": "NP1", "camera": "NP1", "text": "在下途经贵派,身负旧伤,恳请借宿一晚,天明即走。", "next": "n3"}, {"id": "n3", "kind": "dialogue", "speaker": "P1", "text": "(这位侠客腰间的铁牌……分明是近日劫掠商队的黑风寨样式。)", "next": "n4"}, {"id": "n4", "kind": "choice", "options": [{"text": "江湖救急,先收留他", "goto": "end_ally"}, {"text": "不动声色,擒下他交予掌门", "goto": "fight1"}, {"text": "赠些盘缠,请他即刻离去", "condition": {"kind": "银两", "op": ">=", "value": 500}, "reward": {"grants": [{"kind": "银两", "value": -500}]}, "goto": "end_pay"}]}, {"id": "fight1", "kind": "fight", "fight_type": 1, "camp1": ["P1"], "camp2": ["NP1"], "win": "end_subdue", "lose": "end_lose"}], "endings": [{"id": "end_ally", "summary": "结义同盟", "grants": [{"kind": "友好度", "target": "NP1", "value": 30}, {"kind": "入门", "target": "NP1"}]}, {"id": "end_pay", "summary": "破财消灾", "grants": [{"kind": "友好度", "target": "NP1", "value": 10}]}, {"id": "end_subdue", "summary": "擒贼献掌门", "grants": [{"kind": "银两", "value": 200}, {"kind": "道具", "item": "P6", "value": 1}]}, {"id": "end_lose", "summary": "技不如人", "grants": []}]};
|
||||
|
||||
const roleName = {};
|
||||
(IR.roles||[]).forEach(r => roleName[r.slot] = r.name + (r.archetype?("〔"+r.archetype+"〕"):""));
|
||||
const nameOf = s => (s==="P1"?"玩家":"") || roleName[s] || s;
|
||||
|
||||
// ---- 收集节点(含结局)与边 ----
|
||||
const nodes = {};
|
||||
(IR.nodes||[]).forEach(n => nodes[n.id] = Object.assign({_end:false}, n));
|
||||
(IR.endings||[]).forEach(e => nodes[e.id] = Object.assign({_end:true, kind:"ending"}, e));
|
||||
|
||||
const edges = [];
|
||||
function addEdge(u,v,type,label){ if(v && nodes[v]) edges.push({u,v,type,label:label||""}); }
|
||||
(IR.nodes||[]).forEach(n => {
|
||||
if(n.next) addEdge(n.id,n.next,"next");
|
||||
(n.options||[]).forEach(o => addEdge(n.id,o.goto,"option",o.text));
|
||||
(n.branches||[]).forEach(b => addEdge(n.id,b.goto,"random","权重"+(b.weight!=null?b.weight:"")));
|
||||
if(n.kind==="fight"){ addEdge(n.id,n.win,"win","胜"); addEdge(n.id,n.lose,"lose","败"); }
|
||||
});
|
||||
|
||||
// ---- 最长路径分层 ----
|
||||
const layer = {}; Object.keys(nodes).forEach(id => layer[id]=0);
|
||||
let changed=true, guard=0;
|
||||
while(changed && guard++<999){ changed=false;
|
||||
edges.forEach(e => { if(layer[e.v] < layer[e.u]+1){ layer[e.v]=layer[e.u]+1; changed=true; } });
|
||||
}
|
||||
const byLayer={};
|
||||
Object.keys(nodes).forEach(id => { (byLayer[layer[id]]=byLayer[layer[id]]||[]).push(id); });
|
||||
|
||||
// ---- 节点摘要 ----
|
||||
function summary(n){
|
||||
if(n.kind==="narration") return ["旁白", (n.text||"").slice(0,28)];
|
||||
if(n.kind==="dialogue") return ["对话 · "+nameOf(n.speaker), (n.text||"").slice(0,24)];
|
||||
if(n.kind==="choice") return ["选择 ("+(n.options||[]).length+"项)", (n.options||[]).map(o=>o.text).join(" / ").slice(0,30)];
|
||||
if(n.kind==="choice_once")return ["一次性选择", (n.options||[]).map(o=>o.text).join(" / ").slice(0,30)];
|
||||
if(n.kind==="random") return ["随机分支", (n.branches||[]).length+" 路"];
|
||||
if(n.kind==="fight") return ["战斗", "vs "+(n.camp2||[]).map(nameOf).join("、")];
|
||||
if(n.kind==="move") return ["走位 · "+nameOf(n.actor), "→ "+(n.to||"")];
|
||||
if(n.kind==="anim") return ["动画 · "+nameOf(n.actor), n.ani||""];
|
||||
if(n.kind==="reward") return ["奖励结算", ""];
|
||||
if(n.kind==="ending") return ["★ 结局", n.summary||""];
|
||||
return [n.kind, ""];
|
||||
}
|
||||
|
||||
// ---- 树布局:x 由子树决定(父居中于子),y 由最长路径层级决定 ----
|
||||
const layersDiv=document.getElementById("layers");
|
||||
const ROW=150, SP=232, NW=188;
|
||||
const childMap={}; Object.keys(nodes).forEach(id=>childMap[id]=[]);
|
||||
const indeg={}; Object.keys(nodes).forEach(id=>indeg[id]=0);
|
||||
const seenE=new Set();
|
||||
edges.forEach(e=>{ const k=e.u+">"+e.v; if(!seenE.has(k)){ seenE.add(k); childMap[e.u].push(e.v); indeg[e.v]++; } });
|
||||
let roots=Object.keys(nodes).filter(id=>indeg[id]===0);
|
||||
if(!roots.length) roots=[Object.keys(nodes)[0]];
|
||||
const xpos={}; let nextX=0; const vis=new Set();
|
||||
function assignX(id){
|
||||
if(vis.has(id)) return; vis.add(id);
|
||||
if(childMap[id].length===0){ xpos[id]=nextX; nextX+=SP; return; } // 叶子(结局)从左到右排开
|
||||
childMap[id].forEach(assignX); // 已访问的会直接 return,但 xpos 已在
|
||||
const placed=childMap[id].map(c=>xpos[c]).filter(v=>v!==undefined); // 父居中于其全部子节点
|
||||
xpos[id]= placed.length ? (Math.min(...placed)+Math.max(...placed))/2 : (nextX+=SP, nextX-SP);
|
||||
}
|
||||
roots.forEach(assignX);
|
||||
Object.keys(nodes).forEach(id=>{ if(xpos[id]===undefined){ xpos[id]=nextX; nextX+=SP; } });
|
||||
|
||||
let maxX=0, maxL=0;
|
||||
Object.keys(nodes).forEach(id=>{
|
||||
const n=nodes[id], [k,t]=summary(n);
|
||||
const d=document.createElement("div");
|
||||
d.className="node kind-"+n.kind; d.id="node-"+id;
|
||||
let inner='<div class="k">'+k+'</div><div class="t">'+(t||id)+'</div>';
|
||||
if(n.kind==="ending"){ const g=(n.grants&&n.grants.length)?n.grants.map(grantStr).join(","):"无奖励"; inner+='<div class="rw">'+g+'</div>'; }
|
||||
d.innerHTML=inner;
|
||||
d.style.position="absolute"; d.style.left=xpos[id]+"px"; d.style.top=(layer[id]*ROW)+"px";
|
||||
d.onclick=()=>select(id);
|
||||
layersDiv.appendChild(d);
|
||||
maxX=Math.max(maxX,xpos[id]); maxL=Math.max(maxL,layer[id]);
|
||||
});
|
||||
layersDiv.style.width=(maxX+NW+40)+"px";
|
||||
layersDiv.style.height=(maxL*ROW+180)+"px";
|
||||
|
||||
// ---- 画连线 ----
|
||||
const svg=document.getElementById("svg");
|
||||
const COLOR={next:"#6a6256",option:"#7aa0d8",random:"#a07ad8",win:"#7ac88a",lose:"#d87878"};
|
||||
function draw(){
|
||||
const g=document.getElementById("graph"), gb=g.getBoundingClientRect();
|
||||
svg.setAttribute("width", g.scrollWidth); svg.setAttribute("height", g.scrollHeight);
|
||||
let h='<defs>';
|
||||
Object.entries(COLOR).forEach(([k,c])=>{ h+='<marker id="ar-'+k+'" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="'+c+'"/></marker>'; });
|
||||
h+='</defs>';
|
||||
edges.forEach(e=>{
|
||||
const a=document.getElementById("node-"+e.u), b=document.getElementById("node-"+e.v);
|
||||
if(!a||!b) return;
|
||||
const ra=a.getBoundingClientRect(), rb=b.getBoundingClientRect();
|
||||
const x1=ra.left-gb.left+g.scrollLeft+ra.width/2, y1=ra.bottom-gb.top+g.scrollTop;
|
||||
const x2=rb.left-gb.left+g.scrollLeft+rb.width/2, y2=rb.top-gb.top+g.scrollTop;
|
||||
const c=COLOR[e.type]||"#6a6256", my=(y1+y2)/2;
|
||||
h+='<path d="M'+x1+','+y1+' C'+x1+','+my+' '+x2+','+my+' '+x2+','+y2+'" stroke="'+c+'" stroke-width="2.2" fill="none" opacity="0.92" marker-end="url(#ar-'+e.type+')"'+(e.type==="option"?' stroke-dasharray="7,4"':'')+'/>';
|
||||
if(e.label){
|
||||
// 标签放在靠近目标节点处(t=0.8),多条同源边自然分散到各自目标上方,避免重叠
|
||||
const t=0.8, mt=1-t;
|
||||
const lx=mt*mt*mt*x1 + 3*mt*mt*t*x1 + 3*mt*t*t*x2 + t*t*t*x2;
|
||||
const ly=mt*mt*mt*y1 + 3*mt*mt*t*my + 3*mt*t*t*my + t*t*t*y2;
|
||||
h+='<text x="'+lx+'" y="'+ly+'" fill="'+c+'" font-size="11.5" text-anchor="middle" stroke="#161310" stroke-width="3.5" paint-order="stroke" style="font-weight:bold">'+e.label.slice(0,12)+'</text>';
|
||||
}
|
||||
});
|
||||
svg.innerHTML=h;
|
||||
}
|
||||
|
||||
// ---- 详情 ----
|
||||
function grantStr(gr){
|
||||
if(gr.kind==="银两") return "银两 "+(gr.value>0?"+":"")+gr.value;
|
||||
if(gr.kind==="道具") return "道具 "+gr.item+" ×"+gr.value;
|
||||
if(gr.kind==="友好度") return nameOf(gr.target)+" 友好度+"+gr.value;
|
||||
if(gr.kind==="入门") return nameOf(gr.target)+" 加入门派";
|
||||
return JSON.stringify(gr);
|
||||
}
|
||||
function condStr(c){ return c ? (c.kind+c.op+c.value) : ""; }
|
||||
function select(id){
|
||||
document.querySelectorAll(".node").forEach(x=>x.classList.remove("sel"));
|
||||
document.getElementById("node-"+id).classList.add("sel");
|
||||
const n=nodes[id]; let h='<h3>'+(summary(n)[0])+' <span style="color:#6a6256;font-size:12px">#'+id+'</span></h3>';
|
||||
if(n.speaker) h+='<div class="row"><span class="lab">角色:</span>'+nameOf(n.speaker)+'</div>';
|
||||
if(n.actor) h+='<div class="row"><span class="lab">角色:</span>'+nameOf(n.actor)+'</div>';
|
||||
if(n.text) h+='<div class="row quote">'+n.text+'</div>';
|
||||
if(n.kind==="fight"){
|
||||
h+='<div class="row"><span class="lab">类型:</span>'+(n.fight_type===1?"击倒":"死斗")+'</div>';
|
||||
h+='<div class="row"><span class="lab">敌方:</span>'+(n.camp2||[]).map(nameOf).join("、")+'</div>';
|
||||
h+='<div class="row"><span class="lab">胜→</span>'+(n.win||"")+' <span class="lab">败→</span>'+(n.lose||"")+'</div>';
|
||||
}
|
||||
(n.options||[]).forEach(o=>{
|
||||
h+='<div class="opt">'+o.text;
|
||||
if(o.condition) h+=' <span class="lab">[条件:'+condStr(o.condition)+']</span>';
|
||||
if(o.reward&&o.reward.grants) h+=' <span class="lab">{'+o.reward.grants.map(grantStr).join(",")+'}</span>';
|
||||
h+=' <span class="arrow">→ '+o.goto+'</span></div>';
|
||||
});
|
||||
(n.branches||[]).forEach(b=>{ h+='<div class="opt">权重 '+b.weight+' <span class="arrow">→ '+b.goto+'</span></div>'; });
|
||||
if(n.grants&&n.grants.length) h+='<div class="row"><span class="lab">奖励:</span>'+n.grants.map(grantStr).join(",")+'</div>';
|
||||
if(n.grants&&!n.grants.length) h+='<div class="row"><span class="lab">奖励:</span>无</div>';
|
||||
if(n.next) h+='<div class="row"><span class="lab">下一步 →</span> '+n.next+'</div>';
|
||||
document.getElementById("detail").innerHTML=h;
|
||||
}
|
||||
|
||||
// ---- 奖励总览 ----
|
||||
(function(){
|
||||
let h=""; const collect=[];
|
||||
(IR.nodes||[]).forEach(n=>(n.options||[]).forEach(o=>{ if(o.reward&&o.reward.grants) collect.push(["选项「"+o.text+"」", o.reward.grants]); }));
|
||||
(IR.endings||[]).forEach(e=>collect.push(["结局「"+(e.summary||e.id)+"」", e.grants||[]]));
|
||||
collect.forEach(([k,gr])=>{ h+='<div class="rw"><b>'+k+'</b><br>'+(gr.length?gr.map(grantStr).join(","):"无")+'</div>'; });
|
||||
document.getElementById("rewards").innerHTML=h||'<div class="empty">无奖励配置</div>';
|
||||
})();
|
||||
|
||||
// ---- 头部 ----
|
||||
document.getElementById("h-title").textContent=IR.title+" "+(IR.id?("〔"+IR.id+"〕"):"");
|
||||
document.getElementById("h-meta").textContent=[IR.theme,IR.scale,(IR.stage&&("舞台:"+IR.stage.type+(IR.stage.reuse_hint?(" / 复用 "+IR.stage.reuse_hint):"")))].filter(Boolean).join(" · ");
|
||||
document.getElementById("h-roles").innerHTML=(IR.roles||[]).map(r=>'<span class="role"><b>'+r.slot+'</b> '+r.name+' 〔'+r.archetype+'〕</span>').join("");
|
||||
|
||||
function exportIR(){
|
||||
const blob=new Blob([JSON.stringify(IR,null,2)],{type:"application/json"});
|
||||
const a=document.createElement("a"); a.href=URL.createObjectURL(blob);
|
||||
a.download=(IR.id||"story")+".ir.json"; a.click();
|
||||
}
|
||||
|
||||
window.addEventListener("load", ()=>{ draw(); });
|
||||
window.addEventListener("resize", draw);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
11
samples/yuye_koumen.i18n.tsv
Normal file
11
samples/yuye_koumen.i18n.tsv
Normal file
@ -0,0 +1,11 @@
|
||||
# 简体中文(key,勿改) 韩文(待译;繁体无需填,SGameText 自动转换)
|
||||
暴雨倾盆,山门外的灯笼在风里摇晃。一阵急促的叩门声,盖过了雷声。
|
||||
在下途经贵派,身负旧伤,恳请借宿一晚,天明即走。
|
||||
(这位侠客腰间的铁牌……分明是近日劫掠商队的黑风寨样式。)
|
||||
江湖救急,先收留他
|
||||
不动声色,擒下他交予掌门
|
||||
赠些盘缠,请他即刻离去
|
||||
结义同盟
|
||||
破财消灾
|
||||
擒贼献掌门
|
||||
技不如人
|
||||
|
75
samples/yuye_koumen.ir.json
Normal file
75
samples/yuye_koumen.ir.json
Normal file
@ -0,0 +1,75 @@
|
||||
{
|
||||
"id": "QY_YYKM",
|
||||
"title": "雨夜叩门",
|
||||
"theme": "正统武侠·道德抉择",
|
||||
"scale": "标准奇遇",
|
||||
"roles": [
|
||||
{ "slot": "P1", "name": "值夜弟子", "archetype": "玩家", "camp": 1 },
|
||||
{ "slot": "NP1", "name": "神秘剑客", "archetype": "负伤外门高手", "camp": 0 },
|
||||
{ "slot": "NP2", "name": "掌门", "archetype": "本门掌门", "camp": 0 }
|
||||
],
|
||||
"stage": { "type": "门派入口·夜", "reuse_hint": "K3_A" },
|
||||
"nodes": [
|
||||
{
|
||||
"id": "n1", "kind": "narration",
|
||||
"text": "暴雨倾盆,山门外的灯笼在风里摇晃。一阵急促的叩门声,盖过了雷声。",
|
||||
"next": "n2"
|
||||
},
|
||||
{
|
||||
"id": "n2", "kind": "dialogue", "speaker": "NP1", "camera": "NP1",
|
||||
"text": "在下途经贵派,身负旧伤,恳请借宿一晚,天明即走。",
|
||||
"next": "n3"
|
||||
},
|
||||
{
|
||||
"id": "n3", "kind": "dialogue", "speaker": "P1",
|
||||
"text": "(这位侠客腰间的铁牌……分明是近日劫掠商队的黑风寨样式。)",
|
||||
"next": "n4"
|
||||
},
|
||||
{
|
||||
"id": "n4", "kind": "choice",
|
||||
"options": [
|
||||
{ "text": "江湖救急,先收留他", "goto": "end_ally" },
|
||||
{ "text": "不动声色,擒下他交予掌门", "goto": "fight1" },
|
||||
{
|
||||
"text": "赠些盘缠,请他即刻离去",
|
||||
"condition": { "kind": "银两", "op": ">=", "value": 500 },
|
||||
"reward": { "grants": [ { "kind": "银两", "value": -500 } ] },
|
||||
"goto": "end_pay"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "fight1", "kind": "fight",
|
||||
"fight_type": 1,
|
||||
"camp1": [ "P1" ],
|
||||
"camp2": [ "NP1" ],
|
||||
"win": "end_subdue",
|
||||
"lose": "end_lose"
|
||||
}
|
||||
],
|
||||
"endings": [
|
||||
{
|
||||
"id": "end_ally", "summary": "结义同盟",
|
||||
"grants": [
|
||||
{ "kind": "友好度", "target": "NP1", "value": 30 },
|
||||
{ "kind": "入门", "target": "NP1" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "end_pay", "summary": "破财消灾",
|
||||
"grants": [ { "kind": "友好度", "target": "NP1", "value": 10 } ]
|
||||
},
|
||||
{
|
||||
"id": "end_subdue", "summary": "擒贼献掌门",
|
||||
"grants": [
|
||||
{ "kind": "银两", "value": 200 },
|
||||
{ "kind": "道具", "item": "P6", "value": 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "end_lose", "summary": "技不如人",
|
||||
"grants": [],
|
||||
"result": "fail"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
samples/yuye_koumen.preview.png
Normal file
BIN
samples/yuye_koumen.preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
Reference in New Issue
Block a user