From 0b4a64fe549af0ff965e654aa4f5fb7f2ff054b4 Mon Sep 17 00:00:00 2001 From: wusumac <736139669@qq.com> Date: Wed, 12 Aug 2026 10:14:14 +0800 Subject: [PATCH] feat: build opening company experience --- prototype/site/app.js | 226 +++++++++++++++++++++++++++++-- prototype/site/data/catalog.json | 6 +- prototype/site/index.html | 25 ++-- prototype/site/styles.css | 186 +++++++++++++++++++++++++ prototype/site/verify-p8-1.js | 174 ++++++++++++++++++++++++ prototype/site/verify-r7.js | 6 +- tools/build_gallery_bundle.py | 10 +- 7 files changed, 599 insertions(+), 34 deletions(-) create mode 100644 prototype/site/verify-p8-1.js diff --git a/prototype/site/app.js b/prototype/site/app.js index 8488bf2a..ce49b31c 100644 --- a/prototype/site/app.js +++ b/prototype/site/app.js @@ -1,6 +1,6 @@ const state = { data: null, - view: "firstplay", + view: "playtest", locale: "zh_CN", screenId: "SYS-005", screenGroup: "ALL", @@ -12,6 +12,14 @@ const state = { catalogQuery: "", referenceIndex: 2, openingSelected: [], + experienceStep: "contract", + experienceContract: null, + experienceDifficulty: null, + experienceSelected: [], + experienceInspected: null, + experienceInspectedIds: [], + experienceNotice: "", + experienceCommitted: false, partySwapped: false, battlePaused: false, battleSpeed: 1, @@ -30,15 +38,17 @@ const state = { const copy = { en: { - brandSubtitle: "Production UI atlas", - navFirst: "First play", + brandSubtitle: "Phased experience prototype", + navPlaytest: "Play P8.1", + navFirst: "First-play map", navScreens: "Screen atlas", navRegions: "Regions", navCatalogs: "Catalogs", navReference: "Reference map", navReview: "Review gates", - runtimeFrozen: "Runtime development frozen", - conceptBadge: "Production UI", + runtimeFrozen: "Godot development frozen", + conceptBadge: "P8.1 prototype", + playtest: "Play P8.1", firstplay: "First play", screens: "Screen atlas", regions: "Regions", @@ -68,15 +78,17 @@ const copy = { returnSafe: "Return safely", }, zh_CN: { - brandSubtitle: "生产级 UI 图册", - navFirst: "首次游玩", + brandSubtitle: "分阶段体验原型", + navPlaytest: "体验 P8.1", + navFirst: "首次流程图", navScreens: "页面图谱", navRegions: "八大区域", navCatalogs: "完整图鉴", navReference: "参考映射", navReview: "审核闸门", - runtimeFrozen: "运行时开发仍冻结", - conceptBadge: "生产级 UI", + runtimeFrozen: "Godot 开发仍冻结", + conceptBadge: "P8.1 体验原型", + playtest: "体验 P8.1", firstplay: "首次游玩", screens: "页面图谱", regions: "八大区域", @@ -548,6 +560,29 @@ const generatedOffers = [ { 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 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" }, @@ -1105,6 +1140,96 @@ function viewHeading(kicker, title, detail, badges = []) { return `
${escapeHtml(kicker)}

${escapeHtml(title)}

${escapeHtml(detail)}

${badges.map((badge) => `${escapeHtml(badge)}`).join("")}
`; } +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 `
    ${experienceSteps.map((step, index) => `
  1. ${index < currentIndex ? "✓" : step[1]}${escapeHtml(experienceStepLabel(step))}
  2. `).join("")}
`; +} + +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 = `
+
P8.1 · ${bi("STEP 1 OF 4", "第 1 / 4 步")}

${bi("Choose how this expedition begins", "选择这次远征如何开始")}

${bi("One Contract is unlocked. You still choose its difficulty before seeing any Recruit offers.", "目前只解锁一个契约;在看到队员候选前,你仍需自己选择难度。")}

+
+ +
${bi("WHAT YOU KNOW", "已知情报")}
${bi("Pressure", "主要威胁")}
${bi(openingContract.pressure[0], openingContract.pressure[1])}
${bi("Boss rule", "首领规则")}
${bi(openingContract.boss[0], openingContract.boss[1])}
${bi("Reward breadth", "奖励范围")}
${bi(openingContract.reward[0], openingContract.reward[1])}
+
${bi("CHOOSE DIFFICULTY", "选择难度")}
${openingDifficulties.map((entry) => { const selected = entry.id === state.experienceDifficulty; return ``; }).join("")}
+
+ +
`; + 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 ? `
${escapeHtml(state.experienceNotice)}
` : ""; + const body = `
+
P8.1 · ${bi("STEP 2 OF 4", "第 2 / 4 步")}

${bi("Inspect four Recruits, then choose exactly two", "查看四名队员,然后恰好选择两人")}

${bi("Offer set WIND-01-A stays unchanged until creation. No pair is recommended.", "候选组 WIND-01-A 在创建前不会变化;界面不会推荐任何组合。")}

+
${generatedOffers.map((offer) => { const selected = state.experienceSelected.includes(offer.id); const seen = state.experienceInspectedIds.includes(offer.id); const tradeoff = recruitTradeoff(offer); return `
${actorImage(offer, "offer-character")}${offer.id}
${localizedText(offer.name)}${localizedText(offer.profession)} · ${bi("LV", "等级")} 1${offer.itemId} · ${localizedText(offer.item)}

+ ${bi(tradeoff[0][0], tradeoff[0][1])}

− ${bi(tradeoff[1][0], tradeoff[1][1])}

`; }).join("")}
+ + ${notice} + +
`; + 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 = `
+
P8.1 · ${bi("STEP 3 OF 4", "第 3 / 4 步")}

${bi("Review before creating the expedition", "创建远征前请确认")}

${bi("This is the only commit boundary. You can still revise the pair.", "这里是唯一确认边界;现在仍可返回修改队伍。")}

+
${bi("SELECTED COMPANY", "选择的初始队伍")}
${chosen.map((offer) => recruitMarker(offer, true)).join("")}
${bi("NOT RETAINED AFTER COMMIT", "确认后不会保留")}
${unchosen.map((offer) => recruitMarker(offer, false)).join("")}
+ +
`; + 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 = `
+
✓
P8.1 · ${bi("STEP 4 OF 4", "第 4 / 4 步")}

${bi("Expedition created", "远征已创建")}

${bi("One receipt was committed. Repeated input cannot create another run.", "已生成唯一创建回执;重复点击不会再次创建远征。")}

P8-RUN-001
+
${bi("COMPANY 2 / CAPACITY 4", "队伍 2 / 容量 4")}${chosen.map((offer) => recruitMarker(offer, true)).join("")}
${bi("UNCHOSEN OFFERS DISCARDED", "未选候选已离开")}${unchosen.map((offer) => `

${offer.id}${localizedText(offer.name)}

`).join("")}
+
${bi("P8.2 LOCKED", "P8.2 尚未开启")}${bi("Market preparation begins only after your P8.1 review.", "你确认 P8.1 后,才会开发集市整备体验。")}
+ +
`; + return frame(screen, body, { variant: "p8-receipt", resources: false }); +} + +function renderExperiencePrototype() { + 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 `
+ ${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", "等待你的体验结论")])} +
${experienceProgress()}
${stage}
P8.1 ${bi("Opening Company", "开局建队")}P8.2 ${bi("Market preparation · locked", "集市整备 · 未开启")}P8.3 ${bi("Risk and battle · locked", "风险与战斗 · 未开启")}P8.4 ${bi("Result and recovery · locked", "结果与恢复 · 未开启")}P8.5 ${bi("Integrated loop · locked", "完整循环 · 未开启")}
+
`; +} + function renderFirstPlay() { const step = flowSteps[state.flowIndex]; const screen = getScreen(step[0]); @@ -1224,7 +1349,7 @@ function applySettings() { function render() { if (!state.data) return; - const renderers = { firstplay: renderFirstPlay, screens: renderScreens, regions: renderRegions, catalogs: renderCatalogs, reference: renderReferenceMap, review: renderReview }; + 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(); @@ -1257,6 +1382,23 @@ function advanceFlow(delta) { 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 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; @@ -1265,6 +1407,68 @@ function handleClick(event) { state.locale = button.dataset.locale; 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) { @@ -1409,7 +1613,7 @@ async function init() { state.data = await response.json(); } const hashView = location.hash.replace("#", ""); - if (["firstplay", "screens", "regions", "catalogs", "reference", "review"].includes(hashView)) state.view = hashView; + if (["playtest", "firstplay", "screens", "regions", "catalogs", "reference", "review"].includes(hashView)) state.view = hashView; render(); } catch (error) { $("#content").innerHTML = `

Prototype data unavailable

${escapeHtml(error.message)}

`; diff --git a/prototype/site/data/catalog.json b/prototype/site/data/catalog.json index 2cdbca10..c2ed5c5a 100644 --- a/prototype/site/data/catalog.json +++ b/prototype/site/data/catalog.json @@ -1,7 +1,7 @@ { - "version": 3, - "status": "production-ui-atlas-candidate", - "authorityRevision": "phase7-ui-production-atlas", + "version": 4, + "status": "phase8-p8-1-experience-prototype", + "authorityRevision": "phase8-p8-1-opening-company", "counts": { "screens": 50, "professions": 42, diff --git a/prototype/site/index.html b/prototype/site/index.html index cb95bb30..8f36fc15 100644 --- a/prototype/site/index.html +++ b/prototype/site/index.html @@ -4,9 +4,9 @@ - Aetherbound Guild - Production UI Atlas + Aetherbound Guild - Experience Prototype - + @@ -17,27 +17,28 @@
Aetherbound Guild - Final design prototype + Phased experience prototype
- +
@@ -67,6 +68,6 @@
- + diff --git a/prototype/site/styles.css b/prototype/site/styles.css index 1a7928cb..5ae5793c 100644 --- a/prototype/site/styles.css +++ b/prototype/site/styles.css @@ -104,6 +104,95 @@ main { min-width: 0; padding: 24px; } .stage-secondary { color: var(--mist-050); background: rgba(24,33,39,.88); border: 1px solid rgba(255,255,255,.36); } .sheet-row { min-height: 30px; display: flex; align-items: center; padding: 6px 8px; color: var(--ink-950); font-size: 10px; background: var(--mist-050); border-bottom: 1px solid var(--line); } +/* Phase 8 experience prototype */ +.experience-shell { display: grid; gap: 12px; } +.experience-progress { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px; margin: 0; padding: 0; list-style: none; background: var(--line); border: 1px solid var(--line); } +.experience-progress li { min-height: 48px; display: flex; align-items: center; gap: 9px; padding: 8px 12px; color: var(--ink-700); background: var(--white); } +.experience-progress li > span { width: 25px; height: 25px; display: grid; flex: 0 0 auto; place-items: center; color: var(--white); font-size: 9px; font-weight: 900; background: var(--neutral-silver); border-radius: 50%; } +.experience-progress li > strong { font-size: 11px; } +.experience-progress li.is-current { color: var(--ink-950); box-shadow: inset 0 -4px var(--work-gold); } +.experience-progress li.is-current > span { color: var(--ink-950); background: var(--work-gold); } +.experience-progress li.is-complete > span { background: var(--safe-jade); } +.package-roadmap { display: grid; grid-template-columns: repeat(5, 1fr); gap: 1px; color: var(--ink-700); background: var(--line); border: 1px solid var(--line); } +.package-roadmap > * { min-width: 0; padding: 8px 10px; overflow: hidden; font-size: 9px; background: var(--white); text-overflow: ellipsis; white-space: nowrap; } +.package-roadmap strong { color: var(--ink-950); box-shadow: inset 4px 0 var(--work-gold); } +.experience-contract, .experience-offers, .experience-review, .experience-receipt { height: 100%; position: relative; padding: 2% 3.2% 4.4%; } +.experience-contract, .experience-review { display: grid; grid-template-rows: auto minmax(0, 1fr) auto; gap: 7px; } +.experience-stage-title h2 { margin: 2px 0; font-size: 20px; line-height: 1.15; } +.experience-stage-title p { max-width: 720px; margin: 0; color: #d3e0e1; font-size: 9px; line-height: 1.35; } +.experience-stage-actions { min-height: 37px; display: flex; align-items: center; gap: 7px; color: var(--mist-050); } +.experience-stage-actions > span { min-width: 0; margin-right: auto; overflow: hidden; font-size: 8px; font-weight: 800; text-overflow: ellipsis; white-space: nowrap; } +.experience-stage-actions > span b { color: var(--work-gold); font-size: 13px; } +.experience-contract-grid { min-height: 0; display: grid; grid-template-columns: .82fr 1.18fr; grid-template-rows: .72fr 1.28fr; gap: 6px; } +.experience-contract-card, .experience-contract-read, .experience-difficulty { min-width: 0; color: var(--ink-950); background: rgba(243,246,242,.96); border: 2px solid transparent; } +.experience-contract-card { grid-row: 1 / 3; display: grid; align-content: center; gap: 5px; padding: 15px; text-align: left; border-left: 5px solid var(--neutral-silver); } +.experience-contract-card.is-selected { border-color: var(--work-gold); border-left-color: var(--work-gold); box-shadow: inset 0 0 0 1px var(--ink-950); } +.experience-contract-card > span, .experience-contract-read > span, .experience-difficulty > span, .experience-inspect > span, .experience-review-grid section > span, .experience-review-grid aside > span, .experience-receipt-grid > * > span { color: var(--route-teal); font-size: 7px; font-weight: 900; } +.experience-contract-card > strong { font-size: 19px; } +.experience-contract-card > em { color: var(--ink-700); font-size: 9px; font-style: normal; font-weight: 800; } +.experience-contract-card > p { margin: 0; font-size: 9px; line-height: 1.35; } +.experience-contract-card > small { width: max-content; padding: 4px 6px; color: var(--white); font-size: 7px; font-weight: 900; background: var(--route-teal); } +.experience-contract-read, .experience-difficulty { padding: 8px 10px; } +.experience-contract-read dl, .experience-review-grid dl, .experience-receipt-grid dl { margin: 4px 0 0; } +.experience-contract-read dl > div, .experience-review-grid dl > div, .experience-receipt-grid dl > div { display: grid; grid-template-columns: 78px 1fr; gap: 5px; padding: 4px 0; font-size: 7px; border-bottom: 1px solid var(--line); } +.experience-contract-read dt, .experience-review-grid dt, .experience-receipt-grid dt { color: var(--ink-700); } +.experience-contract-read dd, .experience-review-grid dd, .experience-receipt-grid dd { margin: 0; font-weight: 800; } +.experience-difficulty > div { display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px; margin-top: 5px; } +.experience-difficulty button { min-width: 0; display: grid; align-content: start; gap: 3px; padding: 6px; color: var(--ink-950); text-align: left; background: var(--white); border: 1px solid var(--line); border-bottom: 4px solid var(--neutral-silver); } +.experience-difficulty button.is-selected { border-color: var(--work-gold); border-bottom-color: var(--work-gold); } +.experience-difficulty button strong { font-size: 8px; } +.experience-difficulty button small { font-size: 6px; line-height: 1.25; } +.experience-difficulty button em { color: var(--ink-700); font-size: 5px; font-style: normal; line-height: 1.25; } + +.experience-offers { display: grid; grid-template-columns: minmax(0, 1fr) 25%; grid-template-rows: auto minmax(0, 1fr) auto; gap: 6px; } +.experience-offers .experience-stage-title, .experience-offers .experience-stage-actions { grid-column: 1 / 3; } +.experience-offer-grid { min-height: 0; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 5px; } +.experience-offer { min-width: 0; display: grid; grid-template-rows: minmax(0, 1fr) auto auto auto auto; color: var(--ink-950); background: rgba(243,246,242,.97); border: 2px solid transparent; } +.experience-offer.is-selected { border-color: var(--work-gold); box-shadow: inset 0 0 0 1px var(--ink-950); } +.experience-offer.is-inspected { border-top-color: var(--route-teal); } +.experience-offer-art { min-height: 0; position: relative; display: grid; place-items: end center; overflow: hidden; background: #dce8e4; } +.experience-offer-art .offer-character { width: 90%; } +.experience-offer-art > span { position: absolute; left: 4px; top: 4px; padding: 2px 3px; font-size: 5px; font-weight: 900; background: rgba(255,255,255,.9); } +.experience-offer > div:nth-child(2) { min-width: 0; padding: 4px 5px 2px; } +.experience-offer > div:nth-child(2) strong, .experience-offer > div:nth-child(2) small, .experience-offer > div:nth-child(2) em { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.experience-offer > div:nth-child(2) strong { font-size: 8px; } +.experience-offer > div:nth-child(2) small { color: var(--route-teal); font-size: 5px; } +.experience-offer > div:nth-child(2) em { color: var(--ink-700); font-size: 5px; font-style: normal; } +.experience-offer > p { margin: 0; padding: 1px 5px; font-size: 5px; line-height: 1.25; } +.experience-offer > p b { color: var(--safe-jade); } +.experience-offer > p:nth-of-type(2) b { color: var(--danger-coral); } +.experience-offer > footer { display: grid; grid-template-columns: 1fr 1fr; gap: 2px; padding: 4px; } +.experience-offer > footer button { min-height: 23px; padding: 2px; color: var(--ink-950); font-size: 6px; font-weight: 900; background: var(--white); border: 1px solid var(--line-strong); } +.experience-offer > footer button:last-child { background: #fff4ce; } +.experience-inspect { min-width: 0; display: grid; align-content: center; gap: 5px; padding: 10px; color: var(--ink-950); background: rgba(243,246,242,.97); border-top: 4px solid var(--neutral-silver); } +.experience-inspect.has-recruit { border-top-color: var(--route-teal); } +.experience-inspect > strong { font-size: 11px; } +.experience-inspect > p, .experience-inspect > small { margin: 0; font-size: 7px; line-height: 1.4; } +.experience-inspect > small { padding: 5px; background: var(--mist-180); } +.experience-notice { position: absolute; z-index: 8; left: 50%; bottom: 12%; max-width: 70%; padding: 6px 9px; color: var(--white); font-size: 7px; font-weight: 800; background: var(--danger-coral); transform: translateX(-50%); } + +.experience-review-grid { min-height: 0; display: grid; grid-template-columns: 1fr 1fr .82fr; gap: 6px; } +.experience-review-grid > * { min-width: 0; padding: 8px; color: var(--ink-950); background: rgba(243,246,242,.97); } +.experience-review-grid section > div { display: grid; gap: 4px; margin-top: 5px; } +.experience-review-grid .recruit-marker { padding: 4px; } +.experience-review-grid .recruit-marker:not(.is-selected) { opacity: .62; filter: grayscale(.7); } +.experience-review-grid aside { border-top: 4px solid var(--work-gold); } + +.experience-receipt { display: grid; grid-template-rows: auto minmax(0, 1fr) auto auto; gap: 6px; } +.experience-receipt > header { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 9px; } +.experience-receipt > header h2 { margin: 1px 0; font-size: 19px; } +.experience-receipt > header p { margin: 0; font-size: 8px; } +.experience-receipt > header > b { padding: 5px 7px; font-size: 8px; background: var(--work-gold); } +.receipt-check { width: 35px; height: 35px; display: grid; place-items: center; color: var(--white); font-size: 18px; font-weight: 900; background: var(--safe-jade); } +.experience-receipt-grid { min-height: 0; display: grid; grid-template-columns: 1.1fr .8fr .8fr; gap: 6px; } +.experience-receipt-grid > * { min-width: 0; padding: 8px; color: var(--ink-950); background: rgba(243,246,242,.97); } +.experience-receipt-grid section:first-child { display: grid; align-content: start; gap: 4px; } +.experience-receipt-grid section:nth-child(2) p { display: grid; grid-template-columns: auto 1fr; gap: 5px; margin: 5px 0 0; padding: 5px; font-size: 7px; background: var(--mist-120); } +.experience-receipt-grid section:nth-child(2) p b { color: var(--route-teal); } +.experience-next-package { display: flex; align-items: center; gap: 8px; padding: 6px 9px; color: var(--ink-950); background: rgba(243,246,242,.94); border-left: 5px solid var(--neutral-silver); } +.experience-next-package span { padding: 3px 5px; color: var(--white); font-size: 6px; font-weight: 900; background: var(--neutral-silver); } +.experience-next-package strong { font-size: 7px; } + /* Opening company */ .opening-composition, .creation-review, .contract-composition { height: 100%; padding: 2.2% 3.5% 5%; } .opening-title { max-width: 54%; } @@ -925,6 +1014,38 @@ main { min-width: 0; padding: 24px; } .stage-icon { width: 24px; height: 24px; font-size: 10px; } .resource { display: none; } .stage-primary, .stage-secondary { min-height: 26px; padding: 4px 7px; font-size: 8px; } + .experience-contract, .experience-offers, .experience-review, .experience-receipt { padding: 1.6% 2.2% 2.4%; } + .experience-stage-title h2 { font-size: 12px; } + .experience-stage-title p { display: none; } + .experience-stage-actions { min-height: 26px; } + .experience-stage-actions > span { font-size: 6px; } + .experience-contract-grid { grid-template-columns: .72fr 1.28fr; gap: 3px; } + .experience-contract-card { padding: 7px; } + .experience-contract-card > strong { font-size: 12px; } + .experience-contract-card > p { font-size: 6px; } + .experience-contract-read, .experience-difficulty { padding: 4px 6px; } + .experience-contract-read dl > div { padding: 2px 0; font-size: 5px; } + .experience-difficulty button { padding: 3px; } + .experience-difficulty button em { display: none; } + .experience-offers { grid-template-columns: minmax(0, 1fr) 22%; gap: 3px; } + .experience-offer-grid { gap: 2px; } + .experience-offer > div:nth-child(2) { padding: 2px 3px 1px; } + .experience-offer > p { padding: 0 3px; font-size: 4px; } + .experience-offer > footer { padding: 2px; } + .experience-offer > footer button { min-height: 18px; font-size: 5px; } + .experience-inspect { gap: 2px; padding: 5px; } + .experience-inspect > strong { font-size: 7px; } + .experience-inspect > p, .experience-inspect > small { font-size: 5px; } + .experience-review-grid { gap: 3px; } + .experience-review-grid > * { padding: 4px; } + .experience-review-grid .recruit-marker { padding: 2px; } + .experience-review-grid .recruit-silhouette { width: 27px; height: 31px; } + .experience-review-grid dl > div, .experience-receipt-grid dl > div { grid-template-columns: 46px 1fr; padding: 2px 0; font-size: 5px; } + .experience-receipt > header h2 { font-size: 12px; } + .experience-receipt > header p { display: none; } + .experience-receipt-grid { gap: 3px; } + .experience-receipt-grid > * { padding: 4px; } + .experience-next-package { padding: 3px 5px; } .opening-title h2, .market-heading h2, .party-title h2, .risk-heading h2, .result-title h2, .reward-heading h2, .recovery-heading h2 { font-size: 14px; } .opening-title p, .party-title p, .battle-objective p, .region-signal p { display: none; } .offer-grid { height: 62%; gap: 4px; margin-top: 5px; } @@ -1022,6 +1143,58 @@ main { min-width: 0; padding: 24px; } .game-hud { height: 32px; } .stage-body { inset: 32px 0 0; } .hud-left strong, .save-state, .concept-label { display: none; } + .experience-stage-title .stage-kicker { display: none; } + .experience-stage-title h2 { font-size: 8px; } + .experience-stage-actions > span { max-width: 38%; font-size: 4px; } + .experience-stage-actions .stage-primary, .experience-stage-actions .stage-secondary { min-height: 19px; padding: 2px 4px; font-size: 5px; } + .experience-contract-grid { grid-template-columns: .62fr 1.38fr; grid-template-rows: .66fr 1.34fr; gap: 2px; } + .experience-contract-card { gap: 2px; padding: 4px; border-left-width: 3px; } + .experience-contract-card > span, .experience-contract-card > p, .experience-contract-card > small { display: none; } + .experience-contract-card > strong { font-size: 8px; } + .experience-contract-card > em { font-size: 5px; } + .experience-contract-read, .experience-difficulty { padding: 3px; } + .experience-contract-read > span, .experience-difficulty > span { font-size: 4px; } + .experience-contract-read dl > div { grid-template-columns: 35px 1fr; font-size: 4px; } + .experience-difficulty > div { gap: 1px; margin-top: 2px; } + .experience-difficulty button { padding: 2px; border-bottom-width: 2px; } + .experience-difficulty button strong { font-size: 4px; } + .experience-difficulty button small { font-size: 3px; } + .experience-offers { grid-template-columns: minmax(0, 1fr) 19%; grid-template-rows: auto minmax(0, 1fr) auto; gap: 2px; } + .experience-offer-art > span, .experience-offer > p, .experience-offer > div:nth-child(2) em { display: none; } + .experience-offer > div:nth-child(2) strong { font-size: 5px; } + .experience-offer > div:nth-child(2) small { font-size: 4px; } + .experience-offer > footer { grid-template-columns: 1fr; gap: 1px; padding: 1px; } + .experience-offer > footer button { min-height: 14px; padding: 1px; font-size: 4px; } + .experience-inspect { padding: 3px; } + .experience-inspect > span { font-size: 3px; } + .experience-inspect > strong { font-size: 5px; } + .experience-inspect > p { font-size: 4px; } + .experience-inspect > small { display: none; } + .experience-notice { bottom: 11%; padding: 3px 5px; font-size: 4px; } + .experience-review-grid { grid-template-columns: 1fr 1fr .72fr; gap: 2px; } + .experience-review-grid > * { padding: 2px; } + .experience-review-grid section > span, .experience-review-grid aside > span { font-size: 4px; } + .experience-review-grid section > div { gap: 1px; margin-top: 2px; } + .experience-review-grid .recruit-marker { gap: 2px; padding: 1px; } + .experience-review-grid .recruit-silhouette { width: 17px; height: 21px; } + .experience-review-grid .recruit-marker small, .experience-review-grid .recruit-marker em { display: none; } + .experience-review-grid .recruit-marker strong { font-size: 4px; } + .experience-review-grid dl > div { grid-template-columns: 28px 1fr; padding: 1px 0; font-size: 3px; } + .experience-receipt { gap: 2px; } + .experience-receipt > header { gap: 3px; } + .experience-receipt > header h2 { font-size: 8px; } + .experience-receipt > header > b { padding: 2px 3px; font-size: 4px; } + .receipt-check { width: 20px; height: 20px; font-size: 10px; } + .experience-receipt-grid { grid-template-columns: 1fr .7fr .7fr; gap: 2px; } + .experience-receipt-grid > * { padding: 2px; } + .experience-receipt-grid .recruit-marker { padding: 1px; } + .experience-receipt-grid .recruit-silhouette { width: 17px; height: 20px; } + .experience-receipt-grid .recruit-marker small, .experience-receipt-grid .recruit-marker em { display: none; } + .experience-receipt-grid .recruit-marker strong { font-size: 4px; } + .experience-receipt-grid section:nth-child(2) p { gap: 2px; margin: 2px 0 0; padding: 2px; font-size: 4px; } + .experience-receipt-grid dl > div { grid-template-columns: 28px 1fr; padding: 1px 0; font-size: 3px; } + .experience-next-package { padding: 2px 3px; } + .experience-next-package strong { font-size: 4px; } .opening-composition, .creation-review, .contract-composition, .market-composition, .market-review, .item-review, .party-composition, .risk-board, .risk-review, .battle-composition, .battle-static, .result-composition, .loss-composition, .reward-composition, .recovery-composition, .guild-composition, .forecast-composition, .system-workspace, .progress-composition, .records-composition, .generic-composition, .charter-composition, .chronicle-composition, .oath-composition, .oath-result-composition { padding: 2% 2.5% 3%; } .opening-title { max-width: 80%; } .opening-title .stage-kicker { display: none; } @@ -1382,6 +1555,11 @@ main { min-width: 0; padding: 24px; } .view-heading h1 { font-size: 19px; } .view-heading p { display: none; } .heading-badges span:nth-child(n+2) { display: none; } + .experience-progress li { min-height: 38px; padding: 5px; } + .experience-progress li > span { width: 21px; height: 21px; } + .experience-progress li > strong { font-size: 8px; } + .package-roadmap { grid-template-columns: 1fr; } + .package-roadmap > span { display: none; } .firstplay-controls { grid-template-columns: 1fr 1fr; gap: 8px; } .flow-progress { grid-column: 1 / 3; grid-row: 1; justify-content: start; } .page-command { min-height: 40px; } @@ -1421,6 +1599,14 @@ main { min-width: 0; padding: 24px; } .view-heading .eyebrow, .view-heading p, .heading-badges { display: none; } .view-heading h1 { margin: 0; font-size: 14px; } .firstplay-view .stage-wrap, .screens-view .stage-wrap { width: min(100%, calc((100svh - 132px) * 16 / 9)); } + .playtest-view .stage-wrap { width: min(100%, calc((100svh - 128px) * 16 / 9)); } + .playtest-view .view-heading { display: none; } + .experience-shell { gap: 3px; } + .experience-progress { width: min(100%, calc((100svh - 128px) * 16 / 9)); margin: 0 auto; } + .experience-progress li { min-height: 28px; padding: 2px 5px; } + .experience-progress li > span { width: 18px; height: 18px; font-size: 6px; } + .experience-progress li > strong { font-size: 6px; } + .package-roadmap { display: none; } .firstplay-controls { min-height: 38px; grid-template-columns: auto 1fr auto; margin-top: 4px; } .flow-progress { grid-column: auto; grid-row: auto; } .flow-progress button { height: 22px; } diff --git a/prototype/site/verify-p8-1.js b/prototype/site/verify-p8-1.js new file mode 100644 index 00000000..b0b97546 --- /dev/null +++ b/prototype/site/verify-p8-1.js @@ -0,0 +1,174 @@ +#!/usr/bin/env node +"use strict"; + +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const { chromium } = require("playwright"); + +const root = path.resolve(__dirname, "../.."); +const baseURL = process.env.ABG_PROTOTYPE_URL || "http://127.0.0.1:8766"; +const outputDir = process.env.ABG_PROTOTYPE_EVIDENCE || path.join(root, "prototype/gallery/evidence/p8-1"); +const browserPath = process.env.ABG_BROWSER_PATH || "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"; + +async function noHorizontalOverflow(page, label) { + const dimensions = await page.evaluate(() => ({ + scrollWidth: document.documentElement.scrollWidth, + clientWidth: document.documentElement.clientWidth, + })); + assert.ok(dimensions.scrollWidth <= dimensions.clientWidth + 1, `${label}: ${JSON.stringify(dimensions)}`); +} + +async function stageFitsViewport(page, label) { + const metrics = await page.locator(".game-stage").evaluate((stage) => { + const box = stage.getBoundingClientRect(); + return { left: box.left, right: box.right, top: box.top, bottom: box.bottom, viewportWidth: innerWidth, viewportHeight: innerHeight }; + }); + assert.ok(metrics.left >= -0.5 && metrics.right <= metrics.viewportWidth + 0.5, `${label} horizontal: ${JSON.stringify(metrics)}`); + assert.ok(metrics.top >= -0.5 && metrics.bottom <= metrics.viewportHeight + 0.5, `${label} vertical: ${JSON.stringify(metrics)}`); +} + +async function actionFitsStage(page, selector, label) { + const metrics = await page.locator(selector).evaluate((button) => { + const action = button.getBoundingClientRect(); + const stage = button.closest(".game-stage").getBoundingClientRect(); + return { actionLeft: action.left, actionRight: action.right, actionTop: action.top, actionBottom: action.bottom, stageLeft: stage.left, stageRight: stage.right, stageTop: stage.top, stageBottom: stage.bottom }; + }); + assert.ok(metrics.actionLeft >= metrics.stageLeft && metrics.actionRight <= metrics.stageRight && metrics.actionTop >= metrics.stageTop && metrics.actionBottom <= metrics.stageBottom, `${label}: ${JSON.stringify(metrics)}`); +} + +async function screenshot(page, name) { + const destination = path.join(outputDir, name); + await page.screenshot({ path: destination, fullPage: true }); + return destination; +} + +async function selectContract(page) { + assert.equal(await page.locator(".experience-progress .is-current strong").textContent(), "契约与难度"); + assert.equal(await page.locator("[data-experience-difficulty]:enabled").count(), 0, "difficulty locked before Contract"); + assert.ok(await page.locator("[data-experience-next]").isDisabled(), "continue disabled before Contract and difficulty"); + await page.locator('[data-experience-contract="CTR-R1-01"]').click(); + assert.equal(await page.locator("[data-experience-difficulty]:enabled").count(), 3, "three authoritative difficulties enabled"); + await page.locator('[data-experience-difficulty="standard"]').click(); + assert.ok(await page.locator("[data-experience-next]").isEnabled(), "continue enabled after Contract and difficulty"); + await page.locator("[data-experience-next]").click(); +} + +async function inspectAndChoose(page) { + assert.equal(await page.locator(".experience-offer").count(), 4, "four stable offers"); + assert.deepEqual(await page.locator(".experience-offer-art > span").allTextContents(), ["OFF-01", "OFF-02", "OFF-03", "OFF-04"], "stable offer order"); + assert.equal(await page.locator(".experience-offer.is-selected").count(), 0, "no recommended pair"); + const offerSnapshot = await page.locator(".experience-offer").allTextContents(); + for (const id of ["OFF-01", "OFF-02", "OFF-03", "OFF-04"]) await page.locator(`[data-experience-inspect="${id}"]`).click(); + assert.equal(await page.locator(".experience-stage-actions").textContent().then((text) => /已查看\s*4\/4/.test(text)), true, "all offers inspected"); + assert.deepEqual(await page.locator(".experience-offer").allTextContents(), offerSnapshot.map((text) => text.replace("查看", "已查看")), "inspection does not reroll offer values"); + await page.locator('[data-experience-offer="OFF-01"]').click(); + await page.locator('[data-experience-offer="OFF-02"]').click(); + assert.equal(await page.locator(".experience-offer.is-selected").count(), 2, "exactly two selected"); + await page.locator('[data-experience-offer="OFF-03"]').click(); + assert.equal(await page.locator(".experience-offer.is-selected").count(), 2, "third selection rejected"); + assert.match(await page.locator(".experience-notice").textContent(), /已经选择两名队员/, "rejection reason visible"); + await page.locator('[data-experience-offer="OFF-02"]').click(); + await page.locator('[data-experience-offer="OFF-04"]').click(); + assert.deepEqual(await page.locator(".experience-offer.is-selected .experience-offer-art > span").allTextContents(), ["OFF-01", "OFF-04"], "player-selected pair retained"); +} + +async function reviewAndCommit(page) { + assert.equal(await page.locator(".experience-progress .is-current strong").textContent(), "创建前确认"); + assert.match(await page.locator(".experience-review-grid").textContent(), /首次穿越/); + assert.match(await page.locator(".experience-review-grid").textContent(), /标准/); + assert.match(await page.locator(".experience-review-grid").textContent(), /金币消耗\s*0/); + assert.equal(await page.locator(".experience-review-grid .recruit-marker.is-selected").count(), 2, "two selected in review"); + assert.equal(await page.locator(".experience-review-grid .recruit-marker:not(.is-selected)").count(), 2, "two unchosen in review"); + await page.locator("[data-experience-revise]").click(); + assert.deepEqual(await page.locator(".experience-offer.is-selected .experience-offer-art > span").allTextContents(), ["OFF-01", "OFF-04"], "revise preserves same pair"); + assert.deepEqual(await page.locator(".experience-offer-art > span").allTextContents(), ["OFF-01", "OFF-02", "OFF-03", "OFF-04"], "revise preserves same offer set"); + await page.locator("[data-experience-next]").click(); + await page.locator("[data-experience-commit]").click(); + assert.equal(await page.locator(".experience-progress .is-current strong").textContent(), "创建回执"); + assert.equal(await page.locator("text=P8-RUN-001").count(), 1, "one creation receipt"); + assert.match(await page.locator(".experience-receipt").textContent(), /队伍 2 \/ 容量 4/); + assert.match(await page.locator(".experience-receipt").textContent(), /P8\.2 尚未开启/); + assert.equal(await page.locator("[data-experience-commit]").count(), 0, "commit unavailable after receipt"); +} + +async function desktopAudit(browser, evidence) { + const page = await browser.newPage({ viewport: { width: 1440, height: 1000 }, deviceScaleFactor: 1 }); + const errors = []; + page.on("console", (message) => { if (message.type() === "error") errors.push(message.text()); }); + page.on("pageerror", (error) => errors.push(error.message)); + await page.goto(`${baseURL}#playtest`, { waitUntil: "networkidle" }); + assert.equal(await page.locator("html").getAttribute("lang"), "zh-CN", "Chinese default"); + assert.equal(await page.locator(".nav-item").count(), 7, "P8.1 plus six atlas views"); + await noHorizontalOverflow(page, "desktop Contract"); + evidence.push(await screenshot(page, "desktop-contract.png")); + await selectContract(page); + await inspectAndChoose(page); + evidence.push(await screenshot(page, "desktop-company-selected.png")); + await page.locator("[data-experience-next]").click(); + evidence.push(await screenshot(page, "desktop-review.png")); + await page.locator("[data-experience-revise]").click(); + await page.locator("[data-experience-next]").click(); + await page.locator("[data-experience-commit]").click(); + evidence.push(await screenshot(page, "desktop-receipt.png")); + assert.equal(await page.locator("text=P8-RUN-001").count(), 1); + await page.locator("[data-experience-restart]").click(); + assert.equal(await page.locator(".experience-contract-card.is-selected").count(), 0, "restart clears Contract"); + assert.equal(await page.locator(".experience-progress .is-current strong").textContent(), "契约与难度"); + assert.deepEqual(errors, [], `desktop browser errors: ${errors.join(" | ")}`); + await page.close(); +} + +async function responsiveAudit(browser, evidence) { + for (const fixture of [ + { name: "phone-landscape-offers.png", viewport: { width: 844, height: 390 } }, + { name: "phone-portrait-offers.png", viewport: { width: 390, height: 844 } }, + ]) { + const page = await browser.newPage({ viewport: fixture.viewport, deviceScaleFactor: 1 }); + const errors = []; + page.on("pageerror", (error) => errors.push(error.message)); + await page.goto(`${baseURL}#playtest`, { waitUntil: "networkidle" }); + await selectContract(page); + await page.locator('[data-experience-inspect="OFF-01"]').click(); + await page.locator('[data-experience-offer="OFF-01"]').click(); + await page.locator('[data-experience-offer="OFF-04"]').click(); + await noHorizontalOverflow(page, fixture.name); + await stageFitsViewport(page, fixture.name); + await actionFitsStage(page, ".experience-stage-actions [data-experience-next]", `${fixture.name} primary action`); + assert.equal(await page.locator(".experience-offer:visible").count(), 4, `${fixture.name} four offers visible`); + evidence.push(await screenshot(page, fixture.name)); + assert.deepEqual(errors, [], `${fixture.name} browser errors: ${errors.join(" | ")}`); + await page.close(); + } +} + +async function bundleAudit(browser) { + const bundle = path.join(root, "prototype/gallery/aetherbound-guild-prototype.html"); + const page = await browser.newPage({ viewport: { width: 1280, height: 800 } }); + const external = []; + page.on("request", (request) => { if (!request.url().startsWith("file:") && !request.url().startsWith("data:")) external.push(request.url()); }); + await page.goto(`file://${bundle}#playtest`, { waitUntil: "load" }); + assert.equal(await page.locator(".experience-contract-card").count(), 1, "self-contained P8.1 starts offline"); + assert.deepEqual(external, [], `Gallery external requests: ${external.join(" | ")}`); + await page.close(); +} + +async function main() { + fs.mkdirSync(outputDir, { recursive: true }); + const browser = await chromium.launch({ headless: true, executablePath: browserPath }); + const evidence = []; + try { + await desktopAudit(browser, evidence); + await responsiveAudit(browser, evidence); + await bundleAudit(browser); + } finally { + await browser.close(); + } + console.log("ABG_P8_1_BROWSER_OK states=4 contracts=1 difficulties=3 offers=4 selected=2 commit_receipts=1 responsive=desktop,phone_landscape,phone_portrait bundle=offline"); + console.log(`evidence=${evidence.join(",")}`); +} + +main().catch((error) => { + console.error(error.stack || error); + process.exitCode = 1; +}); diff --git a/prototype/site/verify-r7.js b/prototype/site/verify-r7.js index 73eb81bd..b74039c9 100644 --- a/prototype/site/verify-r7.js +++ b/prototype/site/verify-r7.js @@ -92,14 +92,14 @@ async function desktopAudit(browser, evidence) { const page = await browser.newPage({ viewport: { width: 1440, height: 1000 }, deviceScaleFactor: 1 }); page.on("console", (message) => { if (message.type() === "error") errors.push(`console: ${message.text()}`); }); page.on("pageerror", (error) => errors.push(`pageerror: ${error.message}`)); - await page.goto(baseURL, { waitUntil: "networkidle" }); + await page.goto(`${baseURL}#firstplay`, { waitUntil: "networkidle" }); assert.equal(await page.locator("#loading").count(), 0, "loading state clears"); assert.equal(await page.locator("html").getAttribute("lang"), "zh-CN", "Simplified Chinese is the owner-facing default"); assert.match(await page.locator(".opening-title h2").textContent(), /选择两名队员/, "opening decision is readable in Chinese"); await page.locator('[data-locale="en"]').first().click(); assert.equal(await page.locator("html").getAttribute("lang"), "en", "English remains available"); - assert.equal(await page.locator(".nav-item").count(), 6, "gallery view count"); + assert.equal(await page.locator(".nav-item").count(), 7, "gallery view count including P8.1 playtest"); assert.equal(await page.locator(".flow-progress button").count(), 23, "complete first-play state count"); assert.equal(await page.locator(".recruit-offer").count(), 4, "four provisional offers"); assert.equal(await page.locator(".offer-character").count(), 4, "four profession identity anchors"); @@ -309,7 +309,7 @@ async function bundleAudit(browser) { page.on("request", (request) => { if (!request.url().startsWith("file:") && !request.url().startsWith("data:")) external.push(request.url()); }); - await page.goto(`file://${bundle}`, { waitUntil: "load" }); + await page.goto(`file://${bundle}#firstplay`, { waitUntil: "load" }); assert.equal(await page.locator(".recruit-offer").count(), 4, "Gallery bundle initializes offline"); assert.deepEqual(external, [], `Gallery external requests: ${external.join(" | ")}`); await page.close(); diff --git a/tools/build_gallery_bundle.py b/tools/build_gallery_bundle.py index 2307c8a7..32a8558c 100644 --- a/tools/build_gallery_bundle.py +++ b/tools/build_gallery_bundle.py @@ -184,9 +184,9 @@ def build_catalog() -> dict[str, object]: require_unique(entries[label], count, label) payload: dict[str, object] = { - "version": 3, - "status": "production-ui-atlas-candidate", - "authorityRevision": "phase7-ui-production-atlas", + "version": 4, + "status": "phase8-p8-1-experience-prototype", + "authorityRevision": "phase8-p8-1-opening-company", "counts": expected, **entries, } @@ -224,9 +224,9 @@ def main() -> None: catalog_json = json.dumps(catalog, ensure_ascii=False, separators=(",", ":")).replace("<", "\\u003c") app = app.replace("', f"") + index = index.replace('', f"") index = index.replace( - '', + '', f'\n', )