剧情游戏 组件

注意啦!:

这是世界机构联合体网站内部组件页,用于在其他页面中使用。
所以请别乱动,谢谢啦!

这是什么?

这是由BANTTXBANTTX制作的剧情游戏组件,使用该组件后可以制作简单的互动游戏。

如何使用?

[[include :pin-wiki:component:storyline
|css= 此处可写CSS(可选)
]]
这里写入代码
[[include :pin-wiki:component:storyline-end]]

代码教程

核心块

块标识 格式 说明
[config] title = 游戏标题 全局配置
[role] 角色ID = 名称 \| #十六进制颜色 \| 解锁阶段 定义联系人,ID为英文唯一标识
[chat:角色ID] 每个角色对应一个 存放该角色全部剧情,首个标签必须为 start

剧情内元素

1. 标签锚点标签名:(英文冒号结尾),跳转目标,仅支持英文数字下划线
2. 对话行:角色名 对话内容 | 特效1 特效2

  • 支持特效:glitch故障闪烁 / stutter卡顿打字 / alert红色告警 / delay2000前置停顿2秒

3. 分支选项[按钮文字] -> 目标标签名
4. 系统指令!指令名 参数

  • !signal good/bad 切换信号状态
  • !unlock id1,id2 强制解锁指定角色
  • !stage 数字 推进全局阶段
  • !jump 标签名 跳转到当前角色指定剧情
  • !switch 角色ID 切换到另一角色窗口
  • !end 标题|描述 触发结局(竖线为英文,两侧无空格)

常见错误排查

  1. 报错「未找到剧情块」:缺少 [chat:角色ID]
  2. 选项点击无反应:目标标签不存在 / 标签名拼写错误
  3. 特效不生效:| 两侧缺少空格 / 特效名拼写错误
  4. 结局不弹出:!end 竖线为中文 / 两侧有多余空格
  5. 角色锁定无法点击:全局stage低于角色解锁stage / 未执行 !unlock 指令

下面的东西不用看哦

