const state = { data: null, view: "firstplay", locale: "zh_CN", screenId: "SYS-005", screenGroup: "ALL", screenVariant: "primary", flowIndex: 1, regionIndex: 0, catalogType: "professions", catalogFilter: "all", catalogQuery: "", referenceIndex: 2, openingSelected: [], partySwapped: false, battlePaused: false, battleSpeed: 1, charterRegionIndex: 2, charterPacketIndex: 0, chronicleChoice: "chronicle", oathLaws: ["measured-supply"], settings: { contrast: false, reducedMotion: false, captions: true, lowPower: false, textScale: "100", }, }; const copy = { en: { brandSubtitle: "Final design prototype", navFirst: "First play", navScreens: "Screen atlas", navRegions: "Regions", navCatalogs: "Catalogs", navReference: "Reference map", navReview: "Review gates", runtimeFrozen: "Runtime development frozen", conceptBadge: "Concept direction", firstplay: "First play", screens: "Screen atlas", regions: "Regions", catalogs: "Catalogs", reference: "Reference map", review: "Review gates", prev: "Previous", next: "Next decision", back: "Back", inspect: "Inspect", compare: "Compare", concept: "Concept direction - interface authored in HTML", saved: "Saved", company: "Company", capacity: "Line capacity", protected: "Protected rear", exposed: "Exposed front", selected: "Selected", select: "Select", reviewCompany: "Review Company", createRun: "Create Run", openMarket: "Open First Market", noCoin: "No Coin cost", notRetained: "Not retained", loading: "Loading", retry: "Retry same state", returnSafe: "Return safely", }, zh_CN: { brandSubtitle: "最终设计原型", navFirst: "首次游玩", navScreens: "页面图谱", navRegions: "八大区域", navCatalogs: "完整图鉴", navReference: "参考映射", navReview: "审核闸门", runtimeFrozen: "运行时开发仍冻结", conceptBadge: "概念方向", firstplay: "首次游玩", screens: "页面图谱", regions: "八大区域", catalogs: "完整图鉴", reference: "参考映射", review: "审核闸门", prev: "上一步", next: "下一决策", back: "返回", inspect: "查看", compare: "比较", concept: "概念方向 - 界面由 HTML 独立绘制", saved: "已保存", company: "队伍", capacity: "出战容量", protected: "受保护后排", exposed: "暴露前排", selected: "已选择", select: "选择", reviewCompany: "审核队伍", createRun: "创建远征", openMarket: "进入首个集市", noCoin: "不消耗金币", notRetained: "不会保留", loading: "正在载入", retry: "重试同一状态", returnSafe: "安全返回", }, }; const screenNamesZh = { "SYS-001": "启动与校验", "SYS-002": "首次启动设置", "SYS-003": "标题", "SYS-004": "远征档案", "SYS-005": "新远征设置", "SYS-006": "继续前审核", "SYS-007": "存档恢复", "SYS-008": "制作与数据说明", "HUB-001": "公会桌面", "HUB-002": "威胁预报", "SHP-001": "集市", "SHP-002": "招募审核", "SHP-003": "装备报价审核", "REC-001": "成员名册", "REC-002": "成员详情", "REC-003": "遣散审核", "EQP-001": "装备工台", "EQP-002": "装备比较", "ART-001": "神器账册", "PTY-001": "队伍横列", "PTY-002": "出战准备审核", "RSK-001": "风险看板", "RSK-002": "风险承诺", "BAT-001": "战斗开场", "BAT-002": "自动战斗", "BAT-003": "角色与事件查看", "BAT-004": "暂停与速度", "BAT-005": "撤退审核", "BAT-006": "战斗恢复审核", "OUT-001": "战果与诊断", "OUT-002": "死亡与损失", "OUT-003": "奖励选择", "OUT-004": "恢复选择", "OUT-005": "远征结束总结", "OUT-006": "放弃远征审核", "PRO-001": "本轮成长", "PRO-002": "公会宪章", "PRO-003": "记录与图鉴", "PST-001": "首部编年史抉择", "PST-002": "誓约季审核", "PST-003": "誓约季结果", "SET-001": "设置", "SET-002": "显示与文字", "SET-003": "动效与闪烁", "SET-004": "音频与字幕", "SET-005": "控制方式", "SET-006": "语言与地区", "SET-007": "玩法信息", "SET-008": "存档与数据", "SET-009": "重置与删除", }; const zhText = { "OWNER REVIEW · FIRST 16 MINUTES": "玩家审核 · 首次16分钟", "Real screen state": "真实游戏页面", "50 CURRENT PAGE / STATE IDS": "50个游戏页面与状态", "Page-specific composition": "当前页面完整构图", "EIGHT CAMPAIGN REGIONS": "八大冒险区域", "CURRENT AUTHORED CATALOGS": "完整内容图鉴", "OWNER DECISION SURFACE": "玩家验收入口", "Design review only": "设计稿审核", "Human gate open": "等待玩家确认", "Windglass Verge": "风镜原野", "Coin": "金币", "Credit": "补员凭证", "PROVISIONAL COMPANY": "初始队伍候选", "Choose two generated recruits": "从四名候选中选择两名队员", "Offer set WIND-01-A remains stable until run creation.": "本组候选在创建远征前保持不变,可以放心比较后再选择。", "Choose two": "请选择两人", "Hearthguard": "护心卫士", "Farstring": "远弦射手", "Wayrunner": "逐风行者", "Kindhand": "仁心医者", "Narrow Guard": "窄阵护卫", "Patient Start": "耐心起手", "Open Stance": "开放步法", "Shared Cup": "共饮药杯", "Intercepts a neighbor / slower steps": "替相邻队员承受攻击,但行动速度较慢", "Rear pressure / delayed first basic": "擅长攻击敌方后排,但第一次普通攻击较晚", "Reorders safely / briefly Exposed": "可以安全调整站位,但会短暂暴露", "Restores a neighbor / weaker self-heal": "治疗相邻队员,但治疗自己时效果较弱", "Elian Rook": "Elian Rook(伊利安)", "Nia Orrow": "Nia Orrow(妮娅)", "Tomas Vale": "Tomas Vale(托马斯)", "Sable Kest": "Sable Kest(塞布尔)", "COMPANY REVIEW": "队伍确认", "RUN RECEIPT": "远征创建回执", "Creating run": "正在创建远征", "Operation RUN-CREATE-014": "操作编号 RUN-CREATE-014", "The two unchosen offers were not retained.": "未选择的两名候选不会保留。", "No Coin transaction": "本次不消耗金币", "Slot 01 · Standard": "存档01 · 标准难度", "Two unchosen stable IDs will not be retained.": "创建远征后,未选择的两名候选将离开。", "Revise Recruits": "重新选择队员", "NEW CONTRACT": "新契约", "Trade Route: First Crossing": "商路:首次穿越", "Contract 01 - chart the suspended pane crossing.": "契约01:勘察悬浮晶板之间的安全通路。", "Known pressure": "已知威胁", "Push + projectile screen": "击退与远程屏障", "Boss rule": "首领规则", "Rotating damage-family panes": "轮换不同伤害类型的晶板", "Reward breadth": "奖励范围", "Base professions + opening tools": "基础职业与开局装备", "Difficulty": "难度", "Standard - full systems": "标准:启用完整系统", "Load Provisional Offers": "查看初始队员候选", "FIRST MARKET": "首个集市", "Build capability before choosing risk": "先补齐队伍能力,再选择要承担的风险", "Recruits · 3": "队员 · 3", "Equipment · 4": "装备 · 4", "Risks · 3": "风险 · 3", "AFFORDABLE BASE OFFER": "当前可负担的基础队员", "Front hold": "前排承伤", "Needed": "当前需要", "Trait cost": "特性代价", "Visible": "已说明", "Review Recruit": "查看招募详情", "Open Party Line": "打开队伍站位", "Refresh 18 Coin · next 24": "刷新花费18金币 · 下次24金币", "Lock 0/2": "锁定报价 0/2", "RECRUIT REVIEW": "招募审核", "PARTY IMPACT": "对队伍的影响", "Generated identity, exactly one Trait, no authored personal arc.": "随机生成身份;每人只有一个机械特性,不包含角色关系或个人剧情。", "Adds a visible front intercept.": "增加一个明确的前排拦截能力。", "Adds position movement and target change.": "增加站位移动与目标切换能力。", "Recruit - 46 Coin": "花费46金币招募", "Price 46 Coin · Operation HIRE-001": "价格46金币 · 招募操作 HIRE-001", "Price 46 Coin · Operation HIRE-002": "价格46金币 · 招募操作 HIRE-002", "VIT": "生存", "PACE": "速度", "REACH": "射程", "GUARD": "护卫", "ELIGIBLE INVENTORY": "可装备物品", "BEHAVIOR DELTA": "装备后行为变化", "Stepback Soles": "退步靴", "Frontpin Greaves": "钉前护胫", "Rescue Loop": "救援环扣", "Open-Line Cord": "开阵系带", "Behavior class B": "行为型装备", "Farstring · position 3": "远弦射手 · 位置3(后排)", "Current: Quiet Rearplate": "当前装备:静默后甲", "Quiet Rearplate → Stepback Soles": "静默后甲 → 退步靴", "After first hit": "首次受击后", "Step rear": "向后移动一格", "Target relation": "受击位置变化", "Front → Center": "前排 → 中排", "Next basic": "下一次普通攻击", "Wait +1": "延后1拍", "At full rear the trigger is dormant. Apply remains reversible until readiness commit.": "位于最后排时此效果不会触发;确认出战准备前都可以撤销。", "Apply candidate": "应用候选装备", "ORDERED PARTY LINE": "队伍站位顺序", "FROZEN PARTY SNAPSHOT": "已确认的出战队伍", "Position 0 is always the rightmost front. The line is never mirrored by locale.": "最右侧的0号位永远是前排;切换语言不会改变队伍方向。", "Sable protects 0": "Sable保护0号位", "Nia reaches enemy rear": "Nia可以攻击敌方后排", "Rear": "后排", "Center": "中排", "Protected": "受保护", "Exposed": "暴露", "Plan accepted": "方案已确认", "Changed relations": "站位关系已改变", "Elian · first legal target": "Elian · 敌人首先可攻击的目标", "Nia · projectile screen risk": "Nia · 需要防范敌方远程攻击", "Reserve · 1 living recruit": "替补席 · 还有1名存活队员", "Inspect order": "查看站位", "Swap positions 1 / 2": "交换1号位和2号位", "Accept preparation": "确认出战准备", "Save Order": "保存站位", "THREE BOUNDED OFFERS": "三项明确的风险选择", "Choose pressure, not a hidden result": "选择要面对的威胁,而不是猜隐藏结果", "Steady Crossing": "稳健渡口", "Shattered Terrace": "破碎台地", "Bellward Detour": "钟卫绕行路", "Pane Nibbler + Gust Finch": "晶板啮兽 + 阵风雀", "Prism Ox + rear pressure": "棱镜牛 + 后排压力", "Unknown third identity": "第三名敌人身份未知", "Equipment": "装备", "Artifact": "神器", "Profession": "职业", "Low": "低", "Medium": "中", "High": "高", "exposure": "暴露风险", "Coverage": "当前应对能力", "Front hold ✓": "前排承伤 ✓", "Rear screen △": "后排防护 △", "Recovery 214 Coin": "可用恢复资金 214金币", "Review Steady Crossing": "审核“稳健渡口”", "RISK COMMITMENT": "风险确认", "Known threat": "已知威胁", "Projectile screen": "远程屏障", "Entry cost": "进入费用", "0 Coin": "0金币", "Reward class": "奖励类型", "Retreat": "撤退代价", "One Coin tier": "损失一个金币档位", "The accepted order is frozen with the risk receipt. Selection alone changed nothing.": "确认后,当前站位会和风险回执一起锁定;只选中风险不会立即改变游戏状态。", "Begin Battle": "开始战斗", "Return to Risk Board": "返回风险看板", "BATTLE OPENING": "战斗开场", "AUTOMATIC BATTLE": "自动战斗", "Break the rotating pane screen": "击破轮换的晶板屏障", "Player line faces right. Position 0 receives the first legal bite.": "我方朝右作战,最右侧0号位会首先承受可命中的攻击。", "NO COMBAT RESPAWN · NO RESERVE AUTO-ENTRY": "阵亡不返场 · 后备队不自动补位", "Urgent: Guard chip reaches Sable": "紧急:护卫削减即将波及Sable", "The second Pane Bite will remove Elian's Guard and chip the neighbor.": "第二次晶板啃咬会清空Elian的护卫,并伤害相邻队员。", "Pane Bite · 0.8s → Elian": "晶板啃咬 · 0.8秒后 → Elian", "Prism screen": "棱镜屏障", "Guard chip": "削减护卫", "Rear shot": "后排射击", "Start observation": "开始观察战斗", "Inspect objective": "查看战斗目标", "Resume": "继续", "Pause": "暂停", "Inspect event": "查看事件", "00:07.2 · Gust Finch targets Nia": "00:07.2 · 阵风雀锁定Nia", "00:08.0 · Elian intercepts Pane Bite": "00:08.0 · Elian拦截晶板啃咬", "00:08.8 · Sable gains Remedy": "00:08.8 · Sable获得治疗效果", "PINNED CAUSE": "锁定的战斗原因", "Elian intercepts Pane Bite": "Elian拦截晶板啃咬", "Source": "来源", "Target": "目标", "Order effect": "站位影响", "Next cause": "下一原因", "Neighbor protection": "保护相邻队员", "Guard reaches zero": "护卫值归零", "Pin and continue": "记录原因并继续", "Hearthguard · Guard 1": "护心卫士 · 护卫1", "Sable · position 2": "Sable · 2号位", "[Front recruit: Guard consumed] [Enemy forecast: rear projectile]": "[前排队员:护卫已消耗] [敌方预告:攻击后排]", "VICTORY": "胜利", "FIRST MEANINGFUL DIVERGENCE": "第一个关键分歧点", "Guard broke before the second rear shot": "第二次后排射击前,护卫提前崩溃", "Expected protection ended at 00:18.4; actual pressure reached Nia at 00:19.1.": "原本预计保护持续到00:18.4;实际在00:19.1时Nia已经受到攻击。", "EXPECTED": "预计过程", "ACTUAL": "实际过程", "00:17 Guard 1": "00:17 护卫1", "00:19 Rear shot blocked": "00:19 后排射击被挡", "00:22 Stable line": "00:22 队形稳定", "00:17 Pane chip": "00:17 晶板削减", "00:18 Guard 0": "00:18 护卫0", "00:19 Nia hit": "00:19 Nia受击", "Cause": "原因", "Second uninterrupted Pane Bite": "第二次晶板啃咬没有被打断", "Order": "站位", "Sable remained adjacent": "Sable仍与其相邻", "Adapt": "下次调整", "Move or interrupt before bite 2": "第二次啃咬前移动或打断", "Continue to Reward": "继续领取奖励", "Full event log": "完整战斗记录", "FIXED REWARD DRAFT": "固定奖励三选一", "Choose the next decision you want to change": "选择你希望下一步改变的战术决策", "Quiet Rearplate": "静默后甲", "Narrow Vanguard": "精简先锋", "Farstring insight": "远弦职业领悟", "Converts first area hit to direct": "将第一次范围伤害改为单体伤害", "One fewer deploy; front starts Guarded": "少上一名队员;前排开局获得护卫", "Opens a profession trial": "解锁一次职业试炼", "Selected": "已选择", "Compare": "比较", "Claim Quiet Rearplate": "领取静默后甲", "Compare build": "比较构筑", "COMMITTED LOSS": "已确定的永久损失", "FALLEN": "阵亡", "Farstring · Patient Start · Level 3 · survived 4 battles": "远弦射手 · 耐心起手 · 3级 · 曾存活4场战斗", "Returned to inventory": "退回仓库", "Party gap": "队伍缺口", "Rear pressure lost": "失去后排输出能力", "Replacement Credit": "补员凭证", "+1 tier": "+1档", "Death is permanent for this run. No relationship or personal-arc system is implied.": "本轮远征中阵亡不可逆;游戏不包含关系或个人剧情系统。", "Acknowledge loss": "确认损失", "Inspect receipt": "查看回执", "RECOVERY": "恢复方案", "Company remains viable": "队伍仍可继续远征", "Replacement Credit reduces only a later hire. Lost level, Trait, profession and history stay lost.": "补员凭证只能降低后续招募费用;失去的等级、特性、职业和战绩不会恢复。", "Protected base hire": "优惠招募基础队员", "46 → 28 Coin · consumes 1 Credit": "46 → 28金币 · 消耗1张补员凭证", "Return with a new base profession at Market.": "回到集市并招募一名新的基础职业队员。", "Continue short-handed": "缺员继续", "0 Coin · Company 3 / capacity 4": "0金币 · 当前3人 / 上限4人", "Keep resources and accept reduced coverage.": "保留资源,但接受战术覆盖不足。", "Abandon run": "放弃本轮远征", "Run-close transaction": "结束远征操作", "Retain history and committed progression only.": "只保留战绩和已经确认的永久进度。", "Commit protected hire": "确认优惠招募", "Review consequences": "查看后果", "Primary": "主要状态", "Loading / empty": "加载或空状态", "Error / confirm": "错误或确认状态", "Return": "中断返回", "Return to first Market": "返回首个集市", "Loading": "加载", "Error": "错误", "COMPOSED": "已构图", "Purpose": "页面目的", "Primary action": "主要操作", "Safe state": "中断后状态", "System": "系统", "Guild": "公会", "Market": "集市", "Roster": "成员", "Artifacts": "神器", "Party": "队伍", "Risk": "风险", "Battle": "战斗", "Outcome": "结算", "Progress": "成长", "Postgame": "终局", "Settings": "设置", "All 50": "全部50页", "All 42": "全部42个", "12 base": "12个基础职业", "24 advanced": "24个进阶职业", "6 hidden": "6个隐藏职业", "All 36": "全部36个", "All 320": "全部320件", "All 60": "全部60件", "All 100": "全部100种", "All 16": "全部16名", "Ordinary": "普通敌人", "Elite": "精英敌人", "Search ID, name or mechanic": "搜索编号、名称或规则", "No filler rows": "无占位内容", "Professions": "职业", "Traits": "特性", "Enemies": "敌人", "Bosses": "首领", "Regions": "区域", "Seven critical review groups": "七组关键体验审核", "Open group ›": "打开该组 ›", "Opening company": "开局队伍", "Market hire": "集市招募", "Formation and equip": "站位与装备", "Horizontal battle": "横向自动战斗", "Result and recovery": "结果与恢复", "Catalog and settings": "图鉴与设置", "Charter and final choice": "公会宪章与终章选择", "Four stable offers, exactly-two gating, revision, atomic receipt and first-Market handoff.": "四名固定候选、恰好选两人、原子回执与首个集市交接。", "Two separate affordable base hires move Company 2 → 3 → 4 while capacity remains 4.": "两次独立且可负担的基础招募让队伍从2人增至3人再增至4人,出战容量始终为4。", "Behavior delta and one rear-left to front-right line with exposed position 0.": "展示装备行为变化,以及从左后排到右前排、0号位暴露的单横列。", "Automatic execution, one ordered line, urgent cause, pause, speed, inspect and retreat review.": "自动执行、单横列、紧急原因、暂停、速度、查看与撤退审核。", "Earliest divergence precedes totals; loss, reward, recovery and abandonment remain distinct.": "先解释最早分歧,再显示汇总;损失、奖励、恢复与放弃彼此独立。", "Searchable current counts plus EN/zh_CN, 130%, contrast, captions, reduced motion and input states.": "可搜索完整数量,并检查中英文、130%文字、对比度、字幕、减少动效和输入状态。", "Spend Renown only for breadth, then compare finite closure with a new generated Oath Season run.": "声望只购买玩法广度,并比较有限结局与生成全新队伍的誓约季。", }; const screenPurposeZh = { SYS: "检查启动、设置、存档与远征创建是否清楚可靠。", HUB: "查看当前威胁、队伍准备情况、资源与下一处可去地点。", SHP: "比较队员或装备的费用、收益、代价以及对当前队伍的影响。", REC: "查看队员状态、职业、特性、装备、位置与存活记录。", EQP: "比较装备前后的具体行为变化,确认后才改变配置。", ART: "查看影响整轮远征的神器规则、冲突和持续时间。", PTY: "从左侧后排到右侧前排排列队伍,并确认保护、射程和承伤关系。", RSK: "比较已知威胁、奖励、费用、撤退代价和当前队伍覆盖能力。", BAT: "观察自动战斗的目标、因果、站位、事件顺序,并可暂停或查看详情。", OUT: "理解战斗结果、关键失败原因、损失、奖励与恢复选择。", PRO: "查看本轮成长、永久解锁以及新开放的策略选择。", PST: "查看终局挑战规则、结果与下一次可尝试的方案。", SET: "调整显示、文字、动效、声音、控制、语言和存档选项。", }; function localizedText(value) { const raw = String(value ?? ""); if (state.locale !== "zh_CN") return raw; const leading = raw.match(/^\s*/)?.[0] || ""; const trailing = raw.match(/\s*$/)?.[0] || ""; const core = raw.trim(); if (!core) return raw; if (zhText[core]) return `${leading}${zhText[core]}${trailing}`; return raw; } function localizeRenderedContent(root = document) { if (state.locale !== "zh_CN") return; const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); const nodes = []; while (walker.nextNode()) nodes.push(walker.currentNode); nodes.forEach((node) => { node.nodeValue = localizedText(node.nodeValue); }); $$('[placeholder], [title], [aria-label]', root).forEach((element) => { for (const attribute of ["placeholder", "title", "aria-label"]) { if (element.hasAttribute(attribute)) element.setAttribute(attribute, localizedText(element.getAttribute(attribute))); } }); } function screenPurpose(screen) { if (state.locale !== "zh_CN") return screen.purpose; return screenPurposeZh[screen.id.split("-")[0]] || `${screenName(screen)}页面的用途与主要决策。`; } function screenAction(screen) { if (state.locale !== "zh_CN") return screen.action; return `执行“${screenName(screen)}”页面中高亮的主要操作;确认前不会改变已保存状态。`; } function screenSafeState(screen) { if (state.locale !== "zh_CN") return screen.interruptionReturn; return "中断时保留最近一次已确认状态;返回后先复核,再继续执行。"; } const flowSteps = [ ["SYS-005", "contract", "Contract and difficulty", "契约与难度"], ["SYS-005", "offers0", "Four stable offers - 0/2", "四个固定候选 - 0/2"], ["SYS-005", "offers1", "Read one tradeoff - 1/2", "读懂一个取舍 - 1/2"], ["SYS-005", "offers2", "Choose exactly two - 2/2", "恰好选择两人 - 2/2"], ["SYS-005", "review", "Creation review", "创建前审核"], ["SYS-005", "pending", "Atomic creation", "原子化创建"], ["SYS-005", "receipt", "Run receipt", "远征回执"], ["SHP-001", "company2", "First Market - Company 2/4", "首个集市 - 队伍 2/4"], ["SHP-002", "hire1", "First hire review", "第一次招募审核"], ["SHP-001", "company3", "Company 3/4", "队伍 3/4"], ["SHP-002", "hire2", "Second hire review", "第二次招募审核"], ["SHP-001", "company4", "Capacity filled - 4/4", "容量填满 - 4/4"], ["PTY-001", "reorder", "Order the horizontal line", "调整横向队列"], ["RSK-001", "compare", "Compare three risks", "比较三种风险"], ["RSK-002", "commit", "Commit party and exposure", "确认队伍与风险"], ["BAT-001", "opening", "Battle opening", "战斗开场"], ["BAT-002", "live", "Automatic resolution", "自动执行"], ["BAT-003", "inspect", "Inspect first cause", "查看首个因果"], ["OUT-001", "diagnosis", "Read the divergence", "阅读分歧点"], ["OUT-003", "reward", "Choose a fixed reward", "选择固定奖励"], ["EQP-002", "adapt", "Compare one adaptation", "比较一次调整"], ["OUT-002", "casualty", "Understand permanent loss", "理解永久损失"], ["OUT-004", "recovery", "Purchase recovery", "购买恢复方案"], ]; const screenGroups = { ALL: "All 50", SYS: "System", HUB: "Guild", SHP: "Market", REC: "Roster", EQP: "Equipment", ART: "Artifacts", PTY: "Party", RSK: "Risk", BAT: "Battle", OUT: "Outcome", PRO: "Progress", PST: "Postgame", SET: "Settings", }; const sceneImages = [ "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 generatedOffers = [ { id: "OFF-01", name: "Elian Rook", profession: "Hearthguard", trait: "Narrow Guard", read: "Intercepts a neighbor / slower steps", mark: "HG" }, { id: "OFF-02", name: "Nia Orrow", profession: "Farstring", trait: "Patient Start", read: "Rear pressure / delayed first basic", mark: "FS" }, { id: "OFF-03", name: "Tomas Vale", profession: "Wayrunner", trait: "Open Stance", read: "Reorders safely / briefly Exposed", mark: "WR" }, { id: "OFF-04", name: "Sable Kest", profession: "Kindhand", trait: "Shared Cup", read: "Restores a neighbor / weaker self-heal", mark: "KH" }, ]; const stageDetails = { "SYS-001": ["Verifying local rules", "Content revision 69f671", "Save lineage healthy"], "SYS-002": ["Language: English", "Text: 115%", "Captions: On"], "SYS-003": ["Continue safely", "New Run", "Records"], "SYS-004": ["Slot 01 - Market, Round 3", "Slot 02 - No run", "Local save verified"], "SYS-006": ["Last receipt: equipment return", "Next: Recovery", "Snapshot: 00:42.6"], "SYS-007": ["Local lineage - healthy", "Backup - 12 min older", "Conflict - review required"], "SYS-008": ["Original design and media", "Offline core play", "No third-party commercial assets"], "HUB-001": ["Next pressure: projectile screen", "Company ready 4/4", "Last receipt: reward claimed"], "HUB-002": ["Pane Nibbler - Guard pressure", "Gust Finch - rear projectile", "Confidence: exact"], "SHP-003": ["Stepback Soles", "After first direct hit: step rear", "Tradeoff: next basic waits"], "REC-001": ["Deployed 4", "Reserve 1", "Cumulative hires 6"], "REC-002": ["Nia Orrow - Farstring", "Trait: Patient Start", "Survived 4 battles"], "REC-003": ["Equipment returns", "No Replacement Credit", "Leaves 3 living recruits"], "ART-001": ["Open Ledger - active", "Narrow Vanguard - conflict", "2 shared rules inspected"], "PTY-002": ["Order frozen: 3 > 2 > 1 > 0", "Rear protected", "Known pressure covered"], "RSK-002": ["Threat: projectile screen", "Reward: equipment choice", "Retreat: one Coin tier"], "BAT-004": ["Paused at 00:08.4", "Speed 1x / 2x / 4x", "Event log available"], "BAT-005": ["Retreat resolves after current event", "No fallen recruits", "Return to Market"], "BAT-006": ["Verified time 00:08.4", "Next event: Pane Bite", "Resume static at 1x"], "OUT-005": ["Contract clear", "Renown +4", "Discoveries 7"], "OUT-006": ["Closes this run", "Coin and Credit cleared", "History retained"], "PRO-001": ["Capacity milestone 4 > 5", "Hearthguard branch visible", "One choice pending"], "PRO-002": ["Renown 34", "Eight Region seals", "Breadth only - no permanent power"], "PST-001": ["Final boss settled", "Company archives on both choices", "Ordinary New Run remains"], "PST-002": ["Optional Oath laws", "New generated Company", "Ordinary New Run remains"], "PST-003": ["Oath challenge cleared", "Rules that mattered: 2", "Record and Renown receipts applied"], }; const guildCharterRegions = [ ["RG-01", "Windglass Verge", "风镜原野"], ["RG-02", "Ember Archive", "余烬典库"], ["RG-03", "Rainbell Reach", "雨钟长境"], ["RG-04", "Violet Causeway", "紫辉堤道"], ["RG-05", "Hollow Meridian", "空鸣子午线"], ["RG-06", "Sunken Loom", "沉没织庭"], ["RG-07", "Starwake Crown", "星醒王冠"], ["RG-08", "Aetherbound Spire", "以太盟誓塔"], ]; const charterPackets = [ { id: "CH-R3-CATALOG", cost: 8, family: ["Catalog packet", "图鉴扩展包"], title: ["Rainline Implements", "雨线器具"], effect: ["Adds six sidegrade equipment rows to eligible future Markets.", "向未来符合条件的集市加入六件侧向选择装备。"], prerequisite: ["Region 03 first Contract clear", "完成第三区域首个契约"] }, { id: "CH-R3-THREAT", cost: 10, family: ["Threat archive", "威胁档案"], title: ["Pressure Forecast II", "压力预报 II"], effect: ["Shows displacement order for encountered Rainbell enemy families.", "显示已遭遇雨钟敌人家族的位移顺序。"], prerequisite: ["Inspect three Rainbell enemy families", "查看三类雨钟敌人"] }, { id: "CH-R3-OATH", cost: 16, family: ["Oath law", "誓约法则"], title: ["Measured Supply", "定量补给"], effect: ["Adds one optional postgame Market law with its completion Record.", "加入一条可选终局集市法则及其完成记录。"], prerequisite: ["Complete both Region 03 Contracts", "完成第三区域的两个契约"] }, ]; const oathLawCatalog = [ { id: "measured-supply", title: ["Measured Supply", "定量补给"], effect: ["Markets show fewer offers; each lock costs less.", "集市报价更少,但每次锁定费用降低。"], status: ["Eligible", "可用"] }, { id: "broken-front", title: ["Broken Front", "破阵前线"], effect: ["Front position begins Exposed; rear control rewards increase.", "前排开局处于暴露状态,后排控制奖励提高。"], status: ["Eligible", "可用"] }, { id: "named-pressure", title: ["Named Pressure", "具名压力"], effect: ["One elite family is revealed before each risk choice.", "每次风险选择前揭示一个精英敌人家族。"], status: ["Charter clue 1/1", "宪章线索 1/1"] }, ]; const referenceMappings = [ { id: "REF-01", title: ["Title and run entry", "标题与远征入口"], evidence: "PROVEN", target: "SYS-003", variant: "primary", template: "entry", zones: [["top", "Save / version", "存档与版本"], ["main", "New / Continue", "新远征 / 继续"], ["side", "Persistent systems", "永久系统入口"], ["actions", "One clear start", "一个明确开始操作"]], options: ["Continue · New Run · Records · Settings", "继续 · 新远征 · 记录 · 设置"], adopt: ["One first-viewport run decision with save health.", "首屏只突出一次远征决策,并显示存档健康状态。"], change: ["Original guild-threshold composition and hierarchy.", "使用原创公会门厅构图与层级。"], reject: ["Reference title ornament, logo and wording.", "参考游戏的标题装饰、标志和文案。"] }, { id: "REF-02", title: ["Difficulty and run setup", "难度与远征设置"], evidence: "PROVEN", target: "SYS-005", variant: "contract", template: "choice", zones: [["top", "Run consequences", "远征后果"], ["main", "Few large choices", "少量大选项"], ["lower", "Opening setup", "开局设置"], ["actions", "Review then commit", "审核后确认"]], options: ["Difficulty · slot · opening offers", "难度 · 存档位 · 初始候选"], adopt: ["Failure rules and opening state are visible before commit.", "确认前显示失败规则和开局状态。"], change: ["Four stable generated Recruit offers follow Contract review.", "契约审核后提供四名固定生成队员候选。"], reject: ["Copied clan identities or difficulty prose.", "照搬氏族身份或难度文案。"] }, { id: "REF-03", title: ["Integrated Market", "队伍与库存同屏集市"], evidence: "PROVEN", target: "SHP-001", variant: "company4", template: "market", zones: [["top", "Company bench", "队伍席位"], ["main", "Three offer cards", "三张报价卡"], ["lower", "Run inventory", "本轮库存"], ["side", "Shop modes", "商店模式"], ["actions", "Refresh / lock / next", "刷新 / 锁定 / 下一步"]], options: ["Recruit · inspect · buy · refresh · lock · sell", "招募 · 查看 · 购买 · 刷新 · 锁定 · 出售"], adopt: ["Company, offers, inventory and resources stay visible together.", "队伍、报价、库存与资源保持同屏。"], change: ["Generated-Recruit role/Tradeoff and Coin/Credit economy.", "采用生成队员的职业/代价信息与金币/补员凭证经济。"], reject: ["Copied frames, symbols, exact geometry and price values.", "照搬边框、符号、精确几何与价格数值。"] }, { id: "REF-04", title: ["Recruit inspect and buy", "队员查看与招募"], evidence: "PROVEN", target: "SHP-002", variant: "hire1", template: "inspect", zones: [["top", "Offer identity", "候选身份"], ["main", "Role and stats", "职责与属性"], ["lower", "Company impact", "对队伍的影响"], ["actions", "Inspect / buy", "查看 / 招募"]], options: ["Profession · Trait · price · fit", "职业 · 特性 · 价格 · 适配度"], adopt: ["Inspect precedes purchase and shows Company delta.", "购买前先查看,并显示队伍变化。"], change: ["Exactly one visible Trait with benefit and cost.", "每名队员只有一个明确收益与代价的特性。"], reject: ["Fixed commercial identity or relationship content.", "固定商业角色身份或关系内容。"] }, { id: "REF-05", title: ["Inventory, equipment and sell", "库存、装备与出售"], evidence: "PROVEN", target: "EQP-002", variant: "primary", template: "inventory", zones: [["top", "Current wearer", "当前装备者"], ["main", "Slot grid", "物品槽位网格"], ["lower", "Behavior delta", "行为变化"], ["side", "Selling mode", "出售模式"], ["actions", "Apply / cancel", "应用 / 取消"]], options: ["Inspect · equip · compare · sell review", "查看 · 装备 · 比较 · 出售审核"], adopt: ["Persistent capacity and focused tooltip reduce navigation.", "持续显示容量与聚焦详情,减少页面跳转。"], change: ["Equipment comparisons lead with behavior, target and tradeoff.", "装备比较优先显示行为、目标与代价变化。"], reject: ["Copied rarity palette, items or sell formula.", "照搬稀有度配色、物品或出售公式。"] }, { id: "REF-06", title: ["Ordered Company line", "横向队伍顺序"], evidence: "PROVEN", target: "PTY-001", variant: "primary", template: "line", zones: [["top", "Stable positions", "固定位置"], ["main", "One horizontal line", "一条横向队列"], ["lower", "Reserve / equipment", "替补 / 装备"], ["actions", "Tap then destination", "点击后再点目标位"]], options: ["Select · inspect · swap · undo", "选择 · 查看 · 换位 · 撤销"], adopt: ["Formation is a direct, repeated build decision.", "站位是直接且反复发生的构筑决策。"], change: ["Protected rear is always left and exposed front right.", "受保护后排始终在左,暴露前排始终在右。"], reject: ["Locale mirroring or mandatory precision drag.", "语言镜像或必须精准拖动。"] }, { id: "REF-07", title: ["Three-card encounter choice", "三卡遭遇选择"], evidence: "PROVEN", target: "RSK-001", variant: "primary", template: "choice", zones: [["top", "Current Company", "当前队伍"], ["main", "Three encounters", "三个遭遇"], ["lower", "Threat / reward compare", "威胁 / 奖励比较"], ["actions", "Review chosen risk", "审核所选风险"]], options: ["Enemies · reward · exposure · retreat", "敌人 · 奖励 · 暴露风险 · 撤退"], adopt: ["All three options remain adjacent and comparable.", "三个选项始终相邻并可比较。"], change: ["Current coverage and retreat consequence are explicit.", "明确显示当前应对能力和撤退后果。"], reject: ["Copied card backs, enemy names or bounty formula.", "照搬卡背、敌人名称或赏金公式。"] }, { id: "REF-08", title: ["Automatic horizontal battle", "横向自动战斗"], evidence: "PROVEN", target: "BAT-002", variant: "primary", template: "battle", zones: [["top", "Objective / counts", "目标 / 数量"], ["main", "Company vs enemies", "队伍对抗敌人"], ["lower", "Unit state panels", "单位状态条"], ["side", "Observation tools", "观察工具"], ["actions", "Pause / speed / inspect", "暂停 / 速度 / 查看"]], options: ["Automatic actions · forecast · event log", "自动行动 · 预告 · 事件记录"], adopt: ["Battle is the central visual payoff of preparation.", "战斗是前期准备的核心视觉回报。"], change: ["Causal forecast and permanent-casualty rule stay readable.", "保持因果预告和永久阵亡规则可读。"], reject: ["Manual attack or ultimate buttons and copied HUD art.", "手动攻击/终极技能按钮及照搬HUD美术。"] }, { id: "REF-09", title: ["Battle observation controls", "战斗观察控制"], evidence: "PROVEN", target: "BAT-003", variant: "primary", template: "battle", zones: [["top", "Urgent state", "紧急状态"], ["main", "Battle remains visible", "战斗保持可见"], ["side", "Inspect panel", "查看面板"], ["actions", "Pause / 1x / 2x / 4x / retreat", "暂停 / 1x / 2x / 4x / 撤退"]], options: ["Inspect cause · pin event · resume", "查看原因 · 固定事件 · 继续"], adopt: ["Observation controls never replace the arena.", "观察控制不会遮蔽战斗主体。"], change: ["Pinned causal event explains target and order effect.", "固定因果事件解释目标与站位影响。"], reject: ["Hidden targeting or pointer-only shortcuts.", "隐藏目标选择或仅鼠标可用的快捷操作。"] }, { id: "REF-10", title: ["Victory report and reward", "胜利报告与奖励"], evidence: "PROVEN", target: "OUT-001", variant: "primary", template: "report", zones: [["top", "Outcome", "结果"], ["main", "Unit result rows", "单位结果行"], ["lower", "Reward / next decision", "奖励 / 下一决策"], ["actions", "Continue to build", "继续构筑"]], options: ["Diagnosis · losses · reward · next Market", "诊断 · 损失 · 奖励 · 下一集市"], adopt: ["Result immediately points to the next build decision.", "结果立即指向下一次构筑决策。"], change: ["First meaningful divergence appears before totals.", "汇总前先显示第一个关键分歧。"], reject: ["Copied report ornament, reward identities or formulas.", "照搬报告装饰、奖励身份或公式。"] }, { id: "REF-11", title: ["Game Over and persistent handoff", "失败结算与永久成长交接"], evidence: "PROVEN", target: "OUT-005", variant: "primary", template: "terminal", zones: [["top", "Terminal reason", "终止原因"], ["main", "Company result", "队伍结果"], ["lower", "Persistent award", "永久奖励"], ["actions", "Progression / title / new run", "成长 / 标题 / 新远征"]], options: ["Finish · inspect Records · spend Renown", "结束 · 查看记录 · 使用声望"], adopt: ["Failure still advances knowledge and breadth.", "失败仍推进知识与玩法广度。"], change: ["Closure receipt separates retained and cleared state.", "结束回执分开说明保留与清除内容。"], reject: ["Mandatory victory farming or inferred reward formula.", "强制刷胜场或推测奖励公式。"] }, { id: "REF-12", title: ["Persistent path spending", "永久路径消费"], evidence: "PROVEN", target: "PRO-002", variant: "primary", template: "graph", zones: [["top", "Persistent balance", "永久余额"], ["main", "Prerequisite paths", "前置路径"], ["lower", "Node preview", "节点预览"], ["actions", "Inspect cost / claim", "查看费用 / 领取"]], options: ["Path · prerequisite · cost · future effect", "路径 · 前置条件 · 费用 · 未来效果"], adopt: ["Spending visibly changes a persistent planning surface.", "消费会在永久规划界面产生可见变化。"], change: ["Eight horizontal Region seals and breadth-only packets.", "改为八个横向区域印章与仅扩展广度的内容包。"], reject: ["Radial graph, copied nodes, raw power and unknown refunds.", "放射图、照搬节点、永久数值和未知退款机制。"] }, { id: "REF-13", title: ["Forge, storage and veteran operation", "锻造、仓储与资深操作"], evidence: "LOWER_BOUND", target: "EQP-001", variant: "primary", template: "operation", zones: [["top", "Owned inventory", "已拥有库存"], ["main", "Recipe / target review", "配方 / 目标审核"], ["lower", "Ingredients and result", "材料与结果"], ["actions", "Confirm operation", "确认操作"]], options: ["Source items · target · conflict · receipt", "来源物品 · 目标 · 冲突 · 回执"], adopt: ["Transactions keep source, target and result visible.", "事务始终显示来源、目标和结果。"], change: ["Equipment Workbench uses deterministic behavior deltas.", "装备工台使用确定性的行为变化。"], reject: ["New crafting economy, copied recipes or probabilities.", "新增锻造经济、照搬配方或概率。"] }, { id: "REF-14", title: ["Tavern retirement surface", "酒馆退休界面"], evidence: "PROVEN_UI_ONLY", target: "PRO-003", variant: "primary", template: "operation", zones: [["top", "Between-run resource", "局外资源"], ["main", "Veteran slots", "资深成员槽位"], ["lower", "Retirement explanation", "退休说明"], ["actions", "Review irreversible action", "审核不可逆操作"]], options: ["Inspect veteran · preview value · review", "查看资深成员 · 预览价值 · 审核"], adopt: ["A completed Company needs a readable historical home.", "完成远征的队伍需要可读的历史归宿。"], change: ["Archive generated Company into Records only.", "仅把生成队伍存入历史记录。"], reject: ["Unproven XP conversion, equipment disposal or save behavior.", "未证明的经验转换、装备处置或存档行为。"] }, { id: "REF-15", title: ["Raid, final boss and final choice", "大型首领、最终战与终章选择"], evidence: "PROVEN", target: "PST-001", variant: "primary", template: "terminal", zones: [["top", "Boss preview", "首领预览"], ["main", "Large automatic fight", "大型自动战斗"], ["lower", "Victory report", "胜利报告"], ["actions", "Finish / continue", "完成 / 继续"]], options: ["Boss rules · reward · completion · postgame", "首领规则 · 奖励 · 完成 · 终局后玩法"], adopt: ["Final victory leads to two explicit motivations.", "最终胜利后提供两个明确动机。"], change: ["Both paths archive Company; Oath Season generates a new run.", "两个路径都会存档队伍;誓约季生成全新远征。"], reject: ["Copied Raid page, boss, reward or endless current Company.", "照搬大型首领页、首领、奖励或无限延续当前队伍。"] }, ]; const settingRows = { "SET-001": [["Display and text", "115%"], ["Motion and power", "Reduced"], ["Audio and captions", "Captions on"], ["Controls", "Touch + keyboard"]], "SET-002": [["UI and text scale", "100 / 115 / 130"], ["Safe area", "Reference"], ["High contrast", "Off"], ["Color-independent cues", "On"]], "SET-003": [["Reduced motion", "On"], ["Reduced flashes", "On"], ["Camera shake", "25%"], ["Low-power mode", "Off"]], "SET-004": [["Master", "80%"], ["Music", "65%"], ["Effects", "80%"], ["Captions", "Critical + full log"]], "SET-005": [["Inspect", "I / South"], ["Pause", "Space / Menu"], ["Reorder", "Select then destination"], ["Hold timing", "600 ms"]], "SET-006": [["Language", "English / 简体中文"], ["Numbers", "Locale format"], ["Text preview", "130%"], ["Reload", "No run change"]], "SET-007": [["Forecast detail", "Full"], ["Tutorial reminders", "On"], ["Battle log density", "Detailed"], ["Default speed", "1x"]], "SET-008": [["Local save", "Healthy"], ["Backup", "12 min ago"], ["Offline", "Core play available"], ["Diagnostics", "Export ready"]], "SET-009": [["Delete current run", "Review"], ["Reset settings", "Review"], ["Reset profile", "Locked"], ["Diagnostics", "Clear"]], }; const $ = (selector, root = document) => root.querySelector(selector); const $$ = (selector, root = document) => [...root.querySelectorAll(selector)]; const t = (key) => copy[state.locale][key] || copy.en[key] || key; const bi = (en, zh) => state.locale === "zh_CN" ? zh : en; function escapeHtml(value = "") { return String(value) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function truncate(value, length = 150) { const text = String(value || ""); return text.length > length ? `${text.slice(0, length - 1)}...` : text; } function screenName(screen) { return state.locale === "zh_CN" ? screenNamesZh[screen.id] || screen.name : screen.name; } function getScreen(id) { return state.data.screens.find((screen) => screen.id === id); } function flowLabel(step) { return step[state.locale === "zh_CN" ? 3 : 2]; } function iconButton(symbol, label, attrs = "") { return ``; } function stageArt(index = 0) { return ``; } function conceptLabel() { return `${escapeHtml(t("concept"))}`; } function stageHeader(screen, context = "Windglass Verge", resources = true) { return `
${iconButton("‹", t("back"))}${escapeHtml(screen.id)}${escapeHtml(localizedText(context))}
${resources ? `214 Coin0 Credit` : ""}◆ ${escapeHtml(t("saved"))}
`; } function actionDock(primary, secondary = t("inspect"), attrs = "") { return `
`; } function frame(screen, body, options = {}) { const classes = ["game-stage", `family-${screen.id.split("-")[0].toLowerCase()}`, options.className || ""].filter(Boolean).join(" "); const art = options.art === false ? "" : stageArt(options.region || 0); const header = options.header === false ? "" : stageHeader(screen, options.context, options.resources !== false); return `
${art}
${header}
${body}
${conceptLabel()}
`; } function renderOpening(screen, variant = "offers2") { const oneSelected = state.openingSelected.length === 1 ? state.openingSelected : ["OFF-02"]; const twoSelected = state.openingSelected.length === 2 ? state.openingSelected : ["OFF-02", "OFF-04"]; const selectionByVariant = { offers0: state.openingSelected, offers1: oneSelected, offers2: twoSelected, review: twoSelected, pending: twoSelected, receipt: twoSelected }; const selected = selectionByVariant[variant] || state.openingSelected; if (variant === "contract") { const body = `
RG-01

Windglass Verge

Contract 01 - chart the suspended pane crossing.

NEW CONTRACT

Trade Route: First Crossing

Known pressure
Push + projectile screen
Boss rule
Rotating damage-family panes
Reward breadth
Base professions + opening tools
Difficulty
Standard - full systems
`; return frame(screen, body, { variant, resources: false }); } if (variant === "review" || variant === "pending" || variant === "receipt") { const chosen = generatedOffers.filter((offer) => selected.includes(offer.id)); const unchosen = generatedOffers.filter((offer) => !selected.includes(offer.id)); const stateBlock = variant === "pending" ? `
Creating runOperation RUN-CREATE-014
` : variant === "receipt" ? `
✓ RUN-CREATE-014${t("company")} 2 / ${t("capacity")} 4The two unchosen offers were not retained.
` : `
No Coin transactionSlot 01 · StandardTwo unchosen stable IDs will not be retained.
`; const body = `
${variant === "receipt" ? "RUN RECEIPT" : "COMPANY REVIEW"}
${chosen.map((offer) => recruitMarker(offer, true)).join("")}
${unchosen.map((offer) => `${offer.id} ${offer.name}${t("notRetained")}`).join("")}
${stateBlock}
${variant !== "pending" ? `` : ""}
`; return frame(screen, body, { variant, resources: false }); } const body = `
PROVISIONAL COMPANY

Choose two generated recruits

Offer set WIND-01-A remains stable until run creation.

${generatedOffers.map((offer) => recruitOffer(offer, selected.includes(offer.id))).join("")}
`; return frame(screen, body, { variant, resources: false }); } function recruitMarker(offer, selected = false) { return `
${offer.mark}
${offer.id}${localizedText(offer.name)}${localizedText(offer.profession)}
`; } function recruitOffer(offer, selected) { return `
${offer.mark}${offer.id}
${localizedText(offer.name)}${localizedText(offer.profession)}

${localizedText(offer.trait)} - ${localizedText(offer.read)}

${t("noCoin")}${iconButton("i", `${t("inspect")} ${offer.name}`)}
`; } function companyCountForVariant(variant) { if (variant === "company2") return 2; if (variant === "company3") return 3; return 4; } function renderMarket(screen, variant) { const count = companyCountForVariant(variant); const isRecruit = screen.id === "SHP-002"; const isItem = screen.id === "SHP-003"; if (isRecruit) { const offer = variant === "hire2" ? generatedOffers[2] : generatedOffers[0]; const body = `
${recruitMarker(offer, true)}⬡
RECRUIT REVIEW

${localizedText(offer.name)}

${localizedText(offer.profession)} · ${localizedText(offer.trait)}

VIT APACE CREACH ${offer.profession === "Hearthguard" ? "1" : "2"}GUARD B

${localizedText(offer.read)}。${state.locale === "zh_CN" ? "随机生成身份;每人只有一个机械特性,不包含角色关系或个人剧情。" : "Generated identity, exactly one Trait, no authored personal arc."}

PARTY IMPACT${t("company")} ${count} → ${Math.min(4, count + 1)}

${offer.profession === "Hearthguard" ? "Adds a visible front intercept." : "Adds position movement and target change."}

Price 46 Coin · Operation HIRE-${variant === "hire2" ? "002" : "001"}
${actionDock("Recruit - 46 Coin", t("compare"), "data-flow-next")}
`; return frame(screen, body, { variant }); } if (isItem) { const body = `
⇣EQ-001
ITEM OFFER

Stepback Soles

After the first direct hit, step one position rearward if legal.

Target exposureFront → Center
Next basicWait +1 tick
${actionDock("Buy - 32 Coin", t("inspect"))}
`; return frame(screen, body, { variant }); } const offers = [generatedOffers[0], generatedOffers[2], generatedOffers[3]]; const company = generatedOffers.slice(0, count); const inventory = [["EQ-001", "⇣"], ["EQ-014", "▰"], ["AR-011", "◇"], ["EQ-028", "⌁"]]; const body = `
${bi("INTEGRATED MARKET", "一体化集市")}

${bi("Company, offers and inventory stay visible", "队伍、报价与库存保持同屏")}

${t("company")}${count}/${t("capacity")}4
${bi("COMPANY BENCH", "队伍席位")}${bi("Tap Recruit to inspect or reorder", "点击队员查看或调整顺序")}
${company.map((offer, index) => ``).join("")}${Array.from({ length: 4 - count }, (_, index) => ``).join("")}
${offers.map((offer, index) => ``).join("")}
${bi("RUN INVENTORY", "本轮库存")}4 / 12 · ${bi("Tap item to compare", "点击物品进行比较")}
${Array.from({ length: 12 }, (_, index) => { const item = inventory[index]; return ``; }).join("")}
`; return frame(screen, body, { variant }); } function partyActors() { const original = [ ["3", "Nia", "FS", "Rear"], ["2", "Sable", "KH", "Protected"], ["1", "Tomas", "WR", "Center"], ["0", "Elian", "HG", "Exposed"], ]; const swapped = [original[0], original[2], original[1], original[3]]; return (state.partySwapped ? swapped : original).map(([position, name, mark, status], index) => ``).join(""); } function renderPreparation(screen, variant) { if (screen.id.startsWith("EQP")) { const comparison = screen.id === "EQP-002"; const body = `
ELIGIBLE INVENTORY${["Stepback Soles", "Frontpin Greaves", "Rescue Loop", "Open-Line Cord"].map((name, index) => ``).join("")}
FSNia OrrowFarstring · position 3
Current: Quiet Rearplate
BEHAVIOR DELTA

${comparison ? "Quiet Rearplate → Stepback Soles" : "Stepback Soles"}

After first hitStep rear
Target relationFront → Center
Next basicWait +1

At full rear the trigger is dormant. Apply remains reversible until readiness commit.

`; return frame(screen, body, { variant }); } const ready = screen.id === "PTY-002"; const body = `
${ready ? "FROZEN PARTY SNAPSHOT" : "ORDERED PARTY LINE"}

${t("protected")} → ${t("exposed")}

Position 0 is always the rightmost front. The line is never mirrored by locale.

Sable protects 0Nia reaches enemy rear
${t("protected")}${partyActors()}${t("exposed")}
${iconButton("↶", "Undo")}${iconButton("↺", "Reset")}
`; return frame(screen, body, { variant }); } function renderRisk(screen, variant) { const review = screen.id === "RSK-002"; const risks = [ ["Steady Crossing", "Pane Nibbler + Gust Finch", "Equipment", "Low"], ["Shattered Terrace", "Prism Ox + rear pressure", "Artifact", "Medium"], ["Bellward Detour", "Unknown third identity", "Profession", "High"], ]; const body = review ? `
RISK COMMITMENT

Steady Crossing

Known threatProjectile screenEntry cost0 CoinReward classEquipmentRetreatOne Coin tier
${partyActors()}

The accepted order is frozen with the risk receipt. Selection alone changed nothing.

${actionDock("Begin Battle", "Return to Risk Board", "data-flow-next")}
` : `
THREE BOUNDED OFFERS

Choose pressure, not a hidden result

${risks.map((risk, index) => ``).join("")}
CoverageFront hold ✓Rear screen △Recovery 214 Coin
`; return frame(screen, body, { variant }); } function battleLine() { return `
${t("protected")}${partyActors()}${t("exposed")}
Pane Bite · 0.8s → Elian
OXPrism screenPNGuard chipGFRear shot
`; } function renderBattle(screen, variant) { const id = screen.id; if (["BAT-004", "BAT-005", "BAT-006"].includes(id)) { const title = id === "BAT-004" ? "Observation paused" : id === "BAT-005" ? "Retreat consequence" : "Resume verified snapshot"; const primary = id === "BAT-004" ? "Resume battle" : id === "BAT-005" ? "Begin retreat" : "Resume at 1x"; const rows = stageDetails[id]; const body = `
${battleLine()}
${id}

${title}

${rows.map((row) => `
${row}
`).join("")}
${actionDock(primary, id === "BAT-005" ? "Cancel" : "Event log")}
`; return frame(screen, body, { variant }); } const inspect = id === "BAT-003"; const opening = id === "BAT-001"; const body = `
${opening ? "BATTLE OPENING" : "AUTOMATIC BATTLE"}

${opening ? "Break the rotating pane screen" : "Urgent: Guard chip reaches Sable"}

${opening ? "Player line faces right. Position 0 receives the first legal bite." : "The second Pane Bite will remove Elian's Guard and chip the neighbor."}

NO COMBAT RESPAWN · NO RESERVE AUTO-ENTRY
${iconButton(state.battlePaused ? "▶" : "Ⅱ", state.battlePaused ? "Resume" : "Pause", "data-battle-pause")}
${[1, 2, 4].map((speed) => ``).join("")}
${iconButton("i", "Inspect event", "data-open-screen=\"BAT-003\"")}
${battleLine()}
00:07.2 · Gust Finch targets Nia00:08.0 · Elian intercepts Pane Bite00:08.8 · Sable gains Remedy
${inspect ? `` : ""}
[Front recruit: Guard consumed] [Enemy forecast: rear projectile]
${opening ? actionDock("Start observation", "Inspect objective", "data-flow-next") : ""}
`; return frame(screen, body, { variant }); } function renderOutcome(screen, variant) { if (screen.id === "OUT-001") { const body = `
VICTORY
FIRST MEANINGFUL DIVERGENCE

Guard broke before the second rear shot

Expected protection ended at 00:18.4; actual pressure reached Nia at 00:19.1.

EXPECTED00:17 Guard 100:19 Rear shot blocked00:22 Stable line
ACTUAL00:17 Pane chip00:18 Guard 000:19 Nia hit
CauseSecond uninterrupted Pane BiteOrderSable remained adjacentAdaptMove or interrupt before bite 2
${actionDock("Continue to Reward", "Full event log", "data-flow-next")}
`; return frame(screen, body, { variant }); } if (screen.id === "OUT-002") { const body = `
FSFALLEN
COMMITTED LOSS

Nia Orrow

Farstring · Patient Start · Level 3 · survived 4 battles

EquipmentReturned to inventory
Party gapRear pressure lost
Replacement Credit+1 tier
Death is permanent for this run. No relationship or personal-arc system is implied.
${actionDock("Acknowledge loss", "Inspect receipt", "data-flow-next")}
`; return frame(screen, body, { variant }); } if (screen.id === "OUT-003") { const rewards = [["EQ-014", "Quiet Rearplate", "Converts first area hit to direct"], ["AR-011", "Narrow Vanguard", "One fewer deploy; front starts Guarded"], ["PF-B05", "Farstring insight", "Opens a profession trial"]]; const body = `
FIXED REWARD DRAFT

Choose the next decision you want to change

${rewards.map((reward, index) => ``).join("")}
${actionDock("Claim Quiet Rearplate", "Compare build", "data-flow-next")}
`; return frame(screen, body, { variant }); } if (screen.id === "OUT-004") { const body = `
RECOVERY

Company remains viable

Replacement Credit reduces only a later hire. Lost level, Trait, profession and history stay lost.

${actionDock("Commit protected hire", "Review consequences", "data-flow-next")}
`; return frame(screen, body, { variant }); } const rows = stageDetails[screen.id] || ["Outcome receipt verified", "No unresolved casualty", "Safe destination named"]; const body = `
${screen.id}

${escapeHtml(screenName(screen))}

${rows.map((row) => `
${escapeHtml(row)}
`).join("")}

${truncate(screen.purpose, 180)}

${actionDock(screen.id === "OUT-006" ? "Hold to abandon run" : "Finish result", "Cancel")}
`; return frame(screen, body, { variant }); } function renderRoster(screen, variant) { if (screen.id === "REC-003") { const body = `
${recruitMarker(generatedOffers[2], true)}
DESTRUCTIVE REVIEW

Dismiss Tomas Vale?

Equipment returns to run inventory
No Replacement Credit
Living Company 4 → 3

Eligibility is revalidated at commit. The last living recruit can never be dismissed.

${actionDock("Dismiss Recruit", "Cancel")}
`; return frame(screen, body, { variant }); } const roster = generatedOffers.concat([{ id: "RC-017", name: "Miro Sen", profession: "Coilsmith", trait: "Deep Pocket", mark: "CS" }]); const body = `
LIVING COMPANY · 5${roster.map((offer, index) => ``).join("")}
`; return frame(screen, body, { variant }); } function renderGuild(screen, variant) { const body = screen.id === "HUB-001" ? `
CURRENT PRESSURE

Rear projectile at the next crossing

Gust Finch enters behind a visible Pane screen.

${partyActors()}
Latest receiptQuiet Rearplate claimed · no unresolved transaction
` : `
NOWROUND 4BOSS MUSTER
KNOWN

Gust Finch

Targets rearmost ally, then steps forward.

Counter: screen or pull
KNOWN

Pane Nibbler

Second uninterrupted bite chips neighbor Guard.

Counter: reorder or interrupt
PARTIAL

Third identity

Family known: forced movement.

Confidence 68%
`; return frame(screen, body, { variant }); } function charterPrerequisite(index, region) { const name = bi(region[1], region[2]); if (index === 0) return bi(`Clear ${region[0]} Contract 1`, `完成${name}的首个契约`); if (index === 1) return bi(`Inspect three ${name} enemy families`, `查看三类${name}敌人`); return bi(`Complete both ${region[0]} Contracts`, `完成${name}的两个契约`); } function renderGuildCharter(screen, variant) { const region = guildCharterRegions[state.charterRegionIndex]; const packet = charterPackets[state.charterPacketIndex]; const remaining = 34 - packet.cost; const regionName = bi(region[1], region[2]); const body = `
${bi("GUILD CHARTER · BREADTH ONLY", "公会宪章 · 仅扩展玩法广度")}

${bi("Shape future Companies", "规划未来队伍的选择空间")}

${bi("Tap a Region, then Tap a packet. Every claim changes future-run options, never permanent combat power.", "点击区域,再点击扩展包。所有领取只改变未来远征的选择,不增加永久战斗数值。")}

${bi("RENOWN", "声望")}34${bi("Persistent breadth resource", "永久广度资源")}
${guildCharterRegions.map((entry, index) => ``).join("")}
${region[0]}${regionName}${bi("Tap one packet to preview", "点击一个扩展包查看详情")}
${charterPackets.map((entry, index) => ``).join("")}
`; return frame(screen, body, { variant, art: false, context: bi("Guild Charter", "公会宪章"), resources: false }); } function renderChronicleDecision(screen, variant) { const choice = state.chronicleChoice; const details = choice === "chronicle" ? [bi("Finite ending and Final Ledger", "观看有限结局与最终账册"), bi("Return to Title", "随后返回标题页")] : [bi("Enter Oath Season hub", "进入誓约季大厅"), bi("Review laws, then generate a new Company", "审核法则后生成一支全新队伍")]; const body = `
${bi("FINAL BOSS SETTLED · FIRST CLEAR", "最终首领已结算 · 首次通关")}

${bi("How will the Guild record this victory?", "公会将如何记录这场胜利?")}

${bi("Both choices close this run and archive the surviving Company.", "两个选择都会结束本轮远征,并把幸存队伍存入历史记录。")}

✓
${bi("SHARED RESULT", "共同结果")}

${bi("Company archived as Records · Coin and Credit close · Renown and discoveries retained · no Recruit remains playable", "队伍存入记录 · 金币与补员凭证清零 · 保留声望与发现 · 不会保留任何可继续使用的队员")}

`; return frame(screen, body, { variant, art: false, context: bi("Final Ledger", "最终账册"), resources: false }); } function renderOathSeason(screen, variant) { const selected = new Set(state.oathLaws); const body = `
${bi("OATH SEASON · NEW GENERATED RUN", "誓约季 · 全新生成远征")}

${bi("Choose laws that change valuation", "选择会改变决策价值的法则")}

${bi("The cleared Company is archived and cannot be selected here.", "已通关队伍已存档,无法在这里继续选择。")}

${oathLawCatalog.map((law, index) => ``).join("")}
`; return frame(screen, body, { variant, art: false, context: bi("Oath Season", "誓约季"), resources: false }); } function renderOathResult(screen, variant) { const body = `
${bi("OATH SEASON RESULT", "誓约季结果")}

${bi("The Company held under changed laws", "队伍在变更法则下完成了远征")}

${bi("Result receipts are complete. This generated Company is now a Record.", "结果回执已完成。这支生成队伍现已存入记录。")}

01${bi("Measured Supply", "定量补给")}

${bi("Fewer offers changed refresh and lock timing in five Markets.", "更少的报价改变了五个集市中的刷新与锁定时机。")}

02${bi("Broken Front", "破阵前线")}

${bi("Front exposure made rear control the decisive counter twice.", "前排暴露让后排控制两次成为关键应对。")}

${bi("RECEIPTS", "结算回执")}

${bi("Oath Record", "誓约记录")} +1

${bi("Renown", "声望")} +6

${bi("Archived Company", "存档队伍")} · 8 ${bi("survivors", "名幸存者")}

`; return frame(screen, body, { variant, art: false, context: bi("Oath Result", "誓约结果"), resources: false }); } function renderSystem(screen, variant) { if (screen.id === "SYS-005") return renderOpening(screen, variant === "primary" ? "offers2" : variant); if (screen.id === "SYS-003") { const body = `
AETHERBOUNDGUILDBuild the company. Order the line. Read the result.
Safe resumeWindglass Verge · MarketCompany 5 · Round 3 · save healthy
${iconButton("⚙", "Settings")}${iconButton("Aa", "Accessibility")}
`; return frame(screen, body, { variant, resources: false, context: "Aetherbound Guild" }); } if (screen.id === "SYS-001") { const body = `
AG

Aetherbound Guild

Verifying local content · 76%Rules · Content · Settings · Save lineage
`; return frame(screen, body, { variant, art: false, header: false }); } if (screen.id === "SYS-002") return renderSettings(screen, variant, "FIRST-LAUNCH SETUP"); const details = stageDetails[screen.id] || [screen.purpose, screen.action]; const body = `
${screen.id}

${escapeHtml(screenName(screen))}

${details.map((detail, index) => ``).join("")}
`; return frame(screen, body, { variant, resources: false }); } function renderArtifact(screen, variant) { const entries = state.data.artifacts.slice(0, 3); const body = `
RUN-WIDE RULES · NOT EQUIPMENT

Active Artifact Ledger

${entries.map((entry, index) => ``).join("")}
`; return frame(screen, body, { variant }); } function renderProgress(screen, variant) { if (screen.id === "PRO-002") return renderGuildCharter(screen, variant); if (screen.id === "PST-001") return renderChronicleDecision(screen, variant); if (screen.id === "PST-002") return renderOathSeason(screen, variant); if (screen.id === "PST-003") return renderOathResult(screen, variant); if (screen.id === "PRO-003") { const body = `
COMPENDIUM
${state.data.professions.slice(0, 6).map((entry, index) => ``).join("")}
`; return frame(screen, body, { variant, art: false }); } const rows = stageDetails[screen.id] || ["Decision grammar unlocked", "Prerequisite verified", "Receipt available"]; const isPostgame = screen.id.startsWith("PST"); const body = `
${isPostgame ? "POSTGAME" : "PROGRESSION"}

${escapeHtml(screenName(screen))}

${escapeHtml(truncate(screenPurpose(screen), 170))}

BaseChoiceMastery
${rows.map((row) => `
${escapeHtml(localizedText(row))}
`).join("")}
${actionDock(isPostgame ? "Review active rules" : "Inspect choice", "Ordinary New Run")}
`; return frame(screen, body, { variant, art: false }); } function renderSettings(screen, variant, kicker = "SETTINGS") { const rows = settingRows[screen.id] || settingRows["SET-002"]; const categoryNames = ["Display", "Motion", "Audio", "Controls", "Language", "Information", "Save / Data", "Reset"]; const settingKeys = { "SET-002-2": "contrast", "SET-003-0": "reducedMotion", "SET-003-3": "lowPower", "SET-004-3": "captions", }; const controls = rows.map(([label, value], index) => { if (screen.id === "SET-002" && index === 0) { return `
${["100", "115", "130"].map((scale) => ``).join("")}
`; } if (screen.id === "SET-006" && index === 0) { return `
`; } const key = settingKeys[`${screen.id}-${index}`]; if (key) { const enabled = Boolean(state.settings[key]); return ``; } if (index % 3 === 1) return ``; if (index % 3 === 2) return ``; return `
`; }); const body = `
CALLER · ${variant === "pause" ? "PAUSED BATTLE" : "TITLE"}

${escapeHtml(screenName(screen))}

${rows.map(([label], index) => `
${escapeHtml(label)}${index === 0 ? "Live preview" : "Current value"}
${controls[index]}
`).join("")}
`; return frame(screen, body, { variant, art: false, header: false }); } function renderGeneric(screen, variant) { const details = stageDetails[screen.id] || [screenPurpose(screen), screenAction(screen), screenSafeState(screen)]; const primary = state.locale === "zh_CN" ? `打开${screenName(screen)}` : truncate(screen.action.split(";")[0], 46); const body = `
${screen.id.split("-")[0]}${escapeHtml(screenName(screen))}
CURRENT DECISION

${escapeHtml(screenName(screen))}

${escapeHtml(truncate(screenPurpose(screen), 180))}

${details.map((detail) => `
${escapeHtml(localizedText(detail))}
`).join("")}
${actionDock(primary, t("inspect"))}
`; return frame(screen, body, { variant }); } function renderPrimaryStage(screen, variant = "primary") { const prefix = screen.id.split("-")[0]; if (prefix === "SYS") return renderSystem(screen, variant); if (prefix === "HUB") return renderGuild(screen, variant); if (prefix === "SHP") return renderMarket(screen, variant); if (prefix === "REC") return renderRoster(screen, variant); if (prefix === "EQP" || prefix === "PTY") return renderPreparation(screen, variant); if (prefix === "ART") return renderArtifact(screen, variant); if (prefix === "RSK") return renderRisk(screen, variant); if (prefix === "BAT") return renderBattle(screen, variant); if (prefix === "OUT") return renderOutcome(screen, variant); if (prefix === "PRO" || prefix === "PST") return renderProgress(screen, variant); if (prefix === "SET") return renderSettings(screen, variant); return renderGeneric(screen, variant); } function renderStateVariant(screen, variant) { if (variant === "primary" || (screen.id === "SYS-005" && !["loading", "error", "return"].includes(variant))) return renderPrimaryStage(screen, variant); const source = state.locale === "zh_CN" ? (variant === "loading" ? "正在加载当前页面;没有可用内容时会显示明确的空状态和返回路径。" : variant === "error" ? "操作未完成;请先核对发生变化的内容,再决定重试或返回。" : screenSafeState(screen)) : (variant === "loading" ? screen.emptyLoading : variant === "error" ? screen.errorConfirmation : screen.interruptionReturn); const label = variant === "loading" ? "LOADING / EMPTY" : variant === "error" ? "ERROR / CONFIRMATION" : "INTERRUPTION / RETURN"; const base = renderPrimaryStage(screen, "primary"); const action = variant === "loading" ? t("retry") : variant === "error" ? "Review changed state" : t("returnSafe"); return base.replace( "", `
${label}

${escapeHtml(screenName(screen))}

${escapeHtml(truncate(source, 260))}

`, ); } function viewHeading(kicker, title, detail, badges = []) { return `
${escapeHtml(kicker)}

${escapeHtml(title)}

${escapeHtml(detail)}

${badges.map((badge) => `${escapeHtml(badge)}`).join("")}
`; } function renderFirstPlay() { const step = flowSteps[state.flowIndex]; const screen = getScreen(step[0]); return `
${viewHeading("OWNER REVIEW · FIRST 16 MINUTES", flowLabel(step), `${screen.id} · ${screenName(screen)}`, [`${state.flowIndex + 1} / ${flowSteps.length}`, "Real screen state"])}
${renderPrimaryStage(screen, step[1])}
${flowSteps.map((entry, index) => ``).join("")}
`; } function renderScreens() { const filtered = state.data.screens.filter((screen) => state.screenGroup === "ALL" || screen.id.startsWith(`${state.screenGroup}-`)); 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 previous = filtered[(index - 1 + filtered.length) % filtered.length]; const next = filtered[(index + 1) % filtered.length]; const variants = screen.id === "SYS-005" ? [["contract", "A"], ["offers2", "B"], ["review", "C"], ["pending", "D"], ["receipt", "E"], ["loading", "Loading"], ["error", "Error"], ["return", "Return"]] : [["primary", "Primary"], ["loading", "Loading / empty"], ["error", "Error / confirm"], ["return", "Return"]]; return `
${viewHeading("50 CURRENT PAGE / STATE IDS", screenName(screen), `${screen.id} · ${state.locale === "zh_CN" ? "页面操作" : screen.input}`, [`${index + 1} / ${filtered.length}`, "Page-specific composition"])}
${Object.entries(screenGroups).map(([key, label]) => ``).join("")}
${variants.map(([key, label]) => ``).join("")}
${renderStateVariant(screen, state.screenVariant)}
Purpose

${escapeHtml(screenPurpose(screen))}

Primary action

${escapeHtml(screenAction(screen))}

Safe state

${escapeHtml(screenSafeState(screen))}

`; } function renderRegions() { const region = state.data.regions[state.regionIndex]; const enemies = state.data.enemies.filter((entry) => entry.id.startsWith(`EN-R${state.regionIndex + 1}-`)); const bosses = state.data.bosses.filter((entry) => entry.id.startsWith(`BO-R${state.regionIndex + 1}-`)); return `
${viewHeading("EIGHT CAMPAIGN REGIONS", region.name, region.framing, [`${region.id}`, `${enemies.length} enemies · ${bosses.length} bosses`])}
${state.data.regions.map((entry, index) => ``).join("")}
${escapeHtml(region.name)} concept direction${t("concept")}
${region.id}

${region.name}

${region.framing}

Build pressure

${region.pressure}

Encounter grammar

${region.progression}

Current identities
${enemies.slice(0, 5).map((enemy) => `${enemy.id}${enemy.name}`).join("")}${bosses.map((boss) => `${boss.id}${boss.name}`).join("")}
`; } function catalogConfig() { const configs = { professions: { label: "Professions", image: "assets/web/el_profession_kits.webp", filters: [["all", "All 42"], ["base", "12 base"], ["advanced", "24 advanced"], ["hidden", "6 hidden"]], meta: (entry) => `${entry.family} · ${entry.role}`, body: (entry) => `${entry.rules} ${entry.strategy}` }, traits: { label: "Traits", image: "assets/web/el_heroes.webp", filters: [["all", "All 36"]], meta: () => "one per recruit", body: (entry) => `Benefit: ${entry.benefit} Cost: ${entry.cost}` }, equipment: { label: "Equipment", image: "assets/web/el_items.webp", filters: [["all", "All 320"], ["B", "Behavior B"], ["T", "Tradeoff T"]], meta: (entry) => `${entry.class} · ${entry.fit}`, body: (entry) => `${entry.effect} Tradeoff: ${entry.tradeoff}` }, artifacts: { label: "Artifacts", image: "assets/web/el_items.webp", filters: [["all", "All 60"]], meta: () => "run-wide rule", body: (entry) => `${entry.rule} ${entry.tradeoff}` }, enemies: { label: "Enemies", image: "assets/web/el_enemies.webp", filters: [["all", "All 100"], ["ordinary", "Ordinary"], ["elite", "Elite"]], meta: (entry) => entry.class, body: (entry) => `${entry.rule} Responses: ${entry.responses}` }, bosses: { label: "Bosses", image: "assets/web/el_enemies.webp", filters: [["all", "All 16"]], meta: (entry) => entry.class, body: (entry) => `${entry.rule} Responses: ${entry.responses}` }, }; return configs[state.catalogType]; } function renderCatalogs() { const config = catalogConfig(); const entries = state.data[state.catalogType]; const normalized = state.catalogQuery.trim().toLocaleLowerCase(); const filtered = entries.filter((entry) => { const filterMatch = state.catalogFilter === "all" || entry.family === state.catalogFilter || entry.class === state.catalogFilter; const queryMatch = !normalized || Object.values(entry).join(" ").toLocaleLowerCase().includes(normalized); return filterMatch && queryMatch; }); return `
${viewHeading("CURRENT AUTHORED CATALOGS", config.label, "Search stable IDs, names, mechanics, tradeoffs and counterplay.", [`${filtered.length} / ${entries.length}`, "No filler rows"])}
${["professions", "traits", "equipment", "artifacts", "enemies", "bosses"].map((key) => ``).join("")}
${config.filters.map(([key, label]) => ``).join("")}
${filtered.length ? filtered.slice(0, 100).map((entry) => `
${entry.id}${entry.name}${escapeHtml(config.meta(entry))}

${escapeHtml(truncate(config.body(entry), 240))}

`).join("") : `
No matching identities
`}${filtered.length > 100 ? `
100 shown · ${filtered.length - 100} more remain searchable
` : ""}
`; } function renderReferenceWireframe(mapping) { return `
${mapping.id}${bi("OBSERVED FUNCTIONAL STRUCTURE", "观察到的功能结构")}${mapping.evidence}
${mapping.zones.map(([position, en, zh], index) => `
0${index + 1}${bi(en, zh)}
`).join("")}
`; } function renderReferenceMap() { const mapping = referenceMappings[state.referenceIndex]; const screen = getScreen(mapping.target); return `
${viewHeading("REFERENCE EXPERIENCE MAP", bi(mapping.title[0], mapping.title[1]), `${mapping.id} · ${mapping.evidence} · ${mapping.target}`, [`${state.referenceIndex + 1} / ${referenceMappings.length}`, bi("Functional learning, original expression", "学习功能结构,保持原创表达")])}
${referenceMappings.map((entry, index) => ``).join("")}
${bi("REFERENCE LEARNING", "参考学习")}

${bi("Observed structure", "观察到的结构")}

${bi(mapping.options[0], mapping.options[1])}

${renderReferenceWireframe(mapping)}
${bi("AETHERBOUND ADAPTATION", "Aetherbound 原创改造")}

${screenName(screen)}

${screen.id} · ${bi("Current page composition", "当前页面构图")}

${renderPrimaryStage(screen, mapping.variant)}
${bi("ADOPTED", "采用")}

${bi(mapping.adopt[0], mapping.adopt[1])}

${bi("CHANGED", "改造")}

${bi(mapping.change[0], mapping.change[1])}

${bi("REJECTED", "拒绝")}

${bi(mapping.reject[0], mapping.reject[1])}

`; } function renderReview() { const groups = [ ["01", "Opening company", "SYS-005 A-E", "Four stable offers, exactly-two gating, revision, atomic receipt and first-Market handoff.", "firstplay", "0"], ["02", "Market hire", "SHP-001 / 002", "Two separate affordable base hires move Company 2 → 3 → 4 while capacity remains 4.", "firstplay", "7"], ["03", "Formation and equip", "EQP-001 / 002 · PTY-001 / 002", "Behavior delta and one rear-left to front-right line with exposed position 0.", "screens", "EQP-002"], ["04", "Horizontal battle", "BAT-001..006", "Automatic execution, one ordered line, urgent cause, pause, speed, inspect and retreat review.", "screens", "BAT-002"], ["05", "Result and recovery", "OUT-001..006", "Earliest divergence precedes totals; loss, reward, recovery and abandonment remain distinct.", "screens", "OUT-001"], ["06", "Catalog and settings", "PRO-003 · SET-001..009", "Searchable current counts plus EN/zh_CN, 130%, contrast, captions, reduced motion and input states.", "catalogs", "professions"], ["07", "Charter and final choice", "PRO-002 · PST-001..003", "Spend Renown only for breadth, then compare finite closure with a new generated Oath Season run.", "screens", "PRO-002"], ]; return `
${viewHeading("OWNER DECISION SURFACE", "Seven critical review groups", "Each group opens the real composition used in the 50-state atlas.", ["Design review only", "Human gate open"])}
${groups.map((group) => `
${group[0]}
${group[2]}

${group[1]}

${group[3]}

`).join("")}
Machine-verifiable50 unique states · current counts · responsive layout · interactions · self-contained bundle
Owner remains responsibleComprehension · visual acceptance · fun · listening · device ergonomics · development approval
`; } function applyLocale() { document.documentElement.lang = state.locale === "zh_CN" ? "zh-CN" : "en"; $$('[data-copy]').forEach((element) => { element.textContent = t(element.dataset.copy); }); $$('[data-locale]').forEach((button) => button.classList.toggle("is-active", button.dataset.locale === state.locale)); $("#view-title").textContent = t(state.view); localizeRenderedContent($("#content")); } function applySettings() { document.body.classList.toggle("high-contrast", state.settings.contrast); document.body.classList.toggle("reduced-motion", state.settings.reducedMotion); document.body.classList.toggle("low-power", state.settings.lowPower); document.body.dataset.textScale = state.settings.textScale; } function render() { if (!state.data) return; const renderers = { firstplay: renderFirstPlay, screens: renderScreens, regions: renderRegions, catalogs: renderCatalogs, reference: renderReferenceMap, review: renderReview }; $("#content").innerHTML = renderers[state.view](); $$('.nav-item').forEach((button) => button.classList.toggle("is-active", button.dataset.view === state.view)); applyLocale(); applySettings(); } function setView(view) { state.view = view; history.replaceState(null, "", `#${view}`); render(); closeMobileNav(); $("#content").focus({ preventScroll: true }); } function closeMobileNav() { $("#sidebar").classList.remove("is-open"); $("#nav-scrim").hidden = true; } function openMobileNav() { $("#sidebar").classList.add("is-open"); $("#nav-scrim").hidden = false; } function advanceFlow(delta) { const next = state.flowIndex + delta; state.flowIndex = next < 0 ? 0 : next >= flowSteps.length ? 7 : next; if (state.view !== "firstplay") state.view = "firstplay"; if ($("#stage-dialog").open) $("#stage-dialog").close(); render(); } function handleClick(event) { const button = event.target.closest("button"); if (!button) return; if (button.dataset.view) return setView(button.dataset.view); if (button.dataset.locale) { state.locale = button.dataset.locale; return render(); } if (button.hasAttribute("data-flow-next")) return advanceFlow(1); if (button.hasAttribute("data-flow-prev")) return advanceFlow(-1); if (button.dataset.flowIndex) { state.flowIndex = Number(button.dataset.flowIndex); return render(); } if (button.dataset.screenGroup) { state.screenGroup = button.dataset.screenGroup; state.screenVariant = "primary"; return render(); } if (button.dataset.screenId) { state.screenId = button.dataset.screenId; state.screenVariant = "primary"; if (state.view !== "screens") state.view = "screens"; return render(); } if (button.dataset.screenVariant) { state.screenVariant = button.dataset.screenVariant; return render(); } if (button.dataset.regionIndex) { state.regionIndex = Number(button.dataset.regionIndex); return render(); } if (button.dataset.catalogType) { state.catalogType = button.dataset.catalogType; state.catalogFilter = "all"; state.catalogQuery = ""; return render(); } if (button.dataset.catalogFilter) { state.catalogFilter = button.dataset.catalogFilter; return render(); } if (button.hasAttribute("data-reference-index")) { state.referenceIndex = Number(button.dataset.referenceIndex); return render(); } if (button.hasAttribute("data-charter-region")) { state.charterRegionIndex = Number(button.dataset.charterRegion); state.charterPacketIndex = 0; return render(); } if (button.hasAttribute("data-charter-packet")) { state.charterPacketIndex = Number(button.dataset.charterPacket); return render(); } if (button.dataset.chronicleChoice) { state.chronicleChoice = button.dataset.chronicleChoice; return render(); } if (button.dataset.oathLaw) { const id = button.dataset.oathLaw; const index = state.oathLaws.indexOf(id); if (index >= 0) state.oathLaws.splice(index, 1); else state.oathLaws.push(id); return render(); } if (button.dataset.offerId) { const id = button.dataset.offerId; const index = state.openingSelected.indexOf(id); if (index >= 0) state.openingSelected.splice(index, 1); else if (state.openingSelected.length < 2) state.openingSelected.push(id); state.flowIndex = state.openingSelected.length === 0 ? 1 : state.openingSelected.length === 1 ? 2 : 3; return render(); } if (button.hasAttribute("data-party-swap")) { state.partySwapped = !state.partySwapped; return render(); } if (button.hasAttribute("data-battle-pause")) { state.battlePaused = !state.battlePaused; return render(); } if (button.dataset.battleSpeed) { state.battleSpeed = Number(button.dataset.battleSpeed); return render(); } if (button.dataset.openScreen) { state.screenId = button.dataset.openScreen; state.screenVariant = "primary"; return setView("screens"); } if (button.dataset.settingKey) { const key = button.dataset.settingKey; state.settings[key] = !state.settings[key]; return render(); } if (button.dataset.textScale) { state.settings.textScale = button.dataset.textScale; return render(); } if (button.dataset.reviewView) { const target = button.dataset.reviewTarget; if (button.dataset.reviewView === "firstplay") state.flowIndex = Number(target); if (button.dataset.reviewView === "screens") state.screenId = target; if (button.dataset.reviewView === "catalogs") state.catalogType = target; return setView(button.dataset.reviewView); } if (button.hasAttribute("data-clear-search")) { state.catalogQuery = ""; return render(); } } function handleInput(event) { if (!event.target.matches("[data-catalog-search]")) return; state.catalogQuery = event.target.value; render(); const replacement = $("[data-catalog-search]"); replacement.focus(); replacement.setSelectionRange(replacement.value.length, replacement.value.length); } let viewerZoom = 1; function openCurrentStage() { const stage = $(".game-stage") || $(".region-stage"); if (!stage) return; viewerZoom = 1; $("#zoom-reset").textContent = "100%"; $("#dialog-stage").style.width = "100%"; $("#dialog-stage").replaceChildren(stage.cloneNode(true)); $("#viewer-label").textContent = stage.dataset.stageId || "Region composition"; $("#stage-dialog").showModal(); } function setViewerZoom(delta) { viewerZoom = Math.min(2, Math.max(0.75, Math.round((viewerZoom + delta) * 4) / 4)); $("#dialog-stage").style.width = `${viewerZoom * 100}%`; $("#zoom-reset").textContent = `${Math.round(viewerZoom * 100)}%`; } 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 (["firstplay", "screens", "regions", "catalogs", "reference", "review"].includes(hashView)) state.view = hashView; render(); } catch (error) { $("#content").innerHTML = `

Prototype data unavailable

${escapeHtml(error.message)}

`; } } document.addEventListener("click", handleClick); document.addEventListener("input", handleInput); $("#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()); $("#zoom-out").addEventListener("click", () => setViewerZoom(-0.25)); $("#zoom-in").addEventListener("click", () => setViewerZoom(0.25)); $("#zoom-reset").addEventListener("click", () => { viewerZoom = 1; setViewerZoom(0); }); $("#stage-dialog").addEventListener("click", (event) => { if (event.target === event.currentTarget) event.currentTarget.close(); }); window.addEventListener("keydown", (event) => { if (event.key === "Escape") closeMobileNav(); }); init();