feat(timeline): 结局金色浮层(重新观看 + 回到上一个选择)
播到结局时在镜头下方弹金色星标浮层:显示结局文案/成败/奖励 + 两个按钮—— 「⟲ 重新观看」从头再播、「↶ 回到上一个选择」跳回最近分支点(位置重放正确)直接试另一条。 记录最近 choice/random/fight 节点供回跳;纯线性到结局则只显示重新观看。
This commit is contained in:
@ -283,6 +283,10 @@ body.perform-mode .mode-switch { border-color:#2f7a60; }
|
|||||||
border:1px solid #8a7038; border-radius:6px; padding:8px 14px; font-size:13px;
|
border:1px solid #8a7038; border-radius:6px; padding:8px 14px; font-size:13px;
|
||||||
cursor:pointer; box-shadow:0 2px 8px rgba(0,0,0,.55); }
|
cursor:pointer; box-shadow:0 2px 8px rgba(0,0,0,.55); }
|
||||||
.tl-choice-btn:hover { background:#5a4a26; border-color:#e6c878; }
|
.tl-choice-btn:hover { background:#5a4a26; border-color:#e6c878; }
|
||||||
|
/* 结局浮层:金色星标,区别于普通选项 */
|
||||||
|
.tl-ending-q { color:#f2c463 !important; background:rgba(42,30,14,.9) !important; border:1px solid #8a6a30; font-size:13px; max-width:92%; }
|
||||||
|
.tl-ending-btn { border-color:#b8893a; }
|
||||||
|
.tl-ending-btn:hover { background:#5a4426; border-color:#f2c463; }
|
||||||
/* 时间轴面板:固定/可拖高度、自己横向滚动 */
|
/* 时间轴面板:固定/可拖高度、自己横向滚动 */
|
||||||
.tl-timelinepanel { flex:none; height:200px; min-height:80px; display:flex; }
|
.tl-timelinepanel { flex:none; height:200px; min-height:80px; display:flex; }
|
||||||
.tl-tracks { position:relative; flex:1; min-width:0; overflow-x:auto; overflow-y:auto; cursor:grab;
|
.tl-tracks { position:relative; flex:1; min-width:0; overflow-x:auto; overflow-y:auto; cursor:grab;
|
||||||
|
|||||||
@ -219,7 +219,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===================== 状态 & 挂载 =====================
|
// ===================== 状态 & 挂载 =====================
|
||||||
let S, model, playT = 0, playing = false, rafId = 0, lastTs = 0, stageCv, stageCtx, els = {}, pending = null, selNode = null, PX = 80, fitMode = false;
|
let S, model, playT = 0, playing = false, rafId = 0, lastTs = 0, stageCv, stageCtx, els = {}, pending = null, selNode = null, PX = 80, fitMode = false, lastDecisionId = null;
|
||||||
|
|
||||||
const TEMPLATE =
|
const TEMPLATE =
|
||||||
'<div class="tl-stagepanel">' +
|
'<div class="tl-stagepanel">' +
|
||||||
@ -309,7 +309,7 @@
|
|||||||
}
|
}
|
||||||
// 从指定节点开始:先重放途中走位算进入位置,再从该节点构建演出。
|
// 从指定节点开始:先重放途中走位算进入位置,再从该节点构建演出。
|
||||||
function startFrom(targetId, autoplay) {
|
function startFrom(targetId, autoplay) {
|
||||||
stopPlay(); hideChoices(); clearSelection();
|
stopPlay(); hideChoices(); clearSelection(); lastDecisionId = null;
|
||||||
const path = pathTo(S, targetId);
|
const path = pathTo(S, targetId);
|
||||||
S.entryPos = path ? replayPositions(S, path) : {};
|
S.entryPos = path ? replayPositions(S, path) : {};
|
||||||
resetAccum(S);
|
resetAccum(S);
|
||||||
@ -436,6 +436,7 @@
|
|||||||
|
|
||||||
// ---- 选项浮层(镜头下方)----
|
// ---- 选项浮层(镜头下方)----
|
||||||
function showChoices(node, kind) {
|
function showChoices(node, kind) {
|
||||||
|
lastDecisionId = node.id; // 记录最近分支点,供结局「回到上一个选择」
|
||||||
const box = els.choices; box.innerHTML = "";
|
const box = els.choices; box.innerHTML = "";
|
||||||
let q, opts;
|
let q, opts;
|
||||||
if (kind === "fight") {
|
if (kind === "fight") {
|
||||||
@ -483,8 +484,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function onReachEnd() {
|
function onReachEnd() {
|
||||||
if (pending && (pending.kind === "choice" || pending.kind === "choice_once" || pending.kind === "fight" || pending.kind === "random"))
|
if (!pending) return;
|
||||||
|
if (pending.kind === "choice" || pending.kind === "choice_once" || pending.kind === "fight" || pending.kind === "random")
|
||||||
showChoices(pending.node, pending.kind);
|
showChoices(pending.node, pending.kind);
|
||||||
|
else if (pending.kind === "ending")
|
||||||
|
showEnding(pending.node);
|
||||||
|
}
|
||||||
|
function grantText(g) {
|
||||||
|
if (g.kind === "银两") return "银两" + (g.value > 0 ? "+" : "") + g.value;
|
||||||
|
if (g.kind === "道具") return "道具" + g.item + "×" + g.value;
|
||||||
|
if (g.kind === "友好度") return S.nm(g.target) + " 友好+" + g.value;
|
||||||
|
if (g.kind === "入门") return S.nm(g.target) + " 入门";
|
||||||
|
return g.kind;
|
||||||
|
}
|
||||||
|
function showEnding(node) {
|
||||||
|
const box = els.choices; box.innerHTML = "";
|
||||||
|
const res = { success: "成功", fail: "失败", end: "中性" }[node.result || "success"];
|
||||||
|
const grants = (node.grants && node.grants.length) ? " 奖励:" + node.grants.map(grantText).join(",") : "";
|
||||||
|
box.appendChild(Object.assign(document.createElement("div"), { className: "tl-choices-q tl-ending-q", textContent: "★ 结局:" + (node.summary || node.id) + "(" + res + ")" + grants }));
|
||||||
|
const mk = (txt, fn) => { const b = document.createElement("button"); b.className = "tl-choice-btn tl-ending-btn"; b.textContent = txt; b.onclick = fn; box.appendChild(b); };
|
||||||
|
mk("⟲ 重新观看", () => startFrom(firstNode(S.IR), true));
|
||||||
|
if (lastDecisionId && S.nodes[lastDecisionId]) mk("↶ 回到上一个选择", () => startFrom(lastDecisionId, true));
|
||||||
|
box.classList.remove("hidden");
|
||||||
}
|
}
|
||||||
function tick(ts) {
|
function tick(ts) {
|
||||||
if (!playing) return;
|
if (!playing) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user