[[html]]
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>区</title>
<style>
body { width: 100%; height: 100%; margin: 0; background: #0a0f1a; }
#gameContainer { width: 100%; height: 100%; }
</style>
</head>
<body>
<div id="gameContainer"></div>

<script>
/* ========== 1.编译器 ========== */
function compileScript(script) {
const result = {
config: { title: "对话游戏" },
roles: [],
chats: {}
};

try {
// 解析配置块
const configMatch = script.match(/\[config\]([\s\S]*?)(?=\n\[|\s*$)/);
if (configMatch) {
configMatch[1].split("\n").forEach(line => {
line = line.trim();
if (!line || !line.includes("=")) return;
const [k, v] = line.split("=").map(s => s.trim());
result.config[k] = v;
});
}

// 解析角色块
const roleMatch = script.match(/\[role\]([\s\S]*?)(?=\n\[|\s*$)/);
if (roleMatch) {
roleMatch[1].split("\n").forEach(line => {
line = line.trim();
if (!line || !line.includes("=")) return;
const [id, rest] = line.split("=").map(s => s.trim());
const parts = rest.split("|").map(s => s.trim());
result.roles.push({
id,
name: parts[0],
color: parts[1] || "#ffffff",
stage: parseInt(parts[2]) || 0
});
});
}

// 解析所有剧情块
const chatBlocks = script.match(/\[chat:\w+\][\s\S]*?(?=\n\[chat:|\s*$)/g);
if (!chatBlocks) throw new Error("未找到 [chat:角色ID] 剧情块");

chatBlocks.forEach(block => {
const header = block.match(/\[chat:(\w+)\]/);
if (!header) return;
const roleId = header[1];
const lines = block.replace(/\[chat:\w+\]\n?/, "").split("\n");

const nodes = {};
let curLabel = "start"; // 统一入口标签为start
nodes[curLabel] = { lines: [], options: [] };

lines.forEach(line => {
line = line.trim();
if (!line) return;

// 1. 剧情标签锚点
if (/^[\w]+:/.test(line) && !line.startsWith(":") && !line.startsWith("[")) {
curLabel = line.slice(0, -1);
if (!nodes[curLabel]) nodes[curLabel] = { lines: [], options: [] };
return;
}

// 2. 对话行 :角色名 内容 | 特效
if (line.startsWith(":")) {
const rest = line.slice(1);
const spaceIdx = rest.indexOf(" ");
if (spaceIdx === -1) return;
const sender = rest.slice(0, spaceIdx);
let text = rest.slice(spaceIdx + 1);
const mod = { glitch: false, stutter: false, alert: false, delay: 0 };

if (text.includes(" | ")) {
const [content, flagStr] = text.split(" | ");
text = content;
flagStr.split(/\s+/).forEach(f => {
if (f === "glitch") mod.glitch = true;
if (f === "stutter") mod.stutter = true;
if (f === "alert") mod.alert = true;
if (f.startsWith("delay")) mod.delay = parseInt(f.slice(5)) || 0;
});
}
nodes[curLabel].lines.push({ sender, text, …mod });
return;
}

// 3. 分支选项 [选项文本] -> 目标标签(修复空格匹配bug)
const optMatch = line.match(/^\[(.+?)\]\s*->\s*(\w+)$/);
if (optMatch) {
nodes[curLabel].options.push({ text: optMatch[1], target: optMatch[2] });
return;
}

// 4. 系统指令 !指令名 参数
if (line.startsWith("!")) {
nodes[curLabel].lines.push({ type: "command", cmd: line.slice(1) });
return;
}
});

result.chats[roleId] = nodes;
});

return result;
} catch (e) {
console.error("编译失败:", e.message);
return null;
}
}

/* ========== 2. 剧情渲染运行时 ========== */
function renderGame(data, containerId) {
const container = document.getElementById(containerId);
if (!container || !data) return;

// 游戏全局状态
const state = {
stage: 0,
currentRole: data.roles[0].id,
signal: "good",
labelMap: {}, // 每个角色当前所在标签
printing: false
};

// 每个角色初始定位到start标签
data.roles.forEach(r => {
state.labelMap[r.id] = data.chats[r.id]?.start ? "start" : Object.keys(data.chats[r.id])[0];
});

// 角色名 -> 颜色映射
const colorMap = {};
data.roles.forEach(r => colorMap[r.name.split("|")[0]] = r.color);

// 注入样式
const style = document.createElement("style");
style.textContent = `
.game-wrap{width:100%;height:100%;display:grid;grid-template-columns:240px 1fr;background:#040812;color:#e2e8f0;font-family:Consolas,"微软雅黑",monospace;overflow:hidden;position:relative}
.game-sidebar{border-right:1px solid #1e293b;background:#070b16;display:flex;flex-direction:column}
.game-sidebar h3{padding:16px 14px;color:#e6c068;font-size:14px;border-bottom:1px solid #1e293b;margin:0}
.game-sidebar .divider{padding:12px 14px 6px;font-size:12px;color:#64748b}
.role-list{flex:1;overflow-y:auto;padding:0 12px 16px;display:flex;flex-direction:column;gap:6px}
.role-item{padding:10px 12px;border-radius:4px;cursor:pointer;border:1px solid transparent;font-size:14px}
.role-item.active{border-color:#38bdf8;background:rgba(56,189,248,.07);color:#38bdf8}
.role-item.locked{color:#334155;border-color:#141c2c;cursor:not-allowed;opacity:.55}
.role-dot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:8px}

.game-chat{display:flex;flex-direction:column;height:100%}
.chat-header{padding:12px 20px;border-bottom:1px solid #1e293b;background:#050911;display:flex;justify-content:space-between;align-items:center;flex-shrink:0}
.chat-title{color:#38bdf8;font-size:15px}
.signal-bar{font-size:13px;color:#64748b;display:flex;gap:8px;align-items:center}
.signal-dot{width:10px;height:10px;border-radius:50%}
.msg-box{flex:1;padding:20px;overflow-y:auto;background:#0b1220}
.msg-line{margin-bottom:16px;display:flex;flex-direction:column;align-items:flex-start}
.msg-name{font-size:12px;margin-bottom:4px;padding-left:6px}
.msg-bubble{background:#162032;border:1px solid #1e293b;border-radius:0 10px 10px 10px;padding:10px 14px;font-size:14px;line-height:1.65;max-width:82%;word-break:break-all}
.msg-glitch .msg-bubble{border-color:#38bdf8;box-shadow:0 0 8px rgba(56,189,248,.2);animation:flicker 2.6s infinite}
.msg-alert .msg-bubble{animation:alertBlink .8s ease-in-out 2;background:#2a1215}
@keyframes flicker{0%,92%,100%{opacity:1}94%{opacity:.25}96%{opacity:.8}}
@keyframes alertBlink{0%,100%{border-color:#ef4444;box-shadow:0 0 8px rgba(239,68,68,.3)}50%{border-color:#fca5a5;box-shadow:0 0 16px rgba(239,68,68,.5)}}

.opt-bar{padding:14px 20px;border-top:1px solid #1e293b;background:#050911;display:flex;flex-wrap:wrap;gap:10px;flex-shrink:0}
.opt-btn{padding:9px 14px;background:#1e293b;border:1px solid #334155;color:#e2e8f0;border-radius:3px;cursor:pointer;font-family:inherit;font-size:13px}
.opt-btn:hover:not(:disabled){background:#334155;border-color:#38bdf8}
.opt-btn:disabled{opacity:.5;cursor:not-allowed}

.end-screen{position:absolute;inset:0;background:#020509;display:none;flex-direction:column;align-items:center;justify-content:center;padding:40px;text-align:center;z-index:99}
.end-screen h2{font-size:28px;margin-bottom:24px;color:#38bdf8}
.end-screen p{max-width:720px;line-height:1.9;color:#64748b;font-size:15px}

.net-bad::after{content:"";position:absolute;inset:0;background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='turbulence' baseFrequency='1.4' numOctaves='6'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");animation:noise 1.2s infinite;pointer-events:none;z-index:10}
@keyframes noise{0%,100%{opacity:0}40%{opacity:.12}60%{opacity:.04}}
{$css}
`;
document.head.appendChild(style);

// 构建DOM结构
container.innerHTML = `
<div class="game-wrap" id="gameWrap">
<div class="game-sidebar">
<h3>世界机构联合体 · PIN</h3>
<div class="divider">联络人员</div>
<div class="role-list" id="roleList"></div>
</div>
<div class="game-chat">
<div class="chat-header">
<div class="chat-title" id="chatTitle">${data.config.title}</div>
<div class="signal-bar">
信号:<span class="signal-dot" id="signalDot"></span>
<span id="signalText">稳定</span>
</div>
</div>
<div class="msg-box" id="msgBox"></div>
<div class="opt-bar" id="optBar"></div>
</div>
<div class="end-screen" id="endScreen">
<h2 id="endTitle"></h2>
<p id="endDesc"></p>
</div>
</div>
`;

// DOM缓存
const wrap = document.getElementById("gameWrap");
const roleListEl = document.getElementById("roleList");
const chatTitleEl = document.getElementById("chatTitle");
const signalDotEl = document.getElementById("signalDot");
const signalTextEl = document.getElementById("signalText");
const msgBoxEl = document.getElementById("msgBox");
const optBarEl = document.getElementById("optBar");
const endScreenEl = document.getElementById("endScreen");
const endTitleEl = document.getElementById("endTitle");
const endDescEl = document.getElementById("endDesc");

// 刷新联系人列表
function refreshRoles() {
roleListEl.innerHTML = "";
data.roles.forEach(r => {
const unlocked = state.stage >= r.stage;
const item = document.createElement("div");
item.className = "role-item" + (state.currentRole === r.id ? " active" : "") + (unlocked ? "" : " locked");
item.innerHTML = `<span class="role-dot" style="background:${r.color}"></span>${r.name}` + (unlocked ? "" : " 【未解锁】");
if (unlocked) item.onclick = () => switchRole(r.id);
roleListEl.appendChild(item);
});
}

// 切换联系人
function switchRole(rid) {
if (rid === state.currentRole) return;
state.currentRole = rid;
const info = data.roles.find(r => r.id === rid);
chatTitleEl.textContent = "当前联络:" + info.name.split("|")[0];
refreshRoles();
msgBoxEl.innerHTML = "";
optBarEl.innerHTML = "";
runNode(rid, state.labelMap[rid]);
}

// 切换信号状态
function setSignal(mode) {
state.signal = mode;
wrap.classList.remove("net-bad");
if (mode === "good") {
signalDotEl.style.background = "#10b981";
signalTextEl.textContent = "链路稳定";
} else {
signalDotEl.style.background = "#ef4444";
signalTextEl.textContent = "数据流波动";
wrap.classList.add("net-bad");
}
}

// 执行系统指令
function execCommand(cmd) {
const [name, …args] = cmd.split(/\s+/);
const arg = args.join(" ");
switch (name) {
case "signal": setSignal(arg); break;
case "unlock":
arg.split(",").forEach(id => {
const r = data.roles.find(x => x.id === id.trim());
if (r) r.stage = state.stage;
});
refreshRoles();
break;
case "stage":
state.stage = parseInt(arg);
refreshRoles();
break;
case "jump":
setTimeout(() => runNode(state.currentRole, arg), 300);
break;
case "switch":
setTimeout(() => switchRole(arg), 300);
break;
case "end":
const [t, d] = arg.split("|");
endScreenEl.style.display = "flex";
endTitleEl.textContent = t;
endDescEl.textContent = d;
break;
}
}

// 运行指定剧情节点
async function runNode(rid, label) {
if (state.printing) return;
state.printing = true;
optBarEl.querySelectorAll(".opt-btn").forEach(b => b.disabled = true);

const node = data.chats[rid]?.[label];
if (!node) { state.printing = false; return; }
state.labelMap[rid] = label;

for (let i = 0; i < node.lines.length; i++) {
const line = node.lines[i];
// 系统指令直接执行
if (line.type === "command") {
execCommand(line.cmd);
continue;
}

// 前置停顿
if (line.delay) await new Promise(r => setTimeout(r, line.delay));

// 创建气泡DOM
const msgLine = document.createElement("div");
let cls = "msg-line";
if (line.glitch) cls += " msg-glitch";
if (line.alert) cls += " msg-alert";
msgLine.className = cls;

const nameEl = document.createElement("div");
nameEl.className = "msg-name";
nameEl.style.color = colorMap[line.sender] || "#38bdf8";
nameEl.textContent = line.sender + ":";

const bubble = document.createElement("div");
bubble.className = "msg-bubble";

msgLine.append(nameEl, bubble);
msgBoxEl.appendChild(msgLine);
msgBoxEl.scrollTop = msgBoxEl.scrollHeight;

// 逐字打字
const full = line.text;
const baseSpeed = 42;
for (let j = 0; j < full.length; j++) {
bubble.textContent = full.slice(0, j + 1);
msgBoxEl.scrollTop = msgBoxEl.scrollHeight;
let wait = baseSpeed;
if (line.stutter) {
wait += Math.floor(Math.random() * 35);
if (j > 0 && j % 5 === 0) wait += 80;
}
await new Promise(r => setTimeout(r, wait));
}
}

// 渲染选项按钮
optBarEl.innerHTML = "";
node.options.forEach(opt => {
const btn = document.createElement("button");
btn.className = "opt-btn";
btn.textContent = opt.text;
btn.onclick = () => runNode(rid, opt.target);
optBarEl.appendChild(btn);
});

state.printing = false;
}

// 游戏启动
refreshRoles();
setSignal("good");
const first = data.roles[0];
chatTitleEl.textContent = "当前联络:" + first.name.split("|")[0];
runNode(first.id, state.labelMap[first.id]);
}

/* ========== 3. 调用部分 ========== */
const gameScript = `

除非特别注明,本页内容采用以下授权方式: Creative Commons Attribution-ShareAlike 3.0 License