2959 lines
285 KiB
JavaScript
2959 lines
285 KiB
JavaScript
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("<", "<")
|
||
.replaceAll(">", ">")
|
||
.replaceAll('"', """)
|
||
.replaceAll("'", "'");
|
||
}
|
||
|
||
function truncate(value, length = 150) {
|
||
const text = String(value || "");
|
||
return text.length > length ? `${text.slice(0, length - 1)}...` : text;
|
||
}
|
||
|
||
function screenName(screen) {
|
||
return state.locale === "zh_CN" ? screenNamesZh[screen.id] || screen.name : screen.name;
|
||
}
|
||
|
||
function getScreen(id) {
|
||
return state.data.screens.find((screen) => screen.id === id);
|
||
}
|
||
|
||
function flowLabel(step) {
|
||
return step[state.locale === "zh_CN" ? 3 : 2];
|
||
}
|
||
|
||
function iconButton(symbol, label, attrs = "") {
|
||
return `<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();
|