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: "公会",
WRK: "工坊",
REC: "档案",
WLD: "世界",
EXP: "远征",
BAT: "战斗",
RES: "结果",
SET: "设置",
};
const localizedScreenNames = {
"SYS-001": "启动画面",
"SYS-002": "首次无障碍设置",
"SYS-003": "语言与安全区校准",
"SYS-004": "标题首页",
"SYS-005": "游戏存档",
"SYS-006": "新游戏设置",
"SYS-007": "回归摘要",
"SYS-008": "存档恢复",
"SYS-009": "完成度总览",
"SYS-010": "制作人员、许可与数据说明",
};
function screenName(screen) {
return localizedScreenNames[screen.id] || screen.name;
}
function layoutKind(screen) {
const prefix = screen.id.slice(0, 3);
if (screen.id === "SYS-001") return "dedicated";
if (screen.id === "GLD-005") return "family-formation";
if (["EXP-002", "EXP-003", "WLD-001", "WLD-002", "WLD-003"].includes(screen.id)) return "family-route";
if (prefix === "BAT" || ["EXP-004", "RES-001", "RES-002", "RES-003", "RES-004"].includes(screen.id)) return "family-battle";
if (prefix === "SET") return "family-settings";
if (prefix === "REC") return "family-records";
return "missing";
}
function coverage(screen) {
const kind = layoutKind(screen);
if (kind === "dedicated") return { key: "dedicated", label: "逐页设计" };
if (kind.startsWith("family-")) return { key: "family", label: "页面族示意" };
return { key: "missing", label: "未设计" };
}
function reviewCopy(screen) {
if (screen.id === "SYS-001") {
return {
focal: "游戏标志、加载光线、当前载入状态",
action: "无需操作;完成后自动进入首次设置或标题首页",
variants: "首次启动、内容迁移、可恢复载入错误;错误时才显示重试与诊断入口",
};
}
return screen;
}
const chapterImages = [
"assets/web/ch_ch01.webp",
"assets/web/ch_ch02.webp",
"assets/web/ch_ch03.webp",
"assets/web/ch_ch04.webp",
"assets/web/ch_ch05.webp",
"assets/web/ch_ch06.webp",
"assets/web/ch_ch07.webp",
"assets/web/ch_ch08.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 `
`;
}
function metric(value, label) {
return `
${escapeHtml(value)} ${escapeHtml(label)}
`;
}
function chip(label) {
return `${escapeHtml(label)} `;
}
function renderOverview() {
const counts = state.data.counts;
return `
${viewHeading(
"Complete pre-production",
"先把整款游戏设计清楚,再进入开发",
"Aetherbound Guild 是一款横屏 2D 日系手绘的六人编队策略 RPG。玩家不是连续点技能,而是读懂威胁、调整编队与规则、让方案自动执行,并在关键时刻用两次 Directive 改变战局。",
`${chip("25-35 小时主线")} ${chip("一次性买断")} ${chip("iOS / Android / PC / Console")}`,
)}
CH-01 DYNAMIC SLICE
看预告、改规则、验证判断
第一视觉焦点是敌方的中心线预告,第二是 Mara 保护后排,第三是可用的 Retune Directive。概念图只确定方向,HUD 和所有文字由真实界面实现。
${metric(counts.screens, "页面需求定义;UI 尚未逐页完成")}
${metric("8 × 3", "章节路线 / 战场 / 修复画面族")}
${metric(counts.characters, "有姓名、有成长弧的角色")}
${metric(counts.professions, "12 基础 + 24 进阶职业")}
${metric(counts.enemies, "具有独立机制的敌人")}
${metric(counts.items, "逐件定义效果与视觉的装备")}
${metric("30", "自适应音乐套件")}
${metric("710", "需要被验证的 SFX 事件身份")}
PLAYER LOOP
玩家真正反复做什么
读懂问题 先看敌人的目标、倒计时、路线压力与奖励风险。所有必要信息在承诺前可见,随机只提出问题,不直接判玩家输。
改变方案 调整六人位置、职业、技能、装备、目标策略和 Standing Orders。每个选择都必须改变首个失败点或后续机会。
形成世界后果 战斗执行准备,奖励进入 Unsecured Cache;到 Anchor 决定安全与风险。每章修复一种公共服务,并改变下一轮路线。
REVIEW PATH
建议你怎么检查
1. 先看核心流程 从 Title 点到 Guild、编队、地图、路线、战斗、诊断、奖励、Anchor 与修复,判断每一步是否知道为什么要点。
2. 再看八章差异 检查每章是否具有不同的环境、学习目标、敌人问题、Boss 测试和永久世界变化,而不是换背景和数值。
3. 最后看完整内容 搜索 30 角色、36 职业、100 敌人、320 装备,确认它们都有玩法用途、反制关系和生产需求。
开始体验核心流程 →
`;
}
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 `
${escapeHtml(screen.id)}
目标 · 修复风车中继
Saved
Cache 180
`;
}
function renderRouteStage(screen) {
return `
${stageTop(screen)}
CH-01 · 风车盆地 ${escapeHtml(screen.name)} 路线提前显示威胁、奖励、Cache 风险与 Anchor 距离。
查看比较 选择风车高地 →
`;
}
function renderFormationStage(screen) {
const slots = ["Mara · Front", "Oren · Mid", "Tamsin · Rear", "Suri · Front", "Ilyo · Rear", "空位"];
return `
${stageTop(screen)}
WEAVE GRID ${escapeHtml(screen.name)} 点角色再点目标位置;拖动只是可选快捷方式。
${slots.map((slot, index) => `${escapeHtml(slot)} `).join("")}
首次受击预测 Mara 拦截中心线
Tamsin 暴露于 Gust
角色标签 4/6 不重复
预设 确认 Company →
`;
}
function renderBattleStage(screen) {
const directiveFlow = ["BAT-005", "BAT-006", "BAT-007"].includes(screen.id);
return `
${stageTop(screen)}
UNSECURED CACHE 180
${directiveFlow ? "RETUNE · LEGAL" : "RETUNE · 1"}
HOLD · 0
事件记录 1× · 2×
`;
}
function renderSettingsStage(screen) {
return `
SETTINGS 显示与大字 动效与闪烁 音频与字幕 控制方式 存档与云端
${escapeHtml(screen.name)}
减少动态
高对比目标轮廓
字幕与事件标签
低功耗模式
`;
}
function renderRecordsStage(screen, art) {
return `
${stageTop(screen)}
${escapeHtml(screen.name)} 图鉴同时展示身份、机制、反制、获取与完成状态。
已研究 · 机制完整
可用反制 · 2
章节关联 · CH-01
生产状态 · Concept
`;
}
function renderBootStage() {
return `
AETHERBOUND GUILD 裂脉远征团 让每一次修复,改变下一段远征。
正在校验远征档案 · 68%
完成后自动进入,无需点击
`;
}
function renderMissingStage(screen) {
const copy = reviewCopy(screen);
return `
UI COVERAGE GAP · ${escapeHtml(screen.id)}
${escapeHtml(screenName(screen))}尚未完成页面设计
目前只有页面需求,不能把这块内容当作游戏画面审核。
玩家首先需要看见 ${escapeHtml(copy.focal)}
页面必须支持 ${escapeHtml(copy.action || "无需操作")}
`;
}
function renderGameStage(screen) {
const prefix = screen.id.slice(0, 3);
const kind = layoutKind(screen);
const art = artForScreen(screen);
const light = prefix === "SET" || prefix === "REC";
let content;
if (kind === "dedicated") content = renderBootStage();
else if (kind === "family-formation") content = renderFormationStage(screen);
else if (kind === "family-route") content = renderRouteStage(screen);
else if (kind === "family-battle") content = renderBattleStage(screen);
else if (kind === "family-settings") content = renderSettingsStage(screen);
else if (kind === "family-records") content = renderRecordsStage(screen, art);
else content = renderMissingStage(screen);
const decorativeArt = art && prefix !== "REC" && kind !== "dedicated" && kind !== "missing" ? ` ` : "";
return `${decorativeArt}${content}
`;
}
function screenMeta(screen, index, screens) {
const copy = reviewCopy(screen);
const status = coverage(screen);
const previous = screens[(index - 1 + screens.length) % screens.length];
const next = screens[(index + 1) % screens.length];
return `
`;
}
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);
const coverageCounts = state.data.screens.reduce((result, entry) => {
result[coverage(entry).key] += 1;
return result;
}, { dedicated: 0, family: 0, missing: 0 });
return `
${viewHeading("UI COVERAGE AUDIT", "82 个页面需求,不是 82 张完成的 UI", "当前只有启动页完成逐页设计;部分页面仅有共用页面族示意,其余仍是规格缺口。这里用于识别要继续设计什么,不能作为完整 UI 验收。", `${chip(`${coverageCounts.dedicated} 逐页设计`)} ${chip(`${coverageCounts.family} 页面族示意`)} ${chip(`${coverageCounts.missing} 未设计`)}`)}
${filtered.map((entry) => { const status = coverage(entry); return `${escapeHtml(entry.id)} ${escapeHtml(screenName(entry))} ${escapeHtml(status.label)} `; }).join("")}
${renderGameStage(screen)}
${screenMeta(screen, index, filtered)}
`;
}
function renderFlow() {
const flowScreens = flowIds.map(getScreen);
const screen = flowScreens[state.flowIndex];
return `
${viewHeading("NAVIGATION SPECIFICATION", "从首页到章节修复的流程规格链", "这条链只证明 17 个需求节点和顺序已经定义;其中多个节点仍没有独立 UI,因此它不是可以进行理解或乐趣验收的产品原型。", `${chip(`${state.flowIndex + 1} / ${flowScreens.length}`)} ${chip("UI 未完成")}`)}
${renderGameStage(screen)}
${screenMeta(screen, state.flowIndex, flowScreens)}
${flowScreens.map((entry, index) => `${String(index + 1).padStart(2, "0")} ${escapeHtml(entry.id)} ${escapeHtml(screenName(entry))} `).join("")}
← 上一步 ${state.flowIndex === flowScreens.length - 1 ? "回到流程开头" : "执行主操作,进入下一步 →"}
`;
}
function renderChapters() {
const chapter = state.data.chapters[state.chapter];
const { id, name } = splitIdentity(chapter.identity);
return `
${viewHeading("8 CHAPTERS · 24 VISUAL FAMILIES", "每一章都提出新的策略问题", "每章都有独立的路线/聚落、代表战场和修复/抉择画面。概念图用于确认环境方向,完整细节以章节策划文档为准。")}
${state.data.chapters.map((entry, index) => `CH-${String(index + 1).padStart(2, "0")} `).join("")}
${id}A · 路线 / 聚落 ${escapeHtml(chapter.region)} · ${escapeHtml(chapter.route)}
${id}B · 代表战场 ${escapeHtml(chapter.learning)},Boss 通过真实机制检验这一章。
${id}C · 修复 / 抉择 ${escapeHtml(chapter.change)}
${escapeHtml(id)} ${escapeHtml(name)} 本章学习目标 ${escapeHtml(chapter.learning)}
Boss 测试与奖励 ${escapeHtml(chapter.boss)}
永久变化 ${escapeHtml(chapter.change)}
${chip("Concept art")} ${chip("Owner review open")}
`;
}
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 `
${viewHeading(config.eyebrow, config.title, config.description, `${chip(`${filtered.length} / ${entries.length}`)} ${chip("完整权威索引")}`)}
${shown.length ? shown.map((entry) => config.row(entry)).join("") : `
没有匹配内容。请清除搜索条件。
`}
`;
}
function catalogEntry(identity, meta, body) {
const { id, name } = splitIdentity(identity);
return `${escapeHtml(id)} ${escapeHtml(name)} ${escapeHtml(meta)}
${escapeHtml(body)}
`;
}
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 `
${viewHeading("48 ACHIEVEMENTS · 20 COLLECTIONS", "完成度记录玩家学会了什么", "成就验证真实机制、构筑和章节结果;收藏把区域证据变成 Guild 中可见的物理陈列。它们不靠重复刷次数延长游戏。")}
${metric(48, "机制与叙事成就")}${metric(20, "区域收藏")}${metric("100 h", "全收集与高难完成目标")}${metric("0", "每日任务、通行证与抽卡")}
成就 ${achievements.map((entry) => catalogEntry(entry.identity, "Achievement", `${entry.requirement} 奖励:${entry.reward}`)).join("")}
收藏 ${collections.map((entry) => catalogEntry(entry.identity, "Collection", `${entry.entries} 陈列:${entry.display}`)).join("")}
`;
}
function renderProduction() {
return `
${viewHeading("COMPLETE PRODUCTION INVENTORY", "美术、动画、音乐和声音怎么做", "完整数量已经拆成可生产的身份包、模块、动作、场景和事件。当前四张 AI 图都是概念方向,不是最终运行时资源。")}
${metric("30", "角色身份 rig + 6 表情")}${metric("72", "主动技能动作 / VFX 合同")}${metric("752+", "非 Boss 敌人动作状态")}${metric("88", "章节环境主状态")}${metric("101", "模块化剧情构图")}${metric("30", "音乐套件")}${metric("94", "环境声配置")}${metric("2,602", "双语最低角色录音资产")}
角色与职业 30 套稳定角色身份 + 36 套职业装备、资源装置和动作模块。职业改变人物的战斗轮廓,但不把角色替换成无名 class sprite。
敌人与装备 100 个敌人各自拥有机制轮廓、动作与预告;320 件物品都有独立物件图,160 件行为物品还必须显示真实触发。
环境与章节 八章共 88 个环境状态和至少 200 个独立道具/结构族。背景、交互物、前景、危险与修复层分开,不能用一张平图冒充地图。
音乐 30 套自适应音乐包含 base、pulse、tension 和 resolution;八章保留不同材料与节拍,最终章组合而不抹平差异。
语音 角色圣经定义 773 条战斗/Hub 语义额度和 144 个角色场景。英文与简中最低 2,602 个录音资产,语义语音严格控制并发。
SFX 与无障碍 710 个事件身份必须与真实提交状态一致。静音、字幕、单声道、输出切换、低功耗和振动关闭都不能改变玩法信息。
先做哪一块 正式批量生产前只做 CH-01 风车中继动态切片:Mara / Oren / Ilyo、Bell-Feeder、两名支援敌人、三条战线、Retune Directive、诊断、Reward → Cache → Anchor,以及 Clinic / Warning Tower 两个永久修复结果。
查看动态切片流程 →
`;
}
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 个页面需求已定义;1 页逐页设计、40 页页面族示意", "41 页没有独立 UI;其余页面族也必须逐页深化"],
["美术与动画", "完整清单", "Identity/modular production and CH-01 slice", "仅 4 张概念图;无 final/integrated/runtime"],
["音乐 / 环境 / 语音 / SFX", "完整清单", "30 music, 94 ambience, 2602 voice, 710 events", "未生成、未试听、未授权批量 SFX/music"],
["HTML 产品原型", "失败待修", "内容索引和响应式外壳可用", "Owner 已证明页面表达不可理解;当前只能用于缺口审计"],
["引擎开发", "冻结", "No Godot project or runtime code", "完整设计 Owner 批准后另开 Goal"],
["设备 / 打包 / 上架", "开放", "No build, signing, device or store action", "必须在运行时完成后分别授权"],
];
const blockedStatuses = new Set(["冻结", "开放", "未完成", "失败待修"]);
return `
${viewHeading("PRE-DEVELOPMENT GATE", "UI 设计没有完成,不能进入正式开发", "系统和内容文档已经建立,但 82 页 UI 只有 1 页逐页设计。Owner 已确认现有页面难以理解,因此完整产品原型验收失败。")}
领域 状态 已经具备 仍然开放 ${rows.map(([area, status, ready, open]) => `${escapeHtml(area)} ${escapeHtml(status)} ${escapeHtml(ready)} ${escapeHtml(open)} `).join("")}
下一步不是让你继续猜 先逐页完成标题、公会、编队、地图、路线、战斗、结算、奖励、锚点和修复等核心链 UI,再扩展其余页面。新的审核候选必须让页面目的、当前状态和唯一主操作直接可见。
查看 UI 缺口 →
`;
}
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 embeddedData = $("#prototype-data");
if (embeddedData) {
state.data = JSON.parse(embeddedData.textContent);
} else {
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 = `设计索引载入失败 ${escapeHtml(error.message)}
请通过本地 HTTP 服务或 Artifact Gallery 打开此页面。
`;
}
}
$("#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();