231 lines
12 KiB
JavaScript
231 lines
12 KiB
JavaScript
const FRAME_IDS = [
|
|
"formation-review",
|
|
"approach-contact",
|
|
"damage-feedback",
|
|
"heal-response",
|
|
"ranged-finish",
|
|
"first-diagnosis",
|
|
];
|
|
|
|
const ASSETS = {
|
|
field: "assets/backgrounds/first_expedition_field.png",
|
|
farstring: "assets/actors/farstring.png",
|
|
kindhand: "assets/actors/kindhand.png",
|
|
farstringIdle: "assets/actions/farstring/idle.png",
|
|
farstringShot: "assets/actions/farstring/draw-release.png",
|
|
kindhandIdle: "assets/actions/kindhand/idle.png",
|
|
kindhandHeal: "assets/actions/kindhand/lantern-heal.png",
|
|
enemy: "assets/enemies/riftblade-idle.png",
|
|
damage: "assets/vfx/damage-heavy.png",
|
|
heal: "assets/vfx/heal-safe.png",
|
|
};
|
|
|
|
const frame = new URLSearchParams(window.location.search).get("frame") || FRAME_IDS[0];
|
|
const selectedFrame = FRAME_IDS.includes(frame) ? frame : FRAME_IDS[0];
|
|
|
|
function img(src, className, alt) {
|
|
return `<img class="${className}" src="${src}" alt="${alt}">`;
|
|
}
|
|
|
|
function icon(name) {
|
|
const icons = {
|
|
home: '<path d="M4 11 12 4l8 7v8h-5v-5H9v5H4z"/>',
|
|
restart: '<path d="M5 8a8 8 0 1 1-1 7m1-7V3m0 5h5"/>',
|
|
pause: '<path d="M7 5h3v14H7zm7 0h3v14h-3z"/>',
|
|
inspect: '<circle cx="11" cy="11" r="6"/><path d="m16 16 4 4"/>',
|
|
captions: '<path d="M4 6h16v11H9l-4 3v-3H4z"/><path d="M8 10h3m2 0h3m-8 3h5"/>',
|
|
arrow: '<path d="m7 12 9-7v14z"/>',
|
|
check: '<path d="m5 12 4 4L19 6"/>',
|
|
};
|
|
return `<svg class="icon" viewBox="0 0 24 24" aria-hidden="true">${icons[name]}</svg>`;
|
|
}
|
|
|
|
function statusRail(context, battle = false) {
|
|
return `
|
|
<header class="status-rail layer-ui">
|
|
<button class="tool" aria-label="返回首页">${icon("home")}</button>
|
|
<div class="status-copy">
|
|
<span class="eyebrow">第一份委托 · 断裂风路</span>
|
|
<strong>${context}</strong>
|
|
</div>
|
|
<div class="status-spacer"></div>
|
|
${battle ? '<div class="battle-index"><span>战斗 1 / 5</span><b>自动战斗</b></div>' : '<div class="resource"><span>小队</span><b>2</b></div>'}
|
|
<button class="tool" aria-label="重新开始">${icon("restart")}</button>
|
|
${battle ? `<button class="tool" aria-label="暂停">${icon("pause")}</button><button class="tool" aria-label="查看事件">${icon("inspect")}</button><button class="tool active" aria-label="字幕开启">${icon("captions")}</button>` : ''}
|
|
</header>`;
|
|
}
|
|
|
|
function actor({who, action = "idle", x, y, order, label, hp = 100, urgent = ""}) {
|
|
const actorSrc = who === "farstring"
|
|
? (action === "shot" ? ASSETS.farstringShot : action === "full" ? ASSETS.farstring : ASSETS.farstringIdle)
|
|
: (action === "heal" ? ASSETS.kindhandHeal : action === "full" ? ASSETS.kindhand : ASSETS.kindhandIdle);
|
|
const name = who === "farstring" ? "妮娅" : "赛布尔";
|
|
const role = who === "farstring" ? "弓手" : "治疗";
|
|
return `
|
|
<section class="actor actor-${who} ${urgent ? "urgent" : ""} layer-actors" style="--x:${x}px;--y:${y}px" aria-label="${order} ${name} ${role} 生命 ${hp}">
|
|
<div class="actor-badge"><b>${order}</b><span>${label}</span></div>
|
|
<img src="${actorSrc}" alt="${name} · ${role}">
|
|
<div class="actor-meta">
|
|
<strong>${name} · ${role}</strong>
|
|
<span class="hp"><i style="--hp:${hp}%"></i></span>
|
|
<small>${hp} / 100${urgent ? ` · ${urgent}` : ""}</small>
|
|
</div>
|
|
</section>`;
|
|
}
|
|
|
|
function enemy(hp = 100, urgent = "") {
|
|
return `
|
|
<section class="enemy ${urgent ? "urgent" : ""} layer-actors" aria-label="裂影刃手 生命 ${hp}">
|
|
<div class="enemy-name"><strong>裂影刃手</strong><span>路线守卫</span></div>
|
|
${img(ASSETS.enemy, "enemy-sprite", "裂影刃手")}
|
|
<span class="hp enemy-hp"><i style="--hp:${hp}%"></i></span>
|
|
<small>${hp} / 100${urgent ? ` · ${urgent}` : ""}</small>
|
|
</section>`;
|
|
}
|
|
|
|
function battlefieldBase(title, objective) {
|
|
return `
|
|
${statusRail(title, true)}
|
|
<section class="battlefield layer-environment">
|
|
<img class="field-art" src="${ASSETS.field}" alt="断裂风路遗迹战场">
|
|
<div class="field-wash"></div>
|
|
<div class="objective layer-ui"><span>当前目标</span><strong>${objective}</strong></div>
|
|
<div class="party-ground layer-connectors"><i></i><i></i></div>
|
|
</section>`;
|
|
}
|
|
|
|
function eventRibbon(step, text, source, action, target, result) {
|
|
return `
|
|
<footer class="event-ribbon layer-captions">
|
|
<span class="event-step">事件 ${step}</span>
|
|
<strong>${text}</strong>
|
|
<div class="event-chain"><span>${source}</span><i>→</i><span>${action}</span><i>→</i><span>${target}</span><i>→</i><b>${result}</b></div>
|
|
</footer>`;
|
|
}
|
|
|
|
function formationReview() {
|
|
return `
|
|
${statusRail("系统编队 · 检查两人的职责")}
|
|
<section class="formation-world layer-environment">
|
|
<img class="field-art" src="${ASSETS.field}" alt="断裂风路遗迹">
|
|
<div class="formation-wash"></div>
|
|
<div class="formation-cord layer-connectors"><span></span></div>
|
|
${actor({who:"kindhand", action:"full", x:64, y:82, order:"1", label:"第二位", hp:100})}
|
|
${actor({who:"farstring", action:"full", x:282, y:74, order:"0", label:"先行位", hp:100, urgent:"暴露"})}
|
|
<div class="threat-gap layer-connectors"><span>敌人方向</span><i></i></div>
|
|
<div class="formation-note note-second layer-ui"><b>第二位 · 1</b><span>受击后举灯,为先行者恢复生命</span></div>
|
|
<div class="formation-note note-lead layer-ui"><b>先行位 · 0</b><span>先接触威胁,以弓术回应敌人</span></div>
|
|
</section>
|
|
<aside class="context-rail layer-ui">
|
|
<span class="rail-kicker">系统排序原因</span>
|
|
<h2>职责决定顺序</h2>
|
|
<p>妮娅先接近威胁;赛布尔留在受保护位置,等待实际受伤目标。</p>
|
|
<div class="evidence-row"><i class="symbol order">0</i><span><b>妮娅 · 弓手</b>远程职责先行,但会承受更高暴露。</span></div>
|
|
<div class="evidence-row"><i class="symbol heal">+</i><span><b>赛布尔 · 治疗</b>第二位行动,恢复实际受伤成员。</span></div>
|
|
<p class="system-note">系统依据两人的当前职责形成此顺序。</p>
|
|
</aside>
|
|
<footer class="action-dock layer-ui">
|
|
<button class="secondary">重新选择成员</button>
|
|
<span>选择顺序不会改变系统编队</span>
|
|
<button class="primary">${icon("arrow")}确认编队并出发</button>
|
|
</footer>`;
|
|
}
|
|
|
|
function approachContact() {
|
|
return `
|
|
${battlefieldBase("第一战 · 接敌", "守住断裂风路 · 读取敌人锁定")}
|
|
${actor({who:"kindhand", x:112, y:137, order:"1", label:"第二位", hp:100})}
|
|
${actor({who:"farstring", x:270, y:125, order:"0", label:"先行位", hp:100, urgent:"被锁定"})}
|
|
${enemy(100, "蓄势")}
|
|
<div class="target-line layer-connectors"><span>锁定先行者</span></div>
|
|
<div class="threat-mark layer-feedback"><i></i><i></i><span>!</span></div>
|
|
${eventRibbon("01 · 1.20s", "危险:裂影刃手锁定妮娅", "裂影刃手", "重击预兆", "妮娅 · 先行位", "即将命中")}`;
|
|
}
|
|
|
|
function damageFeedback() {
|
|
return `
|
|
${battlefieldBase("第一战 · 敌方压力", "确认受击对象与生命变化")}
|
|
${actor({who:"kindhand", x:112, y:137, order:"1", label:"第二位", hp:100})}
|
|
${actor({who:"farstring", x:270, y:125, order:"0", label:"先行位", hp:58, urgent:"高暴露"})}
|
|
${enemy(100, "攻击已结算")}
|
|
<div class="impact-line layer-connectors"></div>
|
|
<div class="damage-burst layer-feedback">${img(ASSETS.damage, "vfx", "重击伤害")}</div>
|
|
<div class="number damage-number layer-feedback"><b>-42</b><span>100 → 58</span></div>
|
|
${eventRibbon("02 · 2.35s", "妮娅在先行位承受高额伤害", "裂影刃手", "裂隙重击", "妮娅", "-42 · 58/100")}`;
|
|
}
|
|
|
|
function healResponse() {
|
|
return `
|
|
${battlefieldBase("第一战 · 职业回应", "赛布尔恢复实际受伤的妮娅")}
|
|
${actor({who:"kindhand", action:"heal", x:112, y:137, order:"1", label:"第二位", hp:100, urgent:"正在恢复"})}
|
|
${actor({who:"farstring", x:270, y:125, order:"0", label:"先行位", hp:70, urgent:"恢复后"})}
|
|
${enemy(100, "等待")}
|
|
<div class="heal-path layer-connectors"><i></i><i></i><i></i></div>
|
|
<div class="heal-burst layer-feedback">${img(ASSETS.heal, "vfx", "恢复效果")}</div>
|
|
<div class="number heal-number layer-feedback"><b>+12</b><span>58 → 70</span></div>
|
|
${eventRibbon("03 · 3.60s", "赛布尔从第二位举灯恢复妮娅", "赛布尔", "灵灯恢复", "妮娅", "+12 · 70/100")}`;
|
|
}
|
|
|
|
function rangedFinish() {
|
|
return `
|
|
${battlefieldBase("第一战 · 两人连携", "妮娅完成本轮远程回应")}
|
|
${actor({who:"kindhand", x:112, y:137, order:"1", label:"第二位", hp:100})}
|
|
${actor({who:"farstring", action:"shot", x:270, y:125, order:"0", label:"先行位", hp:70, urgent:"远程射击"})}
|
|
${enemy(34, "即将倒下")}
|
|
<div class="projectile-line layer-connectors"><span></span></div>
|
|
<div class="number enemy-number layer-feedback"><b>-30</b><span>64 → 34</span></div>
|
|
${eventRibbon("04 · 5.10s", "妮娅以弓术把敌人压入终结线", "妮娅", "远程射击", "裂影刃手", "-30 · 34/100")}`;
|
|
}
|
|
|
|
function firstDiagnosis() {
|
|
return `
|
|
${statusRail("第一战诊断 · 这支小队如何配合")}
|
|
<section class="result-world layer-environment">
|
|
<img class="field-art" src="${ASSETS.field}" alt="战后断裂风路">
|
|
<div class="result-wash"></div>
|
|
<div class="result-party layer-actors">
|
|
${img(ASSETS.kindhand, "result-kindhand", "赛布尔 · 治疗")}
|
|
${img(ASSETS.farstring, "result-farstring", "妮娅 · 弓手")}
|
|
<span class="result-order second">1 · 第二位</span>
|
|
<span class="result-order lead">0 · 先行位</span>
|
|
</div>
|
|
<div class="cause-path layer-connectors">
|
|
<div class="cause-node decision"><span>系统编队</span><b>妮娅先行</b><small>远程职责,但高暴露</small></div>
|
|
<i></i>
|
|
<div class="cause-node divergence"><span>最早差异 · 2.35s</span><b>重击 -42</b><small>妮娅 100 → 58</small></div>
|
|
<i></i>
|
|
<div class="cause-node response"><span>职业回应 · 3.60s</span><b>恢复 +12</b><small>妮娅 58 → 70</small></div>
|
|
</div>
|
|
</section>
|
|
<aside class="context-rail result-rail layer-ui">
|
|
<span class="rail-kicker">首战因果结果</span>
|
|
<h2>治疗补回部分损失</h2>
|
|
<dl class="result-copy">
|
|
<div><dt>发生了什么</dt><dd>系统让妮娅先行;她承受重击后完成远程回应。</dd></div>
|
|
<div><dt>为什么</dt><dd>赛布尔随后为实际受伤的妮娅恢复 12 生命。</dd></div>
|
|
<div><dt>系统编队造成的差异</dt><dd>远程成员先行而高暴露;治疗只补回部分损失。</dd></div>
|
|
<div><dt>现在改变了</dt><dd>妮娅 70/100;金币 0 → 20(仅一次)。</dd></div>
|
|
<div><dt>下一步</dt><dd>比较安全与节奏两种应对方案。</dd></div>
|
|
</dl>
|
|
</aside>
|
|
<footer class="action-dock result-dock layer-ui">
|
|
<button class="secondary">${icon("inspect")}查看完整事件</button>
|
|
<span class="receipt">${icon("check")}奖励已结算一次 · 金币 +20</span>
|
|
<button class="primary">${icon("arrow")}查看下一场威胁</button>
|
|
</footer>`;
|
|
}
|
|
|
|
const renderers = {
|
|
"formation-review": formationReview,
|
|
"approach-contact": approachContact,
|
|
"damage-feedback": damageFeedback,
|
|
"heal-response": healResponse,
|
|
"ranged-finish": rangedFinish,
|
|
"first-diagnosis": firstDiagnosis,
|
|
};
|
|
|
|
const root = document.querySelector("#slice");
|
|
root.dataset.frame = selectedFrame;
|
|
root.innerHTML = renderers[selectedFrame]();
|
|
document.documentElement.dataset.ready = "true";
|