feat: build opening company experience
This commit is contained in:
Vendored
+215
-11
@@ -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 `<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 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 `<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]);
|
||||
@@ -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 = `<div class="empty-state"><h1>Prototype data unavailable</h1><p>${escapeHtml(error.message)}</p></div>`;
|
||||
|
||||
@@ -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,
|
||||
|
||||
+13
-12
@@ -4,9 +4,9 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#182127" />
|
||||
<title>Aetherbound Guild - Production UI Atlas</title>
|
||||
<title>Aetherbound Guild - Experience Prototype</title>
|
||||
<link rel="icon" type="image/webp" href="favicon.webp" />
|
||||
<link rel="stylesheet" href="styles.css?v=7" />
|
||||
<link rel="stylesheet" href="styles.css?v=8" />
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#content">Skip to prototype</a>
|
||||
@@ -17,27 +17,28 @@
|
||||
<span class="brand-mark" aria-hidden="true">AG</span>
|
||||
<div>
|
||||
<strong>Aetherbound Guild</strong>
|
||||
<span data-copy="brandSubtitle">Final design prototype</span>
|
||||
<span data-copy="brandSubtitle">Phased experience prototype</span>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="primary-nav" id="primary-nav">
|
||||
<button class="nav-item is-active" data-view="firstplay" type="button"><span>01</span><b data-copy="navFirst">First play</b><em>23</em></button>
|
||||
<button class="nav-item" data-view="screens" type="button"><span>02</span><b data-copy="navScreens">Screen atlas</b><em>50</em></button>
|
||||
<button class="nav-item" data-view="regions" type="button"><span>03</span><b data-copy="navRegions">Regions</b><em>8</em></button>
|
||||
<button class="nav-item" data-view="catalogs" type="button"><span>04</span><b data-copy="navCatalogs">Catalogs</b><em>574</em></button>
|
||||
<button class="nav-item" data-view="reference" type="button"><span>05</span><b data-copy="navReference">Reference map</b><em>15</em></button>
|
||||
<button class="nav-item" data-view="review" type="button"><span>06</span><b data-copy="navReview">Review gates</b><em>7</em></button>
|
||||
<button class="nav-item is-active" data-view="playtest" type="button"><span>01</span><b data-copy="navPlaytest">Play P8.1</b><em>LIVE</em></button>
|
||||
<button class="nav-item" data-view="firstplay" type="button"><span>02</span><b data-copy="navFirst">First-play map</b><em>23</em></button>
|
||||
<button class="nav-item" data-view="screens" type="button"><span>03</span><b data-copy="navScreens">Screen atlas</b><em>50</em></button>
|
||||
<button class="nav-item" data-view="regions" type="button"><span>04</span><b data-copy="navRegions">Regions</b><em>8</em></button>
|
||||
<button class="nav-item" data-view="catalogs" type="button"><span>05</span><b data-copy="navCatalogs">Catalogs</b><em>574</em></button>
|
||||
<button class="nav-item" data-view="reference" type="button"><span>06</span><b data-copy="navReference">Reference map</b><em>15</em></button>
|
||||
<button class="nav-item" data-view="review" type="button"><span>07</span><b data-copy="navReview">Review gates</b><em>7</em></button>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<span class="status-dot" aria-hidden="true"></span>
|
||||
<div><strong>PRODUCTION UI ATLAS</strong><span data-copy="runtimeFrozen">Runtime development frozen</span></div>
|
||||
<div><strong>PHASE 8 · P8.1</strong><span data-copy="runtimeFrozen">Godot development frozen</span></div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="page-shell">
|
||||
<header class="topbar">
|
||||
<button class="icon-button mobile-only" id="open-nav" type="button" aria-label="Open navigation">☰</button>
|
||||
<div class="breadcrumbs"><span>Aetherbound Guild</span><b>/</b><strong id="view-title">First play</strong></div>
|
||||
<div class="breadcrumbs"><span>Aetherbound Guild</span><b>/</b><strong id="view-title">Play P8.1</strong></div>
|
||||
<div class="top-actions">
|
||||
<div class="locale-control" aria-label="Language">
|
||||
<button class="is-active" type="button" data-locale="en">EN</button>
|
||||
@@ -67,6 +68,6 @@
|
||||
<div class="viewer-viewport"><div id="dialog-stage"></div></div>
|
||||
</dialog>
|
||||
<div class="sr-only" id="live-region" aria-live="polite"></div>
|
||||
<script src="app.js?v=7" defer></script>
|
||||
<script src="app.js?v=8" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Vendored
+174
@@ -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;
|
||||
});
|
||||
Vendored
+3
-3
@@ -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();
|
||||
|
||||
@@ -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("</script", "<\\/script")
|
||||
styles = styles.replace("</style", "<\\/style")
|
||||
index = index.replace('<link rel="stylesheet" href="styles.css?v=7" />', f"<style>\n{styles}\n</style>")
|
||||
index = index.replace('<link rel="stylesheet" href="styles.css?v=8" />', f"<style>\n{styles}\n</style>")
|
||||
index = index.replace(
|
||||
'<script src="app.js?v=7" defer></script>',
|
||||
'<script src="app.js?v=8" defer></script>',
|
||||
f'<script id="prototype-data" type="application/json">{catalog_json}</script>\n<script>\n{app}\n</script>',
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user