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 ``;
}
+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) => `- ${index < currentIndex ? "✓" : step[1]}${escapeHtml(experienceStepLabel(step))}
`).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 = `
+
+
+
+
${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 = `
+
+
${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 = `
+
+
${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 = `
+
+
${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
-
+
Skip to prototype
@@ -17,27 +17,28 @@
AG
Aetherbound Guild
- Final design prototype
+ Phased experience prototype