Files
aetherbound-guild/prototype/site/app.js
T

583 lines
33 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
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();