分支剧情节点图谱 (JSON Graph)
{
"slug": "campus-romance",
"title": "心跳重逢:社团修罗场",
"en_title": "Heartbeat Reunion: Studio Rivalry",
"player_url": "https://media.huangguodrama.ai/motion/interactive/campus-romance/index.html",
"start": "open",
"nodes": {
"open": {
"id": "open",
"title": "重逢首日:三位女主同时向你发出邀约",
"act": 1,
"route": "主线",
"video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/open-c6685c48ed8ebcb9.mp4",
"ending": false,
"choices": [
{
"label": "找青梅师姐·林晚晚",
"to": "lin_intro"
},
{
"label": "找高冷主唱·顾清野",
"to": "gu_intro"
},
{
"label": "找甜系策划·苏小糖",
"to": "su_intro"
},
{
"label": "成年人全都要(触发修罗场)",
"to": "all_pick"
}
]
},
"all_pick": {
"id": "all_pick",
"title": "成年人不做选择:同时应下三份邀约",
"act": 1,
"route": "主线",
"video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/all_pick-c506c9f0e8a8df79.mp4",
"ending": false,
"choices": [
{
"label": "硬着头皮赴约晚宴",
"to": "harem_arena"
},
{
"label": "冷静一下重新选择",
"to": "reselect"
}
]
},
"lin_intro": {
"id": "lin_intro",
"title": "青梅师姐·林晚晚:画室里的旧相逢",
"act": 1,
"route": "林晚晚",
"video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/lin_intro-78fe491a788f2c5c.mp4",
"ending": false,
"choices": [
{
"label": "进入林晚晚专属剧情线",
"to": "lin_route"
},
{
"label": "同时去见其他人",
"to": "harem_arena"
}
]
},
"lin_route": {
"id": "lin_route",
"title": "林晚晚主线:微雨夜的独处试探",
"act": 2,
"route": "林晚晚",
"video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/lin_route-4303505eab785aab.mp4",
"ending": false,
"choices": [
{
"label": "强势壁咚挑明心意",
"to": "lin_bold"
},
{
"label": "温柔攻势循序渐进",
"to": "lin_pursue"
}
]
},
"lin_bold": {
"id": "lin_bold",
"title": "强势壁咚:直接挑明心意",
"act": 3,
"route": "林晚晚",
"video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/lin_bold-067aa886e6405f61.mp4",
"ending": false,
"choices": [
{
"label": "直接追求解锁高甜时刻",
"to": "lin_fast"
},
{
"label": "克制轻吻她的额头",
"to": "lin_kiss"
}
]
},
"lin_pursue": {
"id": "lin_pursue",
"title": "温柔攻势:一步步卸下她的心防",
"act": 3,
"route": "林晚晚",
"video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/lin_pursue-fbd1dc3ac44a0af0.mp4",
"ending": false,
"choices": [
{
"label": "双向奔赴拥她入怀",
"to": "lin_mutual"
},
{
"label": "步入完美终章",
"to": "lin_he"
}
]
},
"lin_mutual": {
"id": "lin_mutual",
"title": "双向奔赴:多年暗恋终于宣之于口",
"act": 4,
"route": "林晚晚",
"video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/lin_mutual-0d9842680740c9c7.mp4",
"ending": "HAPPY END",
"choices": []
},
"lin_kiss": {
"id": "lin_kiss",
"title": "额头轻吻:克制而暧昧的约定",
"act": 4,
"route": "林晚晚",
"video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/lin_kiss-b3fdf264afd18595.mp4",
"ending": "NORMAL END",
"choices": []
},
"harem_arena": {
"id": "harem_arena",
"title": "四人修罗场:晚宴厅里的三方对峙",
"act": 3,
"route": "修罗场",
"video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/harem_arena-7ceced93950feece.mp4",
"ending": false,
"choices": [
{
"label": "牵起林晚晚的手",
"to": "harem_lin"
},
{
"label": "走向顾清野",
"to": "harem_gu"
},
{
"label": "护在苏小糖身前",
"to": "harem_su"
}
]
},
"harem_lin": {
"id": "harem_lin",
"title": "修罗场抉择:当众牵起林晚晚的手",
"act": 4,
"route": "林晚晚",
"video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/harem_lin-3bc5691b318042e4.mp4",
"ending": false,
"choices": [
{
"label": "坚定表白心意",
"to": "lin_he"
}
]
},
"lin_he": {
"id": "lin_he",
"title": "林晚晚完美结局:余生皆是你",
"act": 4,
"route": "林晚晚",
"video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/lin_he-eb481b75ca1c6dee.mp4",
"ending": "TRUE END",
"choices": []
},
"harem_gu": {
"id": "harem_gu",
"title": "修罗场抉择:走向高冷主唱顾清野",
"act": 4,
"route": "顾清野",
"video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/harem_gu-1d3ae1df02b82559.mp4",
"ending": false,
"choices": [
{
"label": "回应她的目光",
"to": "gu_he"
}
]
},
"gu_he": {
"id": "gu_he",
"title": "顾清野完美结局:只为你一人驻唱",
"act": 4,
"route": "顾清野",
"video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/gu_he-a0c2f4bd4fe8c748.mp4",
"ending": "TRUE END",
"choices": []
},
"harem_su": {
"id": "harem_su",
"title": "修罗场抉择:护住甜系策划苏小糖",
"act": 4,
"route": "苏小糖",
"video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/harem_su-9b2f42ff0d4663c4.mp4",
"ending": false,
"choices": [
{
"label": "给她满分安全感",
"to": "su_he"
}
]
},
"su_he": {
"id": "su_he",
"title": "苏小糖完美结局:专属甜蜜满糖",
"act": 4,
"route": "苏小糖",
"video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/su_he-fe238c5a666715e3.mp4",
"ending": "TRUE END",
"choices": []
},
"reselect": {
"id": "reselect",
"title": "擦肩而过:退回分岔口重新审视内心",
"act": 2,
"route": "主线",
"video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/reselect-21e6b52db51f3288.mp4",
"ending": "BAD END",
"choices": []
},
"lin_fast": {
"id": "lin_fast",
"title": "心跳加速:雨夜画室里的炽热相拥",
"act": 4,
"route": "林晚晚",
"video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/lin_fast-5e1ea87b1aa1bda1.mp4",
"ending": "HAPPY END",
"choices": []
},
"gu_intro": {
"id": "gu_intro",
"title": "高冷主唱·顾清野:天台排练室的冷艳目光",
"act": 1,
"route": "顾清野",
"video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/gu_intro-eaa8fd98f0b4464f.mp4",
"ending": false,
"choices": [
{
"label": "进入顾清野专属剧情线",
"to": "gu_route"
},
{
"label": "转入修罗场对峙",
"to": "harem_arena"
}
]
},
"gu_route": {
"id": "gu_route",
"title": "顾清野主线:午夜Livehouse的秘密赌约",
"act": 2,
"route": "顾清野",
"video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/gu_route-1f4d3178f8a2c058.mp4",
"ending": false,
"choices": [
{
"label": "与她极限拉扯",
"to": "gu_tension"
},
{
"label": "故意输掉赌约逗她",
"to": "gu_lose"
}
]
},
"gu_lose": {
"id": "gu_lose",
"title": "赌约落败:沦为她的专属调音助理",
"act": 3,
"route": "顾清野",
"video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/gu_lose-beb401523fb55e3f.mp4",
"ending": "NORMAL END",
"choices": []
},
"gu_tension": {
"id": "gu_tension",
"title": "极限拉扯:眼神交锋中的心动失守",
"act": 3,
"route": "顾清野",
"video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/gu_tension-a6d1535365c7cd02.mp4",
"ending": false,
"choices": [
{
"label": "赢下赌约听她告白",
"to": "gu_win"
},
{
"label": "解锁顾清野完美结局",
"to": "gu_he"
}
]
},
"gu_win": {
"id": "gu_win",
"title": "赌约获胜:高冷女神的主动告白",
"act": 4,
"route": "顾清野",
"video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/gu_win-707b88e1faf8604f.mp4",
"ending": "HAPPY END",
"choices": []
},
"su_intro": {
"id": "su_intro",
"title": "甜系策划·苏小糖:转角撞满怀的奶茶香",
"act": 1,
"route": "苏小糖",
"video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/su_intro-7e6605f99cc45b2b.mp4",
"ending": false,
"choices": [
{
"label": "进入苏小糖专属剧情线",
"to": "su_route"
},
{
"label": "转入修罗场对峙",
"to": "harem_arena"
}
]
},
"su_route": {
"id": "su_route",
"title": "苏小糖主线:夜间游乐园的曖昧捉迷藏",
"act": 2,
"route": "苏小糖",
"video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/su_route-30b947a01932d8a8.mp4",
"ending": false,
"choices": [
{
"label": "调侃她偷偷跟着你",
"to": "su_follow"
},
{
"label": "请她喝最爱的招牌奶茶",
"to": "su_tea"
}
]
},
"su_follow": {
"id": "su_follow",
"title": "反客为主:识破她小心翼翼的尾随",
"act": 3,
"route": "苏小糖",
"video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/su_follow-30c6c9f3d085fc84.mp4",
"ending": false,
"choices": [
{
"label": "在烟花下给她惊喜",
"to": "su_moved"
},
{
"label": "解锁苏小糖完美结局",
"to": "su_he"
}
]
},
"su_moved": {
"id": "su_moved",
"title": "烟花下的眼泪:融化她所有不安",
"act": 4,
"route": "苏小糖",
"video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/su_moved-849ed363812b6250.mp4",
"ending": "HAPPY END",
"choices": []
},
"su_tea": {
"id": "su_tea",
"title": "同饮一杯:微甜气泡里的日常幸福",
"act": 4,
"route": "苏小糖",
"video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/su_tea-f033a05d394a82c4.mp4",
"ending": "NORMAL END",
"choices": []
}
}
}
HTML5 互动短剧独立播放器 (index.html)
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>心跳重逢:社团修罗场 · Heartbeat Reunion: Studio Rivalry | 互动短剧播放器</title>
<style>
:root {
--bg: #0c0a12; --surface: #161222; --surface-2: #221c34;
--line: rgba(255,255,255,0.14); --text: #f8f5ff; --muted: rgba(248,245,255,0.65);
--accent: #ff4d79; --accent2: #fbbf24;
--good: #34d399; --gold: #fbbf24; --bad: #f87171;
}
* { box-sizing: border-box; }
html, body { margin: 0; background: var(--bg); color: var(--text); font: 15px/1.6 -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; min-height: 100svh; }
button { font: inherit; color: inherit; cursor: pointer; }
.app { max-width: 1024px; margin: 0 auto; min-height: 100svh; display: flex; flex-direction: column; }
.bar { display: flex; align-items: center; gap: 10px; padding: 10px 14px; background: rgba(22,18,34,0.88); backdrop-filter: blur(12px); border-bottom: 1px solid var(--line); }
.bar h1 { flex: 1; margin: 0; font-size: 16px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bar .sub { font-size: 12px; color: var(--muted); margin-left: 8px; font-weight: 400; }
.chip { border: 1px solid var(--line); background: rgba(255,255,255,0.05); border-radius: 999px; padding: 5px 12px; font-size: 12px; white-space: nowrap; transition: .2s; }
.chip:hover { border-color: var(--accent); background: rgba(255,255,255,0.12); }
.stage-wrap { position: relative; background: #000; width: 100%; aspect-ratio: 16 / 9; max-height: 76svh; overflow: hidden; }
video { width: 100%; height: 100%; object-fit: contain; display: block; background: #000; }
.stage-wrap.frozen video { filter: brightness(0.48) saturate(0.85) blur(2px); transition: filter .35s; }
.hud { position: absolute; inset: auto 0 0; padding: 20px 14px 10px; background: linear-gradient(transparent, rgba(0,0,0,0.82)); display: flex; align-items: center; gap: 10px; font-size: 12px; }
.track { flex: 1; height: 5px; border-radius: 99px; background: rgba(255,255,255,0.24); position: relative; cursor: pointer; }
.fill { position: absolute; inset: 0 auto 0 0; background: linear-gradient(90deg, var(--accent), var(--accent2)); border-radius: 99px; width: 0%; }
.skip-btn { position: absolute; right: 14px; top: 14px; border: 1px solid rgba(255,255,255,0.35); background: rgba(12,10,18,0.68); backdrop-filter: blur(8px); border-radius: 999px; padding: 6px 14px; font-size: 13px; z-index: 5; }
.skip-btn:hover { border-color: var(--accent); }
.choice-overlay { position: absolute; inset: 0; display: none; flex-direction: column; justify-content: flex-end; padding: 28px 24px; background: linear-gradient(180deg, rgba(12,10,18,0.15) 0%, rgba(12,10,18,0.92) 65%); z-index: 10; }
.choice-overlay.show { display: flex; animation: fadeIn .3s ease; }
.choice-kicker { font-size: 12px; letter-spacing: .14em; text-transform: uppercase; color: var(--accent2); font-weight: 700; margin-bottom: 4px; }
.choice-title { margin: 0 0 14px; font-size: 20px; font-weight: 800; text-shadow: 0 2px 10px rgba(0,0,0,0.6); }
.options { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 10px; }
.opt { display: flex; align-items: center; gap: 12px; padding: 13px 16px; border-radius: 14px; border: 1px solid rgba(255,255,255,0.22); background: rgba(22,18,34,0.82); backdrop-filter: blur(10px); text-align: left; transition: transform .15s, border-color .15s, background .15s; }
.opt:hover { transform: translateY(-2px); border-color: var(--accent); background: rgba(255,255,255,0.12); }
.opt .key { width: 28px; height: 28px; border-radius: 8px; display: grid; place-items: center; background: linear-gradient(135deg, var(--accent), var(--accent2)); color: #fff; font-weight: 800; font-size: 14px; flex: none; }
.opt .lbl { flex: 1; font-size: 15px; font-weight: 600; }
.timer-bar { height: 4px; border-radius: 4px; background: rgba(255,255,255,0.16); overflow: hidden; margin-top: 12px; }
.timer-bar i { display: block; height: 100%; background: linear-gradient(90deg, var(--accent), var(--accent2)); width: 100%; transition: width 12s linear; }
.below { padding: 16px; display: flex; flex-direction: column; gap: 12px; }
.status-card { padding: 14px 16px; border-radius: 14px; background: var(--surface); border: 1px solid var(--line); display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px; }
.status-card .route-tag { color: var(--accent2); font-size: 12px; font-weight: 700; letter-spacing: .08em; }
.status-card h2 { margin: 2px 0 0; font-size: 17px; }
.breadcrumbs { display: flex; flex-wrap: wrap; gap: 6px; }
.breadcrumbs span { font-size: 12px; padding: 3px 10px; border-radius: 999px; background: var(--surface-2); color: var(--muted); }
.ending-modal { display: none; padding: 20px; border-radius: 16px; background: linear-gradient(145deg, var(--surface), var(--surface-2)); border: 1px solid var(--accent); flex-direction: column; gap: 12px; }
.ending-modal.show { display: flex; }
.ending-badge { display: inline-block; align-self: flex-start; padding: 4px 12px; border-radius: 999px; background: linear-gradient(90deg, var(--accent), var(--accent2)); color: #fff; font-size: 12px; font-weight: 800; letter-spacing: .1em; }
.map-sheet { position: fixed; inset: 0; background: rgba(0,0,0,0.78); z-index: 30; display: none; align-items: center; justify-content: center; padding: 16px; }
.map-sheet.show { display: flex; }
.map-box { width: min(680px, 100%); max-height: 84svh; overflow: auto; background: var(--surface); border: 1px solid var(--line); border-radius: 18px; padding: 18px; display: flex; flex-direction: column; gap: 10px; }
.map-node { padding: 10px 12px; border-radius: 12px; background: var(--surface-2); border: 1px solid var(--line); display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.map-node.visited { border-color: var(--accent); }
@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
</style>
</head>
<body>
<div class="app">
<header class="bar">
<button class="chip" id="restartBtn">↺ 重头开始</button>
<h1>心跳重逢:社团修罗场 <span class="sub">Heartbeat Reunion: Studio Rivalry</span></h1>
<button class="chip" id="endingsBtn">结局 0/11</button>
<button class="chip" id="mapBtn">剧情图谱</button>
</header>
<div class="stage-wrap" id="stageWrap">
<video id="video" playsinline preload="auto" controls></video>
<button class="skip-btn" id="skipBtn">跳到选择 ›</button>
<div class="choice-overlay" id="choiceOverlay">
<div class="choice-kicker" id="choiceKicker">MAKE YOUR CHOICE · 关键抉择</div>
<h3 class="choice-title" id="choiceTitle"></h3>
<div class="options" id="optionsBox"></div>
<div class="timer-bar"><i id="timerFill"></i></div>
</div>
</div>
<div class="below">
<div class="status-card">
<div>
<div class="route-tag" id="routeTag">第 1 幕 · 主线</div>
<h2 id="nodeTitle">心跳重逢:社团修罗场</h2>
</div>
<div class="breadcrumbs" id="pathBox"></div>
</div>
<div class="ending-modal" id="endingModal">
<span class="ending-badge" id="endingBadge">TRUE END</span>
<h3 id="endingTitle" style="margin:0;font-size:22px;"></h3>
<p id="endingHint" style="margin:0;color:var(--muted);">已收录至你的互动结局图鉴。你可以回溯到任意分支节点解锁其他路线!</p>
<div style="display:flex;gap:10px;flex-wrap:wrap;">
<button class="chip" id="backChoiceBtn" style="background:var(--accent);color:#fff;border:0;padding:8px 16px;font-weight:700;">返回上一选择</button>
<button class="chip" id="openMapEndBtn">查看完整剧情图谱</button>
<button class="chip" id="restartEndBtn">重新开始</button>
</div>
</div>
</div>
</div>
<div class="map-sheet" id="mapSheet">
<div class="map-box">
<div style="display:flex;justify-content:space-between;align-items:center;">
<h3 style="margin:0;">互动剧情分支图谱 · 心跳重逢:社团修罗场</h3>
<button class="chip" id="closeMapBtn">关闭 ✕</button>
</div>
<div id="mapList" style="display:flex;flex-direction:column;gap:8px;"></div>
</div>
</div>
<script>
const NODES = {"open": {"id": "open", "title": "重逢首日:三位女主同时向你发出邀约", "act": 1, "route": "主线", "video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/open-c6685c48ed8ebcb9.mp4", "ending": false, "choices": [{"label": "找青梅师姐·林晚晚", "to": "lin_intro"}, {"label": "找高冷主唱·顾清野", "to": "gu_intro"}, {"label": "找甜系策划·苏小糖", "to": "su_intro"}, {"label": "成年人全都要(触发修罗场)", "to": "all_pick"}]}, "all_pick": {"id": "all_pick", "title": "成年人不做选择:同时应下三份邀约", "act": 1, "route": "主线", "video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/all_pick-c506c9f0e8a8df79.mp4", "ending": false, "choices": [{"label": "硬着头皮赴约晚宴", "to": "harem_arena"}, {"label": "冷静一下重新选择", "to": "reselect"}]}, "lin_intro": {"id": "lin_intro", "title": "青梅师姐·林晚晚:画室里的旧相逢", "act": 1, "route": "林晚晚", "video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/lin_intro-78fe491a788f2c5c.mp4", "ending": false, "choices": [{"label": "进入林晚晚专属剧情线", "to": "lin_route"}, {"label": "同时去见其他人", "to": "harem_arena"}]}, "lin_route": {"id": "lin_route", "title": "林晚晚主线:微雨夜的独处试探", "act": 2, "route": "林晚晚", "video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/lin_route-4303505eab785aab.mp4", "ending": false, "choices": [{"label": "强势壁咚挑明心意", "to": "lin_bold"}, {"label": "温柔攻势循序渐进", "to": "lin_pursue"}]}, "lin_bold": {"id": "lin_bold", "title": "强势壁咚:直接挑明心意", "act": 3, "route": "林晚晚", "video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/lin_bold-067aa886e6405f61.mp4", "ending": false, "choices": [{"label": "直接追求解锁高甜时刻", "to": "lin_fast"}, {"label": "克制轻吻她的额头", "to": "lin_kiss"}]}, "lin_pursue": {"id": "lin_pursue", "title": "温柔攻势:一步步卸下她的心防", "act": 3, "route": "林晚晚", "video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/lin_pursue-fbd1dc3ac44a0af0.mp4", "ending": false, "choices": [{"label": "双向奔赴拥她入怀", "to": "lin_mutual"}, {"label": "步入完美终章", "to": "lin_he"}]}, "lin_mutual": {"id": "lin_mutual", "title": "双向奔赴:多年暗恋终于宣之于口", "act": 4, "route": "林晚晚", "video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/lin_mutual-0d9842680740c9c7.mp4", "ending": "HAPPY END", "choices": []}, "lin_kiss": {"id": "lin_kiss", "title": "额头轻吻:克制而暧昧的约定", "act": 4, "route": "林晚晚", "video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/lin_kiss-b3fdf264afd18595.mp4", "ending": "NORMAL END", "choices": []}, "harem_arena": {"id": "harem_arena", "title": "四人修罗场:晚宴厅里的三方对峙", "act": 3, "route": "修罗场", "video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/harem_arena-7ceced93950feece.mp4", "ending": false, "choices": [{"label": "牵起林晚晚的手", "to": "harem_lin"}, {"label": "走向顾清野", "to": "harem_gu"}, {"label": "护在苏小糖身前", "to": "harem_su"}]}, "harem_lin": {"id": "harem_lin", "title": "修罗场抉择:当众牵起林晚晚的手", "act": 4, "route": "林晚晚", "video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/harem_lin-3bc5691b318042e4.mp4", "ending": false, "choices": [{"label": "坚定表白心意", "to": "lin_he"}]}, "lin_he": {"id": "lin_he", "title": "林晚晚完美结局:余生皆是你", "act": 4, "route": "林晚晚", "video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/lin_he-eb481b75ca1c6dee.mp4", "ending": "TRUE END", "choices": []}, "harem_gu": {"id": "harem_gu", "title": "修罗场抉择:走向高冷主唱顾清野", "act": 4, "route": "顾清野", "video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/harem_gu-1d3ae1df02b82559.mp4", "ending": false, "choices": [{"label": "回应她的目光", "to": "gu_he"}]}, "gu_he": {"id": "gu_he", "title": "顾清野完美结局:只为你一人驻唱", "act": 4, "route": "顾清野", "video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/gu_he-a0c2f4bd4fe8c748.mp4", "ending": "TRUE END", "choices": []}, "harem_su": {"id": "harem_su", "title": "修罗场抉择:护住甜系策划苏小糖", "act": 4, "route": "苏小糖", "video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/harem_su-9b2f42ff0d4663c4.mp4", "ending": false, "choices": [{"label": "给她满分安全感", "to": "su_he"}]}, "su_he": {"id": "su_he", "title": "苏小糖完美结局:专属甜蜜满糖", "act": 4, "route": "苏小糖", "video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/su_he-fe238c5a666715e3.mp4", "ending": "TRUE END", "choices": []}, "reselect": {"id": "reselect", "title": "擦肩而过:退回分岔口重新审视内心", "act": 2, "route": "主线", "video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/reselect-21e6b52db51f3288.mp4", "ending": "BAD END", "choices": []}, "lin_fast": {"id": "lin_fast", "title": "心跳加速:雨夜画室里的炽热相拥", "act": 4, "route": "林晚晚", "video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/lin_fast-5e1ea87b1aa1bda1.mp4", "ending": "HAPPY END", "choices": []}, "gu_intro": {"id": "gu_intro", "title": "高冷主唱·顾清野:天台排练室的冷艳目光", "act": 1, "route": "顾清野", "video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/gu_intro-eaa8fd98f0b4464f.mp4", "ending": false, "choices": [{"label": "进入顾清野专属剧情线", "to": "gu_route"}, {"label": "转入修罗场对峙", "to": "harem_arena"}]}, "gu_route": {"id": "gu_route", "title": "顾清野主线:午夜Livehouse的秘密赌约", "act": 2, "route": "顾清野", "video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/gu_route-1f4d3178f8a2c058.mp4", "ending": false, "choices": [{"label": "与她极限拉扯", "to": "gu_tension"}, {"label": "故意输掉赌约逗她", "to": "gu_lose"}]}, "gu_lose": {"id": "gu_lose", "title": "赌约落败:沦为她的专属调音助理", "act": 3, "route": "顾清野", "video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/gu_lose-beb401523fb55e3f.mp4", "ending": "NORMAL END", "choices": []}, "gu_tension": {"id": "gu_tension", "title": "极限拉扯:眼神交锋中的心动失守", "act": 3, "route": "顾清野", "video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/gu_tension-a6d1535365c7cd02.mp4", "ending": false, "choices": [{"label": "赢下赌约听她告白", "to": "gu_win"}, {"label": "解锁顾清野完美结局", "to": "gu_he"}]}, "gu_win": {"id": "gu_win", "title": "赌约获胜:高冷女神的主动告白", "act": 4, "route": "顾清野", "video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/gu_win-707b88e1faf8604f.mp4", "ending": "HAPPY END", "choices": []}, "su_intro": {"id": "su_intro", "title": "甜系策划·苏小糖:转角撞满怀的奶茶香", "act": 1, "route": "苏小糖", "video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/su_intro-7e6605f99cc45b2b.mp4", "ending": false, "choices": [{"label": "进入苏小糖专属剧情线", "to": "su_route"}, {"label": "转入修罗场对峙", "to": "harem_arena"}]}, "su_route": {"id": "su_route", "title": "苏小糖主线:夜间游乐园的曖昧捉迷藏", "act": 2, "route": "苏小糖", "video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/su_route-30b947a01932d8a8.mp4", "ending": false, "choices": [{"label": "调侃她偷偷跟着你", "to": "su_follow"}, {"label": "请她喝最爱的招牌奶茶", "to": "su_tea"}]}, "su_follow": {"id": "su_follow", "title": "反客为主:识破她小心翼翼的尾随", "act": 3, "route": "苏小糖", "video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/su_follow-30c6c9f3d085fc84.mp4", "ending": false, "choices": [{"label": "在烟花下给她惊喜", "to": "su_moved"}, {"label": "解锁苏小糖完美结局", "to": "su_he"}]}, "su_moved": {"id": "su_moved", "title": "烟花下的眼泪:融化她所有不安", "act": 4, "route": "苏小糖", "video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/su_moved-849ed363812b6250.mp4", "ending": "HAPPY END", "choices": []}, "su_tea": {"id": "su_tea", "title": "同饮一杯:微甜气泡里的日常幸福", "act": 4, "route": "苏小糖", "video": "https://media.huangguodrama.ai/motion/interactive/campus-romance/su_tea-f033a05d394a82c4.mp4", "ending": "NORMAL END", "choices": []}};
const START = "open";
const ENDINGS = ["lin_mutual", "lin_kiss", "lin_he", "gu_he", "su_he", "reselect", "lin_fast", "gu_lose", "gu_win", "su_moved", "su_tea"];
const STORAGE_KEY = "hg-interactive-campus-romance-v1";
let state = JSON.parse(localStorage.getItem(STORAGE_KEY) || "null") || {
current: START,
history: [],
visited: [START],
endings: []
};
let autoTimer = null;
const video = document.getElementById("video");
const stageWrap = document.getElementById("stageWrap");
const choiceOverlay = document.getElementById("choiceOverlay");
const endingModal = document.getElementById("endingModal");
function saveState() {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (e) {}
}
function enterNode(nodeId, pushHistory = false) {
clearTimeout(autoTimer);
const node = NODES[nodeId] || NODES[START];
state.current = node.id;
if (!state.visited.includes(node.id)) state.visited.push(node.id);
stageWrap.classList.remove("frozen");
choiceOverlay.classList.remove("show");
endingModal.classList.remove("show");
document.getElementById("routeTag").textContent = `第 ${node.act || 1} 幕 · ${node.route || "主线"}`;
document.getElementById("nodeTitle").textContent = node.title;
document.getElementById("endingsBtn").textContent = `结局 ${state.endings.length}/${ENDINGS.length}`;
const pathBox = document.getElementById("pathBox");
pathBox.innerHTML = "";
state.history.slice(-5).forEach(h => {
const s = document.createElement("span");
s.textContent = h.label;
pathBox.appendChild(s);
});
video.src = node.video;
video.currentTime = 0;
video.play().catch(() => {});
saveState();
}
function onClipEnd() {
const node = NODES[state.current];
if (!node) return;
stageWrap.classList.add("frozen");
if (node.ending) {
if (!state.endings.includes(node.id)) state.endings.push(node.id);
document.getElementById("endingsBtn").textContent = `结局 ${state.endings.length}/${ENDINGS.length}`;
document.getElementById("endingBadge").textContent = `${node.ending} · 已收集 ${state.endings.length}/${ENDINGS.length}`;
document.getElementById("endingTitle").textContent = node.title;
endingModal.classList.add("show");
saveState();
return;
}
if (!node.choices || !node.choices.length) return;
document.getElementById("choiceTitle").textContent = node.title;
const box = document.getElementById("optionsBox");
box.innerHTML = "";
node.choices.forEach((c, idx) => {
const btn = document.createElement("button");
btn.className = "opt";
btn.innerHTML = `<span class="key">${"ABCD"[idx] || (idx+1)}</span><span class="lbl"></span>`;
btn.querySelector(".lbl").textContent = c.label;
btn.onclick = () => {
state.history.push({ from: node.id, label: c.label });
enterNode(c.to);
};
box.appendChild(btn);
});
choiceOverlay.classList.add("show");
const fill = document.getElementById("timerFill");
fill.style.transition = "none";
fill.style.width = "100%";
requestAnimationFrame(() => {
requestAnimationFrame(() => {
fill.style.transition = "width 12s linear";
fill.style.width = "0%";
});
});
autoTimer = setTimeout(() => {
if (choiceOverlay.classList.contains("show") && node.choices[0]) {
state.history.push({ from: node.id, label: node.choices[0].label });
enterNode(node.choices[0].to);
}
}, 12000);
}
video.addEventListener("ended", onClipEnd);
document.getElementById("skipBtn").onclick = () => {
video.pause();
onClipEnd();
};
document.getElementById("restartBtn").onclick = () => {
state.history = [];
enterNode(START);
};
document.getElementById("restartEndBtn").onclick = () => {
state.history = [];
enterNode(START);
};
document.getElementById("backChoiceBtn").onclick = () => {
const last = state.history.pop();
enterNode(last ? last.from : START);
};
function openMap() {
const list = document.getElementById("mapList");
list.innerHTML = "";
Object.values(NODES).forEach(n => {
const visited = state.visited.includes(n.id);
const row = document.createElement("div");
row.className = "map-node" + (visited ? " visited" : "");
row.innerHTML = `<div><strong>${visited ? n.title : "??? (未解锁节点)"}</strong><div style="font-size:12px;color:var(--muted);">第 ${n.act || 1} 幕 · ${n.ending || n.route || "分支"}</div></div>`;
if (visited) {
const jb = document.createElement("button");
jb.className = "chip";
jb.textContent = "跳转至此";
jb.onclick = () => {
document.getElementById("mapSheet").classList.remove("show");
enterNode(n.id);
};
row.appendChild(jb);
}
list.appendChild(row);
});
document.getElementById("mapSheet").classList.add("show");
}
document.getElementById("mapBtn").onclick = openMap;
document.getElementById("endingsBtn").onclick = openMap;
document.getElementById("openMapEndBtn").onclick = openMap;
document.getElementById("closeMapBtn").onclick = () => document.getElementById("mapSheet").classList.remove("show");
enterNode(state.current || START);
</script>
</body>
</html>