583 lines
33 KiB
JavaScript
583 lines
33 KiB
JavaScript
const state = {
|
||
data: null,
|
||
view: "overview",
|
||
screenId: "SYS-004",
|
||
screenCategory: "ALL",
|
||
chapter: 0,
|
||
flowIndex: 0,
|
||
queries: {},
|
||
};
|
||
|
||
const viewNames = {
|
||
overview: "总览",
|
||
screens: "全部页面",
|
||
flow: "核心流程",
|
||
chapters: "八章画面",
|
||
characters: "角色图鉴",
|
||
professions: "职业系统",
|
||
enemies: "敌人图鉴",
|
||
items: "装备与物品",
|
||
records: "成就与收藏",
|
||
production: "美术与音频生产",
|
||
readiness: "开发准备度",
|
||
};
|
||
|
||
const screenCategoryNames = {
|
||
ALL: "全部",
|
||
SYS: "系统",
|
||
GLD: "Guild",
|
||
WRK: "工坊",
|
||
REC: "档案",
|
||
WLD: "世界",
|
||
EXP: "远征",
|
||
BAT: "战斗",
|
||
RES: "结果",
|
||
SET: "设置",
|
||
};
|
||
|
||
const chapterImages = Array.from({ length: 8 }, (_, index) =>
|
||
`assets/web/ch_ch${String(index + 1).padStart(2, "0")}.webp`,
|
||
);
|
||
|
||
const flowIds = [
|
||
"SYS-004",
|
||
"GLD-001",
|
||
"GLD-005",
|
||
"WLD-001",
|
||
"EXP-002",
|
||
"EXP-004",
|
||
"BAT-002",
|
||
"BAT-005",
|
||
"BAT-006",
|
||
"BAT-007",
|
||
"RES-001",
|
||
"RES-002",
|
||
"EXP-008",
|
||
"EXP-009",
|
||
"EXP-010",
|
||
"RES-005",
|
||
"GLD-001",
|
||
];
|
||
|
||
const $ = (selector, root = document) => root.querySelector(selector);
|
||
const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
|
||
|
||
function escapeHtml(value = "") {
|
||
return String(value)
|
||
.replaceAll("&", "&")
|
||
.replaceAll("<", "<")
|
||
.replaceAll(">", ">")
|
||
.replaceAll('"', """)
|
||
.replaceAll("'", "'");
|
||
}
|
||
|
||
function splitIdentity(identity) {
|
||
const match = identity.match(/^([^ ]+)\s+(.+)$/);
|
||
return match ? { id: match[1], name: match[2] } : { id: identity, name: identity };
|
||
}
|
||
|
||
function getScreen(id) {
|
||
return state.data.screens.find((screen) => screen.id === id);
|
||
}
|
||
|
||
function chapterNumberFromIdentity(identity) {
|
||
const match = identity.match(/CH-(\d{2})/);
|
||
return match ? Number(match[1]) : 1;
|
||
}
|
||
|
||
function setView(view, options = {}) {
|
||
state.view = view;
|
||
if (options.screenId) state.screenId = options.screenId;
|
||
if (Number.isInteger(options.chapter)) state.chapter = options.chapter;
|
||
history.replaceState(null, "", `#${view}`);
|
||
render();
|
||
closeMobileNav();
|
||
$("#content")?.focus({ preventScroll: true });
|
||
}
|
||
|
||
function viewHeading(eyebrow, title, description, aside = "") {
|
||
return `
|
||
<header class="view-heading">
|
||
<div>
|
||
<span class="eyebrow">${escapeHtml(eyebrow)}</span>
|
||
<h1>${escapeHtml(title)}</h1>
|
||
<p>${escapeHtml(description)}</p>
|
||
</div>
|
||
${aside}
|
||
</header>`;
|
||
}
|
||
|
||
function metric(value, label) {
|
||
return `<div class="metric"><strong>${escapeHtml(value)}</strong><span>${escapeHtml(label)}</span></div>`;
|
||
}
|
||
|
||
function chip(label) {
|
||
return `<span class="chip">${escapeHtml(label)}</span>`;
|
||
}
|
||
|
||
function renderOverview() {
|
||
const counts = state.data.counts;
|
||
return `
|
||
<section class="view">
|
||
${viewHeading(
|
||
"Complete pre-production",
|
||
"先把整款游戏设计清楚,再进入开发",
|
||
"Aetherbound Guild 是一款横屏 2D 日系手绘的六人编队策略 RPG。玩家不是连续点技能,而是读懂威胁、调整编队与规则、让方案自动执行,并在关键时刻用两次 Directive 改变战局。",
|
||
`${chip("25-35 小时主线")} ${chip("一次性买断")} ${chip("iOS / Android / PC / Console")}`,
|
||
)}
|
||
<div class="overview-hero">
|
||
<img src="assets/web/ch01_battle.webp" alt="第一章风车中继战斗概念图:三名冒险者面对机械钟兽" />
|
||
<div class="overview-hero-copy">
|
||
<span class="eyebrow">CH-01 DYNAMIC SLICE</span>
|
||
<h2>看预告、改规则、验证判断</h2>
|
||
<p>第一视觉焦点是敌方的中心线预告,第二是 Mara 保护后排,第三是可用的 Retune Directive。概念图只确定方向,HUD 和所有文字由真实界面实现。</p>
|
||
</div>
|
||
</div>
|
||
<div class="metric-row">
|
||
${metric(counts.screens, "完整页面与状态定义")}
|
||
${metric("8 × 3", "章节路线 / 战场 / 修复画面族")}
|
||
${metric(counts.characters, "有姓名、有成长弧的角色")}
|
||
${metric(counts.professions, "12 基础 + 24 进阶职业")}
|
||
${metric(counts.enemies, "具有独立机制的敌人")}
|
||
${metric(counts.items, "逐件定义效果与视觉的装备")}
|
||
${metric("30", "自适应音乐套件")}
|
||
${metric("710", "需要被验证的 SFX 事件身份")}
|
||
</div>
|
||
<section class="section-band">
|
||
<span class="eyebrow">PLAYER LOOP</span>
|
||
<h2>玩家真正反复做什么</h2>
|
||
<div class="thesis-grid">
|
||
<article class="info-block"><h3>读懂问题</h3><p>先看敌人的目标、倒计时、路线压力与奖励风险。所有必要信息在承诺前可见,随机只提出问题,不直接判玩家输。</p></article>
|
||
<article class="info-block"><h3>改变方案</h3><p>调整六人位置、职业、技能、装备、目标策略和 Standing Orders。每个选择都必须改变首个失败点或后续机会。</p></article>
|
||
<article class="info-block"><h3>形成世界后果</h3><p>战斗执行准备,奖励进入 Unsecured Cache;到 Anchor 决定安全与风险。每章修复一种公共服务,并改变下一轮路线。</p></article>
|
||
</div>
|
||
</section>
|
||
<section class="section-band">
|
||
<span class="eyebrow">REVIEW PATH</span>
|
||
<h2>建议你怎么检查</h2>
|
||
<div class="production-grid">
|
||
<article class="info-block"><h3>1. 先看核心流程</h3><p>从 Title 点到 Guild、编队、地图、路线、战斗、诊断、奖励、Anchor 与修复,判断每一步是否知道为什么要点。</p></article>
|
||
<article class="info-block"><h3>2. 再看八章差异</h3><p>检查每章是否具有不同的环境、学习目标、敌人问题、Boss 测试和永久世界变化,而不是换背景和数值。</p></article>
|
||
<article class="info-block"><h3>3. 最后看完整内容</h3><p>搜索 30 角色、36 职业、100 敌人、320 装备,确认它们都有玩法用途、反制关系和生产需求。</p></article>
|
||
</div>
|
||
<div class="flow-controls">
|
||
<button class="flow-action" type="button" data-open-view="flow">开始体验核心流程 →</button>
|
||
</div>
|
||
</section>
|
||
</section>`;
|
||
}
|
||
|
||
function artForScreen(screen) {
|
||
const prefix = screen.id.slice(0, 3);
|
||
if (prefix === "BAT" || ["EXP-004", "RES-001", "RES-002", "RES-003", "RES-004"].includes(screen.id)) {
|
||
return "assets/web/ch01_battle.webp";
|
||
}
|
||
if (prefix === "REC") {
|
||
if (screen.id === "REC-002") return "assets/web/el_enemies.webp";
|
||
if (screen.id === "REC-003") return "assets/web/el_items.webp";
|
||
return "assets/web/el_heroes.webp";
|
||
}
|
||
if (["GLD-003", "GLD-004", "GLD-007", "GLD-008", "GLD-009", "GLD-011", "GLD-012"].includes(screen.id)) {
|
||
return screen.id === "GLD-007" ? "assets/web/el_profession_kits.webp" : "assets/web/el_heroes.webp";
|
||
}
|
||
if (prefix === "WLD" || prefix === "EXP" || prefix === "WRK" || prefix === "GLD" || prefix === "SYS" || prefix === "RES") {
|
||
return "assets/web/ch_ch01.webp";
|
||
}
|
||
return "";
|
||
}
|
||
|
||
function stageTop(screen) {
|
||
return `
|
||
<div class="hud-top">
|
||
<div class="hud-group">
|
||
<span class="hud-chip"><i></i>${escapeHtml(screen.id)}</span>
|
||
<span class="hud-chip">目标 · 修复风车中继</span>
|
||
</div>
|
||
<div class="hud-group">
|
||
<span class="hud-chip safe"><i></i>Saved</span>
|
||
<span class="hud-chip risk"><i></i>Cache 180</span>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
function renderRouteStage(screen) {
|
||
return `
|
||
${stageTop(screen)}
|
||
<div class="stage-title"><small>CH-01 · 风车盆地</small><strong>${escapeHtml(screen.name)}</strong><p>路线提前显示威胁、奖励、Cache 风险与 Anchor 距离。</p></div>
|
||
<div class="route-map">
|
||
<div class="route-line"></div>
|
||
<button class="route-node" type="button" title="营地">营</button>
|
||
<button class="route-node" type="button" title="救援目标">救</button>
|
||
<button class="route-node" type="button" title="精英威胁">危</button>
|
||
<button class="route-node" type="button" title="Anchor">锚</button>
|
||
</div>
|
||
<div class="stage-bottom"><button class="stage-secondary" type="button">查看比较</button><button class="stage-primary" type="button" data-flow-next>选择风车高地 →</button></div>`;
|
||
}
|
||
|
||
function renderFormationStage(screen) {
|
||
const slots = ["Mara · Front", "Oren · Mid", "Tamsin · Rear", "Suri · Front", "Ilyo · Rear", "空位"];
|
||
return `
|
||
${stageTop(screen)}
|
||
<div class="stage-title"><small>WEAVE GRID</small><strong>${escapeHtml(screen.name)}</strong><p>点角色再点目标位置;拖动只是可选快捷方式。</p></div>
|
||
<div class="formation-board">
|
||
${slots.map((slot, index) => `<button class="formation-slot ${index < 5 ? "is-filled" : ""} ${index === 2 ? "is-risk" : ""}" type="button">${escapeHtml(slot)}</button>`).join("")}
|
||
</div>
|
||
<div class="stage-generic-panel"><h3>首次受击预测</h3><div class="stage-list-row">Mara 拦截中心线</div><div class="stage-list-row">Tamsin 暴露于 Gust</div><div class="stage-list-row">角色标签 4/6 不重复</div></div>
|
||
<div class="stage-bottom"><button class="stage-secondary" type="button">预设</button><button class="stage-primary" type="button" data-flow-next>确认 Company →</button></div>`;
|
||
}
|
||
|
||
function renderBattleStage(screen) {
|
||
const directiveFlow = ["BAT-005", "BAT-006", "BAT-007"].includes(screen.id);
|
||
return `
|
||
${stageTop(screen)}
|
||
<div class="battle-telegraph"></div>
|
||
<div class="cache-strap"><b>UNSECURED CACHE</b><strong>180</strong></div>
|
||
<div class="directive-dock">
|
||
<button class="directive" type="button" data-flow-next>${directiveFlow ? "RETUNE · LEGAL" : "RETUNE · 1"}</button>
|
||
<button class="directive is-locked" type="button" title="需要另一个 Command">HOLD · 0</button>
|
||
</div>
|
||
<div class="stage-bottom"><button class="stage-secondary" type="button">事件记录</button><button class="stage-secondary" type="button">1× · 2×</button></div>`;
|
||
}
|
||
|
||
function renderSettingsStage(screen) {
|
||
return `
|
||
<div class="settings-stage">
|
||
<div class="settings-nav"><strong>SETTINGS</strong><span>显示与大字</span><span>动效与闪烁</span><span>音频与字幕</span><span>控制方式</span><span>存档与云端</span></div>
|
||
<div class="settings-content"><h3>${escapeHtml(screen.name)}</h3>
|
||
<div class="setting-row"><span>UI 与文字缩放 · 130%</span><div class="fake-slider"><i></i></div></div>
|
||
<div class="setting-row"><span>减少动态</span><i class="toggle"></i></div>
|
||
<div class="setting-row"><span>高对比目标轮廓</span><i class="toggle"></i></div>
|
||
<div class="setting-row"><span>字幕与事件标签</span><i class="toggle"></i></div>
|
||
<div class="setting-row"><span>低功耗模式</span><i class="toggle"></i></div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
function renderRecordsStage(screen, art) {
|
||
return `
|
||
${stageTop(screen)}
|
||
<div class="records-stage">
|
||
<div class="records-art"><img src="${art}" alt="概念元素图" /></div>
|
||
<div class="records-list"><strong>${escapeHtml(screen.name)}</strong><p>图鉴同时展示身份、机制、反制、获取与完成状态。</p><div class="record-line">已研究 · 机制完整</div><div class="record-line">可用反制 · 2</div><div class="record-line">章节关联 · CH-01</div><div class="record-line">生产状态 · Concept</div></div>
|
||
</div>`;
|
||
}
|
||
|
||
function renderGenericStage(screen) {
|
||
const prefix = screen.id.slice(0, 3);
|
||
const light = prefix === "WRK" || prefix === "GLD";
|
||
return `
|
||
${stageTop(screen)}
|
||
<div class="stage-title"><small>${escapeHtml(screen.id)} · DESIGN SPEC</small><strong>${escapeHtml(screen.name)}</strong><p>${escapeHtml(screen.focal)}</p></div>
|
||
<div class="stage-generic-panel"><h3>当前操作</h3><p>${escapeHtml(screen.action)}</p><div class="stage-list-row">状态与成本可见</div><div class="stage-list-row">取消不会消耗资源</div><div class="stage-list-row">所有拖动都有点选路径</div></div>
|
||
<div class="stage-bottom"><button class="stage-secondary" type="button">返回</button><button class="stage-primary" type="button" data-flow-next>${escapeHtml(screen.action)}</button></div>
|
||
${light ? "" : ""}`;
|
||
}
|
||
|
||
function renderGameStage(screen) {
|
||
const prefix = screen.id.slice(0, 3);
|
||
const art = artForScreen(screen);
|
||
const light = prefix === "SET" || prefix === "REC";
|
||
let content;
|
||
if (screen.id === "GLD-005") content = renderFormationStage(screen);
|
||
else if (["EXP-002", "EXP-003", "WLD-001", "WLD-002", "WLD-003"].includes(screen.id)) content = renderRouteStage(screen);
|
||
else if (prefix === "BAT" || ["EXP-004", "RES-001", "RES-002", "RES-003", "RES-004"].includes(screen.id)) content = renderBattleStage(screen);
|
||
else if (prefix === "SET") content = renderSettingsStage(screen);
|
||
else if (prefix === "REC") content = renderRecordsStage(screen, art);
|
||
else content = renderGenericStage(screen);
|
||
|
||
const decorativeArt = art && prefix !== "REC" ? `<img class="stage-art" src="${art}" alt="" />` : "";
|
||
return `<div class="game-stage ${light ? "stage-light" : ""}" data-stage-id="${escapeHtml(screen.id)}">${decorativeArt}${content}</div>`;
|
||
}
|
||
|
||
function screenMeta(screen, index, screens) {
|
||
const previous = screens[(index - 1 + screens.length) % screens.length];
|
||
const next = screens[(index + 1) % screens.length];
|
||
return `
|
||
<div class="screen-meta">
|
||
<div class="screen-meta-head">
|
||
<div><span class="screen-id">${escapeHtml(screen.id)}</span><h2>${escapeHtml(screen.name)}</h2>${chip("Design specification")} ${chip("Human review open")}</div>
|
||
<div class="pager"><button type="button" data-screen-id="${escapeHtml(previous.id)}">← 上一页</button><button type="button" data-screen-id="${escapeHtml(next.id)}">下一页 →</button></div>
|
||
</div>
|
||
<div class="spec-grid">
|
||
<div class="spec-cell"><span>三秒焦点顺序</span><p>${escapeHtml(screen.focal)}</p></div>
|
||
<div class="spec-cell"><span>唯一主操作</span><p>${escapeHtml(screen.action)}</p></div>
|
||
<div class="spec-cell"><span>状态、异常与退出</span><p>${escapeHtml(screen.variants)}</p></div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
function renderScreens() {
|
||
const category = state.screenCategory;
|
||
const filtered = state.data.screens.filter((screen) => category === "ALL" || screen.id.startsWith(`${category}-`));
|
||
if (!filtered.some((screen) => screen.id === state.screenId)) state.screenId = filtered[0].id;
|
||
const screen = getScreen(state.screenId);
|
||
const index = filtered.findIndex((entry) => entry.id === screen.id);
|
||
return `
|
||
<section class="view">
|
||
${viewHeading("82 SCREEN DEFINITIONS", "每一个页面和关键状态", "页面按系统、Guild、工坊、档案、世界、远征、战斗、结果与设置分类。每一项都锁定玩家先看什么、主操作是什么,以及错误和返回怎么处理。")}
|
||
<div class="toolbar"><div class="segmented">${Object.entries(screenCategoryNames).map(([key, label]) => `<button class="${key === category ? "is-active" : ""}" type="button" data-screen-category="${key}">${escapeHtml(label)}</button>`).join("")}</div></div>
|
||
<div class="screen-layout">
|
||
<aside class="screen-index" aria-label="页面列表">${filtered.map((entry) => `<button class="${entry.id === screen.id ? "is-active" : ""}" type="button" data-screen-id="${escapeHtml(entry.id)}"><span>${escapeHtml(entry.id)}</span><b>${escapeHtml(entry.name)}</b></button>`).join("")}</aside>
|
||
<div class="screen-detail"><div class="screen-stage-wrap">${renderGameStage(screen)}</div>${screenMeta(screen, index, filtered)}</div>
|
||
</div>
|
||
</section>`;
|
||
}
|
||
|
||
function renderFlow() {
|
||
const flowScreens = flowIds.map(getScreen);
|
||
const screen = flowScreens[state.flowIndex];
|
||
return `
|
||
<section class="view">
|
||
${viewHeading("CLICKABLE EXPERIENCE", "从首页到章节修复的完整点击链", "这是压缩的设计原型路径,用来检查玩家是否始终知道当前位置、压力、下一步和退出方式。它不模拟战斗公式或存档。", `${chip(`${state.flowIndex + 1} / ${flowScreens.length}`)} ${chip("无死按钮")}`)}
|
||
<div class="flow-shell">
|
||
<div><div class="screen-stage-wrap">${renderGameStage(screen)}</div>${screenMeta(screen, state.flowIndex, flowScreens)}</div>
|
||
<aside class="flow-steps" aria-label="核心流程步骤">${flowScreens.map((entry, index) => `<button class="${index === state.flowIndex ? "is-active" : ""}" type="button" data-flow-index="${index}"><span>${String(index + 1).padStart(2, "0")}</span><b>${escapeHtml(entry.id)}</b>${escapeHtml(entry.name)}</button>`).join("")}</aside>
|
||
</div>
|
||
<div class="flow-controls"><button class="text-button" type="button" data-flow-prev>← 上一步</button><button class="flow-action" type="button" data-flow-next>${state.flowIndex === flowScreens.length - 1 ? "回到流程开头" : "执行主操作,进入下一步 →"}</button></div>
|
||
</section>`;
|
||
}
|
||
|
||
function renderChapters() {
|
||
const chapter = state.data.chapters[state.chapter];
|
||
const { id, name } = splitIdentity(chapter.identity);
|
||
return `
|
||
<section class="view">
|
||
${viewHeading("8 CHAPTERS · 24 VISUAL FAMILIES", "每一章都提出新的策略问题", "每章都有独立的路线/聚落、代表战场和修复/抉择画面。概念图用于确认环境方向,完整细节以章节策划文档为准。")}
|
||
<div class="chapter-tabs">${state.data.chapters.map((entry, index) => `<button class="${index === state.chapter ? "is-active" : ""}" type="button" data-chapter="${index}">CH-${String(index + 1).padStart(2, "0")}</button>`).join("")}</div>
|
||
<div class="chapter-layout">
|
||
<div><div class="chapter-art"><img src="${chapterImages[state.chapter]}" alt="${escapeHtml(name)} 章节环境概念图" /></div><div class="chapter-states"><div class="chapter-state"><b>${id}A · 路线 / 聚落</b><span>${escapeHtml(chapter.region)} · ${escapeHtml(chapter.route)}</span></div><div class="chapter-state"><b>${id}B · 代表战场</b><span>${escapeHtml(chapter.learning)},Boss 通过真实机制检验这一章。</span></div><div class="chapter-state"><b>${id}C · 修复 / 抉择</b><span>${escapeHtml(chapter.change)}</span></div></div></div>
|
||
<article class="chapter-copy"><span class="eyebrow">${escapeHtml(id)}</span><h2>${escapeHtml(name)}</h2><h3>本章学习目标</h3><p>${escapeHtml(chapter.learning)}</p><h3>Boss 测试与奖励</h3><p>${escapeHtml(chapter.boss)}</p><h3>永久变化</h3><p>${escapeHtml(chapter.change)}</p>${chip("Concept art")} ${chip("Owner review open")}</article>
|
||
</div>
|
||
</section>`;
|
||
}
|
||
|
||
function searchableValues(entries, query) {
|
||
const normalized = query.trim().toLocaleLowerCase();
|
||
if (!normalized) return entries;
|
||
return entries.filter((entry) => Object.values(entry).join(" ").toLocaleLowerCase().includes(normalized));
|
||
}
|
||
|
||
function renderCatalog(view, entries, config) {
|
||
const query = state.queries[view] || "";
|
||
const filtered = searchableValues(entries, query);
|
||
const shown = filtered.slice(0, config.limit || 320);
|
||
return `
|
||
<section class="view">
|
||
${viewHeading(config.eyebrow, config.title, config.description, `${chip(`${filtered.length} / ${entries.length}`)} ${chip("完整权威索引")}`)}
|
||
<div class="toolbar"><input class="search-input" data-catalog-search="${view}" type="search" value="${escapeHtml(query)}" placeholder="搜索 ID、名称、技能、机制、章节…" aria-label="搜索${escapeHtml(config.title)}" /></div>
|
||
<div class="catalog-layout">
|
||
<aside class="catalog-visual"><img src="${config.image}" alt="${escapeHtml(config.alt)}" /><div class="catalog-visual-copy"><span class="eyebrow">CONCEPT ELEMENT BOARD</span><p>${escapeHtml(config.visualNote)}</p></div></aside>
|
||
<div class="catalog-list">${shown.length ? shown.map((entry) => config.row(entry)).join("") : `<div class="empty-state">没有匹配内容。请清除搜索条件。</div>`}</div>
|
||
</div>
|
||
</section>`;
|
||
}
|
||
|
||
function catalogEntry(identity, meta, body) {
|
||
const { id, name } = splitIdentity(identity);
|
||
return `<article class="catalog-entry"><div><small>${escapeHtml(id)}</small><strong>${escapeHtml(name)}</strong><span class="chip">${escapeHtml(meta)}</span></div><p>${escapeHtml(body)}</p></article>`;
|
||
}
|
||
|
||
function renderCharacters() {
|
||
return renderCatalog("characters", state.data.characters, {
|
||
eyebrow: "30 NAMED ADVENTURERS",
|
||
title: "每个角色都有玩法、动机与成长弧",
|
||
description: "角色不是随机兵种皮肤。每个人有稳定身份、职业亲和、招募路径、关系、个人弧线、视觉和双语声音需求。",
|
||
image: "assets/web/el_heroes.webp",
|
||
alt: "Mara、Oren 和 Ilyo 三名核心角色概念图",
|
||
visualNote: "概念板用于锁定成熟比例、职业仍保留人物身份、以及 Guild 修复者的工作质感。并非最终角色立绘或 sprite。",
|
||
row: (entry) => catalogEntry(entry.identity, "Character", `${entry.gameplay} ${entry.story}`),
|
||
});
|
||
}
|
||
|
||
function renderProfessions() {
|
||
return renderCatalog("professions", state.data.professions, {
|
||
eyebrow: "12 BASE · 24 ADVANCED",
|
||
title: "36 个职业都改变行动逻辑",
|
||
description: "每个职业拥有资源、两项主动技能、一项被动、AI 优先级、反制面、晋升证明,以及可生产的武器、动作、VFX 与声音身份。",
|
||
image: "assets/web/el_profession_kits.webp",
|
||
alt: "六套职业装备与资源装置概念图",
|
||
visualNote: "职业采用模块化装备和动作套件叠加在 30 个稳定人物身份上,不为每个角色重画一套通用职业人。",
|
||
row: (entry) => catalogEntry(entry.identity, entry.role.split(".")[0], `${entry.skills} ${entry.build}`),
|
||
});
|
||
}
|
||
|
||
function renderEnemies() {
|
||
return renderCatalog("enemies", state.data.enemies, {
|
||
eyebrow: "56 ORDINARY · 24 ELITE · 12 SUB-BOSS · 8 BOSS",
|
||
title: "100 个敌人不是换色数值包",
|
||
description: "每个敌人通过独立目标、时序、空间、资源或信息规则改变准备方案,并具有可读预告、至少两个回应和生产规格。",
|
||
image: "assets/web/el_enemies.webp",
|
||
alt: "第一章机械敌人、暗潮危险和预告几何概念图",
|
||
visualNote: "第一章敌人来自被破坏的公共机械,轮廓和动作直接表达机制。后续七章保留各自材料与社会功能。",
|
||
row: (entry) => catalogEntry(entry.identity, "Enemy mechanic", `${entry.mechanic} 反制读取:${entry.read}`),
|
||
});
|
||
}
|
||
|
||
function renderItems() {
|
||
return renderCatalog("items", state.data.items, {
|
||
eyebrow: "320 AUTHORED ITEMS",
|
||
title: "装备必须改变判断,而不是只抬战力",
|
||
description: "320 件物品逐件定义槽位、章节、效果、状态依赖、协同、反制、获取、升级与美术/声音。160 件属于行为变化物品。",
|
||
image: "assets/web/el_items.webp",
|
||
alt: "Guild 装备、工具、Cache 与 Anchor 元素概念图",
|
||
visualNote: "物品按真实物件展示,稀有度不会用巨大光效抢走机制信息。最终 320 件都需要独立透明物件图。",
|
||
row: (entry) => catalogEntry(entry.identity, `${entry.slot} · ${entry.tier} · ${entry.flag}`, `${entry.effect} ${entry.build}`),
|
||
});
|
||
}
|
||
|
||
function renderRecords() {
|
||
const achievements = state.data.achievements;
|
||
const collections = state.data.collections;
|
||
return `
|
||
<section class="view">
|
||
${viewHeading("48 ACHIEVEMENTS · 20 COLLECTIONS", "完成度记录玩家学会了什么", "成就验证真实机制、构筑和章节结果;收藏把区域证据变成 Guild 中可见的物理陈列。它们不靠重复刷次数延长游戏。")}
|
||
<div class="metric-row">${metric(48, "机制与叙事成就")}${metric(20, "区域收藏")}${metric("100 h", "全收集与高难完成目标")}${metric("0", "每日任务、通行证与抽卡")}</div>
|
||
<section class="section-band"><h2>成就</h2><div class="catalog-list">${achievements.map((entry) => catalogEntry(entry.identity, "Achievement", `${entry.requirement} 奖励:${entry.reward}`)).join("")}</div></section>
|
||
<section class="section-band"><h2>收藏</h2><div class="catalog-list">${collections.map((entry) => catalogEntry(entry.identity, "Collection", `${entry.entries} 陈列:${entry.display}`)).join("")}</div></section>
|
||
</section>`;
|
||
}
|
||
|
||
function renderProduction() {
|
||
return `
|
||
<section class="view">
|
||
${viewHeading("COMPLETE PRODUCTION INVENTORY", "美术、动画、音乐和声音怎么做", "完整数量已经拆成可生产的身份包、模块、动作、场景和事件。当前四张 AI 图都是概念方向,不是最终运行时资源。")}
|
||
<div class="metric-row">${metric("30", "角色身份 rig + 6 表情")}${metric("72", "主动技能动作 / VFX 合同")}${metric("752+", "非 Boss 敌人动作状态")}${metric("88", "章节环境主状态")}${metric("101", "模块化剧情构图")}${metric("30", "音乐套件")}${metric("94", "环境声配置")}${metric("2,602", "双语最低角色录音资产")}</div>
|
||
<div class="production-grid">
|
||
<article class="info-block"><h3>角色与职业</h3><p>30 套稳定角色身份 + 36 套职业装备、资源装置和动作模块。职业改变人物的战斗轮廓,但不把角色替换成无名 class sprite。</p></article>
|
||
<article class="info-block"><h3>敌人与装备</h3><p>100 个敌人各自拥有机制轮廓、动作与预告;320 件物品都有独立物件图,160 件行为物品还必须显示真实触发。</p></article>
|
||
<article class="info-block"><h3>环境与章节</h3><p>八章共 88 个环境状态和至少 200 个独立道具/结构族。背景、交互物、前景、危险与修复层分开,不能用一张平图冒充地图。</p></article>
|
||
<article class="info-block"><h3>音乐</h3><p>30 套自适应音乐包含 base、pulse、tension 和 resolution;八章保留不同材料与节拍,最终章组合而不抹平差异。</p></article>
|
||
<article class="info-block"><h3>语音</h3><p>角色圣经定义 773 条战斗/Hub 语义额度和 144 个角色场景。英文与简中最低 2,602 个录音资产,语义语音严格控制并发。</p></article>
|
||
<article class="info-block"><h3>SFX 与无障碍</h3><p>710 个事件身份必须与真实提交状态一致。静音、字幕、单声道、输出切换、低功耗和振动关闭都不能改变玩法信息。</p></article>
|
||
</div>
|
||
<section class="section-band"><h2>先做哪一块</h2><p>正式批量生产前只做 CH-01 风车中继动态切片:Mara / Oren / Ilyo、Bell-Feeder、两名支援敌人、三条战线、Retune Directive、诊断、Reward → Cache → Anchor,以及 Clinic / Warning Tower 两个永久修复结果。</p><button class="flow-action" type="button" data-open-view="flow">查看动态切片流程 →</button></section>
|
||
</section>`;
|
||
}
|
||
|
||
function renderReadiness() {
|
||
const rows = [
|
||
["产品与玩家体验", "完整", "Product contract, 25-35h campaign, onboarding and evidence gates", "设计一致性已校验;人类理解/乐趣开放"],
|
||
["战斗 / 职业 / 装备", "完整", "Deterministic combat, 12+24 professions, 320 items", "需通过可玩动态切片验证"],
|
||
["经济 / 难度 / 存档", "完整", "1700-min authored budget, five traces, atomic save/failure", "公式静态校验通过;运行时未实现"],
|
||
["世界与内容", "完整", "30 characters, 100 enemies, 8 chapters, ending/postgame", "数量与链接校验通过;无人工完整试玩"],
|
||
["页面与 UI", "完整规格", "82 screens, 24 chapter screen families, 14 layout families", "本 HTML 等 Owner 视觉/流程审核"],
|
||
["美术与动画", "完整清单", "Identity/modular production and CH-01 slice", "仅 4 张概念图;无 final/integrated/runtime"],
|
||
["音乐 / 环境 / 语音 / SFX", "完整清单", "30 music, 94 ambience, 2602 voice, 710 events", "未生成、未试听、未授权批量 SFX/music"],
|
||
["HTML 产品原型", "当前候选", "All indexes + compressed clickable core path", "需 Owner 逐页审阅与修改"],
|
||
["引擎开发", "冻结", "No Godot project or runtime code", "完整设计 Owner 批准后另开 Goal"],
|
||
["设备 / 打包 / 上架", "开放", "No build, signing, device or store action", "必须在运行时完成后分别授权"],
|
||
];
|
||
return `
|
||
<section class="view">
|
||
${viewHeading("PRE-DEVELOPMENT GATE", "现在还没有进入正式开发", "策划与生产规格已经大幅补齐,但 Owner 仍需要先审核页面、八章画面和核心流程。开发不能把‘文档完整’误认为‘游戏好玩’。")}
|
||
<table class="readiness-table"><thead><tr><th>领域</th><th>状态</th><th>已经具备</th><th>仍然开放</th></tr></thead><tbody>${rows.map(([area, status, ready, open]) => `<tr><td><strong>${escapeHtml(area)}</strong></td><td class="${status === "冻结" || status === "开放" ? "state-open" : "state-ready"}">${escapeHtml(status)}</td><td>${escapeHtml(ready)}</td><td>${escapeHtml(open)}</td></tr>`).join("")}</tbody></table>
|
||
<section class="section-band"><h2>你的最短下一步</h2><p>先走一遍“核心流程”,再看八章画面和全部页面。把不理解、不喜欢或认为过于复杂的页面 ID 直接记下来。确认这些设计方向后,才适合制作 CH-01 可玩动态切片。</p><button class="flow-action" type="button" data-open-view="flow">开始 Owner 审核 →</button></section>
|
||
</section>`;
|
||
}
|
||
|
||
function render() {
|
||
if (!state.data) return;
|
||
const renderers = {
|
||
overview: renderOverview,
|
||
screens: renderScreens,
|
||
flow: renderFlow,
|
||
chapters: renderChapters,
|
||
characters: renderCharacters,
|
||
professions: renderProfessions,
|
||
enemies: renderEnemies,
|
||
items: renderItems,
|
||
records: renderRecords,
|
||
production: renderProduction,
|
||
readiness: renderReadiness,
|
||
};
|
||
$("#content").innerHTML = renderers[state.view]();
|
||
$("#view-title").textContent = viewNames[state.view];
|
||
$$(".nav-item").forEach((button) => button.classList.toggle("is-active", button.dataset.view === state.view));
|
||
bindViewEvents();
|
||
}
|
||
|
||
function bindViewEvents() {
|
||
$$('[data-open-view]').forEach((button) => button.addEventListener("click", () => setView(button.dataset.openView)));
|
||
$$('[data-screen-category]').forEach((button) => button.addEventListener("click", () => {
|
||
state.screenCategory = button.dataset.screenCategory;
|
||
render();
|
||
}));
|
||
$$('[data-screen-id]').forEach((button) => button.addEventListener("click", () => {
|
||
state.screenId = button.dataset.screenId;
|
||
render();
|
||
}));
|
||
$$('[data-chapter]').forEach((button) => button.addEventListener("click", () => {
|
||
state.chapter = Number(button.dataset.chapter);
|
||
render();
|
||
}));
|
||
$$('[data-flow-index]').forEach((button) => button.addEventListener("click", () => {
|
||
state.flowIndex = Number(button.dataset.flowIndex);
|
||
render();
|
||
}));
|
||
$$('[data-flow-next]').forEach((button) => button.addEventListener("click", () => {
|
||
state.flowIndex = (state.flowIndex + 1) % flowIds.length;
|
||
if (state.view !== "flow") setView("flow"); else render();
|
||
}));
|
||
$$('[data-flow-prev]').forEach((button) => button.addEventListener("click", () => {
|
||
state.flowIndex = (state.flowIndex - 1 + flowIds.length) % flowIds.length;
|
||
render();
|
||
}));
|
||
$$('[data-catalog-search]').forEach((input) => input.addEventListener("input", () => {
|
||
const view = input.dataset.catalogSearch;
|
||
state.queries[view] = input.value;
|
||
render();
|
||
const replacement = $(`[data-catalog-search="${view}"]`);
|
||
replacement?.focus();
|
||
replacement?.setSelectionRange(replacement.value.length, replacement.value.length);
|
||
}));
|
||
}
|
||
|
||
function openMobileNav() {
|
||
$("#sidebar").classList.add("is-open");
|
||
$("#nav-scrim").hidden = false;
|
||
}
|
||
|
||
function closeMobileNav() {
|
||
$("#sidebar").classList.remove("is-open");
|
||
$("#nav-scrim").hidden = true;
|
||
}
|
||
|
||
function openCurrentStage() {
|
||
const stage = $(".game-stage");
|
||
if (!stage) return;
|
||
const dialog = $("#stage-dialog");
|
||
$("#dialog-stage").replaceChildren(stage.cloneNode(true));
|
||
dialog.showModal();
|
||
}
|
||
|
||
async function init() {
|
||
try {
|
||
const response = await fetch("data/catalog.json");
|
||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||
state.data = await response.json();
|
||
const hashView = location.hash.replace("#", "");
|
||
if (viewNames[hashView]) state.view = hashView;
|
||
render();
|
||
} catch (error) {
|
||
$("#content").innerHTML = `<div class="empty-state"><h1>设计索引载入失败</h1><p>${escapeHtml(error.message)}</p><p>请通过本地 HTTP 服务或 Artifact Gallery 打开此页面。</p></div>`;
|
||
}
|
||
}
|
||
|
||
$("#primary-nav").addEventListener("click", (event) => {
|
||
const button = event.target.closest("[data-view]");
|
||
if (button) setView(button.dataset.view);
|
||
});
|
||
$("#open-nav").addEventListener("click", openMobileNav);
|
||
$("#close-nav").addEventListener("click", closeMobileNav);
|
||
$("#nav-scrim").addEventListener("click", closeMobileNav);
|
||
$("#fullscreen-current").addEventListener("click", openCurrentStage);
|
||
$("#close-dialog").addEventListener("click", () => $("#stage-dialog").close());
|
||
$("#stage-dialog").addEventListener("click", (event) => {
|
||
if (event.target === event.currentTarget) event.currentTarget.close();
|
||
});
|
||
window.addEventListener("keydown", (event) => {
|
||
if (event.key === "Escape") closeMobileNav();
|
||
});
|
||
|
||
init();
|