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

2959 lines
285 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const state = {
data: null,
view: "playtest",
locale: "zh_CN",
screenId: "SYS-005",
screenGroup: "ALL",
screenVariant: "primary",
flowIndex: 1,
regionIndex: 0,
catalogType: "professions",
catalogFilter: "all",
catalogQuery: "",
referenceIndex: 2,
openingSelected: [],
experienceStep: "contract",
experienceContract: null,
experienceDifficulty: null,
experienceSelected: [],
experienceInspected: null,
experienceInspectedIds: [],
experienceNotice: "",
experienceCommitted: false,
experiencePackage: "p8-3",
marketStep: "market2",
marketCoin: 52,
marketHired: [],
marketFocusedOffer: null,
marketCommittedOffers: [],
marketItemPurchased: false,
marketItemEquipped: false,
marketEquipTarget: null,
marketOrder: [],
marketOrderSource: null,
marketOrderChanged: false,
marketNotice: "",
riskStep: "board",
riskSelected: null,
riskCommitted: false,
riskBattleTick: 0,
riskBattlePaused: true,
riskBattleSpeed: 1,
riskInspectOpen: false,
riskInspectEvent: null,
riskInspectPriorPaused: true,
resultStep: "diagnosis",
casualtyCommitted: false,
rewardSelected: null,
artifactSelected: null,
rewardCommitted: false,
recoverySelected: null,
recoveryCommitted: false,
partySwapped: false,
p85Step: "intro",
p85MarketAction: "",
p85Risk1: null,
p85Risk1Committed: false,
p85Battle1Tick: 0,
p85CasualtySettled: false,
p85RewardSelected: null,
p85RewardClaimed: false,
p85RecoveryChoice: null,
p85RecoveryCommitted: false,
p85EquipDecision: null,
p85LoadoutCommitted: false,
p85Risk2: null,
p85Risk2Committed: false,
p85Battle2Tick: 0,
p85SecondResultAcknowledged: false,
p85Muted: 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: "Phased experience prototype",
navPlaytest: "Play P8.3",
navFirst: "First-play map",
navScreens: "Screen atlas",
navRegions: "Regions",
navCatalogs: "Catalogs",
navReference: "Reference map",
navReview: "Review gates",
runtimeFrozen: "Godot development frozen",
conceptBadge: "P8.3 prototype",
playtest: "Play P8.3",
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: "Production UI - native text + traceable candidate art",
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: "分阶段体验原型",
navPlaytest: "体验 P8.3",
navFirst: "首次流程图",
navScreens: "页面图谱",
navRegions: "八大区域",
navCatalogs: "完整图鉴",
navReference: "参考映射",
navReview: "审核闸门",
runtimeFrozen: "Godot 开发仍冻结",
conceptBadge: "P8.3 体验原型",
playtest: "体验 P8.3",
firstplay: "首次游玩",
screens: "页面图谱",
regions: "八大区域",
catalogs: "完整图鉴",
reference: "参考映射",
review: "审核闸门",
prev: "上一步",
next: "下一决策",
back: "返回",
inspect: "查看",
compare: "比较",
concept: "生产级 UI - 原生文字 + 可追踪候选素材",
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 - 14 Coin": "花费14金币招募",
"Price 14 Coin · Operation HIRE-001": "价格14金币 · 招募操作 HIRE-001",
"Price 14 Coin · Operation HIRE-002": "价格14金币 · 招募操作 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 actorAssets = {
Hearthguard: "assets/game/actors/hearthguard.png",
Farstring: "assets/game/actors/farstring.png",
Wayrunner: "assets/game/actors/wayrunner.png",
Kindhand: "assets/game/actors/kindhand.png",
Riftblade: "assets/game/actors/riftblade.png",
TempestSeraph: "assets/game/actors/tempest-seraph.png",
};
const equipmentAssets = {
"Stepback Soles": "assets/game/equipment/eq_001_stepback_soles@1x.png",
"Frontpin Greaves": "assets/game/equipment/eq_002_frontpin_greaves@1x.png",
"Passing Sash": "assets/game/equipment/eq_003_passing_sash@1x.png",
"Gapfinder Sandals": "assets/game/equipment/eq_004_gapfinder_sandals@1x.png",
"Leadweight Belt": "assets/game/equipment/eq_005_leadweight_belt@1x.png",
"Rearhook Cloak": "assets/game/equipment/eq_006_rearhook_cloak@1x.png",
"Pivot Anklet": "assets/game/equipment/eq_007_pivot_anklet@1x.png",
"Stillroad Wrap": "assets/game/equipment/eq_008_stillroad_wrap@1x.png",
"Rescue Loop": "assets/game/equipment/eq_009_rescue_loop@1x.png",
"Forward Claim": "assets/game/equipment/eq_010_forward_claim@1x.png",
"Neighbor's Buckle": "assets/game/equipment/eq_011_neighbors_buckle@1x.png",
"Open-Line Cord": "assets/game/equipment/eq_012_open_line_cord@1x.png",
};
const vfxAssets = {
threat: "assets/game/vfx/threat_imminent@1x.png",
damage: "assets/game/vfx/damage_heavy@1x.png",
protect: "assets/game/vfx/protect_intercept@1x.png",
tempest: "assets/game/vfx/tempest_phase_imminent@1x.png",
};
const p83MotionAssets = {
recruit: {
idle: [
"assets/game/motion/recruit/idle/frame-0.png", "assets/game/motion/recruit/idle/frame-1.png", "assets/game/motion/recruit/idle/frame-2.png",
"assets/game/motion/recruit/idle/frame-3.png", "assets/game/motion/recruit/idle/frame-4.png", "assets/game/motion/recruit/idle/frame-5.png",
],
advance: [
"assets/game/motion/recruit/advance/frame-0.png", "assets/game/motion/recruit/advance/frame-1.png", "assets/game/motion/recruit/advance/frame-2.png", "assets/game/motion/recruit/advance/frame-3.png",
"assets/game/motion/recruit/advance/frame-4.png", "assets/game/motion/recruit/advance/frame-5.png", "assets/game/motion/recruit/advance/frame-6.png", "assets/game/motion/recruit/advance/frame-7.png",
],
attack: [
"assets/game/motion/recruit/attack/frame-0.png", "assets/game/motion/recruit/attack/frame-1.png", "assets/game/motion/recruit/attack/frame-2.png",
"assets/game/motion/recruit/attack/frame-3.png", "assets/game/motion/recruit/attack/frame-4.png", "assets/game/motion/recruit/attack/frame-5.png",
],
guard: [
"assets/game/motion/recruit/guard/frame-0.png", "assets/game/motion/recruit/guard/frame-1.png",
"assets/game/motion/recruit/guard/frame-2.png", "assets/game/motion/recruit/guard/frame-3.png",
],
protect: [
"assets/game/motion/recruit/protect/frame-0.png", "assets/game/motion/recruit/protect/frame-1.png", "assets/game/motion/recruit/protect/frame-2.png", "assets/game/motion/recruit/protect/frame-3.png",
"assets/game/motion/recruit/protect/frame-4.png", "assets/game/motion/recruit/protect/frame-5.png", "assets/game/motion/recruit/protect/frame-6.png", "assets/game/motion/recruit/protect/frame-7.png",
],
},
enemy: {
idle: [
"assets/game/motion/enemy/idle/frame-0.png", "assets/game/motion/enemy/idle/frame-1.png", "assets/game/motion/enemy/idle/frame-2.png",
"assets/game/motion/enemy/idle/frame-3.png", "assets/game/motion/enemy/idle/frame-4.png", "assets/game/motion/enemy/idle/frame-5.png",
],
advance: [
"assets/game/motion/enemy/advance/frame-0.png", "assets/game/motion/enemy/advance/frame-1.png", "assets/game/motion/enemy/advance/frame-2.png", "assets/game/motion/enemy/advance/frame-3.png",
"assets/game/motion/enemy/advance/frame-4.png", "assets/game/motion/enemy/advance/frame-5.png", "assets/game/motion/enemy/advance/frame-6.png", "assets/game/motion/enemy/advance/frame-7.png",
],
telegraph: [
"assets/game/motion/enemy/telegraph/frame-0.png", "assets/game/motion/enemy/telegraph/frame-1.png", "assets/game/motion/enemy/telegraph/frame-2.png",
"assets/game/motion/enemy/telegraph/frame-3.png", "assets/game/motion/enemy/telegraph/frame-4.png", "assets/game/motion/enemy/telegraph/frame-5.png",
],
attack: [
"assets/game/motion/enemy/attack/frame-0.png", "assets/game/motion/enemy/attack/frame-1.png", "assets/game/motion/enemy/attack/frame-2.png",
"assets/game/motion/enemy/attack/frame-3.png", "assets/game/motion/enemy/attack/frame-4.png", "assets/game/motion/enemy/attack/frame-5.png",
],
hit: [
"assets/game/motion/enemy/hit/frame-0.png", "assets/game/motion/enemy/hit/frame-1.png",
"assets/game/motion/enemy/hit/frame-2.png", "assets/game/motion/enemy/hit/frame-3.png",
],
},
};
const generatedOffers = [
{ id: "OFF-01", name: "Elian Rook", profession: "Hearthguard", trait: "Narrow Guard", read: "Intercepts a neighbor / slower steps", mark: "HG", art: actorAssets.Hearthguard, level: 1, xp: 0, xpNext: 30, itemId: "EQ-002", item: "Frontpin Greaves" },
{ id: "OFF-02", name: "Nia Orrow", profession: "Farstring", trait: "Patient Start", read: "Rear pressure / delayed first basic", mark: "FS", art: actorAssets.Farstring, level: 1, xp: 0, xpNext: 30, itemId: "EQ-014", item: "Quiet Rearplate" },
{ id: "OFF-03", name: "Tomas Vale", profession: "Wayrunner", trait: "Open Stance", read: "Reorders safely / briefly Exposed", mark: "WR", art: actorAssets.Wayrunner, level: 1, xp: 0, xpNext: 30, itemId: "EQ-001", item: "Stepback Soles" },
{ id: "OFF-04", name: "Sable Kest", profession: "Kindhand", trait: "Shared Cup", read: "Restores a neighbor / weaker self-heal", mark: "KH", art: actorAssets.Kindhand, level: 1, xp: 0, xpNext: 30, itemId: "EQ-009", item: "Rescue Loop" },
];
const openingContract = {
id: "CTR-R1-01",
name: ["First Crossing", "首次穿越"],
region: ["Windglass Verge", "风镜原野"],
objective: ["Chart the suspended pane crossing", "勘察悬浮晶板之间的安全通路"],
pressure: ["Push + projectile screen", "击退与远程屏障"],
boss: ["Rotating damage-family panes", "轮换不同伤害类型的晶板"],
reward: ["Base professions + opening tools", "基础职业与开局装备"],
};
const openingDifficulties = [
{ id: "wayfinder", name: ["Wayfinder", "向导"], stats: ["Enemy HP 90% · damage 90% · rewards 100%", "敌人生命90% · 伤害90% · 奖励100%"], audience: ["Learn every system with lower combat pressure", "保留全部系统,以较低战斗压力学习"] },
{ id: "standard", name: ["Standard", "标准"], stats: ["Enemy HP 100% · damage 100% · rewards 100%", "敌人生命100% · 伤害100% · 奖励100%"], audience: ["Authored first-clear baseline", "首次通关的标准平衡"] },
{ id: "oathbound", name: ["Oathbound", "誓约"], stats: ["Enemy HP 110% · damage 112% · rewards 115%", "敌人生命110% · 伤害112% · 奖励115%"], audience: ["Prepared high-risk play", "适合已有准备的高风险玩法"] },
];
const firstMarketOffers = [
{ id: "MKT-R1-01", name: "Mael Corin", profession: "Hearthguard", trait: "Steady Brace", read: "Intercepts the foremost ally / slower first step", mark: "HG", art: actorAssets.Hearthguard, level: 1, xp: 0, xpNext: 30, itemId: "EQ-002", item: "Frontpin Greaves", price: 14 },
{ id: "MKT-R1-02", name: "Ilya Venn", profession: "Farstring", trait: "Clear Measure", read: "Reaches enemy rear / delays after retarget", mark: "FS", art: actorAssets.Farstring, level: 1, xp: 0, xpNext: 30, itemId: "EQ-014", item: "Quiet Rearplate", price: 14 },
{ id: "MKT-R1-03", name: "Orin Hale", profession: "Wayrunner", trait: "Quick Exchange", read: "Changes adjacent order / briefly loses Guard", mark: "WR", art: actorAssets.Wayrunner, level: 1, xp: 0, xpNext: 30, itemId: "EQ-003", item: "Passing Sash", price: 14 },
];
const firstMarketItem = {
id: "EQ-012",
name: "Open-Line Cord",
price: 12,
effect: ["With no neighbor ahead, basic attacks gain reach +1.", "前方没有相邻队员时,普通攻击距离 +1。"],
tradeoff: ["A new front neighbor cancels a reserved long target.", "前方出现新队员时,已锁定的远距离目标会取消。"],
};
const marketExperienceSteps = [
["market", "1", "Market", "集市招募"],
["equipment", "2", "Equipment", "购买与换装"],
["order", "3", "Order", "调整横队"],
["receipt", "4", "Ready", "整备回执"],
];
const riskExperienceSteps = [
["risk", "1", "Risk", "比较风险"],
["commit", "2", "Commit", "确认风险"],
["battle", "3", "Battle", "自动战斗"],
["resolved", "4", "Resolved", "解析完成"],
];
const resultExperienceSteps = [
["result", "1", "Result", "战果诊断"],
["casualty", "2", "Loss", "永久损失"],
["reward", "3", "Reward", "选择奖励"],
["recovery", "4", "Recovery", "恢复队伍"],
["market", "5", "Next Market", "下一集市"],
];
const p85RiskOffers = [
{ id: "RSK-P85-01", name: ["Open Span", "开阔跨线"], pressure: ["Tempo burst; no direct-hit rider", "速度爆发;没有直接命中追加效果"], reward: ["Tempo equipment", "速度装备"], detail: ["EQ-014 is useful but not essential here; the route tests faster resolution.", "EQ-014 在这里不是必需品;这条路线考验更快解决战斗。"], enemy: ["Gale Hounds", "疾风猎犬"], kind: "tempo" },
{ id: "RSK-P85-02", name: ["Needle Rain", "针雨走廊"], pressure: ["Direct-hit rear pressure", "后排直接命中压力"], reward: ["Counter equipment", "克制装备"], detail: ["EQ-014 converts the first area hit to direct and removes the rider; without it, Vela is exposed.", "EQ-014 会把第一次范围伤害转为直接命中并移除追加效果;没有它,Vela 会暴露。"], enemy: ["Needle Choir", "针鸣群"], kind: "counter" },
];
const p85Battle1Events = [
["00:00", "Both lines enter", "双方横队进入;Company 4/4,EQ-012 已装备给 Tomas。"],
["00:02", "Frontlines collide", "Tomas 与 Mael 在接触线停止推进,Sable 与 Ilya 保持后排。"],
["00:04", "Kindhand restores Mael", "Sable 从后排把 Mael 从 72 HP 治疗到 88 HP。"],
["00:06", "Farstring fires", "Ilya 从后排射击敌方后排,箭矢命中并降低其生命。"],
["00:08", "Enemy spell demonstration", "敌方 PF-B06 进行蓄力、法弹、命中演示。"],
["00:10", "Battle resolves", "固定结果进入 P8.4:胜利,但 Ilya 的直接命中追加效果触发。"],
];
const p85Battle2Events = [
["00:00", "Vela enters with Quiet Rearplate", "Vela 带着退回库存的 EQ-014 进入第二轮;这次装备选择会影响结果。"],
["00:02", "Needle Rain telegraphs", "针鸣群预告后排直接压力;装备状态保持可见。"],
["00:04", "Quiet Rearplate converts the hit", "EQ-014 把第一次范围命中转为直接命中,移除直接追加效果。"],
["00:06", "Vela holds the rear", "Vela 保住后排,Sable 的治疗窗口保持开放。"],
["00:08", "Second-cycle result", "第二轮完成;结果由风险与装备选择共同解释。"],
];
const p85BattleEventCopyZh = {
"Both lines enter": "双方横队入场",
"Frontlines collide": "前排接敌",
"Kindhand restores Mael": "仁心医者治疗 Mael",
"Farstring fires": "远弦从后排射击",
"Enemy spell demonstration": "敌方法术演示",
"Battle resolves": "第一轮战斗结束",
"Vela enters with Quiet Rearplate": "Vela 携寂静后甲入场",
"Needle Rain telegraphs": "针雨压力预告",
"Quiet Rearplate converts the hit": "寂静后甲转化命中",
"Vela holds the rear": "Vela 守住后排",
"Second-cycle result": "第二轮战斗结束",
};
const p84Artifacts = [
{ id: "AR-021", name: ["First Word", "先声"], rule: ["The slowest ally acts before the first enemy basic; their second action is delayed.", "最慢的我方单位会先于敌方第一次普通攻击行动,但其第二次行动会延后。"], mark: "Ⅰ" },
{ id: "AR-031", name: ["Counter Road", "克制之路"], rule: ["Reveal one future enemy family; victory offers matching counter equipment.", "提前揭示一个未来敌人族群;胜利后提供对应克制装备。"], mark: "◇" },
{ id: "AR-043", name: ["Hard Lesson", "沉痛教训"], rule: ["A death reveals one extra cause/counter comparison and raises next reward quality.", "发生阵亡后额外揭示一组因果/克制比较,并提高下一战奖励品质。"], mark: "△" },
];
const p84Rewards = [
{ id: "artifact", code: "ARTIFACT", name: ["Choose one run-wide Artifact", "选择一件全局神器"], detail: ["Opens three fixed Artifact IDs. An Artifact changes a run rule, not one Recruit's item slot.", "展开三个固定神器;神器改变整轮规则,不占队员的唯一装备槽。"], mark: "◇" },
{ id: "equipment", code: "EQ-017", name: ["Bracehook Cuff", "支撑钩腕带"], detail: ["When a neighbor is pulled, the wearer may share the pull as a swap. Reward copy; sale value 0.", "相邻队员被拉动时,佩戴者可一同移动并交换位置。奖励装备;出售价值为0。"], mark: "⌁" },
{ id: "coin", code: "+13 COIN", name: ["Take the liquid budget", "领取流动金币"], detail: ["Half of reward budget 26, rounded down. Coin 46 -> 59.", "奖励预算26的一半并向下取整;金币46→59。"], mark: "+" },
];
const p84RecoveryOffers = [
{ id: "credit", name: ["Use Replacement Credit", "使用补员凭证"], cost: ["14 Credit · 0 Coin", "14凭证 · 0金币"], result: ["Hire Vela · keep all Coin · Credit 0", "招募 Vela · 保留全部金币 · 凭证归零"], tone: "safe" },
{ id: "coin", name: ["Pay Coin, keep Credit", "支付金币,保留凭证"], cost: ["0 Credit · 14 Coin", "0凭证 · 14金币"], result: ["Hire Vela · Credit remains 14", "招募 Vela · 仍保留14凭证"], tone: "work" },
{ id: "short", name: ["Continue short-handed", "三人短编继续"], cost: ["0 Credit · 0 Coin", "0凭证 · 0金币"], result: ["Company stays 3 · protected offer declined", "队伍维持3人 · 放弃受保护补员"], tone: "danger" },
];
const p83RiskOffers = [
{
id: "RSK-P83-01", name: ["Steady Crossing", "平稳渡口"], exposure: ["Low", "低"], reward: ["Equipment", "装备"],
pressure: ["Projectile screen", "远程屏障"], uncertainty: ["Full enemy order known", "敌方顺序完全已知"],
enemies: [["Pane Nibbler", "晶板啮兽"], ["Gust Finch", "疾风雀"]], coverage: ["Front hold ready; rear screen partial", "前排稳固;后排屏障应对一般"],
},
{
id: "RSK-P83-02", name: ["Shattered Terrace", "破碎台地"], exposure: ["Medium", "中"], reward: ["Artifact", "神器"],
pressure: ["Guarded front + rear pressure", "前排护盾 + 后排压力"], uncertainty: ["Second action order hidden", "第二次行动顺序未知"],
enemies: [["Prism Ox", "棱晶角兽"], ["Gust Finch", "疾风雀"], ["Pane Nibbler", "晶板啮兽"]], coverage: ["Reach ready; guard timing uncertain", "射程应对充分;护盾时机不确定"],
},
{
id: "RSK-P83-03", name: ["Bellward Detour", "钟鸣绕路"], exposure: ["High", "高"], reward: ["Profession", "职业线索"],
pressure: ["Tempo burst + unknown target", "速度爆发 + 未知目标"], uncertainty: ["One enemy identity remains unknown", "一名敌人的身份仍未知"],
enemies: [["Bell Warden", "钟卫"], ["Unknown", "未知身份"], ["Riftblade", "裂隙刃兽"]], coverage: ["Sustain ready; tempo counter absent", "续航充分;缺少速度克制"],
},
];
const p83BattleEvents = [
{ time: "00:00.0", title: ["Both formations enter", "双方横队进入战场"], detail: ["The accepted Company order enters from the left while the enemy line enters from the right.", "已确认的四人横队从左侧入场,敌方横队从右侧入场。"], kind: "entry" },
{ time: "00:01.8", title: ["Frontlines collide and stop", "前排接敌并停止推进"], detail: ["Wayrunner and Hearthguard form the melee screen at the marked contact boundary; Kindhand and Farstring keep rear spacing.", "迅行者与炉卫在标记的接触线上组成近战前排;愈手与远弦保持后排距离。"], kind: "contact" },
{ time: "00:03.2", title: ["Melee acts only after contact", "近战只在接敌后出手"], detail: ["Tomas retargets the nearest legal enemy and Mael follows with Shield Knock; the enemy front loses 28 HP.", "Tomas 改为攻击最近的合法敌人,Mael 随后盾击;敌方前排损失 28 点生命。"], kind: "melee" },
{ time: "00:04.8", title: ["Mael intercepts rear pressure", "Mael 拦截后排压力"], detail: ["Guard absorbs part of the hit aimed through the line. Mael is visibly injured from 100 to 72 HP; Sable remains safe.", "护盾吸收了穿过横队的部分攻击;Mael 的生命从 100 降至 72,Sable 保持安全。"], kind: "protect" },
{ time: "00:06.4", title: ["Ilya fires from behind the frontline", "Ilya 从前排后方射击"], detail: ["Farstring remains behind both melee footprints, launches one arrow and damages the legal rearmost enemy from 100 to 72 HP.", "远弦保持在两名近战之后,射出一箭,使合法的敌方后排生命从 100 降至 72。"], kind: "ranged" },
{ time: "00:08.0", title: ["Sable heals the injured ally", "Sable 治疗受伤队友"], detail: ["Kindhand selects Mael as the lowest-ratio injured ally and restores him from 72 to 88 HP; it is not a self-heal.", "愈手选择生命比例最低的受伤队友 Mael,将其生命从 72 恢复到 88;这不是自我治疗。"], kind: "heal" },
{ time: "00:09.6", title: ["Enemy rear demonstrates spell pressure", "敌方后排演示法术压力"], detail: ["A rear enemy uses the PF-B06 Riftcaller cast language: charge, forked bolt and impact. It is not a fifth Company member.", "敌方后排采用 PF-B06 裂隙召唤师的施法语言:蓄力、分叉法弹、命中;它不是我方第五名队员。"], kind: "spell" },
{ time: "00:11.2", title: ["The next frontline unit advances", "下一名存活前排自动补进"], detail: ["The demonstration front footprint stays down with no respawn; the next living enemy advances to the same contact boundary without Reserve entry.", "演示中的前排倒下后保持倒地且不会复活;下一名存活敌人前进到同一接触线,没有后备入场。"], kind: "replacement" },
{ time: "00:12.8", title: ["Behavior demonstration complete", "战斗行为演示完成"], detail: ["Positions and HP remain visible, while victory, defeat, casualties, rewards and recovery stay withheld for P8.4.", "站位和生命变化继续可见;胜负、伤亡、奖励与恢复仍留到 P8.4。"], kind: "resolved" },
];
const experienceSteps = [
["contract", "1", "Contract", "契约与难度"],
["offers", "2", "Company", "选择初始队伍"],
["review", "3", "Review", "创建前确认"],
["receipt", "4", "Receipt", "创建回执"],
];
const currentRecruitProfiles = {
"OFF-01": { level: 4, xp: 36, xpNext: 60, itemId: "EQ-002", item: "Frontpin Greaves" },
"OFF-02": { level: 3, xp: 44, xpNext: 50, itemId: "EQ-014", item: "Quiet Rearplate" },
"OFF-03": { level: 2, xp: 18, xpNext: 40, itemId: "EQ-001", item: "Stepback Soles" },
"OFF-04": { level: 2, xp: 32, xpNext: 40, itemId: "EQ-009", item: "Rescue Loop" },
};
function recruitProfile(offer, current = false) {
return currentRecruitProfiles[offer.id] && current
? { ...offer, ...currentRecruitProfiles[offer.id] }
: { level: 1, xp: 0, xpNext: 30, itemId: null, item: null, ...offer };
}
function compactRecruitBuild(profile) {
return `${bi("LV", "等级")} ${profile.level} · ${profile.itemId || bi("EMPTY ITEM", "无装备")}`;
}
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("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
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 `<button class="stage-icon" type="button" aria-label="${escapeHtml(label)}" title="${escapeHtml(label)}" ${attrs}>${symbol}</button>`;
}
function stageArt(index = 0) {
return `<img class="stage-art" src="${sceneImages[index % sceneImages.length]}" alt="" />`;
}
function actorImage(offer, className = "actor-art") {
const art = offer.art || actorAssets[offer.profession] || actorAssets.Hearthguard;
return `<img class="${className}" src="${art}" alt="${escapeHtml(localizedText(offer.profession))}" />`;
}
function equipmentImage(name, className = "equipment-art") {
const art = equipmentAssets[name] || equipmentAssets["Stepback Soles"];
return `<img class="${className}" src="${art}" alt="${escapeHtml(localizedText(name))}" />`;
}
function effectImage(kind, className = "battle-vfx") {
return `<img class="${className}" src="${vfxAssets[kind]}" alt="" aria-hidden="true" />`;
}
function conceptLabel() {
return `<span class="concept-label">${escapeHtml(t("concept"))}</span>`;
}
function stageHeader(screen, context = "Windglass Verge", resources = true) {
return `<div class="game-hud">
<div class="hud-left">${iconButton("‹", t("back"))}<span class="screen-stamp">${escapeHtml(screen.id)}</span><strong>${escapeHtml(localizedText(context))}</strong></div>
<div class="hud-right">${resources ? `<span class="resource"><b>214</b> Coin</span><span class="resource"><b>0</b> Credit</span>` : ""}<span class="save-state">◆ ${escapeHtml(t("saved"))}</span></div>
</div>`;
}
function actionDock(primary, secondary = t("inspect"), attrs = "") {
return `<div class="action-dock"><button class="stage-secondary" type="button">${escapeHtml(localizedText(secondary))}</button><button class="stage-primary" type="button" ${attrs}>${escapeHtml(localizedText(primary))}</button></div>`;
}
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 ? "" : options.artSrc ? `<img class="stage-art" src="${options.artSrc}" alt="" />` : stageArt(options.region || 0);
const header = options.header === false ? "" : stageHeader(screen, options.context, options.resources !== false);
return `<section class="${classes}" data-stage-id="${escapeHtml(screen.id)}" data-variant="${escapeHtml(options.variant || "primary")}" aria-label="${escapeHtml(screenName(screen))}">
${art}<div class="stage-wash"></div>${header}<div class="stage-body">${body}</div>${conceptLabel()}
</section>`;
}
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 = `<div class="contract-composition">
<div class="region-signal"><span>RG-01</span><h2>Windglass Verge</h2><p>Contract 01 - chart the suspended pane crossing.</p></div>
<div class="contract-sheet"><span class="stage-kicker">NEW CONTRACT</span><h3>Trade Route: First Crossing</h3>
<dl><div><dt>Known pressure</dt><dd>Push + projectile screen</dd></div><div><dt>Boss rule</dt><dd>Rotating damage-family panes</dd></div><div><dt>Reward breadth</dt><dd>Base professions + opening tools</dd></div><div><dt>Difficulty</dt><dd>Standard - full systems</dd></div></dl>
<button class="stage-primary" type="button" data-flow-next>Load Provisional Offers</button></div>
</div>`;
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"
? `<div class="operation-state"><span class="operation-spinner"></span><strong>Creating run</strong><small>Operation RUN-CREATE-014</small></div>`
: variant === "receipt"
? `<div class="receipt-state"><b>✓ RUN-CREATE-014</b><strong>${t("company")} 2 <i>/</i> ${t("capacity")} 4</strong><small>The two unchosen offers were not retained.</small></div>`
: `<div class="creation-ledger"><span>No Coin transaction</span><b>Slot 01 · Standard</b><small>Two unchosen stable IDs will not be retained.</small></div>`;
const body = `<div class="creation-review"><span class="stage-kicker">${variant === "receipt" ? "RUN RECEIPT" : "COMPANY REVIEW"}</span>
<div class="chosen-pair">${chosen.map((offer) => recruitMarker(offer, true)).join("")}</div>
<div class="unchosen-strip">${unchosen.map((offer) => `<span><b>${offer.id}</b> ${offer.name}<em>${t("notRetained")}</em></span>`).join("")}</div>
${stateBlock}
<div class="review-actions"><button class="stage-secondary" type="button" data-flow-prev>${variant === "review" ? "Revise Recruits" : t("back")}</button>${variant !== "pending" ? `<button class="stage-primary" type="button" data-flow-next>${variant === "receipt" ? t("openMarket") : t("createRun")}</button>` : ""}</div>
</div>`;
return frame(screen, body, { variant, resources: false });
}
const body = `<div class="opening-composition">
<div class="opening-title"><span class="stage-kicker">PROVISIONAL COMPANY</span><h2>Choose two generated recruits</h2><p>Offer set WIND-01-A remains stable until run creation.</p></div>
<div class="offer-grid">${generatedOffers.map((offer) => recruitOffer(offer, selected.includes(offer.id))).join("")}</div>
<div class="opening-footer"><div class="selection-count"><span>${selected.length === 2 ? t("selected") : "Choose two"}</span><strong>${selected.length}/2</strong></div><button class="stage-secondary" type="button">${t("compare")}</button><button class="stage-primary" type="button" data-flow-next ${selected.length === 2 ? "" : "disabled"}>${t("reviewCompany")}</button></div>
</div>`;
return frame(screen, body, { variant, resources: false });
}
function recruitMarker(offer, selected = false, current = false) {
const profile = recruitProfile(offer, current);
return `<article class="recruit-marker ${selected ? "is-selected" : ""}"><span class="recruit-silhouette">${actorImage(offer, "recruit-marker-art")}</span><div><small>${offer.id}</small><strong>${localizedText(offer.name)}</strong><em>${localizedText(offer.profession)} · ${compactRecruitBuild(profile)}</em></div></article>`;
}
function recruitOffer(offer, selected) {
const profile = recruitProfile(offer);
return `<article class="recruit-offer ${selected ? "is-selected" : ""}">
<div class="offer-figure">${actorImage(offer, "offer-character")}<span class="offer-role-mark">${offer.mark}</span><small>${offer.id}</small></div>
<div class="offer-copy"><strong>${localizedText(offer.name)}</strong><span class="profession-seal">${localizedText(offer.profession)}</span><div class="offer-loadout"><span>${bi("LV", "等级")} ${profile.level}</span><span>${profile.itemId} · ${localizedText(profile.item)}</span></div><p><b>${localizedText(offer.trait)}</b> - ${localizedText(offer.read)}</p></div>
<div class="offer-actions"><span>${t("noCoin")}</span><button type="button" data-offer-id="${offer.id}" aria-pressed="${selected}">${selected ? `✓ ${t("selected")}` : t("select")}</button>${iconButton("i", `${t("inspect")} ${offer.name}`)}</div>
</article>`;
}
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 profile = recruitProfile(offer);
const body = `<div class="market-review">
<div class="review-figure">${recruitMarker(offer, true)}<span class="trait-glyph">⬡</span></div>
<div class="review-facts"><span class="stage-kicker">RECRUIT REVIEW</span><h2>${localizedText(offer.name)}</h2><h3>${localizedText(offer.profession)} · ${localizedText(offer.trait)}</h3>
<div class="recruit-level-strip"><strong>${bi("INDIVIDUAL LEVEL", "角色独立等级")} · LV ${profile.level}</strong><span>XP ${profile.xp} / ${profile.xpNext}</span><span>${bi("ONE EQUIPMENT SLOT", "唯一装备槽")} · ${profile.itemId} ${localizedText(profile.item)}</span></div>
<div class="stat-radar"><span>VIT <b>A</b></span><span>PACE <b>C</b></span><span>REACH <b>${offer.profession === "Hearthguard" ? "1" : "2"}</b></span><span>GUARD <b>B</b></span></div>
<p>${localizedText(offer.read)}。${state.locale === "zh_CN" ? "随机生成身份;每人只有一个机械特性,不包含角色关系或个人剧情。" : "Generated identity, exactly one Trait, no authored personal arc."}</p></div>
<div class="party-impact"><span>PARTY IMPACT</span><strong>${t("company")} ${count} → ${Math.min(4, count + 1)}</strong><p>${offer.profession === "Hearthguard" ? "Adds a visible front intercept." : "Adds position movement and target change."}</p><small>Price 14 Coin · Operation HIRE-${variant === "hire2" ? "002" : "001"}</small></div>
${actionDock("Recruit - 14 Coin", t("compare"), "data-flow-next")}
</div>`;
return frame(screen, body, { variant });
}
if (isItem) {
const body = `<div class="item-review"><div class="item-object">${equipmentImage("Stepback Soles", "item-offer-art")}<small>EQ-001</small></div><div><span class="stage-kicker">ITEM OFFER</span><h2>Stepback Soles</h2><p>After the first direct hit, step one position rearward if legal.</p><div class="delta-row"><span>Target exposure</span><b>Front → Center</b></div><div class="delta-row danger"><span>Next basic</span><b>Wait +1 tick</b></div></div><aside><b>Eligible</b><span>Nia · Tomas · Sable</span><b>Conflict</b><span>Full rear makes trigger dormant</span></aside>${actionDock("Buy - 12 Coin", t("inspect"))}</div>`;
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 = `<div class="market-composition reference-informed-market">
<div class="market-heading"><div><span class="stage-kicker">${bi("INTEGRATED MARKET", "一体化集市")}</span><h2>${bi("Company, offers and inventory stay visible", "队伍、报价与库存保持同屏")}</h2></div><div class="company-meter"><span>${t("company")}</span><strong>${count}</strong><i>/</i><span>${t("capacity")}</span><strong>4</strong></div></div>
<div class="market-board">
<section class="market-company-bench"><header><strong>${bi("COMPANY BENCH", "队伍席位")}</strong><span>${bi("Tap Recruit to inspect or reorder", "点击队员查看或调整顺序")}</span></header><div>${company.map((offer, index) => { const profile = recruitProfile(offer, true); return `<button type="button" class="bench-recruit"><span>${actorImage(offer, "bench-actor-art")}</span><strong>${localizedText(offer.name)}</strong><small>LV ${profile.level} · ${profile.itemId} · ${index === count - 1 ? bi("Front", "前排") : bi(`Position ${count - 1 - index}`, `${count - 1 - index}号位`)}</small></button>`; }).join("")}${Array.from({ length: 4 - count }, (_, index) => `<button type="button" class="bench-recruit is-empty"><span>+</span><strong>${bi("Open slot", "空席位")}</strong><small>${bi("Hire below", "从下方招募")}</small></button>`).join("")}</div></section>
<nav class="market-tool-rail" aria-label="${bi("Market modes", "集市模式")}"><button class="is-active" type="button">${bi("Recruit", "招募")}</button><button type="button">${bi("Equipment", "装备")}</button><button type="button">${bi("Risk", "风险")}</button><button type="button">${bi("Sell mode", "出售模式")}</button></nav>
<section class="market-offer-cards">${offers.map((offer, index) => { const profile = recruitProfile(offer); return `<button class="market-offer-card ${index === 0 ? "is-focused" : ""}" type="button" ${index === 0 ? "data-flow-next" : ""}><span class="row-avatar">${actorImage(offer, "market-offer-art")}</span><div><small>${offer.id}</small><strong>${localizedText(offer.name)}</strong><em>${localizedText(offer.profession)} · LV ${profile.level} · ${profile.itemId}</em><p>${localizedText(offer.read)}</p></div><b>${index < 2 ? "46" : "58"}<small>${bi("Coin", "金币")}</small></b></button>`; }).join("")}</section>
<section class="market-inventory"><header><strong>${bi("RUN INVENTORY", "本轮库存")}</strong><span>4 / 12 · ${bi("Tap item to compare", "点击物品进行比较")}</span></header><div>${Array.from({ length: 12 }, (_, index) => { const item = inventory[index]; const inventoryNames = ["Stepback Soles", "Frontpin Greaves", "Rescue Loop", "Open-Line Cord"]; return `<button class="inventory-slot ${item ? "is-filled" : ""}" type="button">${item ? equipmentImage(inventoryNames[index], "inventory-item-art") : ""}<small>${item ? item[0] : bi("Empty", "空")}</small></button>`; }).join("")}</div></section>
</div>
<div class="market-footer"><span>${bi("Refresh 18 Coin · next 24", "刷新18金币 · 下次24金币")}</span><button class="stage-secondary">${bi("Lock 0/2", "锁定 0/2")}</button><button class="stage-secondary">${bi("Selling mode off", "出售模式:关闭")}</button><button class="stage-primary" data-flow-next>${count < 4 ? bi("Review focused Recruit", "审核所选队员") : bi("Open Party Line", "打开队伍站位")}</button></div>
</div>`;
return frame(screen, body, { variant });
}
function partyActors() {
const original = [
["3", "Nia", generatedOffers[1], "Rear"], ["2", "Sable", generatedOffers[3], "Protected"], ["1", "Tomas", generatedOffers[2], "Center"], ["0", "Elian", generatedOffers[0], "Exposed"],
];
const swapped = [original[0], original[2], original[1], original[3]];
return (state.partySwapped ? swapped : original).map(([position, name, offer, status], index) => { const profile = recruitProfile(offer, true); return `<button class="party-actor ${position === "0" ? "is-front" : ""}" type="button" data-party-index="${index}" title="${localizedText(status)} · ${localizedText(profile.item)}"><span class="actor-figure">${actorImage(offer, "party-actor-art")}</span><b>${position}</b><strong>${name}<small>${localizedText(status)}</small></strong><em>LV ${profile.level} · ${profile.itemId}</em></button>`; }).join("");
}
function renderPreparation(screen, variant) {
if (screen.id.startsWith("EQP")) {
const comparison = screen.id === "EQP-002";
const equipmentRows = [["Stepback Soles", "EQ-001"], ["Frontpin Greaves", "EQ-002"], ["Rescue Loop", "EQ-009"], ["Open-Line Cord", "EQ-012"]];
const body = `<div class="equipment-composition ${comparison ? "is-comparison" : ""}">
<div class="equipment-list"><span class="stage-kicker">ELIGIBLE INVENTORY</span>${equipmentRows.map(([name, id], index) => `<button class="equipment-row ${index === 0 ? "is-focused" : ""}"><span>${equipmentImage(name, "equipment-row-art")}</span><div><small>${id}</small><strong>${name}</strong><em>Behavior class B</em></div></button>`).join("")}</div>
<div class="equipment-wearer"><span class="one-slot-badge">${bi("ONE EQUIPMENT SLOT", "唯一装备槽")}</span><span class="large-figure">${actorImage(generatedOffers[1], "equipment-wearer-art")}</span><strong>Nia Orrow</strong><small>Farstring · LV 3 · XP 44/50 · position 3</small><div class="equipped-slot"><b>${bi("CURRENT SLOT", "当前装备槽")}</b>${localizedText("Quiet Rearplate")} · EQ-014</div></div>
<div class="equipment-delta"><span class="stage-kicker">BEHAVIOR DELTA</span><h2>${comparison ? "Quiet Rearplate → Stepback Soles" : "Stepback Soles"}</h2><div class="delta-row"><span>${bi("Slot after commit", "确认后的装备槽")}</span><b>EQ-001 · ${localizedText("Stepback Soles")}</b></div><div class="delta-row"><span>${bi("Prior item", "原装备")}</span><b>${bi("EQ-014 returns to inventory", "EQ-014 退回本轮库存")}</b></div><div class="delta-row"><span>After first hit</span><b>Step rear</b></div><div class="delta-row danger"><span>Next basic</span><b>Wait +1</b></div><p>At full rear the trigger is dormant. Apply remains reversible until readiness commit.</p><button class="stage-primary" type="button" ${variant === "adapt" ? "data-flow-next" : ""}>Apply candidate</button></div>
</div>`;
return frame(screen, body, { variant });
}
const ready = screen.id === "PTY-002";
const body = `<div class="party-composition ${ready ? "is-ready" : ""}">
<div class="party-title"><span class="stage-kicker">${ready ? "FROZEN PARTY SNAPSHOT" : "ORDERED PARTY LINE"}</span><h2>${t("protected")} <i>→</i> ${t("exposed")}</h2><p>Position 0 is always the rightmost front. The line is never mirrored by locale.</p></div>
<div class="relation-layer"><span class="relation protection">Sable protects 0</span><span class="relation reach">Nia reaches enemy rear</span></div>
<div class="party-cord"><span class="cord-label rear">${t("protected")}</span>${partyActors()}<span class="cord-label front">${t("exposed")}</span></div>
<aside class="party-read"><strong>${ready ? "Plan accepted" : "Changed relations"}</strong><span>Elian · first legal target</span><span>Nia · projectile screen risk</span><span>Reserve · 1 living recruit</span></aside>
<div class="party-tools">${iconButton("↶", "Undo")}${iconButton("↺", "Reset")}<button class="stage-secondary" type="button" data-party-swap>${ready ? "Inspect order" : "Swap positions 1 / 2"}</button><button class="stage-primary" type="button" data-flow-next>${ready ? "Accept preparation" : "Save Order"}</button></div>
</div>`;
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 ? `<div class="risk-review"><span class="stage-kicker">RISK COMMITMENT</span><h2>Steady Crossing</h2><div class="threat-band"><span>Known threat<b>Projectile screen</b></span><span>Entry cost<b>0 Coin</b></span><span>Reward class<b>Equipment</b></span><span>Retreat<b>One Coin tier</b></span></div><div class="mini-party-line">${partyActors()}</div><p>The accepted order is frozen with the risk receipt. Selection alone changed nothing.</p>${actionDock("Begin Battle", "Return to Risk Board", "data-flow-next")}</div>` : `<div class="risk-board"><div class="risk-heading"><span class="stage-kicker">THREE BOUNDED OFFERS</span><h2>Choose pressure, not a hidden result</h2></div><div class="risk-options">${risks.map((risk, index) => `<button class="risk-option ${index === 0 ? "is-selected" : ""}" type="button"><span>0${index + 1}</span><strong>${localizedText(risk[0])}</strong><small>${localizedText(risk[1])}</small><div><em>${localizedText(risk[2])}</em><b>${localizedText(risk[3])} ${localizedText("exposure")}</b></div></button>`).join("")}</div><div class="risk-compare"><span>Coverage</span><b>Front hold ✓</b><b>Rear screen △</b><b>Recovery 214 Coin</b><button class="stage-primary" type="button" data-flow-next>Review Steady Crossing</button></div></div>`;
return frame(screen, body, { variant });
}
function battleLine() {
const enemies = Array.from({ length: 4 }, (_, index) => `<span class="enemy-actor ${index === 3 ? "elite" : ""}"><img src="${actorAssets.Riftblade}" alt="Riftblade" /><small>${index === 3 ? "Prism screen" : index % 2 ? "Guard chip" : "Rear step"}</small></span>`).join("");
return `<div class="battle-line"><div class="allied-line"><span class="edge-label">${t("protected")}</span>${partyActors()}<span class="edge-label">${t("exposed")}</span></div><div class="action-gap"><span class="forecast-arrow">Pane Bite · 0.8s → Elian</span>${effectImage("threat")}</div><div class="enemy-line">${enemies}<span class="boss-actor"><img src="${actorAssets.TempestSeraph}" alt="Tempest Seraph" />${effectImage("tempest", "boss-vfx")}<small>Phase 1</small></span></div></div>`;
}
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 = `<div class="battle-static">${battleLine()}<div class="battle-sheet"><span class="stage-kicker">${id}</span><h2>${title}</h2>${rows.map((row) => `<div class="sheet-row">${row}</div>`).join("")}<div class="speed-control"><button class="is-active">1x</button><button>2x</button><button>4x</button></div>${actionDock(primary, id === "BAT-005" ? "Cancel" : "Event log")}</div></div>`;
return frame(screen, body, { variant });
}
const inspect = id === "BAT-003";
const opening = id === "BAT-001";
const body = `<div class="battle-composition ${state.battlePaused ? "is-paused" : ""}">
<div class="battle-objective"><span class="stage-kicker">${opening ? "BATTLE OPENING" : "AUTOMATIC BATTLE"}</span><h2>${opening ? "Break the rotating pane screen" : "Urgent: Guard chip reaches Sable"}</h2><p>${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."}</p><strong class="battle-life-rule">NO COMBAT RESPAWN · NO RESERVE AUTO-ENTRY</strong></div>
<div class="playback-tools">${iconButton(state.battlePaused ? "▶" : "Ⅱ", state.battlePaused ? "Resume" : "Pause", "data-battle-pause")}
<div class="speed-control">${[1, 2, 4].map((speed) => `<button class="${state.battleSpeed === speed ? "is-active" : ""}" data-battle-speed="${speed}">${speed}x</button>`).join("")}</div>${iconButton("i", "Inspect event", "data-open-screen=\"BAT-003\"")}</div>
${battleLine()}
<div class="battle-event-log"><span>00:07.2 · Gust Finch targets Nia</span><b>00:08.0 · Elian intercepts Pane Bite</b><span>00:08.8 · Sable gains Remedy</span></div>
${inspect ? `<aside class="inspect-sheet"><span class="stage-kicker">PINNED CAUSE</span><h3>Elian intercepts Pane Bite</h3><dl><div><dt>Source</dt><dd>Hearthguard · Guard 1</dd></div><div><dt>Target</dt><dd>Sable · position 2</dd></div><div><dt>Order effect</dt><dd>Neighbor protection</dd></div><div><dt>Next cause</dt><dd>Guard reaches zero</dd></div></dl><button class="stage-primary" data-flow-next>Pin and continue</button></aside>` : ""}
<div class="caption-line">[Front recruit: Guard consumed] [Enemy forecast: rear projectile]</div>
${opening ? actionDock("Start observation", "Inspect objective", "data-flow-next") : ""}
</div>`;
return frame(screen, body, { variant });
}
function renderOutcome(screen, variant) {
if (screen.id === "OUT-001") {
const body = `<div class="result-composition"><div class="result-title"><span class="outcome-seal">VICTORY</span><div><span class="stage-kicker">FIRST MEANINGFUL DIVERGENCE</span><h2>Guard broke before the second rear shot</h2><p>Expected protection ended at 00:18.4; actual pressure reached Nia at 00:19.1.</p></div></div><div class="causal-timeline"><div class="expected-track"><b>EXPECTED</b><span>00:17 Guard 1</span><span>00:19 Rear shot blocked</span><span>00:22 Stable line</span></div><div class="actual-track"><b>ACTUAL</b><span>00:17 Pane chip</span><span class="is-divergence">00:18 Guard 0</span><span>00:19 Nia hit</span></div><svg aria-hidden="true" viewBox="0 0 100 20" preserveAspectRatio="none"><path d="M34 3 C42 3 42 17 51 17" /></svg></div><div class="result-effects"><span><b>Cause</b>Second uninterrupted Pane Bite</span><span><b>Order</b>Sable remained adjacent</span><span><b>Adapt</b>Move or interrupt before bite 2</span></div>${actionDock("Continue to Reward", "Full event log", "data-flow-next")}</div>`;
return frame(screen, body, { variant });
}
if (screen.id === "OUT-002") {
const body = `<div class="loss-composition"><div class="fallen-figure">${actorImage(generatedOffers[1], "fallen-actor-art")}<i>FALLEN</i></div><div class="loss-record"><span class="stage-kicker">COMMITTED LOSS</span><h2>Nia Orrow</h2><p>Farstring · Patient Start · LV 3 · XP 44/50 · survived 4 battles</p><div class="loss-row"><span>${bi("Individual progress lost", "失去的个人成长")}</span><b>LV 3 · XP 44/50</b></div><div class="loss-row"><span>${bi("One equipped item", "唯一装备")}</span><b>EQ-014 ${bi("returned to inventory", "退回本轮库存")}</b></div><div class="loss-row"><span>Party gap</span><b>Rear pressure lost</b></div><div class="loss-row"><span>Replacement Credit</span><b>+1 tier</b></div><small>Death is permanent for this run. No relationship or personal-arc system is implied.</small></div>${actionDock("Acknowledge loss", "Inspect receipt", "data-flow-next")}</div>`;
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 rewardArt = [equipmentImage("Stillroad Wrap", "reward-item-art"), effectImage("protect", "reward-item-art"), actorImage(generatedOffers[1], "reward-actor-art")];
const body = `<div class="reward-composition"><div class="reward-heading"><span class="stage-kicker">FIXED REWARD DRAFT</span><h2>Choose the next decision you want to change</h2></div><div class="reward-options">${rewards.map((reward, index) => `<button class="reward-option ${index === 0 ? "is-selected" : ""}"><span class="reward-object">${rewardArt[index]}</span><small>${reward[0]}</small><strong>${reward[1]}</strong><p>${reward[2]}</p><em>${index === 0 ? "Selected" : "Compare"}</em></button>`).join("")}</div>${actionDock("Claim Quiet Rearplate", "Compare build", "data-flow-next")}</div>`;
return frame(screen, body, { variant });
}
if (screen.id === "OUT-004") {
const body = `<div class="recovery-composition"><div class="recovery-heading"><span class="stage-kicker">RECOVERY</span><h2>Company remains viable</h2><p>Replacement Credit reduces only a later hire. Lost level, Trait, profession and history stay lost.</p></div><div class="recovery-paths"><button class="recovery-path is-selected"><span>01</span><strong>Protected base hire</strong><small>46 → 28 Coin · consumes 1 Credit</small><p>Return with a new base profession at Market.</p></button><button class="recovery-path"><span>02</span><strong>Continue short-handed</strong><small>0 Coin · Company 3 / capacity 4</small><p>Keep resources and accept reduced coverage.</p></button><button class="recovery-path"><span>03</span><strong>Abandon run</strong><small>Run-close transaction</small><p>Retain history and committed progression only.</p></button></div>${actionDock("Commit protected hire", "Review consequences", "data-flow-next")}</div>`;
return frame(screen, body, { variant });
}
if (screen.id === "OUT-005") {
const body = `<div class="run-end-composition"><header><span class="outcome-seal">${bi("RUN CLOSED", "远征已结束")}</span><div><small>${bi("COMPANY COLLAPSE · CONTRACT 05", "队伍覆灭 · 契约05")}</small><h2>${bi("The route ends; the Guild keeps what was learned", "本次路线结束,公会保留已获得的知识")}</h2></div></header><section class="run-end-company">${generatedOffers.map((offer) => `<span>${actorImage(offer, "run-end-actor")}<b>${localizedText(offer.profession)}</b></span>`).join("")}</section><div class="run-end-ledger"><article><span>${bi("RETAINED", "永久保留")}</span><strong>${bi("Renown", "声望")} +4</strong><strong>${bi("Discoveries", "发现")} +7</strong><strong>${bi("Records", "历史记录")} +1</strong></article><article class="is-cleared"><span>${bi("CLEARED", "本轮清除")}</span><strong>${bi("Coin", "金币")} 86 → 0</strong><strong>${bi("Credit", "补员凭证")} 1 → 0</strong><strong>${bi("Living Company", "存活队伍")} 0</strong></article><aside><b>${bi("Next meaningful action", "下一步有意义的操作")}</b><p>${bi("Spend Renown on future-run breadth or create an ordinary new run.", "使用声望扩展未来远征的选择,或创建普通新远征。")}</p></aside></div>${actionDock(bi("Open Guild Charter", "打开公会宪章"), bi("Return to Title", "返回标题"))}</div>`;
return frame(screen, body, { variant, resources: false });
}
const body = `<div class="abandon-composition"><section><span class="stage-kicker">${bi("RUN ABANDONMENT REVIEW", "放弃远征审核")}</span><h2>${bi("End this expedition?", "确定结束本轮远征?")}</h2><p>${bi("This is a run-close transaction, not a retreat. It cannot be undone after the receipt is committed.", "这是结束本轮远征的事务,不是撤退;回执提交后无法撤销。")}</p><div class="abandon-impact"><span><b>${bi("Cleared", "清除")}</b>${bi("Coin 214 · Credit 1 · current Company", "金币214 · 补员凭证1 · 当前队伍")}</span><span><b>${bi("Retained", "保留")}</b>${bi("Records · discoveries · committed Guild breadth", "历史记录 · 发现 · 已提交的公会广度")}</span><span><b>${bi("Not granted", "不会获得")}</b>${bi("Battle reward · casualty Credit · completion bonus", "战斗奖励 · 伤亡补员凭证 · 完成奖励")}</span></div></section><aside><span>OUT-006</span><strong>${bi("HOLD TO CONFIRM", "按住确认")}</strong><div class="hold-track"><i></i></div><button class="stage-secondary">${bi("Keep playing", "继续远征")}</button><button class="stage-danger">${bi("Hold to abandon run", "按住放弃远征")}</button></aside></div>`;
return frame(screen, body, { variant, resources: false });
}
function renderRoster(screen, variant) {
if (screen.id === "REC-003") {
const body = `<div class="dismissal-review"><div class="review-figure">${recruitMarker(generatedOffers[2], true, true)}</div><div><span class="stage-kicker">DESTRUCTIVE REVIEW</span><h2>Dismiss Tomas Vale?</h2><div class="sheet-row">LV 2 · XP 18/40 is lost</div><div class="sheet-row">One equipped item · EQ-001 returns</div><div class="sheet-row">No Replacement Credit</div><div class="sheet-row">Living Company 4 → 3</div><p>Eligibility is revalidated at commit. The last living recruit can never be dismissed.</p></div>${actionDock("Dismiss Recruit", "Cancel")}</div>`;
return frame(screen, body, { variant });
}
const roster = generatedOffers.concat([{ id: "RC-017", name: "Miro Sen", profession: "Coilsmith", trait: "Deep Pocket", mark: "CS", level: 2, xp: 6, xpNext: 40, itemId: null, item: null }]);
const focusedOffer = screen.id === "REC-002" ? generatedOffers[1] : generatedOffers[3];
const focusedProfile = recruitProfile(focusedOffer, true);
const body = `<div class="roster-composition"><div class="roster-list"><span class="stage-kicker">LIVING COMPANY · 5</span>${roster.map((offer, index) => `<button class="roster-row ${index === 1 ? "is-focused" : ""}">${recruitMarker(offer, index < 4, index < 4)}<span>${index < 4 ? `Position ${3 - index}` : "Reserve"}</span></button>`).join("")}</div><aside class="recruit-detail"><span class="large-figure">${actorImage(focusedOffer, "recruit-detail-art")}</span><h2>${screen.id === "REC-002" ? "Nia Orrow" : "Sable Kest"}</h2><p>${screen.id === "REC-002" ? "Farstring · Patient Start" : "Kindhand · Shared Cup"}</p><div class="level-progress"><span>${bi("INDIVIDUAL LEVEL", "角色独立等级")}</span><strong>LV ${focusedProfile.level}</strong><b>XP ${focusedProfile.xp} / ${focusedProfile.xpNext}</b><i style="--xp:${Math.round(focusedProfile.xp / focusedProfile.xpNext * 100)}%"></i></div><div class="detail-grid"><span>Readiness<b>Ready</b></span><span>Position<b>3 · Rear</b></span><span>${bi("One equipment slot", "唯一装备槽")}<b>${focusedProfile.itemId} · ${localizedText(focusedProfile.item)}</b></span><span>History<b>4 survived</b></span></div><button class="stage-secondary">Open Equipment</button><button class="stage-primary">Open Party Line</button></aside></div>`;
return frame(screen, body, { variant });
}
function renderGuild(screen, variant) {
const body = screen.id === "HUB-001" ? `<div class="guild-composition"><div class="guild-pressure"><span class="stage-kicker">CURRENT PRESSURE</span><h2>Rear projectile at the next crossing</h2><p>Gust Finch enters behind a visible Pane screen.</p></div><div class="guild-destinations"><button>Market <b>3 / 4 / 3</b></button><button>Roster <b>5 living</b></button><button>Equipment <b>1 new</b></button><button>Party Line <b>Ready</b></button><button class="is-primary">Risk Board <b>3 offers</b></button></div><div class="muster-strip">${partyActors()}</div><div class="receipt-strip"><b>Latest receipt</b><span>Quiet Rearplate claimed · no unresolved transaction</span></div></div>` : `<div class="forecast-composition"><div class="forecast-horizon"><span>NOW</span><i></i><span>ROUND 4</span><i></i><span>BOSS MUSTER</span></div><div class="forecast-sheets"><article><span>KNOWN</span><h3>Gust Finch</h3><p>Targets rearmost ally, then steps forward.</p><b>Counter: screen or pull</b></article><article><span>KNOWN</span><h3>Pane Nibbler</h3><p>Second uninterrupted bite chips neighbor Guard.</p><b>Counter: reorder or interrupt</b></article><article class="uncertain"><span>PARTIAL</span><h3>Third identity</h3><p>Family known: forced movement.</p><b>Confidence 68%</b></article></div></div>`;
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 = `<div class="charter-composition">
<header class="charter-heading"><div><span class="stage-kicker">${bi("GUILD CHARTER · BREADTH ONLY", "公会宪章 · 仅扩展玩法广度")}</span><h2>${bi("Shape future Companies", "规划未来队伍的选择空间")}</h2><p>${bi("Tap a Region, then Tap a packet. Every claim changes future-run options, never permanent combat power.", "点击区域,再点击扩展包。所有领取只改变未来远征的选择,不增加永久战斗数值。")}</p></div><div class="renown-balance"><span>${bi("RENOWN", "声望")}</span><strong>34</strong><small>${bi("Persistent breadth resource", "永久广度资源")}</small></div></header>
<div class="charter-track" aria-label="${bi("Eight Region seals", "八个区域印章")}">${guildCharterRegions.map((entry, index) => `<button class="charter-seal ${index < 2 ? "is-complete" : ""} ${index === state.charterRegionIndex ? "is-current" : ""}" type="button" data-charter-region="${index}"><span>${entry[0]}</span><strong>${bi(entry[1], entry[2])}</strong><em>${index < 2 ? bi("CLAIMED", "已完成") : index === state.charterRegionIndex ? bi("SELECTED", "已选中") : bi("INSPECT", "查看")}</em></button>`).join("<i></i>")}</div>
<div class="charter-workspace"><section class="charter-packets"><div class="charter-section-title"><span>${region[0]}</span><strong>${regionName}</strong><small>${bi("Tap one packet to preview", "点击一个扩展包查看详情")}</small></div>${charterPackets.map((entry, index) => `<button class="charter-packet ${index === state.charterPacketIndex ? "is-selected" : ""}" type="button" data-charter-packet="${index}"><span>${region[0]}-0${index + 1} · ${bi(entry.family[0], entry.family[1])}</span><strong>${state.charterRegionIndex === 2 ? bi(entry.title[0], entry.title[1]) : `${regionName} · ${bi(entry.family[0], entry.family[1])}`}</strong><small>${charterPrerequisite(index, region)}</small><b>${entry.cost} ${bi("Renown", "声望")}</b></button>`).join("")}</section>
<aside class="charter-review"><span class="stage-kicker">${bi("CLAIM PREVIEW", "领取预览")}</span><h3>${state.charterRegionIndex === 2 ? bi(packet.title[0], packet.title[1]) : `${regionName} · ${bi(packet.family[0], packet.family[1])}`}</h3><p>${bi(packet.effect[0], packet.effect[1])}</p><dl><div><dt>${bi("Prerequisite", "前置条件")}</dt><dd>✓ ${charterPrerequisite(state.charterPacketIndex, region)}</dd></div><div><dt>${bi("Renown", "声望")}</dt><dd>34 → ${remaining}</dd></div><div><dt>${bi("Applies", "生效范围")}</dt><dd>${bi("Future generated runs", "未来新生成的远征")}</dd></div></dl><strong class="no-power-boundary">${bi("NO PERMANENT COMBAT POWER", "不增加永久战斗数值")}</strong><div><button class="stage-secondary" type="button">${bi("Inspect prerequisite", "查看前置条件")}</button><button class="stage-primary" type="button">${bi(`Claim for ${packet.cost} Renown`, `花费${packet.cost}声望领取`)}</button></div></aside>
</div>
</div>`;
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 = `<div class="chronicle-composition"><header class="chronicle-heading"><span class="stage-kicker">${bi("FINAL BOSS SETTLED · FIRST CLEAR", "最终首领已结算 · 首次通关")}</span><h2>${bi("How will the Guild record this victory?", "公会将如何记录这场胜利?")}</h2><p>${bi("Both choices close this run and archive the surviving Company.", "两个选择都会结束本轮远征,并把幸存队伍存入历史记录。")}</p></header>
<div class="archive-contract"><span>✓</span><div><strong>${bi("SHARED RESULT", "共同结果")}</strong><p>${bi("Company archived as Records · Coin and Credit close · Renown and discoveries retained · no Recruit remains playable", "队伍存入记录 · 金币与补员凭证清零 · 保留声望与发现 · 不会保留任何可继续使用的队员")}</p></div></div>
<div class="chronicle-choices"><button class="chronicle-choice ${choice === "chronicle" ? "is-selected" : ""}" type="button" data-chronicle-choice="chronicle"><span>01</span><small>${bi("FINITE COMPLETION", "有限结局")}</small><strong>${bi("Complete the First Chronicle", "完成首部编年史")}</strong><p>${bi("Honor this Company in the Final Ledger, then return to Title.", "在最终账册中纪念这支队伍,然后返回标题页。")}</p></button><button class="chronicle-choice ${choice === "oath" ? "is-selected" : ""}" type="button" data-chronicle-choice="oath"><span>02</span><small>${bi("POSTGAME CONTINUATION", "终局后继续")}</small><strong>${bi("Open the Oath Season", "开启誓约季")}</strong><p>${bi("Archive this Company, review optional laws, and start a new generated run.", "存档这支队伍,审核可选法则,并开始一轮全新生成的远征。")}</p></button></div>
<aside class="chronicle-review"><div><span>${bi("SELECTED DESTINATION", "已选去向")}</span>${details.map((entry) => `<strong>✓ ${entry}</strong>`).join("")}</div><button class="stage-secondary" type="button">${bi("Ordinary New Run", "普通新远征")}</button><button class="stage-primary" type="button">${choice === "chronicle" ? bi("Confirm Chronicle", "确认完成编年史") : bi("Confirm Oath Season", "确认开启誓约季")}</button></aside>
</div>`;
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 = `<div class="oath-composition"><header class="oath-heading"><div><span class="stage-kicker">${bi("OATH SEASON · NEW GENERATED RUN", "誓约季 · 全新生成远征")}</span><h2>${bi("Choose laws that change valuation", "选择会改变决策价值的法则")}</h2><p>${bi("The cleared Company is archived and cannot be selected here.", "已通关队伍已存档,无法在这里继续选择。")}</p></div><button class="stage-secondary" type="button">${bi("Ordinary New Run", "普通新远征")}</button></header>
<div class="oath-workspace"><section class="oath-laws">${oathLawCatalog.map((law, index) => `<button class="oath-law ${selected.has(law.id) ? "is-selected" : ""}" type="button" data-oath-law="${law.id}"><span>OATH-0${index + 1}</span><strong>${bi(law.title[0], law.title[1])}</strong><p>${bi(law.effect[0], law.effect[1])}</p><em>${bi(law.status[0], law.status[1])}</em></button>`).join("")}</section><aside class="oath-review"><span class="stage-kicker">${bi("RUN REVIEW", "远征审核")}</span><h3>${selected.size} ${bi("active law(s)", "条启用法则")}</h3><div class="sheet-row">${bi("Difficulty", "难度")} <b>${bi("Standard", "标准")}</b></div><div class="sheet-row">${bi("Save slot", "存档位")} <b>02</b></div><div class="sheet-row">${bi("Opening Company", "初始队伍")} <b>${bi("Four new offers, choose two", "四名新候选中选择两人")}</b></div><div class="sheet-row">${bi("Carryover", "继承内容")} <b>${bi("Charter breadth only", "仅继承宪章广度")}</b></div><strong class="no-carryover">${bi("NO CLEARED RECRUIT CARRYOVER", "不继承已通关队员")}</strong><button class="stage-primary" type="button">${bi("Review and create new run", "审核并创建新远征")}</button></aside></div>
</div>`;
return frame(screen, body, { variant, art: false, context: bi("Oath Season", "誓约季"), resources: false });
}
function renderOathResult(screen, variant) {
const body = `<div class="oath-result-composition"><header><span class="stage-kicker">${bi("OATH SEASON RESULT", "誓约季结果")}</span><h2>${bi("The Company held under changed laws", "队伍在变更法则下完成了远征")}</h2><p>${bi("Result receipts are complete. This generated Company is now a Record.", "结果回执已完成。这支生成队伍现已存入记录。")}</p></header><div class="oath-result-grid"><article><span>01</span><strong>${bi("Measured Supply", "定量补给")}</strong><p>${bi("Fewer offers changed refresh and lock timing in five Markets.", "更少的报价改变了五个集市中的刷新与锁定时机。")}</p></article><article><span>02</span><strong>${bi("Broken Front", "破阵前线")}</strong><p>${bi("Front exposure made rear control the decisive counter twice.", "前排暴露让后排控制两次成为关键应对。")}</p></article><article class="oath-receipts"><strong>${bi("RECEIPTS", "结算回执")}</strong><p>${bi("Oath Record", "誓约记录")} +1</p><p>${bi("Renown", "声望")} +6</p><p>${bi("Archived Company", "存档队伍")} · 8 ${bi("survivors", "名幸存者")}</p></article></div><footer><strong>${bi("The closed Company cannot be resumed.", "已结束的队伍无法继续使用。")}</strong><button class="stage-secondary" type="button">${bi("Final Ledger", "最终账册")}</button><button class="stage-secondary" type="button">${bi("Ordinary New Run", "普通新远征")}</button><button class="stage-primary" type="button">${bi("Return to Oath Season", "返回誓约季")}</button></footer></div>`;
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 = `<div class="title-composition"><div class="title-brand"><span>AETHERBOUND</span><strong>GUILD</strong><small>Build the company. Order the line. Read the result.</small></div><div class="run-summary"><b>Safe resume</b><strong>Windglass Verge · Market</strong><span>Company 5 · Round 3 · save healthy</span><button class="stage-primary">Continue to Resume Review</button><button class="stage-secondary">New Run</button></div><div class="title-tools">${iconButton("⚙", "Settings")}${iconButton("Aa", "Accessibility")}</div></div>`;
return frame(screen, body, { variant, resources: false, context: "Aetherbound Guild" });
}
if (screen.id === "SYS-001") {
const body = `<div class="boot-composition"><div class="boot-mark"><i></i><span>AG</span></div><h2>Aetherbound Guild</h2><div class="verification-track"><i style="width:76%"></i></div><strong>Verifying local content · 76%</strong><small>Rules · Content · Settings · Save lineage</small></div>`;
return frame(screen, body, { variant, art: false, header: false });
}
if (screen.id === "SYS-002") return renderSettings(screen, variant, "FIRST-LAUNCH SETUP");
if (screen.id === "SYS-004") {
const body = `<div class="run-ledger-composition"><header><span class="stage-kicker">RUN LEDGER</span><h2>${bi("Choose a verified expedition", "选择一个已校验的远征")}</h2><button class="stage-primary">${bi("New Run", "新建远征")}</button></header><div class="run-slots"><button class="run-slot is-focused"><img src="${sceneImages[0]}" alt="" /><span>01</span><div><small>${bi("SAFE RESUME", "可安全继续")}</small><strong>${bi("Windglass Verge · Market", "风镜原野 · 集市")}</strong><p>${bi("Company 5 · Round 3 · 01:42:18", "队伍5人 · 第3轮 · 01:42:18")}</p></div><em>✓ ${bi("Healthy", "健康")}</em></button><button class="run-slot"><img src="${sceneImages[2]}" alt="" /><span>02</span><div><small>${bi("OATH SEASON", "誓约季")}</small><strong>${bi("Rainbell Reach · Recovery", "雨钟长境 · 恢复")}</strong><p>${bi("Company 3 · Round 7 · 03:18:02", "队伍3人 · 第7轮 · 03:18:02")}</p></div><em>! ${bi("Review", "需审核")}</em></button><button class="run-slot is-empty"><span>03</span><div><small>${bi("EMPTY SLOT", "空存档位")}</small><strong>${bi("No expedition", "暂无远征")}</strong><p>${bi("Create a new generated Company", "创建一支新的生成队伍")}</p></div><em>+</em></button></div><aside><b>${bi("Selected destination", "所选去向")}</b><strong>${bi("Resume Review", "继续前审核")}</strong><p>${bi("The game will show the last committed receipt before simulation resumes.", "恢复模拟前会先显示上一笔已提交回执。")}</p><button class="stage-primary">${bi("Review Slot 01", "审核存档01")}</button></aside></div>`;
return frame(screen, body, { variant, resources: false });
}
if (screen.id === "SYS-006") {
const body = `<div class="resume-composition"><section><span class="stage-kicker">${bi("RESUME REVIEW", "继续前审核")}</span><h2>${bi("You stopped after equipment returned", "你在装备退回后离开")}</h2><p>${bi("No battle event or purchase is pending.", "没有待处理的战斗事件或购买操作。")}</p><div class="resume-timeline"><span class="is-done">✓ ${bi("Battle finalized", "战斗已结算")}</span><i></i><span class="is-done">✓ ${bi("Equipment returned", "装备已退回")}</span><i></i><span class="is-current">03 ${bi("Choose recovery", "选择恢复方案")}</span></div><div class="resume-party">${partyActors()}</div></section><aside><small>${bi("LAST RECEIPT", "上一笔回执")}</small><strong>LOSS-014</strong><dl><div><dt>${bi("Saved", "保存时间")}</dt><dd>00:42.6</dd></div><div><dt>${bi("Destination", "继续位置")}</dt><dd>${bi("Recovery", "恢复选择")}</dd></div><div><dt>${bi("Unresolved", "未完成事务")}</dt><dd>0</dd></div></dl><button class="stage-primary">${bi("Resume Recovery", "继续恢复选择")}</button><button class="stage-secondary">${bi("Return to Ledger", "返回远征档案")}</button></aside></div>`;
return frame(screen, body, { variant, resources: false });
}
if (screen.id === "SYS-007") {
const body = `<div class="recovery-ledger-composition"><header><span class="stage-kicker">${bi("SAVE RECOVERY", "存档恢复")}</span><h2>${bi("Compare verified lineage", "比较已校验的存档谱系")}</h2><p>${bi("Recovery never overwrites the last healthy source before a receipt exists.", "生成恢复回执前,不会覆盖最后一个健康来源。")}</p></header><div class="lineage-list"><button class="lineage-row is-focused"><span>LOCAL</span><div><strong>${bi("Current local state", "当前本地状态")}</strong><p>${bi("Healthy · Market · 12:41", "健康 · 集市 · 12:41")}</p></div><em>${bi("0 min lost", "无进度损失")}</em></button><button class="lineage-row"><span>BACKUP</span><div><strong>${bi("Automatic backup", "自动备份")}</strong><p>${bi("Healthy · Result · 12:29", "健康 · 结算 · 12:29")}</p></div><em>${bi("12 min lost", "损失12分钟")}</em></button><button class="lineage-row is-conflict"><span>CLOUD</span><div><strong>${bi("Conflict candidate", "冲突候选")}</strong><p>${bi("Ruleset mismatch · diagnostics only", "规则版本不匹配 · 仅可诊断")}</p></div><em>${bi("Blocked", "已阻止")}</em></button></div><aside><b>${bi("Recovery consequence", "恢复后果")}</b><p>${bi("Load LOCAL. Active lineage changes only after RECOVER-003 is written and read back.", "载入本地状态。只有写入并回读 RECOVER-003 后,活动谱系才会改变。")}</p><button class="stage-primary">${bi("Load LOCAL", "载入本地状态")}</button></aside></div>`;
return frame(screen, body, { variant, resources: false });
}
const body = `<div class="data-notice-composition"><header><span class="stage-kicker">${bi("CREDITS · LICENSES · DATA", "制作名单 · 许可 · 数据")}</span><h2>${bi("What belongs to the game and what leaves the device", "游戏包含什么,以及哪些数据会离开设备")}</h2></header><nav><button class="is-active">${bi("Authorship", "制作与著作")}</button><button>${bi("Media licenses", "媒体许可")}</button><button>${bi("Local data", "本地数据")}</button><button>${bi("External links", "外部链接")}</button></nav><section><article><span>01</span><strong>${bi("Original game design", "原创游戏设计")}</strong><p>${bi("Systems, names, rules and presentation are authored for Aetherbound Guild.", "系统、名称、规则与呈现均为 Aetherbound Guild 独立创作。")}</p></article><article><span>02</span><strong>${bi("Offline core play", "核心玩法离线可用")}</strong><p>${bi("Runs, settings and Records remain local unless a future platform feature states otherwise.", "远征、设置和记录保留在本地,除非未来平台功能另有说明。")}</p></article><article><span>03</span><strong>${bi("Traceable production media", "可追踪的制作素材")}</strong><p>${bi("Generated candidates retain provider task, prompt, cost and processing provenance.", "生成候选保留供应商任务、提示词、费用和处理记录。")}</p></article></section><aside><b>${bi("Current network boundary", "当前网络边界")}</b><strong>${bi("No account required", "无需账号")}</strong><strong>${bi("No gameplay telemetry", "不收集玩法遥测")}</strong><strong>${bi("External links require review", "打开外链前需要确认")}</strong></aside></div>`;
return frame(screen, body, { variant, resources: false, art: false });
}
function renderArtifact(screen, variant) {
const entries = state.data.artifacts.slice(0, 3);
const body = `<div class="artifact-composition"><div class="artifact-heading"><span class="stage-kicker">RUN-WIDE RULES · NOT EQUIPMENT</span><h2>Active Artifact Ledger</h2></div><div class="artifact-ledger">${entries.map((entry, index) => `<button class="artifact-row ${index === 0 ? "is-focused" : ""}"><span class="artifact-emblem">${["◇", "⌑", "⬡"][index]}</span><div><small>${entry.id}</small><strong>${entry.name}</strong><p>${truncate(entry.rule, 90)}</p></div><em>${index < 2 ? "Active" : "Conflict"}</em></button>`).join("")}</div><aside class="artifact-inspect"><h3>${entries[0].name}</h3><p>${entries[0].rule}</p><b>Conflict</b><span>${entries[0].tradeoff}</span><small>Lifecycle: current run only</small></aside></div>`;
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-001") {
const body = `<div class="run-growth-composition"><header><div><span class="stage-kicker">${bi("RUN GROWTH · ONE DECISION PENDING", "本轮成长 · 一项决策待定")}</span><h2>${bi("Elian can change how the front is held", "Elian 可以改变前排的承伤方式")}</h2><p>${bi("Promotion preserves this Recruit's level, XP, Trait, one equipped item and current HP percentage.", "晋升会保留这名角色自己的等级、经验、特性、唯一装备与当前生命比例。")}</p></div><span class="growth-level">LV 6<small>XP 12/80</small></span></header><div class="growth-tree"><section class="growth-origin">${actorImage(generatedOffers[0], "growth-actor-art")}<small>PF-B01</small><strong>${bi("Hearthguard", "护心卫士")}</strong><p>${bi("Intercept adjacent direct pressure", "拦截相邻队员承受的直接压力")}</p></section><i></i><button class="growth-choice is-selected"><small>PF-A01</small><strong>${bi("Bell Bastion", "鸣钟壁垒")}</strong><p>${bi("Guard break delays the next enemy forecast.", "护卫崩溃会延后下一个敌方预告。")}</p><em>${bi("Changes timing", "改变行动时序")}</em></button><button class="growth-choice"><small>PF-A02</small><strong>${bi("Road Warden", "巡路守望")}</strong><p>${bi("First forced move becomes a protected swap.", "第一次强制移动改为受保护换位。")}</p><em>${bi("Changes position", "改变站位")}</em></button></div><aside><span>${bi("BUILD CONSEQUENCE", "构筑后果")}</span><div class="sheet-row">${bi("One equipment slot", "唯一装备槽")} <b>EQ-002 ${bi("remains equipped", "继续装备")}</b></div><div class="sheet-row">${bi("Party Line", "队伍站位")} <b>${bi("Position 0 stays first target", "0号位仍是首个目标")}</b></div><div class="sheet-row">${bi("Next risk", "下一风险")} <b>${bi("Pane Bite timing changes", "晶板啃咬时序改变")}</b></div><button class="stage-primary">${bi("Review Bell Bastion", "审核鸣钟壁垒")}</button></aside></div>`;
return frame(screen, body, { variant, art: false });
}
if (screen.id === "PRO-003") {
const professionPreviews = [generatedOffers[0], generatedOffers[1], generatedOffers[2], generatedOffers[3], generatedOffers[0], generatedOffers[1]];
const body = `<div class="records-composition"><div class="records-categories"><strong>COMPENDIUM</strong><button class="is-active">Professions <b>18/42</b></button><button>Traits <b>24/36</b></button><button>Equipment <b>73/320</b></button><button>Artifacts <b>11/60</b></button><button>Enemies <b>31/100</b></button><button>Bosses <b>2/16</b></button><button>Regions <b>2/8</b></button></div><div class="record-grid">${state.data.professions.slice(0, 6).map((entry, index) => `<button class="record-tile ${index === 0 ? "is-focused" : ""}"><span>${index < 4 ? actorImage(professionPreviews[index], "record-tile-art") : "?"}</span><small>${entry.id}</small><strong>${entry.name}</strong><em>${index < 3 ? "Known" : "Undiscovered"}</em></button>`).join("")}</div><aside class="record-inspect"><span class="large-figure">${actorImage(generatedOffers[0], "record-inspect-art")}</span><h2>Hearthguard</h2><p>Front hold · Guard intercept · reach 1</p><b>Counterplay</b><span>Rear pressure and Guard stripping</span></aside></div>`;
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 = `<div class="progress-composition"><div class="progress-heading"><span class="stage-kicker">${isPostgame ? "POSTGAME" : "PROGRESSION"}</span><h2>${escapeHtml(screenName(screen))}</h2><p>${escapeHtml(truncate(screenPurpose(screen), 170))}</p></div><div class="progress-path"><span class="path-node is-complete">Base</span><i></i><span class="path-node is-current">Choice</span><i></i><span class="path-node">Mastery</span></div><div class="progress-details">${rows.map((row) => `<div class="sheet-row">${escapeHtml(localizedText(row))}</div>`).join("")}</div>${actionDock(isPostgame ? "Review active rules" : "Inspect choice", "Ordinary New Run")}</div>`;
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 `<div class="segmented-control">${["100", "115", "130"].map((scale) => `<button class="${state.settings.textScale === scale ? "is-active" : ""}" data-text-scale="${scale}">${scale}</button>`).join("")}</div>`;
}
if (screen.id === "SET-006" && index === 0) {
return `<div class="segmented-control"><button class="${state.locale === "en" ? "is-active" : ""}" data-locale="en">EN</button><button class="${state.locale === "zh_CN" ? "is-active" : ""}" data-locale="zh_CN">简中</button></div>`;
}
const key = settingKeys[`${screen.id}-${index}`];
if (key) {
const enabled = Boolean(state.settings[key]);
return `<button class="switch ${enabled ? "is-on" : ""}" type="button" data-setting-key="${key}" aria-pressed="${enabled}"><i></i></button>`;
}
if (index % 3 === 1) return `<button class="switch is-on" type="button" aria-pressed="true"><i></i></button>`;
if (index % 3 === 2) return `<button class="menu-control">${escapeHtml(value)}⌄</button>`;
return `<div class="segmented-control"><button>Low</button><button class="is-active">${escapeHtml(value)}</button><button>High</button></div>`;
});
const body = `<div class="settings-composition"><nav class="settings-rail"><strong>${kicker}</strong>${categoryNames.map((name, index) => `<button class="${screen.id === `SET-00${Math.min(9, index + 2)}` || (screen.id === "SET-001" && index === 0) ? "is-active" : ""}">${name}</button>`).join("")}</nav><div class="settings-workspace"><span class="stage-kicker">CALLER · ${variant === "pause" ? "PAUSED BATTLE" : "TITLE"}</span><h2>${escapeHtml(screenName(screen))}</h2>${rows.map(([label], index) => `<div class="setting-control"><div><strong>${escapeHtml(label)}</strong><small>${index === 0 ? "Live preview" : "Current value"}</small></div>${controls[index]}</div>`).join("")}<div class="settings-actions"><button class="stage-secondary">Discard</button><button class="stage-primary">Apply settings</button></div></div><aside class="settings-preview"><span>PREVIEW</span><div class="preview-target">TARGET <b>Pane Nibbler</b></div><p>Front recruit protected<br />[Enemy forecast: rear projectile]</p><strong>Readable at ${state.settings.textScale}%</strong></aside></div>`;
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 = `<div class="generic-composition"><div class="generic-subject"><span>${screen.id.split("-")[0]}</span><strong>${escapeHtml(screenName(screen))}</strong></div><div class="generic-decision"><span class="stage-kicker">CURRENT DECISION</span><h2>${escapeHtml(screenName(screen))}</h2><p>${escapeHtml(truncate(screenPurpose(screen), 180))}</p>${details.map((detail) => `<div class="sheet-row">${escapeHtml(localizedText(detail))}</div>`).join("")}</div>${actionDock(primary, t("inspect"))}</div>`;
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(
"</section>",
`<div class="state-variant-overlay ${variant}"><div class="variant-sheet"><span class="stage-kicker">${label}</span><h2>${escapeHtml(screenName(screen))}</h2><p>${escapeHtml(truncate(source, 260))}</p><div><button class="stage-secondary">${t("back")}</button><button class="stage-primary">${escapeHtml(action)}</button></div></div></div></section>`,
);
}
function viewHeading(kicker, title, detail, badges = []) {
return `<header class="view-heading"><div><span class="eyebrow">${escapeHtml(kicker)}</span><h1>${escapeHtml(title)}</h1><p>${escapeHtml(detail)}</p></div><div class="heading-badges">${badges.map((badge) => `<span>${escapeHtml(badge)}</span>`).join("")}</div></header>`;
}
function experienceStepLabel(step) {
return step[state.locale === "zh_CN" ? 3 : 2];
}
function experienceDifficulty() {
return openingDifficulties.find((difficulty) => difficulty.id === state.experienceDifficulty) || null;
}
function experienceProgress() {
const currentIndex = experienceSteps.findIndex((step) => step[0] === state.experienceStep);
return `<ol class="experience-progress" aria-label="${bi("P8.1 progress", "P8.1 进度")}">${experienceSteps.map((step, index) => `<li class="${index === currentIndex ? "is-current" : ""} ${index < currentIndex ? "is-complete" : ""}" ${index === currentIndex ? 'aria-current="step"' : ""}><span>${index < currentIndex ? "✓" : step[1]}</span><strong>${escapeHtml(experienceStepLabel(step))}</strong></li>`).join("")}</ol>`;
}
function recruitTradeoff(offer) {
const reads = {
"OFF-01": [["Intercept one adjacent hit", "替相邻队员承受一次攻击"], ["Moves and acts more slowly", "移动与行动较慢"]],
"OFF-02": [["Pressures the enemy rear", "优先压制敌方后排"], ["First basic attack is delayed", "第一次普通攻击较晚"]],
"OFF-03": [["Can change order safely", "可以安全调整队伍顺序"], ["Becomes briefly Exposed", "会短暂进入暴露状态"]],
"OFF-04": [["Restores an adjacent ally", "治疗相邻队员"], ["Self-healing is weaker", "治疗自己时效果较弱"]],
};
return reads[offer.id];
}
function renderExperienceContract(screen) {
const contractSelected = state.experienceContract === openingContract.id;
const difficulty = experienceDifficulty();
const body = `<div class="experience-contract">
<header class="experience-stage-title"><span class="stage-kicker">P8.1 · ${bi("STEP 1 OF 4", "第 1 / 4 步")}</span><h2>${bi("Choose how this expedition begins", "选择这次远征如何开始")}</h2><p>${bi("One Contract is unlocked. You still choose its difficulty before seeing any Recruit offers.", "目前只解锁一个契约;在看到队员候选前,你仍需自己选择难度。")}</p></header>
<div class="experience-contract-grid">
<button class="experience-contract-card ${contractSelected ? "is-selected" : ""}" type="button" data-experience-contract="${openingContract.id}" aria-pressed="${contractSelected}">
<span>RG-01 · ${openingContract.id}</span><strong>${bi(openingContract.name[0], openingContract.name[1])}</strong><em>${bi(openingContract.region[0], openingContract.region[1])}</em><p>${bi(openingContract.objective[0], openingContract.objective[1])}</p><small>${contractSelected ? bi("SELECTED", "已选择") : bi("SELECT CONTRACT", "选择契约")}</small>
</button>
<section class="experience-contract-read"><span>${bi("WHAT YOU KNOW", "已知情报")}</span><dl><div><dt>${bi("Pressure", "主要威胁")}</dt><dd>${bi(openingContract.pressure[0], openingContract.pressure[1])}</dd></div><div><dt>${bi("Boss rule", "首领规则")}</dt><dd>${bi(openingContract.boss[0], openingContract.boss[1])}</dd></div><div><dt>${bi("Reward breadth", "奖励范围")}</dt><dd>${bi(openingContract.reward[0], openingContract.reward[1])}</dd></div></dl></section>
<section class="experience-difficulty"><span>${bi("CHOOSE DIFFICULTY", "选择难度")}</span><div>${openingDifficulties.map((entry) => { const selected = entry.id === state.experienceDifficulty; return `<button type="button" data-experience-difficulty="${entry.id}" class="${selected ? "is-selected" : ""}" aria-pressed="${selected}" ${contractSelected ? "" : "disabled"}><strong>${bi(entry.name[0], entry.name[1])}</strong><small>${bi(entry.stats[0], entry.stats[1])}</small><em>${bi(entry.audience[0], entry.audience[1])}</em></button>`; }).join("")}</div></section>
</div>
<footer class="experience-stage-actions"><span>${difficulty ? `${bi("Chosen", "已选择")} · ${bi(difficulty.name[0], difficulty.name[1])}` : bi("Choose the Contract and one difficulty", "请选择契约和一种难度")}</span><button class="stage-primary" type="button" data-experience-next ${contractSelected && difficulty ? "" : "disabled"}>${bi("Load four stable offers", "载入四名固定候选")} ›</button></footer>
</div>`;
return frame(screen, body, { variant: "p8-contract", resources: false });
}
function renderExperienceOffers(screen) {
const inspected = generatedOffers.find((offer) => offer.id === state.experienceInspected) || null;
const notice = state.experienceNotice ? `<div class="experience-notice" role="status">${escapeHtml(state.experienceNotice)}</div>` : "";
const body = `<div class="experience-offers">
<header class="experience-stage-title"><span class="stage-kicker">P8.1 · ${bi("STEP 2 OF 4", "第 2 / 4 步")}</span><h2>${bi("Inspect four Recruits, then choose exactly two", "查看四名队员,然后恰好选择两人")}</h2><p>${bi("Offer set WIND-01-A stays unchanged until creation. No pair is recommended.", "候选组 WIND-01-A 在创建前不会变化;界面不会推荐任何组合。")}</p></header>
<div class="experience-offer-grid">${generatedOffers.map((offer) => { const selected = state.experienceSelected.includes(offer.id); const seen = state.experienceInspectedIds.includes(offer.id); const tradeoff = recruitTradeoff(offer); return `<article class="experience-offer ${selected ? "is-selected" : ""} ${state.experienceInspected === offer.id ? "is-inspected" : ""}"><div class="experience-offer-art">${actorImage(offer, "offer-character")}<span>${offer.id}</span></div><div><strong>${localizedText(offer.name)}</strong><small>${localizedText(offer.profession)} · ${bi("LV", "等级")} 1</small><em>${offer.itemId} · ${localizedText(offer.item)}</em></div><p><b>+</b> ${bi(tradeoff[0][0], tradeoff[0][1])}</p><p><b>−</b> ${bi(tradeoff[1][0], tradeoff[1][1])}</p><footer><button type="button" data-experience-inspect="${offer.id}">${seen ? bi("Inspected", "已查看") : bi("Inspect", "查看")}</button><button type="button" data-experience-offer="${offer.id}" aria-pressed="${selected}">${selected ? `✓ ${bi("Selected", "已选择")}` : bi("Select", "选择")}</button></footer></article>`; }).join("")}</div>
<aside class="experience-inspect ${inspected ? "has-recruit" : ""}">${inspected ? (() => { const tradeoff = recruitTradeoff(inspected); return `<span>${bi("INSPECTED TRADEOFF", "当前查看的取舍")}</span><strong>${localizedText(inspected.name)} · ${localizedText(inspected.profession)}</strong><p>${bi(tradeoff[0][0], tradeoff[0][1])};${bi(tradeoff[1][0], tradeoff[1][1])}。</p><small>${bi("One active equipment slot", "一个有效装备槽")} · ${inspected.itemId} ${localizedText(inspected.item)}</small>`; })() : `<span>${bi("INSPECT BEFORE CHOOSING", "选择前先查看")}</span><strong>${bi("No Recruit inspected yet", "尚未查看任何队员")}</strong><p>${bi("Inspect shows each benefit, cost and starting item without selecting for you.", "查看会说明优势、代价与初始装备,但不会替你做选择。")}</p>`}</aside>
${notice}
<footer class="experience-stage-actions"><button class="stage-secondary" type="button" data-experience-back>${bi("Back to Contract", "返回契约")}</button><span>${bi("Selected", "已选择")} <b>${state.experienceSelected.length}/2</b> · ${bi("Inspected", "已查看")} ${state.experienceInspectedIds.length}/4</span><button class="stage-primary" type="button" data-experience-next ${state.experienceSelected.length === 2 ? "" : "disabled"}>${bi("Review Company", "确认初始队伍")} ›</button></footer>
</div>`;
return frame(screen, body, { variant: "p8-offers", resources: false });
}
function renderExperienceReview(screen) {
const chosen = generatedOffers.filter((offer) => state.experienceSelected.includes(offer.id));
const unchosen = generatedOffers.filter((offer) => !state.experienceSelected.includes(offer.id));
const difficulty = experienceDifficulty();
const body = `<div class="experience-review">
<header class="experience-stage-title"><span class="stage-kicker">P8.1 · ${bi("STEP 3 OF 4", "第 3 / 4 步")}</span><h2>${bi("Review before creating the expedition", "创建远征前请确认")}</h2><p>${bi("This is the only commit boundary. You can still revise the pair.", "这里是唯一确认边界;现在仍可返回修改队伍。")}</p></header>
<div class="experience-review-grid"><section><span>${bi("SELECTED COMPANY", "选择的初始队伍")}</span><div>${chosen.map((offer) => recruitMarker(offer, true)).join("")}</div></section><section><span>${bi("NOT RETAINED AFTER COMMIT", "确认后不会保留")}</span><div>${unchosen.map((offer) => recruitMarker(offer, false)).join("")}</div></section><aside><span>${bi("RUN TERMS", "远征条款")}</span><dl><div><dt>${bi("Contract", "契约")}</dt><dd>${bi(openingContract.name[0], openingContract.name[1])}</dd></div><div><dt>${bi("Difficulty", "难度")}</dt><dd>${difficulty ? bi(difficulty.name[0], difficulty.name[1]) : "-"}</dd></div><div><dt>${bi("Save slot", "存档位")}</dt><dd>01</dd></div><div><dt>${bi("Coin cost", "金币消耗")}</dt><dd>0</dd></div><div><dt>${bi("First Market", "首个集市")}</dt><dd>${bi("52 Coin · Company 2/4", "52金币 · 队伍2/4")}</dd></div></dl></aside></div>
<footer class="experience-stage-actions"><button class="stage-secondary" type="button" data-experience-revise>${bi("Revise Recruits", "重新选择队员")}</button><span>${bi("Creating discards the two unchosen stable offers.", "创建后,两名未选择的固定候选将离开。")}</span><button class="stage-primary" type="button" data-experience-commit>${bi("Create this expedition", "创建这次远征")}</button></footer>
</div>`;
return frame(screen, body, { variant: "p8-review", resources: false });
}
function renderExperienceReceipt(screen) {
const chosen = generatedOffers.filter((offer) => state.experienceSelected.includes(offer.id));
const unchosen = generatedOffers.filter((offer) => !state.experienceSelected.includes(offer.id));
const difficulty = experienceDifficulty();
const body = `<div class="experience-receipt">
<header><span class="receipt-check">✓</span><div><span class="stage-kicker">P8.1 · ${bi("STEP 4 OF 4", "第 4 / 4 步")}</span><h2>${bi("Expedition created", "远征已创建")}</h2><p>${bi("One receipt was committed. Repeated input cannot create another run.", "已生成唯一创建回执;重复点击不会再次创建远征。")}</p></div><b>P8-RUN-001</b></header>
<div class="experience-receipt-grid"><section><span>${bi("COMPANY 2 / CAPACITY 4", "队伍 2 / 容量 4")}</span>${chosen.map((offer) => recruitMarker(offer, true)).join("")}</section><section><span>${bi("UNCHOSEN OFFERS DISCARDED", "未选候选已离开")}</span>${unchosen.map((offer) => `<p><b>${offer.id}</b><strong>${localizedText(offer.name)}</strong></p>`).join("")}</section><aside><span>${bi("RECEIPT", "创建回执")}</span><dl><div><dt>${bi("Contract", "契约")}</dt><dd>${openingContract.id}</dd></div><div><dt>${bi("Difficulty", "难度")}</dt><dd>${difficulty ? bi(difficulty.name[0], difficulty.name[1]) : "-"}</dd></div><div><dt>${bi("Coin", "金币")}</dt><dd>+0 / −0</dd></div><div><dt>${bi("Next", "下一步")}</dt><dd>${bi("First Market", "首个集市")}</dd></div></dl></aside></div>
<div class="experience-next-package"><span>${bi("P8.2 LOCKED", "P8.2 尚未开启")}</span><strong>${bi("Market preparation begins only after your P8.1 review.", "你确认 P8.1 后,才会开发集市整备体验。")}</strong></div>
<footer class="experience-stage-actions"><button class="stage-secondary" type="button" data-experience-restart>${bi("Play P8.1 again", "重新体验 P8.1")}</button><span>${bi("End of this review package", "本次体验包到此结束")}</span><button class="stage-primary" type="button" data-view="screens">${bi("Open design atlas", "查看完整设计图册")}</button></footer>
</div>`;
return frame(screen, body, { variant: "p8-receipt", resources: false });
}
function marketCompany() {
const opening = [generatedOffers[2], generatedOffers[3]];
const hired = state.marketHired.map((id) => firstMarketOffers.find((offer) => offer.id === id)).filter(Boolean);
return [...opening, ...hired];
}
function marketStepGroup() {
if (["market2", "hire_review", "hire_receipt", "market3", "hire2_review", "hire2_receipt"].includes(state.marketStep)) return "market";
if (["equipment_offer", "item_review", "item_receipt", "equip_select", "equip_receipt"].includes(state.marketStep)) return "equipment";
if (state.marketStep === "order") return "order";
return "receipt";
}
function marketProgress() {
const group = marketStepGroup();
const currentIndex = marketExperienceSteps.findIndex((step) => step[0] === group);
return `<ol class="experience-progress market-progress" aria-label="${bi("P8.2 progress", "P8.2 进度")}">${marketExperienceSteps.map((step, index) => `<li class="${index === currentIndex ? "is-current" : ""} ${index < currentIndex ? "is-complete" : ""}" ${index === currentIndex ? 'aria-current="step"' : ""}><span>${index < currentIndex ? "✓" : step[1]}</span><strong>${escapeHtml(experienceStepLabel(step))}</strong></li>`).join("")}</ol>`;
}
function marketCompanyBench() {
const company = marketCompany();
return `<div class="p82-company-bench">${company.map((offer, index) => `<article><span>${actorImage(offer, "bench-actor-art")}</span><div><strong>${localizedText(offer.name)}</strong><small>${localizedText(offer.profession)} · LV ${offer.level} · XP ${offer.xp}/${offer.xpNext}</small><em>${offer.itemId} · ${localizedText(offer.item)}</em></div><b>${company.length - 1 - index}</b></article>`).join("")}${Array.from({ length: 4 - company.length }, () => `<article class="is-empty"><span>+</span><div><strong>${bi("Open Company slot", "空队伍席位")}</strong><small>${bi("Hire one Recruit below", "从下方招募一名队员")}</small></div></article>`).join("")}</div>`;
}
function renderMarketExperienceBoard(screen) {
const company = marketCompany();
const available = firstMarketOffers.filter((offer) => !state.marketHired.includes(offer.id));
const focused = available.find((offer) => offer.id === state.marketFocusedOffer) || null;
const body = `<div class="p82-market">
<header class="experience-stage-title"><span class="stage-kicker">P8.2 · ${bi("FIRST MARKET", "首个集市")}</span><div><h2>${bi("Hire two Recruits in separate transactions", "分两次招募队员")}</h2><div class="p82-resource"><span>${bi("Coin", "金币")}</span><strong>${state.marketCoin}</strong><i>${bi("Company", "队伍")} ${company.length} / ${bi("capacity", "容量")} 4</i></div></div><p>${bi("Inspecting changes nothing. Every level-1 base Recruit costs 14 Coin.", "查看不会改变状态;每名一级基础职业队员价格都是14金币。")}</p></header>
${marketCompanyBench()}
<section class="p82-offer-board"><div class="p82-offers">${available.map((offer) => `<button type="button" data-market-focus="${offer.id}" class="${focused && focused.id === offer.id ? "is-focused" : ""}"><span>${actorImage(offer, "market-offer-art")}</span><div><small>${offer.id}</small><strong>${localizedText(offer.name)}</strong><em>${localizedText(offer.profession)} · LV 1 · XP 0/30</em><p>${localizedText(offer.read)}</p><b>${offer.itemId} · ${localizedText(offer.item)}</b></div><i>14<small>${bi("Coin", "金币")}</small></i></button>`).join("")}</div><aside class="p82-offer-inspect">${focused ? `<span>${bi("FOCUSED OFFER", "当前查看")}</span><strong>${localizedText(focused.name)}</strong><p>${localizedText(focused.read)}</p><small>${bi("Individual level", "角色独立等级")} LV 1 · XP 0/30</small><small>${bi("One equipment slot", "唯一装备槽")} · ${focused.itemId}</small><b>${bi("After hire", "招募后")} · ${bi("Company", "队伍")} ${company.length + 1}/4 · ${bi("Coin", "金币")} ${state.marketCoin - 14}</b>` : `<span>${bi("NO OFFER SELECTED", "尚未选择报价")}</span><strong>${bi("Inspect any stable offer", "点击任一固定报价进行查看")}</strong><p>${bi("No Recruit is recommended or preselected.", "界面不会推荐或预选任何队员。")}</p>`}</aside></section>
<footer class="experience-stage-actions"><span>${state.marketHired.length}/2 ${bi("hire receipts complete", "次招募回执已完成")}</span><button class="stage-primary" type="button" data-market-review ${focused ? "" : "disabled"}>${bi("Review this Recruit", "审核这名队员")} ›</button></footer>
</div>`;
return frame(screen, body, { variant: "p8-2-market", resources: false });
}
function renderMarketHireReview(screen) {
const offer = firstMarketOffers.find((entry) => entry.id === state.marketFocusedOffer);
const company = marketCompany();
const operation = state.marketHired.length === 0 ? "HIRE-P82-001" : "HIRE-P82-002";
const body = `<div class="p82-hire-review">
<header class="experience-stage-title"><span class="stage-kicker">P8.2 · ${bi("RECRUIT REVIEW", "招募审核")}</span><h2>${bi("Confirm one Recruit transaction", "确认一次独立招募")}</h2></header>
<section class="p82-hire-figure">${actorImage(offer, "offer-character")}<span>${offer.id}</span></section>
<section class="p82-hire-facts"><span>${bi("GENERATED RECRUIT", "生成队员")}</span><h3>${localizedText(offer.name)}</h3><h4>${localizedText(offer.profession)} · ${localizedText(offer.trait)}</h4><div><b>${bi("Individual level", "角色独立等级")}</b><strong>LV 1 · XP 0/30</strong></div><div><b>${bi("One equipment slot", "唯一装备槽")}</b><strong>${offer.itemId} · ${localizedText(offer.item)}</strong></div><p>${localizedText(offer.read)}</p></section>
<aside class="p82-transaction"><span>${bi("TRANSACTION", "交易内容")}</span><dl><div><dt>${bi("Operation", "操作编号")}</dt><dd>${operation}</dd></div><div><dt>${bi("Price", "价格")}</dt><dd>14 ${bi("Coin", "金币")}</dd></div><div><dt>${bi("Balance", "余额")}</dt><dd>${state.marketCoin} → ${state.marketCoin - 14}</dd></div><div><dt>${bi("Company", "队伍")}</dt><dd>${company.length} → ${company.length + 1}</dd></div><div><dt>${bi("Capacity", "容量")}</dt><dd>4 → 4</dd></div></dl></aside>
<footer class="experience-stage-actions"><button class="stage-secondary" type="button" data-market-cancel>${bi("Cancel", "取消")}</button><span>${bi("No combined Hire Two action", "没有合并招募两人的操作")}</span><button class="stage-primary" type="button" data-market-hire-confirm>${bi("Hire for 14 Coin", "花费14金币招募")}</button></footer>
</div>`;
return frame(screen, body, { variant: "p8-2-hire-review", resources: false });
}
function renderMarketHireReceipt(screen) {
const offer = firstMarketOffers.find((entry) => entry.id === state.marketHired.at(-1));
const number = state.marketHired.length;
const body = `<div class="p82-simple-receipt"><header><span>✓</span><div><small>${bi("HIRE RECEIPT", "招募回执")} ${number}/2</small><h2>${localizedText(offer.name)} ${bi("joined the Company", "已加入队伍")}</h2></div><b>HIRE-P82-00${number}</b></header><section>${marketCompanyBench()}</section><aside><div><span>${bi("Coin", "金币")}</span><strong>${state.marketCoin + 14} → ${state.marketCoin}</strong></div><div><span>${bi("Company", "队伍")}</span><strong>${marketCompany().length - 1} → ${marketCompany().length}</strong></div><div><span>${bi("Capacity", "容量")}</span><strong>4 → 4</strong></div></aside><footer class="experience-stage-actions"><span>${number < 2 ? bi("One more separate hire remains", "还需再完成一次独立招募") : bi("Company capacity is filled", "队伍容量已经填满")}</span><button class="stage-primary" type="button" data-market-continue>${number < 2 ? bi("Return to stable offers", "返回固定报价") : bi("Continue to equipment", "继续处理装备")} ›</button></footer></div>`;
return frame(screen, body, { variant: "p8-2-hire-receipt", resources: false });
}
function renderMarketEquipmentOffer(screen) {
const body = `<div class="p82-equipment-offer"><header class="experience-stage-title"><span class="stage-kicker">P8.2 · ${bi("EQUIPMENT OFFER", "装备报价")}</span><div><h2>${bi("Buy first, equip separately", "先购买,再单独装备")}</h2><div class="p82-resource"><span>${bi("Coin", "金币")}</span><strong>${state.marketCoin}</strong><i>${bi("Inventory", "库存")} 0 / 12</i></div></div><p>${bi("Buying changes inventory ownership only. It will not choose a wearer.", "购买只改变库存归属,不会自动选择装备者。")}</p></header><section class="p82-item-card"><div>${equipmentImage(firstMarketItem.name, "item-offer-art")}<span>${firstMarketItem.id}</span></div><article><small>${bi("TIER 0 · REGION 1", "零阶 · 第一区域")}</small><h3>${localizedText(firstMarketItem.name)}</h3><p><b>+</b> ${bi(firstMarketItem.effect[0], firstMarketItem.effect[1])}</p><p><b>−</b> ${bi(firstMarketItem.tradeoff[0], firstMarketItem.tradeoff[1])}</p></article><aside><span>${bi("PRICE", "价格")}</span><strong>12 ${bi("Coin", "金币")}</strong><small>${state.marketCoin} → ${state.marketCoin - 12}</small></aside></section><footer class="experience-stage-actions"><span>${bi("No Recruit will equip it automatically", "不会自动给任何队员装备")}</span><button class="stage-primary" type="button" data-market-item-review>${bi("Review purchase", "审核购买")} ›</button></footer></div>`;
return frame(screen, body, { variant: "p8-2-equipment-offer", resources: false });
}
function renderMarketItemReview(screen) {
const body = `<div class="p82-item-review"><section>${equipmentImage(firstMarketItem.name, "item-offer-art")}<span>${firstMarketItem.id}</span></section><article><span class="stage-kicker">${bi("PURCHASE REVIEW", "购买审核")}</span><h2>${localizedText(firstMarketItem.name)}</h2><p>${bi(firstMarketItem.effect[0], firstMarketItem.effect[1])}</p><p>${bi(firstMarketItem.tradeoff[0], firstMarketItem.tradeoff[1])}</p><div><b>${bi("Inventory", "库存")}</b><strong>0 → 1</strong></div><div><b>${bi("Equipped Recruit", "装备者")}</b><strong>${bi("None · choose later", "无 · 稍后选择")}</strong></div></article><aside><span>${bi("TRANSACTION", "交易内容")}</span><strong>12 ${bi("Coin", "金币")}</strong><p>${state.marketCoin} → ${state.marketCoin - 12}</p><small>BUY-P82-EQ-001</small></aside><footer class="experience-stage-actions"><button class="stage-secondary" data-market-item-cancel>${bi("Cancel", "取消")}</button><span>${bi("Purchase does not equip", "购买不会自动装备")}</span><button class="stage-primary" data-market-item-confirm>${bi("Buy for 12 Coin", "花费12金币购买")}</button></footer></div>`;
return frame(screen, body, { variant: "p8-2-item-review", resources: false });
}
function renderMarketItemReceipt(screen) {
const body = `<div class="p82-simple-receipt p82-item-receipt"><header><span>✓</span><div><small>${bi("PURCHASE RECEIPT", "购买回执")}</small><h2>${localizedText(firstMarketItem.name)} ${bi("added to run inventory", "已加入本轮库存")}</h2></div><b>BUY-P82-EQ-001</b></header><section class="p82-inventory-receipt"><div>${equipmentImage(firstMarketItem.name, "inventory-item-art")}<strong>${firstMarketItem.id}</strong><small>${localizedText(firstMarketItem.name)}</small></div>${Array.from({length: 11}, () => `<div class="is-empty"><span>+</span><small>${bi("Empty", "空")}</small></div>`).join("")}</section><aside><div><span>${bi("Coin", "金币")}</span><strong>24 → 12</strong></div><div><span>${bi("Inventory", "库存")}</span><strong>0 → 1</strong></div><div><span>${bi("Equipped", "已装备")}</span><strong>${bi("Nobody", "无人")}</strong></div></aside><footer class="experience-stage-actions"><span>${bi("Now choose a wearer explicitly", "现在请明确选择一名装备者")}</span><button class="stage-primary" data-market-open-equip>${bi("Choose wearer", "选择装备者")} ›</button></footer></div>`;
return frame(screen, body, { variant: "p8-2-item-receipt", resources: false });
}
function renderMarketEquipSelect(screen) {
const company = marketCompany();
const target = company.find((offer) => offer.id === state.marketEquipTarget) || null;
const body = `<div class="p82-equip-select"><header class="experience-stage-title"><span class="stage-kicker">P8.2 · ${bi("ONE-SLOT REPLACEMENT", "单装备槽替换")}</span><h2>${bi("Choose exactly one wearer", "明确选择一名装备者")}</h2><p>${bi("The candidate item replaces that Recruit's current item; the prior item returns to inventory.", "候选装备会替换该队员当前装备;原装备将退回库存。")}</p></header><section class="p82-equip-company">${company.map((offer) => `<button type="button" data-market-equip-target="${offer.id}" class="${state.marketEquipTarget === offer.id ? "is-selected" : ""}"><span>${actorImage(offer, "bench-actor-art")}</span><div><strong>${localizedText(offer.name)}</strong><small>LV 1 · XP 0/30</small><em>${bi("Current", "当前")} ${offer.itemId} · ${localizedText(offer.item)}</em></div></button>`).join("")}</section><aside class="p82-equip-delta"><span>${bi("BEHAVIOR DELTA", "行为变化")}</span>${target ? `<h3>${target.itemId} → ${firstMarketItem.id}</h3><p><b>${bi("Equipped after Apply", "应用后装备")}</b>${firstMarketItem.id} · ${localizedText(firstMarketItem.name)}</p><p><b>${bi("Returned to inventory", "退回库存")}</b>${target.itemId} · ${localizedText(target.item)}</p><small>${bi(firstMarketItem.effect[0], firstMarketItem.effect[1])}</small>` : `<h3>${bi("No wearer selected", "尚未选择装备者")}</h3><p>${bi("Select a Recruit to preview the atomic replacement.", "请选择队员,查看一次性替换结果。")}</p>`}</aside><footer class="experience-stage-actions"><span>${bi("One active item before and after", "替换前后都只有一个有效装备")}</span><button class="stage-primary" data-market-equip-confirm ${target ? "" : "disabled"}>${bi("Apply replacement", "应用替换")}</button></footer></div>`;
return frame(screen, body, { variant: "p8-2-equip-select", resources: false });
}
function renderMarketEquipReceipt(screen) {
const target = marketCompany().find((offer) => offer.id === state.marketEquipTarget);
const body = `<div class="p82-simple-receipt"><header><span>✓</span><div><small>${bi("EQUIPMENT RECEIPT", "装备回执")}</small><h2>${localizedText(target.name)} ${bi("now carries", "现在装备")} ${localizedText(firstMarketItem.name)}</h2></div><b>EQUIP-P82-001</b></header><section class="p82-equip-receipt"><article>${actorImage(target, "offer-character")}<strong>${localizedText(target.name)}</strong><small>${bi("ONE SLOT", "唯一装备槽")} · ${firstMarketItem.id}</small></article><div><span>${bi("EQUIPPED", "已装备")}</span>${equipmentImage(firstMarketItem.name, "inventory-item-art")}<strong>${firstMarketItem.id} · ${localizedText(firstMarketItem.name)}</strong></div><div><span>${bi("RETURNED TO INVENTORY", "退回库存")}</span>${equipmentImage(target.item, "inventory-item-art")}<strong>${target.itemId} · ${localizedText(target.item)}</strong></div></section><aside><div><span>${bi("Active items on Recruit", "该队员有效装备数")}</span><strong>1 → 1</strong></div><div><span>${bi("Inventory items", "库存装备数")}</span><strong>1 → 1</strong></div></aside><footer class="experience-stage-actions"><span>${bi("Replacement committed atomically", "替换已作为一次操作完成")}</span><button class="stage-primary" data-market-open-order>${bi("Continue to Party order", "继续调整横队")} ›</button></footer></div>`;
return frame(screen, body, { variant: "p8-2-equip-receipt", resources: false });
}
function marketOrderedCompany() {
const company = marketCompany();
if (!state.marketOrder.length) state.marketOrder = company.map((offer) => offer.id);
return state.marketOrder.map((id) => company.find((offer) => offer.id === id)).filter(Boolean);
}
function renderMarketOrder(screen) {
const ordered = marketOrderedCompany();
const body = `<div class="p82-order"><header class="experience-stage-title"><span class="stage-kicker">P8.2 · ${bi("PARTY ORDER", "队伍横队")}</span><h2>${bi("Tap a Recruit, then tap the destination", "先点击队员,再点击目标位置")}</h2><p>${bi("Protected rear stays left. Exposed position 0 stays right in every language.", "受保护后排始终在左;暴露的0号位始终在右,不随语言镜像。")}</p></header><div class="p82-order-direction"><span>← ${bi("PROTECTED REAR", "受保护后排")}</span><i></i><span>${bi("EXPOSED FRONT", "暴露前排")} →</span></div><section class="p82-order-line">${ordered.map((offer, index) => `<button type="button" data-market-order-unit="${offer.id}" class="${state.marketOrderSource === offer.id ? "is-source" : ""}"><span>${actorImage(offer, "party-actor-art")}</span><b>${ordered.length - 1 - index}</b><strong>${localizedText(offer.name)}</strong><small>${offer.id === state.marketEquipTarget ? firstMarketItem.id : offer.itemId}</small></button>`).join("")}</section><aside class="p82-order-read"><span>${bi("CURRENT EFFECT", "当前影响")}</span><strong>${state.marketOrderChanged ? bi("Order changed", "站位已经改变") : bi("Original order", "初始站位")}</strong><p>${state.marketOrderSource ? bi("Now tap a destination Recruit to swap positions.", "现在点击目标队员以交换位置。") : bi("Position changes alter protection, reach and first legal targets.", "站位会改变保护、攻击距离和首个合法目标。")}</p><small>${bi("Drag is optional; tap-select/tap-destination is complete.", "无需拖动;点击选择再点击目标即可完成。")}</small></aside><footer class="experience-stage-actions"><button class="stage-secondary" data-market-order-undo ${state.marketOrderChanged ? "" : "disabled"}>${bi("Undo order", "撤销站位")}</button><span>${state.marketOrderSource ? bi("Choose destination", "请选择目标位置") : state.marketOrderChanged ? bi("Order ready to save", "站位可以保存") : bi("Make one meaningful swap", "请完成一次有意义的换位")}</span><button class="stage-primary" data-market-order-save ${state.marketOrderChanged && !state.marketOrderSource ? "" : "disabled"}>${bi("Save Order", "保存站位")}</button></footer></div>`;
return frame(screen, body, { variant: "p8-2-order", resources: false });
}
function renderMarketReadyReceipt(screen) {
const ordered = marketOrderedCompany();
const target = marketCompany().find((offer) => offer.id === state.marketEquipTarget);
const body = `<div class="p82-ready"><header><span>✓</span><div><small>P8.2 · ${bi("PREPARATION RECEIPT", "整备回执")}</small><h2>${bi("Company preparation complete", "队伍整备完成")}</h2></div><b>PREP-P82-001</b></header><section class="p82-ready-line"><span>${bi("PROTECTED REAR", "受保护后排")}</span>${ordered.map((offer, index) => `<article><div>${actorImage(offer, "party-actor-art")}</div><b>${ordered.length - 1 - index}</b><strong>${localizedText(offer.name)}</strong><small>${offer.id === target.id ? firstMarketItem.id : offer.itemId}</small></article>`).join("")}<span>${bi("EXPOSED FRONT", "暴露前排")}</span></section><div class="p82-ready-facts"><article><span>${bi("Coin", "金币")}</span><strong>12</strong><small>52 − 14 − 14 − 12</small></article><article><span>${bi("Company / capacity", "队伍 / 容量")}</span><strong>4 / 4</strong><small>${bi("Two separate hire receipts", "两次独立招募回执")}</small></article><article><span>${bi("Equipment", "装备")}</span><strong>${firstMarketItem.id} → ${localizedText(target.name)}</strong><small>${target.itemId} ${bi("returned", "已退回库存")}</small></article><article><span>${bi("Order", "站位")}</span><strong>${ordered.map((offer) => offer.id).join(" → ")}</strong><small>${bi("Rear-left to front-right", "从左后排到右前排")}</small></article></div><div class="experience-next-package"><span>${bi("P8.3 LOCKED", "P8.3 尚未开启")}</span><strong>${bi("Risk and battle begin only after your P8.2 review.", "你确认 P8.2 后,才会开发风险与战斗体验。")}</strong></div><footer class="experience-stage-actions"><button class="stage-secondary" data-market-restart>${bi("Play P8.2 again", "重新体验 P8.2")}</button><span>${bi("End of Market preparation package", "集市整备体验包到此结束")}</span><button class="stage-primary" data-view="screens">${bi("Open design atlas", "查看完整设计图册")}</button></footer></div>`;
return frame(screen, body, { variant: "p8-2-ready", resources: false });
}
function renderMarketExperience() {
const marketScreen = getScreen("SHP-001");
const recruitScreen = getScreen("SHP-002");
const equipmentScreen = getScreen("EQP-002");
const orderScreen = getScreen("PTY-001");
let stage;
if (["market2", "market3"].includes(state.marketStep)) stage = renderMarketExperienceBoard(marketScreen);
else if (["hire_review", "hire2_review"].includes(state.marketStep)) stage = renderMarketHireReview(recruitScreen);
else if (["hire_receipt", "hire2_receipt"].includes(state.marketStep)) stage = renderMarketHireReceipt(recruitScreen);
else if (state.marketStep === "equipment_offer") stage = renderMarketEquipmentOffer(marketScreen);
else if (state.marketStep === "item_review") stage = renderMarketItemReview(getScreen("SHP-003"));
else if (state.marketStep === "item_receipt") stage = renderMarketItemReceipt(getScreen("SHP-003"));
else if (state.marketStep === "equip_select") stage = renderMarketEquipSelect(equipmentScreen);
else if (state.marketStep === "equip_receipt") stage = renderMarketEquipReceipt(equipmentScreen);
else if (state.marketStep === "order") stage = renderMarketOrder(orderScreen);
else stage = renderMarketReadyReceipt(orderScreen);
return `<section class="view playtest-view"><header class="view-heading"><div><span class="eyebrow">PHASE 8 · PACKAGE 2</span><h1>${bi("Play Market preparation", "体验集市整备")}</h1><p>${bi("Complete two hires, one equipment replacement and one Party-order decision. Risk remains locked.", "完成两次招募、一次装备替换和一次横队决策;风险选择仍未开启。")}</p></div><div class="heading-badges"><span>P8.1 ✓</span><span>P8.2 ${bi("Owner play gate", "等待你的体验结论")}</span></div></header><div class="experience-shell">${marketProgress()}<div class="stage-wrap">${stage}</div><div class="package-roadmap"><strong>P8.1 ${bi("Accepted", "已通过")}</strong><strong>P8.2 ${bi("Market preparation", "集市整备")}</strong><span>P8.3 ${bi("Risk and battle · locked", "风险与战斗 · 未开启")}</span><span>P8.4 ${bi("Result and recovery · locked", "结果与恢复 · 未开启")}</span><span>P8.5 ${bi("Integrated loop · locked", "完整循环 · 未开启")}</span></div></div></section>`;
}
function p83PreparedCompany() {
return [generatedOffers[2], firstMarketOffers[0], generatedOffers[3], firstMarketOffers[1]].map((offer) => ({
...offer,
itemId: offer.id === "OFF-03" ? firstMarketItem.id : offer.itemId,
item: offer.id === "OFF-03" ? firstMarketItem.name : offer.item,
}));
}
function p83RiskOffer() {
return p83RiskOffers.find((offer) => offer.id === state.riskSelected) || null;
}
function p83StepGroup() {
if (state.riskStep === "board") return "risk";
if (["review", "receipt"].includes(state.riskStep)) return "commit";
if (["opening", "battle"].includes(state.riskStep)) return "battle";
return "resolved";
}
function p83Progress() {
const group = p83StepGroup();
const currentIndex = riskExperienceSteps.findIndex((step) => step[0] === group);
return `<ol class="experience-progress p83-progress" aria-label="${bi("P8.3 progress", "P8.3 进度")}">${riskExperienceSteps.map((step, index) => `<li class="${index === currentIndex ? "is-current" : ""} ${index < currentIndex ? "is-complete" : ""}" ${index === currentIndex ? 'aria-current="step"' : ""}><span>${index < currentIndex ? "✓" : step[1]}</span><strong>${escapeHtml(experienceStepLabel(step))}</strong></li>`).join("")}</ol>`;
}
function p83PreparedLine(className = "p83-party-line") {
const company = p83PreparedCompany();
return `<div class="${className}"><span>${bi("PROTECTED REAR", "受保护后排")}</span>${company.map((offer, index) => `<article><div>${actorImage(offer, "p83-party-actor")}</div><b>${company.length - 1 - index}</b><strong>${localizedText(offer.name)}</strong><small>${offer.itemId}</small></article>`).join("")}<span>${bi("EXPOSED FRONT", "暴露前排")}</span></div>`;
}
function renderP83RiskBoard(screen) {
const selected = p83RiskOffer();
const body = `<div class="p83-risk-board"><header class="experience-stage-title"><span class="stage-kicker">P8.3 · ${bi("RISK BOARD", "风险看板")}</span><div><h2>${bi("Choose pressure, not a hidden result", "选择压力类型,而不是猜隐藏结果")}</h2><div class="p82-resource"><span>${bi("Coin", "金币")}</span><strong>12</strong><i>${bi("Standard retreat fee", "标准难度撤退费")} 6</i></div></div><p>${bi("Selection changes nothing. Compare known pressure, uncertainty and reward class.", "选择本身不会提交任何内容;请比较已知压力、不确定性和奖励类型。")}</p></header><section class="p83-risk-options">${p83RiskOffers.map((offer) => `<button type="button" data-p83-risk="${offer.id}" class="${selected && selected.id === offer.id ? "is-selected" : ""}" aria-pressed="${selected && selected.id === offer.id}"><span>${offer.id}</span><strong>${bi(offer.name[0], offer.name[1])}</strong><em>${bi(offer.exposure[0], offer.exposure[1])} ${bi("exposure", "暴露")}</em><p><b>${bi("Pressure", "压力")}</b>${bi(offer.pressure[0], offer.pressure[1])}</p><p><b>${bi("Unknown", "未知")}</b>${bi(offer.uncertainty[0], offer.uncertainty[1])}</p><div class="p83-risk-mini">${offer.enemies.map((enemy) => `<i class="${enemy[0] === "Unknown" ? "is-unknown" : ""}">${enemy[0] === "Unknown" ? "?" : `<img src="${actorAssets.Riftblade}" alt="" />`}<small>${bi(enemy[0], enemy[1])}</small></i>`).join("")}</div><small>${bi("Reward", "奖励")} · ${bi(offer.reward[0], offer.reward[1])}</small></button>`).join("")}</section><aside class="p83-risk-read">${selected ? `<span>${bi("CURRENT COMPARISON", "当前比较")}</span><h3>${bi(selected.name[0], selected.name[1])}</h3><strong>${bi(selected.coverage[0], selected.coverage[1])}</strong><div>${selected.enemies.map((enemy, index) => `<p><b>0${index + 1}</b>${bi(enemy[0], enemy[1])}</p>`).join("")}</div><small>${bi("Choosing has not spent Coin, changed order or started battle.", "当前选择没有花费金币、改变站位或开始战斗。")}</small>` : `<span>${bi("NO RISK SELECTED", "尚未选择风险")}</span><h3>${bi("Compare all three stable offers", "比较三个固定风险")}</h3><p>${bi("No option is recommended or preselected.", "界面不会推荐或预选任何选项。")}</p><div class="p83-prepared-facts"><b>${bi("Company", "队伍")} 4/4</b><b>12 ${bi("Coin", "金币")}</b><b>EQ-012 → Tomas</b><b>3 → 2 → 1 → 0</b></div>`}</aside><footer class="experience-stage-actions"><span>${bi("Prepared Company", "已整备队伍")} 4/4 · EQ-012 → Tomas Vale</span><button class="stage-primary" type="button" data-p83-review ${selected ? "" : "disabled"}>${bi("Review selected risk", "审核所选风险")} ›</button></footer></div>`;
return frame(screen, body, { variant: "p8-3-risk", resources: false });
}
function renderP83RiskReview(screen) {
const offer = p83RiskOffer();
const body = `<div class="p83-risk-review"><header class="experience-stage-title"><span class="stage-kicker">P8.3 · ${bi("RISK COMMITMENT", "风险确认")}</span><h2>${bi(offer.name[0], offer.name[1])}</h2><p>${bi("This is the only commit boundary. Cancel preserves the same selected offer.", "这是唯一确认边界;取消会保留当前选中的风险。")}</p></header><section class="p83-review-threat"><span>${bi("KNOWN THREAT", "已知威胁")}</span><h3>${bi(offer.pressure[0], offer.pressure[1])}</h3>${offer.enemies.map((enemy, index) => `<p><b>0${index + 1}</b>${bi(enemy[0], enemy[1])}</p>`).join("")}<small>${bi(offer.uncertainty[0], offer.uncertainty[1])}</small></section><section class="p83-review-company"><span>${bi("FROZEN PREPARATION", "冻结的整备状态")}</span>${p83PreparedLine("p83-review-line")}<p>12 ${bi("Coin", "金币")} · ${bi("Company", "队伍")} 4/4 · EQ-012 → Tomas Vale</p></section><aside class="p83-review-terms"><span>${bi("COMMIT TERMS", "确认条款")}</span><dl><div><dt>${bi("Risk ID", "风险编号")}</dt><dd>${offer.id}</dd></div><div><dt>${bi("Reward", "奖励")}</dt><dd>${bi(offer.reward[0], offer.reward[1])}</dd></div><div><dt>${bi("Exposure", "暴露")}</dt><dd>${bi(offer.exposure[0], offer.exposure[1])}</dd></div><div><dt>${bi("Entry cost", "进入费用")}</dt><dd>0 ${bi("Coin", "金币")}</dd></div><div><dt>${bi("Retreat", "撤退")}</dt><dd>6 ${bi("Coin", "金币")} · ${bi("not executed", "尚未执行")}</dd></div></dl></aside><footer class="experience-stage-actions"><button class="stage-secondary" type="button" data-p83-cancel>${bi("Cancel", "取消")}</button><span>${bi("Commit freezes risk and preparation once", "确认后只会冻结一次风险与整备状态")}</span><button class="stage-primary" type="button" data-p83-commit>${bi("Commit risk", "确认风险")}</button></footer></div>`;
return frame(screen, body, { variant: "p8-3-risk-review", resources: false });
}
function renderP83RiskReceipt(screen) {
const offer = p83RiskOffer();
const body = `<div class="p83-receipt"><header><span>✓</span><div><small>${bi("RISK RECEIPT", "风险回执")}</small><h2>${bi(offer.name[0], offer.name[1])} ${bi("committed", "已确认")}</h2></div><b>RISK-P83-001</b></header><section>${p83PreparedLine("p83-receipt-line")}</section><aside><div><span>${bi("Risk", "风险")}</span><strong>${offer.id}</strong></div><div><span>${bi("Coin", "金币")}</span><strong>12 → 12</strong></div><div><span>${bi("Order", "站位")}</span><strong>3 → 2 → 1 → 0</strong></div><div><span>${bi("Commit", "提交")}</span><strong>1 / 1</strong></div></aside><footer class="experience-stage-actions"><span>${bi("Battle timeline has not started", "战斗时间线尚未开始")}</span><button class="stage-primary" type="button" data-p83-open-battle>${bi("Open Battle", "进入战斗开场")} ›</button></footer></div>`;
return frame(screen, body, { variant: "p8-3-risk-receipt", resources: false });
}
function p83EnemyLine(offer) {
return `<div class="p83-enemy-line"><span>${bi("ENEMY FRONT", "敌方前排")}</span>${offer.enemies.map((enemy, index) => `<article class="${enemy[0] === "Unknown" ? "is-unknown" : ""}"><div>${enemy[0] === "Unknown" ? "?" : `<img src="${actorAssets.Riftblade}" alt="${escapeHtml(bi(enemy[0], enemy[1]))}" />`}</div><b>${index}</b><strong>${bi(enemy[0], enemy[1])}</strong></article>`).join("")}<span>${bi("ENEMY REAR", "敌方后排")}</span></div>`;
}
function p83MotionSprite(side, motion, speed, className = "") {
const frames = p83MotionAssets[side][motion] || p83MotionAssets[side].idle;
const fps = motion === "idle" ? 6 : motion === "advance" ? 10 : 12;
const duration = frames.length / fps;
const frameDuration = duration / frames.length;
return `<span class="p83-motion-sprite frames-${frames.length} ${className}" data-motion-subject="${side}" data-motion-state="${motion}" style="--motion-cycle:${duration.toFixed(4)}s;--motion-poster:url('${frames[0]}')">${frames.map((src, index) => `<img src="${src}" alt="" aria-hidden="true" style="--motion-delay:${(-index * frameDuration).toFixed(4)}s" />`).join("")}</span>`;
}
const p83ContactX = 52.5;
function p83FormationState(eventIndex, frozen = false) {
const entryAllies = [7, 17, 27, 37];
const engagedAllies = [46.5, 36, 16, 26];
const entryEnemies = [72, 82, 92];
const engagedEnemies = [58.2, 69.5, 80.8];
return {
phase: frozen ? "entry" : eventIndex === 0 ? "closing" : eventIndex >= 7 ? "replacement" : "contact",
translating: !frozen && eventIndex === 0,
allyFrom: entryAllies,
allyX: frozen ? entryAllies : engagedAllies,
enemyFrom: entryEnemies,
enemyX: frozen ? entryEnemies : eventIndex >= 7 ? [58.2, 58.2, 69.5] : engagedEnemies,
};
}
function p83AllyMotion(offer, eventIndex) {
if (offer.profession === "Hearthguard") {
if (eventIndex <= 1) return "advance";
if (eventIndex === 2) return "attack";
if (eventIndex === 3) return "protect";
return "idle";
}
if (eventIndex <= 1 && offer.profession === "Wayrunner") return "advance";
if (eventIndex === 2 && offer.profession === "Wayrunner") return "attack";
if (eventIndex === 4 && offer.profession === "Farstring") return "ranged";
if (eventIndex === 5 && offer.profession === "Kindhand") return "sustain";
return "idle";
}
function p83EnemyMotion(index, eventIndex) {
if (eventIndex <= 1) return "advance";
if (eventIndex === 2 && index === 0) return "hit";
if (eventIndex === 3 && index === 0) return "attack";
if (eventIndex === 4 && index > 0) return "hit";
if (eventIndex === 6 && index > 0) return "telegraph";
if (eventIndex >= 7 && index === 0) return "fallen";
if (eventIndex === 7 && index === 1) return "advance";
return "idle";
}
function p83AllyHealth(index, eventIndex) {
const base = [88, 100, 82, 92][index];
if (index === 1 && eventIndex >= 3 && eventIndex < 5) return 72;
if (index === 1 && eventIndex >= 5) return 88;
if (index === 0 && eventIndex >= 6) return 76;
return base;
}
function p83EnemyHealth(index, count, eventIndex) {
if (index === 0 && eventIndex >= 7) return 0;
if (index === 0 && eventIndex >= 2) return 72;
if (index === count - 1 && eventIndex >= 4) return 72;
return 100;
}
function p83AllyFighter(offer, index, eventIndex, formation) {
const motion = p83AllyMotion(offer, eventIndex);
const isFrameAnimated = offer.profession === "Hearthguard";
const body = isFrameAnimated
? p83MotionSprite("recruit", motion === "ranged" || motion === "sustain" ? "idle" : motion, state.riskBattleSpeed, "p83-fighter-frame")
: `<span class="p83-static-motion is-${motion}">${actorImage(offer, "p83-fighter-art")}</span>`;
const x = formation.allyX[index];
const hp = p83AllyHealth(index, eventIndex);
const lineRole = ["Wayrunner", "Hearthguard"].includes(offer.profession) ? "front" : "rear";
return `<article class="p83-fighter is-ally role-${offer.profession.toLowerCase()} motion-${motion} ${formation.translating ? "is-translating" : ""}" data-fighter-id="${offer.id}" data-motion-state="${motion}" data-line-role="${lineRole}" data-x="${x}" data-hp="${hp}" data-contacted="${eventIndex >= 1}" style="--fighter-from-x:${formation.allyFrom[index]}%;--fighter-x:${x}%">
<div class="p83-fighter-state"><span>${localizedText(offer.profession)}</span><b>${localizedText(offer.name)}</b><i role="meter" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${hp}"><em style="width:${hp}%"></em></i></div>
<div class="p83-fighter-body">${body}</div><span class="p83-ground-shadow"></span>
</article>`;
}
function p83EnemyFighter(enemy, index, count, eventIndex, formation) {
const motion = p83EnemyMotion(index, eventIndex);
const x = formation.enemyX[index] ?? formation.enemyX.at(-1);
const hp = p83EnemyHealth(index, count, eventIndex);
const fallen = index === 0 && eventIndex >= 7;
const casterDemo = index === count - 1 && eventIndex === 6;
return `<article class="p83-fighter is-enemy motion-${motion} ${enemy[0] === "Unknown" ? "is-unknown" : ""} ${fallen ? "is-fallen" : ""} ${formation.translating ? "is-translating" : ""}" data-enemy-index="${index}" data-motion-state="${motion}" data-x="${x}" data-hp="${hp}" data-alive="${!fallen}" ${casterDemo ? 'data-demo-role="PF-B06"' : ""} style="--fighter-from-x:${formation.enemyFrom[index] ?? formation.enemyFrom.at(-1)}%;--fighter-x:${x}%">
<div class="p83-fighter-state"><span>${bi("ENEMY", "敌人")} 0${index + 1}</span><b>${bi(enemy[0], enemy[1])}</b><i role="meter" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${hp}"><em style="width:${hp}%"></em></i></div>
<div class="p83-fighter-body">${enemy[0] === "Unknown" ? `<span class="p83-unknown-body">?</span>` : p83MotionSprite("enemy", motion, state.riskBattleSpeed, "p83-fighter-frame")}</div><span class="p83-ground-shadow"></span>
</article>`;
}
function p83ArenaEffects(eventIndex, enemyCount) {
if (eventIndex === 1) return `<div class="p83-contact-marker" data-contact-x="${p83ContactX}" style="--contact-x:${p83ContactX}%"><i></i><span>${bi("CONTACT · STOP", "接敌 · 停止推进")}</span></div>`;
if (eventIndex === 2) return `<div class="p83-contact-marker is-locked" data-contact-x="${p83ContactX}" style="--contact-x:${p83ContactX}%"><i></i><span>${bi("MELEE CONTACT", "近战接触")}</span></div><div class="p83-action-burst is-slash" aria-hidden="true">${effectImage("damage", "")}</div><b class="p83-float-number is-enemy-hit">-28</b><div class="p83-equipment-trigger">${equipmentImage(firstMarketItem.name, "")}<span>EQ-012 · ${bi("LEGAL RETARGET", "合法改换目标")}</span></div>`;
if (eventIndex === 3) return `<div class="p83-action-burst is-guard" aria-hidden="true">${effectImage("protect", "")}</div><b class="p83-float-number is-guarded">${bi("GUARD · 100→72", "格挡 · 100→72")}</b>`;
if (eventIndex === 4) return `<i class="p83-projectile" data-projectile-kind="arrow" data-source="MKT-R1-02" data-target="enemy-${enemyCount - 1}" aria-hidden="true"></i><div class="p83-action-burst is-ranged-hit" aria-hidden="true">${effectImage("damage", "")}</div><b class="p83-float-number is-rear-hit">100→72</b>`;
if (eventIndex === 5) return `<div class="p83-heal-link" data-source="OFF-04" data-target="MKT-R1-01"></div><div class="p83-heal-ring" data-heal-target="MKT-R1-01" aria-hidden="true"></div><b class="p83-float-number is-heal">72→88</b>`;
if (eventIndex === 6) return `<div class="p83-spell-demo" data-spell-demo-source="enemy-rear"><b>PF-B06 · ${bi("RIFTCALLER LANGUAGE", "裂隙召唤师动作语言")}</b><span>${bi("Enemy-side behavior demo · not a fifth ally", "敌方行为演示 · 不是我方第五人")}</span></div><div class="p83-spell-sigil" aria-hidden="true">✦</div><i class="p83-spell-projectile" data-projectile-kind="spell" data-target="OFF-03" aria-hidden="true"></i><div class="p83-spell-impact" aria-hidden="true"></div><b class="p83-float-number is-spell-hit">88→76</b>`;
if (eventIndex === 7) return `<div class="p83-contact-marker is-replacement" data-contact-x="${p83ContactX}" style="--contact-x:${p83ContactX}%"><i></i><span>${bi("NEXT LIVING FRONT ADVANCES", "下一名存活前排补进")}</span></div><div class="p83-no-respawn"><b>${bi("DOWN · STAYS DOWN", "倒下 · 不会复活")}</b><span>${bi("No Reserve entry", "无后备入场")}</span></div>`;
return "";
}
function p83BattleArena(offer, eventIndex, frozen = false) {
const company = p83PreparedCompany();
const formation = p83FormationState(eventIndex, frozen);
return `<section class="p83-arena ${frozen ? "is-frozen" : ""}" data-battle-event="${eventIndex}" data-formation-phase="${formation.phase}" aria-label="${bi("Animated horizontal automatic battle", "动态横向自动战斗")}">
<div class="p83-arena-depth"><i></i><i></i><i></i></div>
<div class="p83-side-marker is-allied"><span>${bi("COMPANY", "我方队伍")}</span><b>4</b></div>
<div class="p83-side-marker is-hostile"><span>${bi("ENEMIES", "敌方单位")}</span><b>${offer.enemies.length}</b></div>
<div class="p83-ground-line"><i></i></div>
${company.map((unit, index) => p83AllyFighter(unit, index, eventIndex, formation)).join("")}
${offer.enemies.map((enemy, index) => p83EnemyFighter(enemy, index, offer.enemies.length, eventIndex, formation)).join("")}
${p83ArenaEffects(eventIndex, offer.enemies.length)}
<div class="p83-direction is-left">${bi("REAR", "后排")} ←</div><div class="p83-direction is-right">→ ${bi("PRESSURE FRONT", "接敌前线")}</div>
</section>`;
}
function renderP83BattleOpening(screen) {
const offer = p83RiskOffer();
const body = `<div class="p83-opening p83-opening-dynamic"><header class="experience-stage-title"><span class="stage-kicker">P8.3R2 · ${bi("FRONTLINE BEHAVIOR", "前线行为演示")}</span><div><h2>${bi("Both lines will move until contact", "双方会推进到接触线,再自动战斗")}</h2><div class="p83-life-rule"><strong>${bi("NO COMBAT RESPAWN", "战斗中不会复活")}</strong><strong>${bi("NO RESERVE AUTO-ENTRY", "后备队员不会自动补位")}</strong></div></div><p>${bi("Watch melee stop at contact, rear healing and ranged attacks, a distinct spell demonstration, then frontline replacement.", "你将看到近战接敌停步、后排治疗与远射、独立法术演示,以及前排倒下后的补进。")}</p></header>${p83BattleArena(offer, 0, true)}<aside class="p83-opening-intel"><span>${bi("FIRST KNOWN THREAT", "首个已知威胁")}</span><strong>${bi(offer.pressure[0], offer.pressure[1])}</strong><p>${bi("The saved order enters unchanged. Role movement is shown visibly after entry; observation controls never issue actions.", "已保存站位原样入场;随后职业移动会直接显示,观察控制不会下达行动。")}</p></aside><footer class="experience-stage-actions"><span>${offer.id} · ${bi("four-person Company frozen", "四人队伍已冻结")}</span><button class="stage-primary" type="button" data-p83-start>${bi("Watch frontline behavior", "观看前线战斗")} ›</button></footer></div>`;
return frame(screen, body, { variant: "p8-3-opening", resources: false, artSrc: "assets/web/ch01_battle.webp" });
}
function renderP83Battle(screen) {
const offer = p83RiskOffer();
const eventIndex = Math.min(state.riskBattleTick, p83BattleEvents.length - 1);
const current = p83BattleEvents[eventIndex];
const inspected = p83BattleEvents[state.riskInspectEvent === null ? eventIndex : state.riskInspectEvent];
const visible = p83BattleEvents.slice(0, eventIndex + 1);
const progress = eventIndex / (p83BattleEvents.length - 1) * 100;
const body = `<div class="p83-battle speed-${state.riskBattleSpeed} ${state.riskBattlePaused ? "is-paused" : ""}"><header><div><span class="stage-kicker">P8.3R2 · ${bi("FRONTLINE BEHAVIOR", "前线行为演示")}</span><h2>${bi(current.title[0], current.title[1])}</h2></div><div class="p83-playback"><button type="button" data-p83-pause aria-label="${state.riskBattlePaused ? bi("Resume", "继续") : bi("Pause", "暂停")}">${state.riskBattlePaused ? "▶" : "Ⅱ"}</button>${[1,2,4].map((speed) => `<button type="button" data-p83-speed="${speed}" class="${state.riskBattleSpeed === speed ? "is-active" : ""}">${speed}x</button>`).join("")}<button type="button" data-p83-inspect>${bi("Cause", "因果")}</button></div></header><div class="p83-battle-rules"><span>${bi("No combat respawn", "战斗中不会复活")}</span><span>${bi("No Reserve auto-entry", "后备不会自动补位")}</span><b>${state.riskBattlePaused ? bi("PAUSED", "已暂停") : `${state.riskBattleSpeed}x · ${bi("AUTO", "自动")}`}</b></div>${p83BattleArena(offer, eventIndex)}<div class="p83-battle-read"><article><span>${current.time} · ${bi("CURRENT ACTION", "当前动作")}</span><strong>${bi(current.title[0], current.title[1])}</strong><p>${bi(current.detail[0], current.detail[1])}</p></article><div class="p83-timeline"><i style="--battle-progress:${progress}%"></i>${p83BattleEvents.map((event, index) => `<button type="button" data-p83-event="${index}" ${index > eventIndex ? "disabled" : ""} class="${index <= eventIndex ? "is-reached" : ""} ${index === eventIndex ? "is-current" : ""}"><span>${event.time}</span></button>`).join("")}</div></div>${eventIndex === p83BattleEvents.length - 1 ? `<footer class="p83-battle-finish"><span>${bi("Behavior demonstration complete; result is still withheld", "战斗行为演示完成;胜负结果仍未揭示")}</span><button class="stage-primary" type="button" data-p83-finish>${bi("Complete observation", "完成观察")} ›</button></footer>` : ""}${state.riskInspectOpen ? `<div class="p83-cause-sheet"><span>${bi("PINNED CAUSE", "固定因果事件")} · ${inspected.time}</span><h3>${bi(inspected.title[0], inspected.title[1])}</h3><p>${bi(inspected.detail[0], inspected.detail[1])}</p><dl><div><dt>${bi("Risk", "风险")}</dt><dd>${offer.id}</dd></div><div><dt>${bi("Prepared order", "整备站位")}</dt><dd>OFF-03 → MKT-R1-01 → OFF-04 → MKT-R1-02</dd></div><div><dt>${bi("Observation", "观察")}</dt><dd>${bi("Motion paused; no progress or result mutation", "动作已暂停;不会改变进度或结果")}</dd></div></dl><button class="stage-primary" type="button" data-p83-close-inspect>${bi("Close cause", "关闭因果说明")}</button></div>` : ""}</div>`;
return frame(screen, body, { variant: "p8-3-battle", resources: false, artSrc: "assets/web/ch01_battle.webp" });
}
function renderP83Resolved(screen) {
const offer = p83RiskOffer();
const body = `<div class="p83-resolved"><header><span>✓</span><div><small>P8.3 · ${bi("TIMELINE RESOLVED", "时间线解析完成")}</small><h2>${bi("Observation complete", "自动战斗观察完成")}</h2></div><b>BATTLE-P83-001</b></header><section><article><span>${bi("Chosen risk", "所选风险")}</span><strong>${bi(offer.name[0], offer.name[1])}</strong><small>${offer.id}</small></article><article><span>${bi("Last event", "最后事件")}</span><strong>${p83BattleEvents.at(-1).time}</strong><small>${bi(p83BattleEvents.at(-1).title[0], p83BattleEvents.at(-1).title[1])}</small></article><article><span>${bi("Observation controls", "观察控制")}</span><strong>${bi("Pause · 1x/2x/4x · Cause", "暂停 · 1x/2x/4x · 因果")}</strong><small>${bi("Result unchanged", "结果未被改变")}</small></article></section><div class="p83-resolved-line">${p83PreparedLine("p83-receipt-line")}<aside><span>${bi("PREPARATION CAUSES SEEN", "已看到的整备因果")}</span><b>EQ-012 ${bi("retargeted", "改变目标")}</b><b>${bi("adjacent Guard intercepted", "相邻护盾完成拦截")}</b><b>${bi("rear reach interrupted pressure", "后排射程打断压力")}</b></aside></div><div class="experience-next-package"><span>${bi("P8.4 LOCKED", "P8.4 尚未开启")}</span><strong>${bi("Victory/defeat, casualties, rewards and recovery begin only after your P8.3 review.", "胜负、伤亡、奖励与恢复要在你确认 P8.3 后才会开发。")}</strong></div><footer class="experience-stage-actions"><button class="stage-secondary" type="button" data-p83-restart>${bi("Play P8.3 again", "重新体验 P8.3")}</button><span>${bi("No result judgment in this package", "本体验包不包含结果判断")}</span><button class="stage-primary" type="button" data-view="screens">${bi("Open design atlas", "查看完整设计图册")}</button></footer></div>`;
return frame(screen, body, { variant: "p8-3-resolved", resources: false });
}
function renderRiskBattleExperience() {
let stage;
if (state.riskStep === "board") stage = renderP83RiskBoard(getScreen("RSK-001"));
else if (state.riskStep === "review") stage = renderP83RiskReview(getScreen("RSK-002"));
else if (state.riskStep === "receipt") stage = renderP83RiskReceipt(getScreen("RSK-002"));
else if (state.riskStep === "opening") stage = renderP83BattleOpening(getScreen("BAT-001"));
else if (state.riskStep === "battle") stage = renderP83Battle(getScreen("BAT-002"));
else stage = renderP83Resolved(getScreen("BAT-002"));
return `<section class="view playtest-view"><header class="view-heading"><div><span class="eyebrow">PHASE 8 · PACKAGE 3</span><h1>${bi("Play Risk and automatic battle", "体验风险与自动战斗")}</h1><p>${bi("Choose one pressure profile, freeze preparation and observe one causal automatic-battle sequence. Results remain locked.", "选择一种压力、冻结整备状态,并观察一条可解释的自动战斗因果链;结果仍未开启。")}</p></div><div class="heading-badges"><span>P8.1 ✓</span><span>P8.2 ✓</span><span>P8.3 ${bi("Owner play gate", "等待你的体验结论")}</span></div></header><div class="experience-shell">${p83Progress()}<div class="stage-wrap">${stage}</div><div class="package-roadmap"><strong>P8.1 ${bi("Accepted", "已通过")}</strong><strong>P8.2 ${bi("Accepted", "已通过")}</strong><strong>P8.3 ${bi("Risk and battle", "风险与战斗")}</strong><span>P8.4 ${bi("Result and recovery · locked", "结果与恢复 · 未开启")}</span><span>P8.5 ${bi("Integrated loop · locked", "完整循环 · 未开启")}</span></div></div></section>`;
}
function p84StepGroup() {
if (["diagnosis"].includes(state.resultStep)) return "result";
if (["casualty_review", "casualty_receipt"].includes(state.resultStep)) return "casualty";
if (["reward", "artifact", "reward_review", "reward_receipt"].includes(state.resultStep)) return "reward";
if (["recovery", "recovery_review", "recovery_receipt"].includes(state.resultStep)) return "recovery";
return "market";
}
function p84Progress() {
const group = p84StepGroup();
const currentIndex = resultExperienceSteps.findIndex((step) => step[0] === group);
return `<ol class="experience-progress p84-progress" aria-label="${bi("P8.4 progress", "P8.4 进度")}">${resultExperienceSteps.map((step, index) => `<li class="${index === currentIndex ? "is-current" : ""} ${index < currentIndex ? "is-complete" : ""}" ${index === currentIndex ? 'aria-current="step"' : ""}><span>${index < currentIndex ? "✓" : step[1]}</span><strong>${escapeHtml(experienceStepLabel(step))}</strong></li>`).join("")}</ol>`;
}
function p84RewardResult() {
if (!state.rewardCommitted || state.rewardSelected === "decline") return { coin: 46, item: null, artifact: null, label: bi("Declined", "已放弃") };
if (state.rewardSelected === "coin") return { coin: 59, item: null, artifact: null, label: bi("13 Coin", "13金币") };
if (state.rewardSelected === "equipment") return { coin: 46, item: "EQ-017", artifact: null, label: bi("EQ-017 Bracehook Cuff", "EQ-017 支撑钩腕带") };
const artifact = p84Artifacts.find((entry) => entry.id === state.artifactSelected);
return { coin: 46, item: null, artifact: artifact?.id || null, label: artifact ? `${artifact.id} ${bi(artifact.name[0], artifact.name[1])}` : "" };
}
function p84RecoveryResult() {
const reward = p84RewardResult();
if (!state.recoveryCommitted) return { ...reward, credit: 14, company: 3, hired: false, protected: true };
if (state.recoverySelected === "credit") return { ...reward, credit: 0, company: 4, hired: true, protected: false };
if (state.recoverySelected === "coin") return { ...reward, coin: reward.coin - 14, credit: 14, company: 4, hired: true, protected: false };
return { ...reward, credit: 14, company: 3, hired: false, protected: false };
}
function p84SurvivorLine(includeRecovery = false) {
const survivors = p83PreparedCompany().filter((entry) => entry.id !== "MKT-R1-02");
const units = includeRecovery && p84RecoveryResult().hired
? [...survivors, { id: "RCV-P84-01", name: "Vela Dorn", profession: "Farstring", art: actorAssets.Farstring, itemId: null }]
: survivors;
return `<div class="p84-company-line">${units.map((unit) => `<article data-company-unit="${unit.id}"><div>${actorImage(unit, "p84-company-actor")}</div><strong>${localizedText(unit.name)}</strong><small>${localizedText(unit.profession)} · ${unit.itemId || bi("no item", "无装备")}</small></article>`).join("")}${units.length === 3 ? `<article class="is-vacant" data-company-vacancy><div>+</div><strong>${bi("Open footprint", "空缺位置")}</strong><small>${bi("No automatic replacement", "不会自动补员")}</small></article>` : ""}</div>`;
}
function renderP84Diagnosis(screen) {
const body = `<div class="p84-diagnosis" data-p84-state="diagnosis"><header><span class="p84-victory-seal">${bi("VICTORY", "胜利")}</span><div><small>P8.4 · ${bi("EARLIEST DIVERGENCE", "最早因果分歧")}</small><h2>${bi("The line won, but Ilya did not return", "队伍赢了,但 Ilya 没有回来")}</h2></div><b>OUTCOME-P84-001</b></header><section class="p84-cause-replay"><div class="p84-cause-arena">${p83PreparedCompany().map((unit) => `<article class="${unit.id === "MKT-R1-02" ? "is-fallen" : ""}">${actorImage(unit, "p84-cause-actor")}<strong>${localizedText(unit.name)}</strong></article>`).join("")}<i class="p84-rider-line"></i><span class="p84-cause-impact">EQ-014</span></div><ol><li><b>00:13.6 · EV-P84-09</b><span>${bi("Rear pressure reaches Ilya as an area hit.", "后排压力以范围攻击命中 Ilya。")}</span></li><li class="is-cause"><b>${bi("FIRST DECISIVE DIVERGENCE", "首个决定性分歧")}</b><span>${bi("Quiet Rearplate converts that area hit to direct. The disclosed direct-hit rider resolves and Ilya becomes Fallen.", "寂静后甲把范围攻击转成直接命中;已公开的直接命中追加效果结算,Ilya 进入阵亡状态。")}</span></li><li><b>00:14.4</b><span>${bi("The remaining three resolve the final legal attack and win.", "其余三人完成最后一次合法攻击并取得胜利。")}</span></li></ol></section><aside class="p84-result-totals"><article><span>${bi("Outcome", "战果")}</span><strong>${bi("Victory", "胜利")}</strong><small>RSK-P83-02 · B0/R1/Q2</small></article><article><span>${bi("Battle Coin", "战斗金币")}</span><strong>+34</strong><small>12 → 46</small></article><article><span>${bi("Living Company", "存活队伍")}</span><strong>3 / 4</strong><small>${bi("before settlement", "等待伤亡结算")}</small></article></aside><footer class="experience-stage-actions"><span>${bi("Diagnosis appears before totals; no loss or reward has been committed.", "先解释因果,再显示汇总;伤亡与奖励都尚未提交。")}</span><button class="stage-primary" type="button" data-p84-casualty-review>${bi("Review permanent loss", "审核永久损失")} ›</button></footer></div>`;
return frame(screen, body, { variant: "p8-4-diagnosis", resources: false });
}
function renderP84CasualtyReview(screen) {
const ilya = firstMarketOffers.find((entry) => entry.id === "MKT-R1-02");
const body = `<div class="p84-casualty" data-p84-state="casualty-review"><header><span class="stage-kicker">P8.4 · ${bi("CASUALTY SETTLEMENT", "伤亡结算")}</span><h2>${bi("One atomic loss, three exact consequences", "一次原子结算,三项明确后果")}</h2><p>${bi("Review changes nothing. Commit permanently removes Ilya, returns her item and grants Credit together.", "审核不会改变状态;提交后会同时永久移除 Ilya、退回装备并发放补员凭证。")}</p></header><section class="p84-casualty-person"><div>${actorImage(ilya, "p84-casualty-actor")}</div><small>MKT-R1-02 · ${bi("FALLEN", "已阵亡")}</small><h3>Ilya Venn</h3><p>${bi("Farstring · Level 1 · Clear Measure", "远弦 · 1级 · 清晰尺度")}</p><strong>${bi("No resurrection · no Reserve auto-entry", "不会复活 · 后备不会自动入场")}</strong></section><section class="p84-casualty-deltas"><article><span>1</span><div><b>${bi("Permanent Recruit removal", "永久移除队员")}</b><p>MKT-R1-02 · Company 4 → 3</p></div></article><article><span>2</span><div><b>${bi("Equipment return", "装备退回")}</b><p>EQ-014 Quiet Rearplate → ${bi("inventory", "库存")}</p></div></article><article><span>3</span><div><b>${bi("Replacement Credit", "补员凭证")}</b><p>min(14, 10 + floor(14 / 2)) = <strong>14</strong></p></div></article></section><aside><span>${bi("OPERATION PREVIEW", "操作预览")}</span><strong>CASUALTY-P84-001</strong><dl><div><dt>${bi("Coin", "金币")}</dt><dd>46 → 46</dd></div><div><dt>${bi("Credit", "凭证")}</dt><dd>0 → 14</dd></div><div><dt>${bi("Inventory", "库存")}</dt><dd>+ EQ-014</dd></div></dl></aside><footer class="experience-stage-actions"><button class="stage-secondary" type="button" data-p84-back-diagnosis>${bi("Back to diagnosis", "返回因果诊断")}</button><span>${bi("All three deltas apply or none apply", "三项后果必须全部提交,否则全部不提交")}</span><button class="stage-danger" type="button" data-p84-casualty-commit>${bi("Settle casualty", "确认伤亡结算")}</button></footer></div>`;
return frame(screen, body, { variant: "p8-4-casualty-review", resources: false });
}
function renderP84CasualtyReceipt(screen) {
const body = `<div class="p84-receipt" data-p84-state="casualty-receipt"><header><span>✓</span><div><small>${bi("CASUALTY RECEIPT", "伤亡回执")}</small><h2>${bi("Ilya is permanently removed", "Ilya 已永久离队")}</h2></div><b>CASUALTY-P84-001</b></header><section>${p84SurvivorLine()}</section><aside><article><span>${bi("Company", "队伍")}</span><strong>4 → 3</strong></article><article><span>${bi("Credit", "凭证")}</span><strong>0 → 14</strong></article><article><span>${bi("Returned", "已退回")}</span><strong>EQ-014</strong></article><article><span>${bi("Commit", "提交")}</span><strong>1 / 1</strong></article></aside><div class="p84-rule-note"><b>${bi("Protected recovery offer created", "已创建受保护补员")}</b><span>RCV-P84-01 · Vela Dorn · Farstring · ${bi("Level 1", "1级")}</span></div><footer class="experience-stage-actions"><span>${bi("No reward has been selected or claimed", "奖励尚未选择或领取")}</span><button class="stage-primary" type="button" data-p84-open-reward>${bi("Open elite reward", "查看精英奖励")} ›</button></footer></div>`;
return frame(screen, body, { variant: "p8-4-casualty-receipt", resources: false });
}
function renderP84Reward(screen) {
const selected = p84Rewards.find((entry) => entry.id === state.rewardSelected);
const body = `<div class="p84-reward" data-p84-state="reward-draft"><header><span class="stage-kicker">P8.4 · ${bi("ELITE REWARD · CHOOSE 1", "精英奖励 · 三选一")}</span><div><h2>${bi("Choose what changes the next decision", "选择下一步要改变的决策")}</h2><b>${bi("Budget", "预算")} 26 · ${bi("Coin already paid", "战斗金币已发放")} 46</b></div><p>${bi("No card is recommended or preselected. Leaving this page never rerolls it.", "没有推荐或预选;离开页面再返回也不会重抽。")}</p></header><section class="p84-reward-options">${p84Rewards.map((reward) => `<button type="button" data-p84-reward="${reward.id}" class="${state.rewardSelected === reward.id ? "is-selected" : ""}" aria-pressed="${state.rewardSelected === reward.id}"><span>${reward.mark}</span><small>${reward.code}</small><strong>${bi(reward.name[0], reward.name[1])}</strong><p>${bi(reward.detail[0], reward.detail[1])}</p><em>${state.rewardSelected === reward.id ? bi("Selected", "已选择") : bi("Compare", "比较")}</em></button>`).join("")}</section><aside class="p84-reward-read">${selected ? `<span>${bi("CURRENT CHOICE", "当前选择")}</span><strong>${bi(selected.name[0], selected.name[1])}</strong><p>${bi(selected.detail[0], selected.detail[1])}</p><small>${selected.id === "artifact" ? bi("Choose one exact Artifact next", "下一步还要选择一件具体神器") : bi("Selection has not been claimed", "当前选择尚未领取")}</small>` : `<span>${bi("NO REWARD SELECTED", "尚未选择奖励")}</span><strong>${bi("All three remain stable", "三个奖励保持不变")}</strong><p>${bi("You may also decline the entire draft.", "也可以放弃整组奖励。")}</p>`}</aside><footer class="experience-stage-actions"><button class="stage-secondary" type="button" data-p84-reward-decline>${bi("Decline all", "放弃全部奖励")}</button><span>${bi("Reward is separate from loss and recovery", "奖励与伤亡、恢复彼此独立")}</span><button class="stage-primary" type="button" data-p84-reward-next ${selected ? "" : "disabled"}>${selected?.id === "artifact" ? bi("Choose Artifact", "选择具体神器") : bi("Review reward", "审核奖励")} ›</button></footer></div>`;
return frame(screen, body, { variant: "p8-4-reward", resources: false });
}
function renderP84ArtifactChoice(screen) {
const body = `<div class="p84-artifact-choice" data-p84-state="artifact-choice"><header><span class="stage-kicker">P8.4 · ${bi("ARTIFACT CARD", "神器奖励卡")}</span><h2>${bi("Choose one run-wide law", "选择一条整轮规则")}</h2><p>${bi("These fixed IDs do not occupy a Recruit equipment slot.", "这三个固定神器不会占用队员的装备槽。")}</p></header><section>${p84Artifacts.map((artifact) => `<button type="button" data-p84-artifact="${artifact.id}" class="${state.artifactSelected === artifact.id ? "is-selected" : ""}" aria-pressed="${state.artifactSelected === artifact.id}"><span>${artifact.mark}</span><small>${artifact.id}</small><strong>${bi(artifact.name[0], artifact.name[1])}</strong><p>${bi(artifact.rule[0], artifact.rule[1])}</p></button>`).join("")}</section><aside><b>${bi("RUN-WIDE", "全局生效")}</b><span>${bi("One stable ID · no duplicate · no stack", "固定一件 · 不重复 · 不叠加")}</span></aside><footer class="experience-stage-actions"><button class="stage-secondary" type="button" data-p84-artifact-back>${bi("Back to reward draft", "返回奖励选择")}</button><span>${state.artifactSelected || bi("No Artifact selected", "尚未选择神器")}</span><button class="stage-primary" type="button" data-p84-artifact-review ${state.artifactSelected ? "" : "disabled"}>${bi("Review Artifact reward", "审核神器奖励")} ›</button></footer></div>`;
return frame(screen, body, { variant: "p8-4-artifact", resources: false });
}
function renderP84RewardReview(screen) {
const reward = state.rewardSelected === "decline" ? null : p84Rewards.find((entry) => entry.id === state.rewardSelected);
const artifact = p84Artifacts.find((entry) => entry.id === state.artifactSelected);
const label = reward?.id === "artifact" ? `${artifact.id} ${bi(artifact.name[0], artifact.name[1])}` : reward ? `${reward.code} ${bi(reward.name[0], reward.name[1])}` : bi("Decline all rewards", "放弃全部奖励");
const body = `<div class="p84-review" data-p84-state="reward-review"><header><span class="stage-kicker">P8.4 · ${bi("REWARD REVIEW", "奖励审核")}</span><h2>${label}</h2><p>${bi("This commit cannot change the casualty receipt or spend Replacement Credit.", "本次提交不会改变伤亡回执,也不会消耗补员凭证。")}</p></header><section><span>${reward ? reward.mark : "×"}</span><div><small>${reward?.id === "artifact" ? artifact.id : reward?.code || "DECLINED"}</small><strong>${label}</strong><p>${reward?.id === "artifact" ? bi(artifact.rule[0], artifact.rule[1]) : reward ? bi(reward.detail[0], reward.detail[1]) : bi("No reward object or Coin will be granted.", "不会获得任何奖励物或金币。")}</p></div></section><aside><dl><div><dt>${bi("Coin", "金币")}</dt><dd>${reward?.id === "coin" ? "46 → 59" : "46 → 46"}</dd></div><div><dt>${bi("Credit", "凭证")}</dt><dd>14 → 14</dd></div><div><dt>${bi("Company", "队伍")}</dt><dd>3 → 3</dd></div><div><dt>${bi("Reward commit", "奖励提交")}</dt><dd>0 → 1</dd></div></dl></aside><footer class="experience-stage-actions"><button class="stage-secondary" type="button" data-p84-reward-revise>${bi("Revise reward", "修改奖励")}</button><span>${bi("One reward operation only", "只会产生一次奖励事务")}</span><button class="stage-primary" type="button" data-p84-reward-commit>${bi("Claim reward", "领取奖励")}</button></footer></div>`;
return frame(screen, body, { variant: "p8-4-reward-review", resources: false });
}
function renderP84RewardReceipt(screen) {
const reward = p84RewardResult();
const artifact = p84Artifacts.find((entry) => entry.id === reward.artifact);
const detail = artifact ? bi(artifact.rule[0], artifact.rule[1])
: reward.item ? bi("When a neighbor is pulled, the wearer may share the pull as a swap.", "相邻队员被拉动时,佩戴者可一同移动并交换位置。")
: state.rewardSelected === "coin" ? bi("Thirteen Coin is immediately available for the recovery decision.", "13金币现在可直接用于恢复决策。")
: bi("The fixed draft was declined without mutating the run.", "固定奖励组已放弃,整轮状态没有新增奖励。") ;
const surface = artifact ? bi("Next trigger surface: result diagnosis and reward quality", "下次触发界面:战果诊断与奖励品质")
: reward.item ? bi("Next surface: inventory; active equipment slots unchanged", "下一界面:库存;所有主动装备槽保持不变")
: state.rewardSelected === "coin" ? bi("Next surface: recovery cost comparison", "下一界面:恢复费用比较")
: bi("Next surface: recovery only", "下一界面:仅恢复选择");
const body = `<div class="p84-receipt" data-p84-state="reward-receipt"><header><span>✓</span><div><small>${bi("REWARD RECEIPT", "奖励回执")}</small><h2>${reward.label}</h2></div><b>REWARD-P84-001</b></header><section class="p84-reward-receipt-object"><span>${reward.artifact ? "◇" : reward.item ? "⌁" : state.rewardSelected === "coin" ? "+" : "×"}</span><div><small>${reward.artifact ? "RUN-WIDE ARTIFACT" : reward.item ? "INVENTORY EQUIPMENT" : state.rewardSelected === "coin" ? "LIQUID REWARD" : "DECLINED"}</small><strong>${reward.label}</strong><p>${detail}</p><em>${surface}</em></div></section><aside><article><span>${bi("Coin", "金币")}</span><strong>${reward.coin}</strong></article><article><span>${bi("Credit", "凭证")}</span><strong>14</strong></article><article><span>${bi("Company", "队伍")}</span><strong>3 / 4</strong></article><article><span>${bi("Commit", "提交")}</span><strong>1 / 1</strong></article></aside><footer class="experience-stage-actions"><span>${bi("Recovery remains unselected and uncommitted", "恢复方案仍未选择、未提交")}</span><button class="stage-primary" type="button" data-p84-open-recovery>${bi("Choose recovery", "选择恢复方案")} ›</button></footer></div>`;
return frame(screen, body, { variant: "p8-4-reward-receipt", resources: false });
}
function renderP84Recovery(screen) {
const reward = p84RewardResult();
const selected = p84RecoveryOffers.find((entry) => entry.id === state.recoverySelected);
const body = `<div class="p84-recovery" data-p84-state="recovery"><header><span class="stage-kicker">P8.4 · ${bi("PROTECTED RECOVERY OFFER", "受保护补员")}</span><div><h2>RCV-P84-01 · Vela Dorn</h2><b>${bi("Farstring · Level 1 · list price 14", "远弦 · 1级 · 标价14")}</b></div><p>${bi("Hiring does not auto-equip returned EQ-014. Choose how to fund the hire, or continue with three.", "招募后不会自动装备退回的EQ-014;请选择支付方式,或以三人短编继续。")}</p></header><section class="p84-recovery-offer"><div>${actorImage({ art: actorAssets.Farstring, name: "Vela Dorn" }, "p84-recovery-actor")}</div><span>${bi("MISSING ROLE", "缺失职责")}</span><strong>${bi("Rear reach restored", "恢复后排射程")}</strong><small>${bi("Protected from refresh until bought or declined", "在购买或明确放弃前不会被刷新")}</small></section><section class="p84-recovery-options">${p84RecoveryOffers.map((offer) => `<button type="button" data-p84-recovery="${offer.id}" class="tone-${offer.tone} ${state.recoverySelected === offer.id ? "is-selected" : ""}" aria-pressed="${state.recoverySelected === offer.id}"><small>${bi(offer.cost[0], offer.cost[1])}</small><strong>${bi(offer.name[0], offer.name[1])}</strong><p>${bi(offer.result[0], offer.result[1])}</p></button>`).join("")}</section><aside><span>${bi("CURRENT FUNDS", "当前资源")}</span><strong>${reward.coin} ${bi("Coin", "金币")} · 14 ${bi("Credit", "凭证")}</strong><small>${selected ? bi("Selection is reversible until review commit.", "提交前可以随时修改选择。") : bi("No recovery option is preselected.", "没有预选任何恢复方案。")}</small></aside><footer class="experience-stage-actions"><span>EQ-014 → ${bi("inventory", "库存")} · ${bi("Company", "队伍")} 3/4</span><button class="stage-primary" type="button" data-p84-recovery-review ${selected ? "" : "disabled"}>${bi("Review recovery", "审核恢复方案")} ›</button></footer></div>`;
return frame(screen, body, { variant: "p8-4-recovery", resources: false });
}
function renderP84RecoveryReview(screen) {
const choice = p84RecoveryOffers.find((entry) => entry.id === state.recoverySelected);
const before = p84RewardResult();
const after = state.recoverySelected === "credit" ? { coin: before.coin, credit: 0, company: 4 }
: state.recoverySelected === "coin" ? { coin: before.coin - 14, credit: 14, company: 4 }
: { coin: before.coin, credit: 14, company: 3 };
const body = `<div class="p84-review" data-p84-state="recovery-review"><header><span class="stage-kicker">P8.4 · ${bi("RECOVERY REVIEW", "恢复审核")}</span><h2>${bi(choice.name[0], choice.name[1])}</h2><p>${bi("This is the only recovery commit. Back returns to the same three stable options.", "这是唯一恢复提交点;返回后仍是同样三个固定选项。")}</p></header><section><span>${state.recoverySelected === "short" ? "3" : "4"}</span><div><small>${state.recoverySelected === "short" ? "SHORT COMPANY" : "RCV-P84-01"}</small><strong>${bi(choice.result[0], choice.result[1])}</strong><p>${state.recoverySelected === "short" ? bi("Vacancy remains visible at the next Market.", "下一集市会继续显示空缺位置。") : bi("Vela joins with no active item; EQ-014 stays in inventory.", "Vela 加入但没有装备;EQ-014仍在库存。")}</p></div></section><aside><dl><div><dt>${bi("Coin", "金币")}</dt><dd>${before.coin} → ${after.coin}</dd></div><div><dt>${bi("Credit", "凭证")}</dt><dd>14 → ${after.credit}</dd></div><div><dt>${bi("Company", "队伍")}</dt><dd>3 → ${after.company}</dd></div><div><dt>${bi("Protected", "受保护状态")}</dt><dd>${bi("open → cleared", "有效→清除")}</dd></div></dl></aside><footer class="experience-stage-actions"><button class="stage-secondary" type="button" data-p84-recovery-revise>${bi("Revise recovery", "修改恢复方案")}</button><span>${bi("No automatic equipment or order change", "不会自动装备或改变站位")}</span><button class="stage-primary" type="button" data-p84-recovery-commit>${bi("Commit recovery", "提交恢复方案")}</button></footer></div>`;
return frame(screen, body, { variant: "p8-4-recovery-review", resources: false });
}
function renderP84RecoveryReceipt(screen) {
const result = p84RecoveryResult();
const body = `<div class="p84-receipt" data-p84-state="recovery-receipt"><header><span>✓</span><div><small>${bi("RECOVERY RECEIPT", "恢复回执")}</small><h2>${result.hired ? bi("Vela joined the Company", "Vela 已加入队伍") : bi("Three-Recruit continuation confirmed", "已确认三人短编继续")}</h2></div><b>RECOVERY-P84-001</b></header><section>${p84SurvivorLine(true)}</section><aside><article><span>${bi("Coin", "金币")}</span><strong>${result.coin}</strong></article><article><span>${bi("Credit", "凭证")}</span><strong>${result.credit}</strong></article><article><span>${bi("Company", "队伍")}</span><strong>${result.company} / 4</strong></article><article><span>${bi("Commit", "提交")}</span><strong>1 / 1</strong></article></aside><div class="p84-rule-note"><b>EQ-014 ${bi("remains in inventory", "仍在库存")}</b><span>${result.hired ? bi("Vela has no active equipment", "Vela 当前无装备") : bi("Vacancy remains explicit", "空缺位置继续显示")}</span></div><footer class="experience-stage-actions"><span>${bi("All P8.4 transactions are now closed", "P8.4的所有事务均已关闭")}</span><button class="stage-primary" type="button" data-p84-open-market>${bi("Open next-Market handoff", "查看下一集市状态")} ›</button></footer></div>`;
return frame(screen, body, { variant: "p8-4-recovery-receipt", resources: false });
}
function renderP84NextMarket(screen) {
const result = p84RecoveryResult();
const inventory = ["EQ-014", result.item].filter(Boolean);
const body = `<div class="p84-next-market" data-p84-state="next-market"><header><span class="stage-kicker">P8.4 · ${bi("NEXT MARKET HANDOFF", "下一集市交接")}</span><div><h2>${bi("Round 2 Market is ready", "第2轮集市已准备")}</h2><b>NEXT-MARKET-P84-001</b></div><p>${bi("This is the exact post-result state, not a promise that P8.5 is implemented.", "这是精确的结算后状态,不代表P8.5已经实现。")}</p></header><section class="p84-market-company">${p84SurvivorLine(true)}</section><section class="p84-market-ledger"><article><span>${bi("Coin", "金币")}</span><strong>${result.coin}</strong><small>${state.recoverySelected === "coin" ? bi("14 spent on Vela", "14用于招募Vela") : bi("No recovery Coin spent", "恢复阶段未花金币")}</small></article><article><span>${bi("Credit", "凭证")}</span><strong>${result.credit}</strong><small>${result.credit ? bi("Available for a later Recruit", "可用于后续招募") : bi("Spent on Vela", "已用于招募Vela")}</small></article><article><span>${bi("Inventory", "库存")}</span><strong>${inventory.length ? inventory.join(" · ") : bi("No new item", "无新增装备")}</strong><small>EQ-014 ${bi("not auto-equipped", "未自动装备")}</small></article><article><span>${bi("Artifact", "神器")}</span><strong>${result.artifact || bi("None claimed", "未领取")}</strong><small>${result.artifact ? bi("Active run law", "当前整轮规则") : bi("No Artifact mutation", "神器状态未改变")}</small></article></section><aside class="experience-next-package"><span>${bi("P8.5 LOCKED", "P8.5 尚未开启")}</span><strong>${bi("The integrated multi-cycle loop begins only after your P8.4 review.", "完整多循环体验要在你确认P8.4后才会开发。")}</strong></aside><footer class="experience-stage-actions"><button class="stage-secondary" type="button" data-p84-restart>${bi("Play P8.4 again", "重新体验P8.4")}</button><span>${bi("No Godot/runtime/save claim in this HTML package", "本HTML体验包不声称已实现Godot运行或存档")}</span><button class="stage-primary" type="button" data-view="screens">${bi("Open design atlas", "查看完整设计图册")}</button></footer></div>`;
return frame(screen, body, { variant: "p8-4-market", resources: false });
}
function renderResultRecoveryExperience() {
let stage;
if (state.resultStep === "diagnosis") stage = renderP84Diagnosis(getScreen("OUT-001"));
else if (state.resultStep === "casualty_review") stage = renderP84CasualtyReview(getScreen("OUT-002"));
else if (state.resultStep === "casualty_receipt") stage = renderP84CasualtyReceipt(getScreen("OUT-002"));
else if (state.resultStep === "reward") stage = renderP84Reward(getScreen("OUT-003"));
else if (state.resultStep === "artifact") stage = renderP84ArtifactChoice(getScreen("OUT-003"));
else if (state.resultStep === "reward_review") stage = renderP84RewardReview(getScreen("OUT-003"));
else if (state.resultStep === "reward_receipt") stage = renderP84RewardReceipt(getScreen("OUT-003"));
else if (state.resultStep === "recovery") stage = renderP84Recovery(getScreen("OUT-004"));
else if (state.resultStep === "recovery_review") stage = renderP84RecoveryReview(getScreen("OUT-004"));
else if (state.resultStep === "recovery_receipt") stage = renderP84RecoveryReceipt(getScreen("OUT-004"));
else stage = renderP84NextMarket(getScreen("HUB-001"));
return `<section class="view playtest-view"><header class="view-heading"><div><span class="eyebrow">PHASE 8 · PACKAGE 4</span><h1>${bi("Play Result and Recovery", "体验战果与恢复")}</h1><p>${bi("Read why the battle ended, settle permanent loss, choose one reward, then decide how the Company continues.", "先看懂战斗为何这样结束,再结算永久损失、选择奖励,并决定队伍如何继续。")}</p></div><div class="heading-badges"><span>P8.1–P8.3 ✓</span><span>P8.4 ${bi("Owner play gate", "等待你的体验结论")}</span></div></header><div class="experience-shell">${p84Progress()}<div class="stage-wrap">${stage}</div><div class="package-roadmap"><strong>P8.1–P8.3 ${bi("Accepted", "已通过")}</strong><strong>P8.4 ${bi("Result and recovery", "战果与恢复")}</strong><span>P8.5 ${bi("Integrated loop · locked", "完整循环 · 未开启")}</span></div></div></section>`;
}
const p85Steps = [
["intro", "1", "Start", "开始"], ["opening", "2", "Company", "开局队伍"],
["market1", "3", "Market I", "第一集市"], ["risk1", "4", "Risk I", "第一轮风险"],
["battle1", "5", "Battle I", "第一轮战斗"], ["result1", "6", "Result", "战果"],
["recovery1", "7", "Recovery", "伤亡与恢复"], ["market2", "8", "Loadout II", "第二轮换装"],
["risk2", "9", "Risk II", "第二轮风险"], ["battle2", "10", "Battle II", "第二轮战斗"],
["result2", "11", "Result II", "第二轮战果"], ["receipt", "12", "Continue", "继续"]
];
function p85StepIndex() { return Math.max(0, p85Steps.findIndex((step) => step[0] === state.p85Step)); }
function p85Progress() {
const current = p85StepIndex();
return `<ol class="experience-progress p85-progress" aria-label="${bi("P8.5 integrated progress", "P8.5完整循环进度")}">${p85Steps.map((step, index) => `<li class="${index === current ? "is-current" : ""} ${index < current ? "is-complete" : ""}"><span>${index < current ? "✓" : step[1]}</span><strong>${escapeHtml(experienceStepLabel(step))}</strong></li>`).join("")}</ol>`;
}
function p85CompanyLine(includeInventory = false, phase = "prepared") {
const vela = { id: "RCV-P84-01", name: "Vela Dorn", profession: "Farstring", itemId: null, item: null, art: actorAssets.Farstring };
const units = phase === "opening" ? [generatedOffers[2], generatedOffers[3]]
: phase === "recovered" ? [generatedOffers[2], generatedOffers[3], firstMarketOffers[0], vela]
: [generatedOffers[2], generatedOffers[3], firstMarketOffers[0], firstMarketOffers[1]];
const vacancies = phase === "opening" ? 2 : 0;
return `<div class="p85-company-line">${units.map((unit) => `<article class="${phase === "fallen" && unit.id === "MKT-R1-02" ? "is-fallen" : ""}"><div>${actorImage(unit, "p85-actor")}</div><strong>${localizedText(unit.name)}</strong><small>${localizedText(unit.profession)} · ${phase === "fallen" && unit.id === "MKT-R1-02" ? "FALLEN / EQ-014" : unit.id === "RCV-P84-01" && state.p85EquipDecision === "equip" ? "EQ-014" : unit.itemId || bi("No equipment", "无装备")}</small></article>`).join("")}${Array.from({ length: vacancies }, () => `<article class="is-vacant"><div>+</div><strong>${bi("Open slot", "空席位")}</strong><small>${bi("First Market", "第一集市")}</small></article>`).join("")}</div>${includeInventory ? `<div class="p85-inventory"><b>${bi("INHERITED", "继承状态")}</b><span>46 Coin · 0 Credit · Company 4/4</span><span>EQ-014 Quiet Rearplate</span><span>${state.p85EquipDecision === "equip" ? bi("Equipped to Vela", "已装备给 Vela") : bi("Not equipped", "尚未装备")}</span></div>` : ""}`;
}
function p85Action(label, attr, disabled = false, secondary = false) {
return `<button class="${secondary ? "stage-secondary" : "stage-primary"}" type="button" ${attr} ${disabled ? "disabled" : ""}>${label}</button>`;
}
function p85EventTitle(title) { return state.locale === "zh_CN" ? p85BattleEventCopyZh[title] || title : title; }
function p85StageIntro() {
const body = `<div class="p85-intro"><div class="p85-intro-art">${stageArt(0)}<span>10–15 MIN</span></div><div class="p85-intro-copy"><span class="stage-kicker">PHASE 8 · P8.5</span><h2>${bi("Play one complete Guild cycle", "体验一次完整的公会循环")}</h2><p>${bi("This fresh-load package carries your decisions from Company creation through two battles. Watch what survives, what returns, and why the second risk is different.", "这个全新加载体验会把你的决定从建队带到两场战斗:看清什么会保留、什么会退回,以及第二轮风险为什么不同。")}</p><div class="p85-pillars"><b>${bi("Choose", "选择")} · ${bi("Prepare", "整备")} · ${bi("Observe", "观察")} · ${bi("Adapt", "调整")}</b><span>${bi("Design prototype only · no Godot save or release claim", "仅为设计原型 · 不代表 Godot 存档或发布状态")}</span></div>${p85Action(bi("Start fresh integrated loop", "开始完整循环"), "data-p85-start")}</div></div>`;
return frame(getScreen("SYS-003"), body, { variant: "p8-5-intro", resources: false });
}
function p85StageOpening() {
const body = `<div class="p85-opening"><header><span class="stage-kicker">P8.1 · ${bi("INHERITED OPENING", "继承的开局")}</span><h2>${bi("Build the Company that will shape both cycles", "建立会影响两轮循环的队伍")}</h2><p>${bi("The accepted opening receipt is fixed here so the integrated experience can focus on consequences, not rerolls.", "这里固定使用已接受的开局回执,让完整体验聚焦于后果,而不是重新抽取。")}</p></header><section class="p85-contract-facts"><b>CTR-R1-01</b><strong>${bi("First Crossing", "首次穿越")}</strong><span>${bi("Standard · Slot 01 · 52 Coin", "标准 · 存档01 · 52金币")}</span></section>${p85CompanyLine(false, "opening")}<aside class="p85-callout"><b>${bi("Opening consequence", "开局后果")}</b><span>${bi("Tomas and Sable are the two retained provisional Recruits. Two other offers leave at commit.", "保留 Tomas 与 Sable 两名初始队员;另外两名候选在确认后离开。")}</span></aside><footer class="experience-stage-actions"><span>${bi("No hidden reroll · no Coin cost", "不会隐藏重抽 · 不消耗金币")}</span>${p85Action(bi("Commit opening Company", "确认开局队伍"), "data-p85-opening-commit")}</footer></div>`;
return frame(getScreen("SYS-005"), body, { variant: "p8-5-opening", resources: false });
}
function p85StageMarket1() {
const actions = [
["mael", "Hire Mael · 14 Coin", "招募 Mael · 14金币"],
["ilya", "Hire Ilya · 14 Coin", "招募 Ilya · 14金币"],
["equip", "Buy EQ-012 · 12 Coin · equip Tomas", "购买 EQ-012 · 12金币 · 装备给 Tomas"],
];
const complete = ["mael", "ilya", "equip"].every((id) => state.p85MarketAction.includes(id));
return frame(getScreen("SHP-001"), `<div class="p85-market"><header><span class="stage-kicker">P8.2 · ${bi("FIRST MARKET", "第一集市")}</span><h2>${bi("Make three preparation transactions", "完成三项整备交易")}</h2><p>${bi("The integrated path repeats the accepted receipts: every action must be visible before risk is unlocked.", "完整路径重复已接受的交易回执:三项操作都可见后,才会解锁风险。")}</p><div class="p85-ledger"><b>${bi("Coin", "金币")} ${52 - (state.p85MarketAction.includes("mael") ? 14 : 0) - (state.p85MarketAction.includes("ilya") ? 14 : 0) - (state.p85MarketAction.includes("equip") ? 12 : 0)}</b><b>${bi("Company", "队伍")} ${2 + (state.p85MarketAction.includes("mael") ? 1 : 0) + (state.p85MarketAction.includes("ilya") ? 1 : 0)} / 4</b><b>EQ-012 → Tomas</b></div></header><section class="p85-market-actions">${actions.map(([id, en, zh]) => `<button type="button" data-p85-market="${id}" class="${state.p85MarketAction.includes(id) ? "is-complete" : ""}" ${state.p85MarketAction.includes(id) ? "disabled" : ""}><span>${state.p85MarketAction.includes(id) ? "✓" : id === "equip" ? "3" : id === "mael" ? "1" : "2"}</span><strong>${bi(en, zh)}</strong><small>${state.p85MarketAction.includes(id) ? bi("Committed once", "已提交一次") : bi("Inspect → confirm", "查看 → 确认")}</small></button>`).join("")}</section><div class="p85-receipt-strip"><span>${bi("Accepted transaction order", "已接受的交易顺序")}</span><b>52 → 38 → 24 → 12 Coin</b><b>${bi("Company 2 → 3 → 4", "队伍 2 → 3 → 4")}</b></div><footer class="experience-stage-actions"><span>${complete ? bi("Preparation complete · 12 Coin remains", "整备完成 · 剩余12金币") : bi("Complete all three visible transactions", "完成三项可见交易")}</span>${p85Action(bi("Continue to Risk", "进入风险选择"), "data-p85-market-next", !complete)}</footer></div>`, { variant: "p8-5-market-1", resources: false });
}
function p85StageRisk1() {
const selected = p85RiskOffers[1];
return frame(getScreen("RSK-001"), `<div class="p85-risk"><header><span class="stage-kicker">P8.3R2 · ${bi("FIRST RISK", "第一轮风险")}</span><h2>${bi("Commit the accepted Shattered Terrace fixture", "确认已接受的破碎台地风险")}</h2><p>${bi("The integrated path keeps the same medium-pressure choice so the P8.4 casualty and recovery remain truthful.", "完整路径保留相同的中等压力选择,确保 P8.4 的伤亡与恢复真实延续。")}</p></header><div class="p85-risk-card is-selected"><span>${selected.id}</span><strong>${bi(selected.name[0], selected.name[1])}</strong><p>${bi(selected.pressure[0], selected.pressure[1])}</p><small>${bi("Reward", "奖励")} · ${bi(selected.reward[0], selected.reward[1])} · ${bi("Medium exposure", "中等暴露")}</small></div><div class="p85-risk-facts"><b>Company 4/4</b><b>12 Coin</b><b>EQ-012 → Tomas</b><b>${bi("No retreat executed", "尚未撤退")}</b></div><footer class="experience-stage-actions"><span>${bi("Selection freezes nothing until the commit", "选中不会改变状态,确认后才冻结")}</span>${p85Action(bi("Commit Shattered Terrace", "确认破碎台地"), "data-p85-risk1-commit")}</footer></div>`, { variant: "p8-5-risk-1", resources: false });
}
function p85RecoveredCompany() {
return [
generatedOffers[2], generatedOffers[3], firstMarketOffers[0],
{ id: "RCV-P84-01", name: "Vela Dorn", profession: "Farstring", itemId: state.p85EquipDecision === "equip" ? "EQ-014" : null, item: state.p85EquipDecision === "equip" ? "Quiet Rearplate" : null, art: actorAssets.Farstring },
];
}
function p85DynamicArena(which, tick) {
if (which === 1) {
const eventMap = [0, 1, 3, 5, 6, 8];
return p83BattleArena(p83RiskOffers[1], eventMap[Math.min(tick, eventMap.length - 1)]);
}
const eventMap = [0, 1, 4, 5, 8];
const eventIndex = eventMap[Math.min(tick, eventMap.length - 1)];
const formation = p83FormationState(eventIndex);
const enemies = state.p85Risk2 === "RSK-P85-02"
? [["Needle Cantor", "针鸣唱者"], ["Glasswing", "晶翼兽"], ["Rain Spindle", "雨线纺锤"]]
: [["Gale Hound", "疾风猎犬"], ["Gale Hound", "疾风猎犬"], ["Span Warden", "跨线守卫"]];
const countered = state.p85EquipDecision === "equip" && state.p85Risk2 === "RSK-P85-02";
const effect = tick === 1 ? `<div class="p83-contact-marker" style="--contact-x:${p83ContactX}%"><i></i><span>${bi("CONTACT · SECOND CYCLE", "接敌 · 第二轮")}</span></div>`
: tick === 2 ? `<i class="p85-direct-bolt ${countered ? "is-countered" : ""}"></i><div class="p85-counter-label"><b>${countered ? "EQ-014 · RIDER REMOVED" : "DIRECT RIDER ACTIVE"}</b><span>${countered ? bi("Vela holds the rear", "Vela 守住后排") : bi("Rear pressure remains", "后排压力仍存在")}</span></div>`
: tick === 3 ? `<div class="p83-heal-link"></div><div class="p83-heal-ring"></div><b class="p83-float-number is-heal">+16</b>`
: tick === 4 ? `<div class="p83-contact-marker is-replacement" style="--contact-x:${p83ContactX}%"><i></i><span>${bi("SECOND PLAN PROVEN", "第二套方案已验证")}</span></div>` : "";
return `<section class="p83-arena p85-dynamic-arena" data-battle-event="${tick}" data-formation-phase="${formation.phase}" aria-label="${bi("Second-cycle animated horizontal battle", "第二轮动态横向战斗")}">
<div class="p83-arena-depth"><i></i><i></i><i></i></div><div class="p83-side-marker is-allied"><span>${bi("COMPANY", "我方队伍")}</span><b>4</b></div><div class="p83-side-marker is-hostile"><span>${bi("ENEMIES", "敌方单位")}</span><b>3</b></div><div class="p83-ground-line"><i></i></div>
${p85RecoveredCompany().map((unit, index) => p83AllyFighter(unit, index, eventIndex, formation)).join("")}
${enemies.map((enemy, index) => p83EnemyFighter(enemy, index, enemies.length, eventIndex, formation)).join("")}${effect}
<div class="p83-direction is-left">${bi("REAR", "后排")} ←</div><div class="p83-direction is-right">→ ${bi("PRESSURE FRONT", "接敌前线")}</div>
</section>`;
}
function p85BattleStage(which) {
const second = which === 2;
const events = second ? p85Battle2Events : p85Battle1Events;
const tick = second ? state.p85Battle2Tick : state.p85Battle1Tick;
const current = events[Math.min(tick, events.length - 1)];
const nextAttr = second ? "data-p85-battle2-next" : "data-p85-battle1-next";
const done = tick >= events.length - 1;
const soundLabel = state.p85Muted ? bi("Sound cues off", "声音提示已关闭") : bi("Prototype sound cues on", "原型声音提示已开启");
return frame(getScreen("BAT-002"), `<div class="p85-battle"><header><div><span class="stage-kicker">${second ? "P8.5 · CYCLE TWO" : "P8.3R2 · CYCLE ONE"}</span><h2>${escapeHtml(p85EventTitle(current[1]))}</h2><p>${escapeHtml(current[2])}</p></div><button class="p85-audio-toggle" type="button" data-p85-audio aria-pressed="${!state.p85Muted}">${state.p85Muted ? "×" : "♪"} ${soundLabel}</button></header>${p85DynamicArena(which, tick)}<div class="p85-battle-log">${events.map((event, index) => `<button type="button" class="${index <= tick ? "is-reached" : ""} ${index === tick ? "is-current" : ""}" ${index <= tick ? "" : "disabled"}><span>${event[0]}</span><strong>${escapeHtml(p85EventTitle(event[1]))}</strong></button>`).join("")}</div><div class="p85-audio-caption" role="status">CC · ${escapeHtml(current[2])}</div><footer class="experience-stage-actions"><span>${done ? bi("Observation complete · result is now readable", "观察完成 · 现在可以读取结果") : bi("Tap Next event to inspect the automatic chain", "点击下一事件查看自动战斗链")}</span>${done ? p85Action(second ? bi("Read second result", "查看第二轮结果") : bi("Read first result", "查看第一轮结果"), second ? "data-p85-battle2-done" : "data-p85-battle1-done") : p85Action(bi("Next event", "下一事件"), nextAttr, false)}</footer></div>`, { variant: second ? "p8-5-battle-2" : "p8-5-battle-1", resources: false, artSrc: "assets/web/ch01_battle.webp" });
}
function p85StageResult1() {
return frame(getScreen("OUT-001"), `<div class="p85-result"><header><span class="p85-victory">${bi("VICTORY", "胜利")}</span><div><span class="stage-kicker">P8.4 · ${bi("FIRST CYCLE DIAGNOSIS", "第一轮因果诊断")}</span><h2>${bi("The line won, but Ilya did not return", "队伍赢了,但 Ilya 没有回来")}</h2></div></header><section class="p85-cause"><b>${bi("FIRST DECISIVE DIVERGENCE", "首个决定性分歧")}</b><p>${bi("EQ-014 Quiet Rearplate converted the area hit to direct; the disclosed rider made Ilya Fallen before the final legal attack.", "EQ-014 寂静后甲把范围命中转成直接命中;已公开的追加效果让 Ilya 在最后一次合法攻击前阵亡。")}</p><div><strong>12 → 46 Coin</strong><span>${bi("Battle payout +34", "战斗奖励 +34")}</span></div></section><aside>${p85CompanyLine(false, "fallen")}<b>${bi("Ilya is permanently removed; EQ-014 returns once.", "Ilya 永久移除;EQ-014 只退回一次。")}</b></aside><footer class="experience-stage-actions"><span>${bi("Diagnosis appears before recovery choices", "先看懂原因,再进入恢复选择")}</span>${p85Action(bi("Settle casualty and reward", "结算伤亡与奖励"), "data-p85-result1-next")}</footer></div>`, { variant: "p8-5-result-1", resources: false });
}
function p85StageRecovery() {
const rewardDone = state.p85RewardClaimed;
const reward = state.p85RewardSelected;
if (!rewardDone) return frame(getScreen("OUT-003"), `<div class="p85-recovery"><header><span class="stage-kicker">P8.4 · ${bi("REWARD AFTER LOSS", "伤亡后的奖励")}</span><h2>${bi("Choose one stable reward, then recover the Company", "选择一项固定奖励,再恢复队伍")}</h2><p>${bi("This integrated path recommends nothing. The main receipt uses AR-043, but you must choose it explicitly.", "完整路径不推荐任何选项;主路径使用 AR-043,但必须由你明确选择。")}</p></header><div class="p85-choice-grid">${p84Artifacts.map((artifact) => `<button type="button" data-p85-reward="${artifact.id}" class="${reward === artifact.id ? "is-selected" : ""}"><span>${artifact.mark}</span><strong>${bi(artifact.name[0], artifact.name[1])}</strong><small>${artifact.id}</small><p>${bi(artifact.rule[0], artifact.rule[1])}</p></button>`).join("")}</div><footer class="experience-stage-actions"><span>${reward ? bi("One Artifact selected", "已选择一件神器") : bi("No preselection · choose one Artifact", "没有预选 · 请选择一件神器")}</span>${p85Action(bi("Claim reward", "领取奖励"), "data-p85-reward-claim", !reward)}</footer></div>`, { variant: "p8-5-recovery-reward", resources: false });
const recovery = state.p85RecoveryChoice;
return frame(getScreen("OUT-004"), `<div class="p85-recovery"><header><span class="stage-kicker">P8.4 · ${bi("RECOVERY RECEIPT", "恢复回执")}</span><h2>${bi("Bring Vela in, or continue short-handed", "招募 Vela,或缺员继续")}</h2><p>${bi("Ilya's Credit is 14. Returned EQ-014 stays in inventory and is never auto-equipped.", "Ilya 的补员凭证为14;退回的 EQ-014 留在库存,不会自动装备。")}</p></header><div class="p85-recovery-summary"><b>AR-043 · ${bi("Hard Lesson claimed", "已领取沉痛教训")}</b><span>${bi("Coin 46 · Credit 14 · Company 3/4 · EQ-014 inventory", "金币46 · 凭证14 · 队伍3/4 · EQ-014在库存")}</span></div><div class="p85-choice-grid p85-recovery-grid">${p84RecoveryOffers.map((entry) => `<button type="button" data-p85-recovery="${entry.id}" class="${recovery === entry.id ? "is-selected" : ""}"><strong>${bi(entry.name[0], entry.name[1])}</strong><small>${bi(entry.cost[0], entry.cost[1])}</small><p>${bi(entry.result[0], entry.result[1])}</p></button>`).join("")}</div><footer class="experience-stage-actions"><span>${recovery ? bi("Recovery choice is ready to commit", "恢复选择已准备确认") : bi("No recovery choice selected", "尚未选择恢复方式")}</span>${p85Action(bi("Commit recovery", "确认恢复"), "data-p85-recovery-commit", !recovery)}</footer></div>`, { variant: "p8-5-recovery", resources: false });
}
function p85StageMarket2() {
const selected = state.p85EquipDecision;
return frame(getScreen("EQP-002"), `<div class="p85-loadout"><header><span class="stage-kicker">P8.5 · ${bi("SECOND MARKET LOADOUT", "第二轮集市换装")}</span><h2>${bi("Learn from the returned equipment", "根据退回的装备做决定")}</h2><p>${bi("Vela is a replacement, not a restored Ilya. Quiet Rearplate is now a deliberate counterplay decision.", "Vela 是替补,不是 Ilya 的复原;寂静后甲现在是一项明确的克制选择。")}</p></header>${p85CompanyLine(true, "recovered")}<section class="p85-equip-choice"><div>${equipmentImage("Quiet Rearplate", "p85-equipment-art")}</div><div><b>EQ-014 · ${bi("Quiet Rearplate", "寂静后甲")}</b><p>${bi("First area hit becomes direct and removes the disclosed direct-hit rider.", "第一次范围命中变为直接命中,并移除已公开的直接命中追加效果。")}</p><small>${bi("Equip to Vela or leave in inventory. Both routes remain legal.", "装备给 Vela,或留在库存。两条路线都合法。")}</small></div></section><div class="p85-choice-grid p85-equip-grid"><button type="button" data-p85-equip="equip" class="${selected === "equip" ? "is-selected" : ""}"><strong>${bi("Equip Vela", "装备给 Vela")}</strong><small>${bi("Counters direct-hit pressure", "克制直接命中压力")}</small></button><button type="button" data-p85-equip="leave" class="${selected === "leave" ? "is-selected" : ""}"><strong>${bi("Leave in inventory", "留在库存")}</strong><small>${bi("Preserve flexibility for later", "保留后续灵活性")}</small></button></div><footer class="experience-stage-actions"><span>${selected ? bi("Loadout choice is reversible until commit", "确认前可以修改换装选择") : bi("Choose how Vela enters cycle two", "选择 Vela 如何进入第二轮")}</span>${p85Action(bi("Commit loadout", "确认换装"), "data-p85-equip-commit", !selected)}</footer></div>`, { variant: "p8-5-loadout", resources: false });
}
function p85StageRisk2() {
const selected = p85RiskOffers.find((offer) => offer.id === state.p85Risk2);
return frame(getScreen("RSK-001"), `<div class="p85-risk"><header><span class="stage-kicker">P8.5 · ${bi("SECOND RISK", "第二轮风险")}</span><h2>${bi("The same equipment means different things now", "同一件装备在此刻有不同价值")}</h2><p>${bi("Compare both legal routes. The cards change value with Vela's actual loadout; neither predicts a hidden result.", "比较两条合法路线。卡片会根据 Vela 的实际装备改变价值;界面不会预言隐藏结果。")}</p></header><div class="p85-risk-grid">${p85RiskOffers.map((offer) => `<button type="button" data-p85-risk2="${offer.id}" class="${selected?.id === offer.id ? "is-selected" : ""}"><span>${offer.id}</span><strong>${bi(offer.name[0], offer.name[1])}</strong><em>${bi(offer.reward[0], offer.reward[1])}</em><p>${bi(offer.pressure[0], offer.pressure[1])}</p><small>${bi(offer.detail[0], offer.detail[1])}</small><b>${state.p85EquipDecision === "equip" && offer.kind === "counter" ? bi("EQ-014 answers the learned threat", "EQ-014 正好应对已学到的威胁") : offer.kind === "tempo" ? bi("Tests speed over protection", "用速度换取防护") : bi("Without EQ-014, direct pressure remains", "没有 EQ-014 时仍有直接压力")}</b></button>`).join("")}</div><footer class="experience-stage-actions"><span>${selected ? `${bi("Selected", "已选择")} · ${bi(selected.name[0], selected.name[1])}` : bi("Select one second-cycle risk", "选择一项第二轮风险")}</span>${p85Action(bi("Commit second risk", "确认第二轮风险"), "data-p85-risk2-commit", !selected)}</footer></div>`, { variant: "p8-5-risk-2", resources: false });
}
function p85StageResult2() {
const countered = state.p85EquipDecision === "equip" && state.p85Risk2 === "RSK-P85-02";
return frame(getScreen("OUT-001"), `<div class="p85-result p85-result-two"><header><span class="p85-victory">${bi("CLEAR", "完成")}</span><div><span class="stage-kicker">P8.5 · ${bi("SECOND CYCLE RESULT", "第二轮结果")}</span><h2>${countered ? bi("The learned counter worked", "学到的克制奏效了") : bi("The route remains readable, but the threat was not countered", "路线仍可读,但威胁没有被克制")}</h2></div></header><section class="p85-cause"><b>${bi("SECOND DECISIVE DIVERGENCE", "第二个决定性分歧")}</b><p>${countered ? bi("Vela wore EQ-014 before Needle Rain. The first area hit became direct and its rider was removed.", "Vela 在针雨前装备 EQ-014;第一次范围命中转为直接命中,追加效果被移除。") : bi("The chosen risk and loadout left a visible gap. The next plan must decide whether to trade flexibility for protection.", "所选风险与换装留下了可见缺口;下一步要决定是否用灵活性换取防护。")}</p><div><strong>${state.p85Risk2 === "RSK-P85-02" ? "Needle Rain" : "Open Span"}</strong><span>${state.p85EquipDecision === "equip" ? bi("Vela equipped", "Vela 已装备") : bi("EQ-014 in inventory", "EQ-014 在库存")}</span></div></section><aside class="p85-continuation-card"><b>${bi("CONTINUATION RECEIPT", "继续回执")}</b><strong>${bi("A new decision is ready", "新的决策已经准备好")}</strong><p>${bi("The next Market will offer a different recruit family and one equipment conflict. P8.6 is not invented here.", "下一集市会提供不同队员家族与一次装备冲突;这里不会虚构 P8.6。")}</p></aside><footer class="experience-stage-actions"><span>${bi("This result is a design handoff, not an authoritative save", "这是设计交接,不是权威存档结果")}</span>${p85Action(bi("Open continuation receipt", "打开继续回执"), "data-p85-result2-next")}</footer></div>`, { variant: "p8-5-result-2", resources: false });
}
function p85StageReceipt() {
const countered = state.p85EquipDecision === "equip" && state.p85Risk2 === "RSK-P85-02";
return frame(getScreen("HUB-001"), `<div class="p85-receipt"><header><span class="receipt-check">✓</span><div><span class="stage-kicker">P8.5 · ${bi("INTEGRATED LOOP COMPLETE", "完整循环完成")}</span><h2>${bi("Your decisions changed the next cycle", "你的决定改变了下一轮")}</h2></div><b>P8-LOOP-001</b></header><div class="p85-receipt-grid"><article><span>${bi("INHERITED", "继承")}</span><strong>Coin 46 · ${bi("Company", "队伍")} 4/4</strong><small>AR-043 · ${bi("Hard Lesson", "沉痛教训")}</small></article><article><span>${bi("APPLIED", "应用")}</span><strong>${state.p85EquipDecision === "equip" ? "EQ-014 → Vela" : bi("EQ-014 stayed in inventory", "EQ-014 留在库存")}</strong><small>${state.p85Risk2 || "RSK-P85-02"} · ${countered ? bi("countered", "已克制") : bi("unanswered pressure", "未应对压力")}</small></article><article><span>${bi("NEXT MEANINGFUL ACTION", "下一项有意义操作")}</span><strong>${bi("Open the next Market", "打开下一集市")}</strong><small>${bi("Different recruit family · no P8.6 claim", "不同队员家族 · 不声明 P8.6")}</small></article></div><div class="p85-final-line">${p85CompanyLine(true, "recovered")}</div><footer class="experience-stage-actions"><span>${bi("One integrated prototype receipt · repeated input is inert", "一份完整原型回执 · 重复输入不会重复提交")}</span>${p85Action(bi("Play P8.5 again", "重新体验 P8.5"), "data-p85-restart", false, true)}<button class="stage-primary" type="button" data-view="screens">${bi("Open design atlas", "打开设计图册")}</button></footer></div>`, { variant: "p8-5-receipt", resources: false });
}
function renderIntegratedLoopExperience() {
let stage;
if (state.p85Step === "intro") stage = p85StageIntro();
else if (state.p85Step === "opening") stage = p85StageOpening();
else if (state.p85Step === "market1") stage = p85StageMarket1();
else if (state.p85Step === "risk1") stage = p85StageRisk1();
else if (state.p85Step === "battle1") stage = p85BattleStage(1);
else if (state.p85Step === "result1") stage = p85StageResult1();
else if (state.p85Step === "recovery1") stage = p85StageRecovery();
else if (state.p85Step === "market2") stage = p85StageMarket2();
else if (state.p85Step === "risk2") stage = p85StageRisk2();
else if (state.p85Step === "battle2") stage = p85BattleStage(2);
else if (state.p85Step === "result2") stage = p85StageResult2();
else stage = p85StageReceipt();
return `<section class="view playtest-view"><header class="view-heading"><div><span class="eyebrow">PHASE 8 · PACKAGE 5</span><h1>${bi("Play the integrated Guild loop", "体验完整公会循环")}</h1><p>${bi("Carry one decision through Company, Market, battle, loss, recovery and a meaningfully different second cycle.", "让一个决定贯穿建队、集市、战斗、伤亡、恢复与有明显不同的第二轮。")}</p></div><div class="heading-badges"><span>P8.1–P8.4 ✓</span><span>P8.5 ${bi("Owner play gate", "等待你的体验结论")}</span></div></header><div class="experience-shell">${p85Progress()}<div class="stage-wrap">${stage}</div><div class="package-roadmap"><strong>P8.1–P8.4 ${bi("Accepted", "已通过")}</strong><strong>P8.5 ${bi("Integrated loop", "完整循环")}</strong><span>${bi("P8.6 not invented", "不会虚构 P8.6")}</span></div></div></section>`;
}
function renderExperiencePrototype() {
if (state.experiencePackage === "p8-5") return renderIntegratedLoopExperience();
if (state.experiencePackage === "p8-4") return renderResultRecoveryExperience();
if (state.experiencePackage === "p8-3") return renderRiskBattleExperience();
if (state.experiencePackage === "p8-2") return renderMarketExperience();
const screen = getScreen("SYS-005");
const stage = state.experienceStep === "contract" ? renderExperienceContract(screen)
: state.experienceStep === "offers" ? renderExperienceOffers(screen)
: state.experienceStep === "review" ? renderExperienceReview(screen)
: renderExperienceReceipt(screen);
return `<section class="view playtest-view">
${viewHeading("PHASE 8 · PACKAGE 1", bi("Play the opening Company experience", "体验开局建队"), bi("Make the real decisions in order. No step can be skipped, and P8.2 remains locked until your review.", "请按顺序完成真实选择;步骤不可跳过,你确认前不会开始 P8.2。"), ["P8.1", bi("Owner play gate", "等待你的体验结论")])}
<div class="experience-shell">${experienceProgress()}<div class="stage-wrap">${stage}</div><div class="package-roadmap"><strong>P8.1 ${bi("Opening Company", "开局建队")}</strong><span>P8.2 ${bi("Market preparation · locked", "集市整备 · 未开启")}</span><span>P8.3 ${bi("Risk and battle · locked", "风险与战斗 · 未开启")}</span><span>P8.4 ${bi("Result and recovery · locked", "结果与恢复 · 未开启")}</span><span>P8.5 ${bi("Integrated loop · locked", "完整循环 · 未开启")}</span></div></div>
</section>`;
}
function renderFirstPlay() {
const step = flowSteps[state.flowIndex];
const screen = getScreen(step[0]);
return `<section class="view firstplay-view">
${viewHeading("OWNER REVIEW · FIRST 16 MINUTES", flowLabel(step), `${screen.id} · ${screenName(screen)}`, [`${state.flowIndex + 1} / ${flowSteps.length}`, "Real screen state"])}
<div class="stage-wrap">${renderPrimaryStage(screen, step[1])}</div>
<div class="firstplay-controls"><button class="page-command" type="button" data-flow-prev ${state.flowIndex === 0 ? "disabled" : ""}>‹ ${t("prev")}</button><div class="flow-progress" aria-label="First-play progress">${flowSteps.map((entry, index) => `<button class="${index === state.flowIndex ? "is-active" : ""} ${index < state.flowIndex ? "is-complete" : ""}" type="button" data-flow-index="${index}" title="${escapeHtml(flowLabel(entry))}"><span>${index + 1}</span></button>`).join("")}</div><button class="page-command is-primary" type="button" data-flow-next>${state.flowIndex === flowSteps.length - 1 ? "Return to first Market" : t("next")} ›</button></div>
</section>`;
}
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 `<section class="view screens-view">
${viewHeading("50 CURRENT PAGE / STATE IDS", screenName(screen), `${screen.id} · ${state.locale === "zh_CN" ? "页面操作" : screen.input}`, [`${index + 1} / ${filtered.length}`, "Page-specific composition"])}
<div class="screen-tools"><div class="filter-scroll">${Object.entries(screenGroups).map(([key, label]) => `<button class="${state.screenGroup === key ? "is-active" : ""}" data-screen-group="${key}">${label}</button>`).join("")}</div><div class="variant-control">${variants.map(([key, label]) => `<button class="${state.screenVariant === key ? "is-active" : ""}" data-screen-variant="${key}">${label}</button>`).join("")}</div></div>
<div class="screen-browser"><aside class="screen-index">${filtered.map((entry) => `<button class="${entry.id === screen.id ? "is-active" : ""}" type="button" data-screen-id="${entry.id}"><span>${entry.id}</span><strong>${escapeHtml(screenName(entry))}</strong><em>COMPOSED</em></button>`).join("")}</aside><div class="screen-inspection"><div class="stage-wrap">${renderStateVariant(screen, state.screenVariant)}</div><div class="screen-pager"><button data-screen-id="${previous.id}">‹ ${t("prev")} · ${previous.id}</button><button data-screen-id="${next.id}">${next.id} · ${t("next")} ›</button></div><div class="authority-summary"><div><span>Purpose</span><p>${escapeHtml(screenPurpose(screen))}</p></div><div><span>Primary action</span><p>${escapeHtml(screenAction(screen))}</p></div><div><span>Safe state</span><p>${escapeHtml(screenSafeState(screen))}</p></div></div></div></div>
</section>`;
}
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 `<section class="view regions-view">
${viewHeading("EIGHT CAMPAIGN REGIONS", region.name, region.framing, [`${region.id}`, `${enemies.length} enemies · ${bosses.length} bosses`])}
<div class="region-tabs">${state.data.regions.map((entry, index) => `<button class="${index === state.regionIndex ? "is-active" : ""}" data-region-index="${index}"><span>${entry.id}</span><strong>${entry.name}</strong></button>`).join("")}</div>
<div class="region-stage"><img src="${sceneImages[state.regionIndex]}" alt="${escapeHtml(region.name)} concept direction" /><span class="concept-label">${t("concept")}</span><div class="region-title"><span>${region.id}</span><h2>${region.name}</h2><p>${region.framing}</p></div><div class="region-route"><i></i><button>Contract A</button><i></i><button>Boss 01</button><i></i><button>Contract B</button><i></i><button>Boss 02</button></div></div>
<div class="region-intel"><article><span>Build pressure</span><p>${region.pressure}</p></article><article><span>Encounter grammar</span><p>${region.progression}</p></article><article><span>Current identities</span><div class="identity-strip">${enemies.slice(0, 5).map((enemy) => `<b>${enemy.id}<small>${enemy.name}</small></b>`).join("")}${bosses.map((boss) => `<b class="boss">${boss.id}<small>${boss.name}</small></b>`).join("")}</div></article></div>
</section>`;
}
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 `<section class="view catalog-view">
${viewHeading("CURRENT AUTHORED CATALOGS", config.label, "Search stable IDs, names, mechanics, tradeoffs and counterplay.", [`${filtered.length} / ${entries.length}`, "No filler rows"])}
<div class="catalog-tabs">${["professions", "traits", "equipment", "artifacts", "enemies", "bosses"].map((key) => `<button class="${state.catalogType === key ? "is-active" : ""}" data-catalog-type="${key}">${key}<b>${state.data.counts[key]}</b></button>`).join("")}</div>
<div class="catalog-toolbar"><div class="filter-scroll">${config.filters.map(([key, label]) => `<button class="${state.catalogFilter === key ? "is-active" : ""}" data-catalog-filter="${key}">${label}</button>`).join("")}</div><label class="search-control"><span>⌕</span><input type="search" data-catalog-search value="${escapeHtml(state.catalogQuery)}" placeholder="Search ID, name or mechanic" aria-label="Search catalog" /></label></div>
<div class="catalog-layout"><aside class="catalog-art"><img src="${config.image}" alt="Concept element direction" /><span class="concept-label">${t("concept")}</span><div><strong>${config.label}</strong><p>Concept board only. Stable IDs and mechanics come from the current authority.</p></div></aside><div class="catalog-list">${filtered.length ? filtered.slice(0, 100).map((entry) => `<article class="catalog-entry"><div><small>${entry.id}</small><strong>${entry.name}</strong><em>${escapeHtml(config.meta(entry))}</em></div><p>${escapeHtml(truncate(config.body(entry), 240))}</p><button class="stage-icon" aria-label="Inspect ${escapeHtml(entry.name)}" title="Inspect">i</button></article>`).join("") : `<div class="empty-state"><strong>No matching identities</strong><button data-clear-search>Clear search</button></div>`}${filtered.length > 100 ? `<div class="catalog-more">100 shown · ${filtered.length - 100} more remain searchable</div>` : ""}</div></div>
</section>`;
}
function renderReferenceWireframe(mapping) {
return `<div class="reference-wireframe template-${mapping.template}" data-reference-template="${mapping.template}">
<header><span>${mapping.id}</span><strong>${bi("OBSERVED FUNCTIONAL STRUCTURE", "观察到的功能结构")}</strong><em>${mapping.evidence}</em></header>
<div class="wireframe-canvas">${mapping.zones.map(([position, en, zh], index) => `<div class="wire-zone zone-${position}"><span>0${index + 1}</span><strong>${bi(en, zh)}</strong></div>`).join("")}</div>
<footer>${bi("Neutral reconstruction only - no commercial media", "仅为中性结构重绘 · 不含商业素材")}</footer>
</div>`;
}
function renderReferenceMap() {
const mapping = referenceMappings[state.referenceIndex];
const screen = getScreen(mapping.target);
return `<section class="view reference-view">
${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", "学习功能结构,保持原创表达")])}
<div class="reference-selector">${referenceMappings.map((entry, index) => `<button class="${index === state.referenceIndex ? "is-active" : ""}" type="button" data-reference-index="${index}"><span>${entry.id}</span><strong>${bi(entry.title[0], entry.title[1])}</strong><em>${entry.target}</em></button>`).join("")}</div>
<div class="fidelity-comparison"><article class="reference-panel"><header><span>${bi("REFERENCE LEARNING", "参考学习")}</span><h2>${bi("Observed structure", "观察到的结构")}</h2><p>${bi(mapping.options[0], mapping.options[1])}</p></header>${renderReferenceWireframe(mapping)}</article><article class="adaptation-panel"><header><span>${bi("AETHERBOUND ADAPTATION", "Aetherbound 原创改造")}</span><h2>${screenName(screen)}</h2><p>${screen.id} · ${bi("Current page composition", "当前页面构图")}</p></header><div class="stage-wrap">${renderPrimaryStage(screen, mapping.variant)}</div></article></div>
<div class="fidelity-decisions"><article><span>${bi("ADOPTED", "采用")}</span><p>${bi(mapping.adopt[0], mapping.adopt[1])}</p></article><article><span>${bi("CHANGED", "改造")}</span><p>${bi(mapping.change[0], mapping.change[1])}</p></article><article><span>${bi("REJECTED", "拒绝")}</span><p>${bi(mapping.reject[0], mapping.reject[1])}</p></article></div>
</section>`;
}
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 `<section class="view review-view">
${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"])}
<div class="review-groups">${groups.map((group) => `<article><span>${group[0]}</span><div><small>${group[2]}</small><h2>${group[1]}</h2><p>${group[3]}</p></div><button class="page-command" data-review-view="${group[4]}" data-review-target="${group[5]}">Open group ›</button></article>`).join("")}</div>
<div class="review-boundary"><div><span>Machine-verifiable</span><strong>50 unique states · current counts · responsive layout · interactions · self-contained bundle</strong></div><div><span>Owner remains responsible</span><strong>Comprehension · visual acceptance · fun · listening · device ergonomics · development approval</strong></div></div>
</section>`;
}
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);
const packageNumber = state.experiencePackage.replace("p8-", "");
const packageTitle = bi(`Play P8.${packageNumber}`, `体验 P8.${packageNumber}`);
$("#playtest-nav-label").textContent = packageTitle;
$("#package-status-label").textContent = `PHASE 8 · P8.${packageNumber}`;
$("#package-badge").textContent = bi(`P8.${packageNumber} prototype`, `P8.${packageNumber} 体验原型`);
if (state.view === "playtest") $("#view-title").textContent = packageTitle;
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;
}
let p83BattleTimer = null;
function clearP83BattleTimer() {
if (p83BattleTimer !== null) window.clearTimeout(p83BattleTimer);
p83BattleTimer = null;
}
function scheduleP83BattleTimer() {
clearP83BattleTimer();
if (state.view !== "playtest" || state.experiencePackage !== "p8-3" || state.riskStep !== "battle" || state.riskBattlePaused || state.riskInspectOpen) return;
if (state.riskBattleTick >= p83BattleEvents.length - 1) return;
const delay = Math.max(180, 900 / state.riskBattleSpeed);
p83BattleTimer = window.setTimeout(() => {
p83BattleTimer = null;
if (state.riskBattlePaused || state.riskInspectOpen || state.riskStep !== "battle") return;
state.riskBattleTick += 1;
if (state.riskBattleTick >= p83BattleEvents.length - 1) state.riskBattlePaused = true;
render();
}, delay);
}
function syncP83PlaybackSurface() {
const battle = $(".p83-battle");
if (!battle) return;
battle.classList.toggle("is-paused", state.riskBattlePaused);
battle.classList.remove("speed-1", "speed-2", "speed-4");
battle.classList.add(`speed-${state.riskBattleSpeed}`);
const pause = battle.querySelector("[data-p83-pause]");
if (pause) {
pause.textContent = state.riskBattlePaused ? "▶" : "Ⅱ";
pause.setAttribute("aria-label", state.riskBattlePaused ? bi("Resume", "继续") : bi("Pause", "暂停"));
}
battle.querySelectorAll("[data-p83-speed]").forEach((button) => button.classList.toggle("is-active", Number(button.dataset.p83Speed) === state.riskBattleSpeed));
const status = battle.querySelector(".p83-battle-rules b");
if (status) status.textContent = state.riskBattlePaused ? bi("PAUSED", "已暂停") : `${state.riskBattleSpeed}x · ${bi("AUTO", "自动")}`;
battle.getAnimations({ subtree: true }).forEach((animation) => {
if (state.riskBattlePaused) {
const frozenTime = animation.currentTime;
animation.pause();
animation.playbackRate = 0;
if (frozenTime !== null) animation.currentTime = frozenTime;
} else {
animation.playbackRate = 1;
animation.play();
}
});
}
function render() {
if (!state.data) return;
clearP83BattleTimer();
const renderers = { playtest: renderExperiencePrototype, 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();
scheduleP83BattleTimer();
}
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 resetExperience() {
state.experienceStep = "contract";
state.experienceContract = null;
state.experienceDifficulty = null;
state.experienceSelected = [];
state.experienceInspected = null;
state.experienceInspectedIds = [];
state.experienceNotice = "";
state.experienceCommitted = false;
}
function resetMarketExperience() {
state.marketStep = "market2";
state.marketCoin = 52;
state.marketHired = [];
state.marketFocusedOffer = null;
state.marketCommittedOffers = [];
state.marketItemPurchased = false;
state.marketItemEquipped = false;
state.marketEquipTarget = null;
state.marketOrder = [];
state.marketOrderSource = null;
state.marketOrderChanged = false;
state.marketNotice = "";
}
function resetRiskBattleExperience() {
clearP83BattleTimer();
state.riskStep = "board";
state.riskSelected = null;
state.riskCommitted = false;
state.riskBattleTick = 0;
state.riskBattlePaused = true;
state.riskBattleSpeed = 1;
state.riskInspectOpen = false;
state.riskInspectEvent = null;
state.riskInspectPriorPaused = true;
}
function resetResultRecoveryExperience() {
state.resultStep = "diagnosis";
state.casualtyCommitted = false;
state.rewardSelected = null;
state.artifactSelected = null;
state.rewardCommitted = false;
state.recoverySelected = null;
state.recoveryCommitted = false;
}
function resetP85Experience() {
state.p85Step = "intro";
state.p85MarketAction = "";
state.p85Risk1 = null;
state.p85Risk1Committed = false;
state.p85Battle1Tick = 0;
state.p85CasualtySettled = false;
state.p85RewardSelected = null;
state.p85RewardClaimed = false;
state.p85RecoveryChoice = null;
state.p85RecoveryCommitted = false;
state.p85EquipDecision = null;
state.p85LoadoutCommitted = false;
state.p85Risk2 = null;
state.p85Risk2Committed = false;
state.p85Battle2Tick = 0;
state.p85SecondResultAcknowledged = false;
state.p85Muted = false;
}
function playP85Cue(kind) {
if (state.p85Muted || typeof AudioContext === "undefined") return;
try {
const context = playP85Cue.context || (playP85Cue.context = new AudioContext());
const oscillator = context.createOscillator();
const gain = context.createGain();
const frequencies = { advance: 196, impact: 124, heal: 392, reward: 523, counter: 659 };
oscillator.frequency.value = frequencies[kind] || 262;
oscillator.type = kind === "impact" ? "square" : "sine";
gain.gain.setValueAtTime(0.0001, context.currentTime);
gain.gain.exponentialRampToValueAtTime(0.035, context.currentTime + 0.01);
gain.gain.exponentialRampToValueAtTime(0.0001, context.currentTime + 0.14);
oscillator.connect(gain).connect(context.destination);
oscillator.start();
oscillator.stop(context.currentTime + 0.15);
} catch (_) { /* Prototype audio remains optional. */ }
}
function announceExperience(message) {
state.experienceNotice = message;
const liveRegion = $("#live-region");
if (liveRegion) liveRegion.textContent = message;
}
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-p85-start")) {
state.p85Step = "opening";
return render();
}
if (button.hasAttribute("data-p85-opening-commit")) {
state.p85Step = "market1";
return render();
}
if (button.dataset.p85Market) {
if (!state.p85MarketAction.includes(button.dataset.p85Market)) state.p85MarketAction += `${state.p85MarketAction ? "," : ""}${button.dataset.p85Market}`;
return render();
}
if (button.hasAttribute("data-p85-market-next")) {
if (["mael", "ilya", "equip"].every((id) => state.p85MarketAction.includes(id))) state.p85Step = "risk1";
return render();
}
if (button.hasAttribute("data-p85-risk1-commit")) {
if (state.p85Risk1Committed) return render();
state.p85Risk1 = "RSK-P83-02";
state.p85Risk1Committed = true;
state.p85Step = "battle1";
return render();
}
if (button.hasAttribute("data-p85-battle1-next")) {
state.p85Battle1Tick = Math.min(p85Battle1Events.length - 1, state.p85Battle1Tick + 1);
playP85Cue(["advance", "impact", "heal", "impact", "counter", "reward"][state.p85Battle1Tick]);
return render();
}
if (button.hasAttribute("data-p85-battle1-done")) {
if (state.p85Battle1Tick >= p85Battle1Events.length - 1) state.p85Step = "result1";
return render();
}
if (button.hasAttribute("data-p85-result1-next")) {
state.p85Step = "recovery1";
return render();
}
if (button.dataset.p85Reward) {
if (!state.p85RewardClaimed) state.p85RewardSelected = button.dataset.p85Reward;
return render();
}
if (button.hasAttribute("data-p85-reward-claim")) {
if (state.p85RewardClaimed || !state.p85RewardSelected) return render();
state.p85RewardClaimed = true;
return render();
}
if (button.dataset.p85Recovery) {
if (!state.p85RecoveryCommitted) state.p85RecoveryChoice = button.dataset.p85Recovery;
return render();
}
if (button.hasAttribute("data-p85-recovery-commit")) {
if (state.p85RecoveryCommitted || !state.p85RecoveryChoice) return render();
state.p85RecoveryCommitted = true;
state.p85Step = "market2";
return render();
}
if (button.dataset.p85Equip) {
if (!state.p85LoadoutCommitted) state.p85EquipDecision = button.dataset.p85Equip;
return render();
}
if (button.hasAttribute("data-p85-equip-commit")) {
if (state.p85LoadoutCommitted || !state.p85EquipDecision) return render();
state.p85LoadoutCommitted = true;
state.p85Step = "risk2";
return render();
}
if (button.dataset.p85Risk2) {
if (!state.p85Risk2Committed) state.p85Risk2 = button.dataset.p85Risk2;
return render();
}
if (button.hasAttribute("data-p85-risk2-commit")) {
if (state.p85Risk2Committed || !state.p85Risk2) return render();
state.p85Risk2Committed = true;
state.p85Step = "battle2";
return render();
}
if (button.hasAttribute("data-p85-battle2-next")) {
state.p85Battle2Tick = Math.min(p85Battle2Events.length - 1, state.p85Battle2Tick + 1);
playP85Cue(["advance", "impact", "counter", "heal", "reward"][state.p85Battle2Tick]);
return render();
}
if (button.hasAttribute("data-p85-audio")) {
state.p85Muted = !state.p85Muted;
if (!state.p85Muted) playP85Cue("counter");
return render();
}
if (button.hasAttribute("data-p85-battle2-done")) {
if (state.p85Battle2Tick >= p85Battle2Events.length - 1) state.p85Step = "result2";
return render();
}
if (button.hasAttribute("data-p85-result2-next")) {
state.p85SecondResultAcknowledged = true;
state.p85Step = "receipt";
return render();
}
if (button.hasAttribute("data-p85-restart")) {
resetP85Experience();
return render();
}
if (button.hasAttribute("data-p84-casualty-review")) {
if (!state.casualtyCommitted) state.resultStep = "casualty_review";
return render();
}
if (button.hasAttribute("data-p84-back-diagnosis")) {
if (!state.casualtyCommitted) state.resultStep = "diagnosis";
return render();
}
if (button.hasAttribute("data-p84-casualty-commit")) {
if (state.casualtyCommitted) return;
state.casualtyCommitted = true;
state.resultStep = "casualty_receipt";
return render();
}
if (button.hasAttribute("data-p84-open-reward")) {
if (state.casualtyCommitted) state.resultStep = "reward";
return render();
}
if (button.dataset.p84Reward) {
if (state.rewardCommitted) return;
state.rewardSelected = button.dataset.p84Reward;
if (state.rewardSelected !== "artifact") state.artifactSelected = null;
return render();
}
if (button.hasAttribute("data-p84-reward-decline")) {
if (state.rewardCommitted) return;
state.rewardSelected = "decline";
state.artifactSelected = null;
state.resultStep = "reward_review";
return render();
}
if (button.hasAttribute("data-p84-reward-next")) {
if (!state.rewardSelected || state.rewardCommitted) return;
state.resultStep = state.rewardSelected === "artifact" ? "artifact" : "reward_review";
return render();
}
if (button.dataset.p84Artifact) {
if (state.rewardCommitted || state.rewardSelected !== "artifact") return;
state.artifactSelected = button.dataset.p84Artifact;
return render();
}
if (button.hasAttribute("data-p84-artifact-back")) {
if (!state.rewardCommitted) state.resultStep = "reward";
return render();
}
if (button.hasAttribute("data-p84-artifact-review")) {
if (!state.rewardCommitted && state.rewardSelected === "artifact" && state.artifactSelected) state.resultStep = "reward_review";
return render();
}
if (button.hasAttribute("data-p84-reward-revise")) {
if (!state.rewardCommitted) state.resultStep = state.rewardSelected === "artifact" ? "artifact" : "reward";
return render();
}
if (button.hasAttribute("data-p84-reward-commit")) {
if (state.rewardCommitted || !state.rewardSelected || (state.rewardSelected === "artifact" && !state.artifactSelected)) return;
state.rewardCommitted = true;
state.resultStep = "reward_receipt";
return render();
}
if (button.hasAttribute("data-p84-open-recovery")) {
if (state.rewardCommitted) state.resultStep = "recovery";
return render();
}
if (button.dataset.p84Recovery) {
if (state.recoveryCommitted) return;
state.recoverySelected = button.dataset.p84Recovery;
return render();
}
if (button.hasAttribute("data-p84-recovery-review")) {
if (!state.recoveryCommitted && state.recoverySelected) state.resultStep = "recovery_review";
return render();
}
if (button.hasAttribute("data-p84-recovery-revise")) {
if (!state.recoveryCommitted) state.resultStep = "recovery";
return render();
}
if (button.hasAttribute("data-p84-recovery-commit")) {
if (state.recoveryCommitted || !state.recoverySelected) return;
state.recoveryCommitted = true;
state.resultStep = "recovery_receipt";
return render();
}
if (button.hasAttribute("data-p84-open-market")) {
if (state.recoveryCommitted) state.resultStep = "market";
return render();
}
if (button.hasAttribute("data-p84-restart")) {
resetResultRecoveryExperience();
return render();
}
if (button.dataset.p83Risk) {
if (state.riskCommitted) return;
state.riskSelected = button.dataset.p83Risk;
return render();
}
if (button.hasAttribute("data-p83-review")) {
if (!p83RiskOffer() || state.riskCommitted) return;
state.riskStep = "review";
return render();
}
if (button.hasAttribute("data-p83-cancel")) {
if (state.riskCommitted) return;
state.riskStep = "board";
return render();
}
if (button.hasAttribute("data-p83-commit")) {
if (!p83RiskOffer() || state.riskCommitted) return;
state.riskCommitted = true;
state.riskStep = "receipt";
return render();
}
if (button.hasAttribute("data-p83-open-battle")) {
if (!state.riskCommitted) return;
state.riskStep = "opening";
return render();
}
if (button.hasAttribute("data-p83-start")) {
if (!state.riskCommitted) return;
state.riskStep = "battle";
state.riskBattleTick = 0;
state.riskBattlePaused = false;
return render();
}
if (button.hasAttribute("data-p83-pause")) {
if (state.riskBattleTick >= p83BattleEvents.length - 1) return;
state.riskBattlePaused = !state.riskBattlePaused;
clearP83BattleTimer();
syncP83PlaybackSurface();
if (!state.riskBattlePaused) scheduleP83BattleTimer();
return;
}
if (button.dataset.p83Speed) {
state.riskBattleSpeed = Number(button.dataset.p83Speed);
clearP83BattleTimer();
syncP83PlaybackSurface();
if (!state.riskBattlePaused) scheduleP83BattleTimer();
return;
}
if (button.hasAttribute("data-p83-inspect") || button.dataset.p83Event) {
const requested = button.dataset.p83Event === undefined ? state.riskBattleTick : Number(button.dataset.p83Event);
if (!Number.isInteger(requested) || requested < 0 || requested > state.riskBattleTick) return;
state.riskInspectPriorPaused = state.riskBattlePaused;
state.riskBattlePaused = true;
state.riskInspectOpen = true;
state.riskInspectEvent = requested;
return render();
}
if (button.hasAttribute("data-p83-close-inspect")) {
state.riskInspectOpen = false;
state.riskInspectEvent = null;
if (state.riskBattleTick < p83BattleEvents.length - 1) state.riskBattlePaused = state.riskInspectPriorPaused;
return render();
}
if (button.hasAttribute("data-p83-finish")) {
if (state.riskBattleTick < p83BattleEvents.length - 1) return;
state.riskStep = "resolved";
return render();
}
if (button.hasAttribute("data-p83-restart")) {
resetRiskBattleExperience();
return render();
}
if (button.dataset.marketFocus) {
if (state.marketHired.includes(button.dataset.marketFocus)) return;
state.marketFocusedOffer = button.dataset.marketFocus;
state.marketNotice = "";
return render();
}
if (button.hasAttribute("data-market-review")) {
if (!state.marketFocusedOffer || state.marketHired.includes(state.marketFocusedOffer)) return;
state.marketStep = state.marketHired.length === 0 ? "hire_review" : "hire2_review";
return render();
}
if (button.hasAttribute("data-market-cancel")) {
state.marketStep = state.marketHired.length === 0 ? "market2" : "market3";
return render();
}
if (button.hasAttribute("data-market-hire-confirm")) {
const id = state.marketFocusedOffer;
const offer = firstMarketOffers.find((entry) => entry.id === id);
if (!offer || state.marketHired.includes(id) || state.marketCoin < offer.price || state.marketHired.length >= 2) return;
state.marketHired.push(id);
state.marketCommittedOffers.push(id);
state.marketCoin -= offer.price;
state.marketStep = state.marketHired.length === 1 ? "hire_receipt" : "hire2_receipt";
return render();
}
if (button.hasAttribute("data-market-continue")) {
state.marketFocusedOffer = null;
state.marketStep = state.marketHired.length < 2 ? "market3" : "equipment_offer";
return render();
}
if (button.hasAttribute("data-market-item-review")) {
if (!state.marketItemPurchased) state.marketStep = "item_review";
return render();
}
if (button.hasAttribute("data-market-item-cancel")) {
state.marketStep = "equipment_offer";
return render();
}
if (button.hasAttribute("data-market-item-confirm")) {
if (state.marketItemPurchased || state.marketCoin < firstMarketItem.price) return;
state.marketItemPurchased = true;
state.marketCoin -= firstMarketItem.price;
state.marketStep = "item_receipt";
return render();
}
if (button.hasAttribute("data-market-open-equip")) {
if (state.marketItemPurchased) state.marketStep = "equip_select";
return render();
}
if (button.dataset.marketEquipTarget) {
if (state.marketItemEquipped) return;
state.marketEquipTarget = button.dataset.marketEquipTarget;
return render();
}
if (button.hasAttribute("data-market-equip-confirm")) {
if (!state.marketItemPurchased || state.marketItemEquipped || !state.marketEquipTarget) return;
state.marketItemEquipped = true;
state.marketStep = "equip_receipt";
return render();
}
if (button.hasAttribute("data-market-open-order")) {
state.marketOrder = marketCompany().map((offer) => offer.id);
state.marketOrderSource = null;
state.marketOrderChanged = false;
state.marketStep = "order";
return render();
}
if (button.dataset.marketOrderUnit) {
const id = button.dataset.marketOrderUnit;
if (!state.marketOrder.includes(id)) return;
if (!state.marketOrderSource) {
state.marketOrderSource = id;
} else if (state.marketOrderSource === id) {
state.marketOrderSource = null;
} else {
const sourceIndex = state.marketOrder.indexOf(state.marketOrderSource);
const destinationIndex = state.marketOrder.indexOf(id);
[state.marketOrder[sourceIndex], state.marketOrder[destinationIndex]] = [state.marketOrder[destinationIndex], state.marketOrder[sourceIndex]];
state.marketOrderSource = null;
state.marketOrderChanged = true;
}
return render();
}
if (button.hasAttribute("data-market-order-undo")) {
state.marketOrder = marketCompany().map((offer) => offer.id);
state.marketOrderSource = null;
state.marketOrderChanged = false;
return render();
}
if (button.hasAttribute("data-market-order-save")) {
if (!state.marketOrderChanged || state.marketOrderSource) return;
state.marketStep = "ready";
return render();
}
if (button.hasAttribute("data-market-restart")) {
resetMarketExperience();
return render();
}
if (button.dataset.experienceContract) {
const selected = state.experienceContract === button.dataset.experienceContract;
state.experienceContract = selected ? null : button.dataset.experienceContract;
if (selected) state.experienceDifficulty = null;
return render();
}
if (button.dataset.experienceDifficulty) {
if (!state.experienceContract) return;
state.experienceDifficulty = button.dataset.experienceDifficulty;
return render();
}
if (button.dataset.experienceInspect) {
const id = button.dataset.experienceInspect;
state.experienceInspected = id;
if (!state.experienceInspectedIds.includes(id)) state.experienceInspectedIds.push(id);
state.experienceNotice = "";
return render();
}
if (button.dataset.experienceOffer) {
const id = button.dataset.experienceOffer;
const index = state.experienceSelected.indexOf(id);
if (index >= 0) {
state.experienceSelected.splice(index, 1);
state.experienceNotice = "";
} else if (state.experienceSelected.length < 2) {
state.experienceSelected.push(id);
state.experienceNotice = "";
} else {
announceExperience(bi("Two Recruits are already selected. Deselect one before choosing another.", "已经选择两名队员;请先取消一人,再选择其他候选。"));
}
return render();
}
if (button.hasAttribute("data-experience-next")) {
if (state.experienceStep === "contract" && state.experienceContract && state.experienceDifficulty) {
state.experienceStep = "offers";
} else if (state.experienceStep === "offers" && state.experienceSelected.length === 2) {
state.experienceStep = "review";
}
state.experienceNotice = "";
return render();
}
if (button.hasAttribute("data-experience-back")) {
state.experienceStep = "contract";
state.experienceNotice = "";
return render();
}
if (button.hasAttribute("data-experience-revise")) {
state.experienceStep = "offers";
state.experienceNotice = "";
return render();
}
if (button.hasAttribute("data-experience-commit")) {
if (!state.experienceCommitted && state.experienceSelected.length === 2 && state.experienceContract && state.experienceDifficulty) {
state.experienceCommitted = true;
state.experienceStep = "receipt";
}
return render();
}
if (button.hasAttribute("data-experience-restart")) {
resetExperience();
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 (["playtest", "firstplay", "screens", "regions", "catalogs", "reference", "review"].includes(hashView)) state.view = hashView;
const packageParam = new URLSearchParams(location.search).get("package");
if (packageParam === "p8-1") state.experiencePackage = "p8-1";
if (packageParam === "p8-2") state.experiencePackage = "p8-2";
if (packageParam === "p8-3") state.experiencePackage = "p8-3";
if (packageParam === "p8-4") state.experiencePackage = "p8-4";
if (packageParam === "p8-5") state.experiencePackage = "p8-5";
render();
} catch (error) {
$("#content").innerHTML = `<div class="empty-state"><h1>Prototype data unavailable</h1><p>${escapeHtml(error.message)}</p></div>`;
}
}
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();