diff --git a/Tools/Dashboard/index.html b/Tools/Dashboard/index.html
index 78c51aa00..3bc135023 100644
--- a/Tools/Dashboard/index.html
+++ b/Tools/Dashboard/index.html
@@ -843,10 +843,10 @@
-
+
-
+
diff --git a/Tools/Dashboard/js/codex_threads.js b/Tools/Dashboard/js/codex_threads.js
index 467051cf1..613eda859 100644
--- a/Tools/Dashboard/js/codex_threads.js
+++ b/Tools/Dashboard/js/codex_threads.js
@@ -40,13 +40,21 @@ function codexPreferredAudioMimeType() {
function codexAudioExtension(mimeType) {
const lower = (mimeType || '').toLowerCase();
- if (lower.includes('mp4')) return 'm4a';
+ if (lower.includes('m4a') || lower.includes('mp4')) return 'm4a';
if (lower.includes('wav')) return 'wav';
if (lower.includes('mpeg') || lower.includes('mp3')) return 'mp3';
if (lower.includes('ogg')) return 'ogg';
return 'webm';
}
+function codexLooksLikeAudioFile(file) {
+ const type = (file?.type || '').toLowerCase();
+ if (type.startsWith('audio/')) return true;
+ if (type.startsWith('video/')) return false;
+ const name = (file?.name || '').toLowerCase();
+ return /\.(m4a|mp3|wav|webm|ogg|aac|flac)$/i.test(name);
+}
+
function codexIsSecureVoiceContext() {
return window.isSecureContext || ['localhost', '127.0.0.1', '::1'].includes(window.location.hostname);
}
@@ -104,11 +112,11 @@ function codexOpenAudioFilePicker(message) {
async function codexStartVoiceInput() {
if (!codexIsSecureVoiceContext()) {
- codexOpenAudioFilePicker('手机 Chrome 需要 HTTPS 才能直接录音,请改用系统录音/音频文件');
+ codexSetVoiceStatus('手机局域网 HTTP 页面不能直接录音;请先用语音备忘录录音,再点“上传音频”。');
return;
}
if (!navigator.mediaDevices?.getUserMedia || !window.MediaRecorder) {
- codexOpenAudioFilePicker('当前浏览器不能直接录音,请改用系统录音/音频文件');
+ codexSetVoiceStatus('当前浏览器不能直接录音;请点“上传音频”选择录音文件。');
return;
}
try {
@@ -158,21 +166,22 @@ async function codexToggleVoiceInput() {
}
}
-async function codexFinishVoiceInput(blob) {
+async function codexFinishVoiceInput(blob, filename = '') {
if (!blob || blob.size === 0) {
codexSetVoiceStatus('没有录到声音');
return;
}
codexSetVoiceStatus('正在转写...');
try {
+ const mimeType = blob.type || (filename ? '' : 'audio/webm');
const audioBase64 = await codexBlobToBase64(blob);
const resp = await fetch('/api/codex/transcribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
audioBase64,
- mimeType: blob.type || 'audio/webm',
- filename: `codex-voice-${Date.now()}.${codexAudioExtension(blob.type)}`,
+ mimeType,
+ filename: filename || `codex-voice-${Date.now()}.${codexAudioExtension(mimeType)}`,
language: 'zh',
}),
});
@@ -188,11 +197,11 @@ async function codexFinishVoiceInput(blob) {
async function codexHandleAudioFilePicked(event) {
const file = event.target?.files?.[0];
if (!file) return;
- if (!file.type.startsWith('audio/')) {
- codexSetVoiceStatus('请选择音频文件');
+ if (!codexLooksLikeAudioFile(file)) {
+ codexSetVoiceStatus('请选择音频文件,不支持视频');
return;
}
- await codexFinishVoiceInput(file);
+ await codexFinishVoiceInput(file, file.name || '');
}
async function codexLoadSessions(force = false) {
@@ -416,7 +425,7 @@ function codexBind() {
const audioPick = document.getElementById('codex-audio-pick');
if (audioPick && !audioPick.dataset.bound) {
audioPick.dataset.bound = '1';
- audioPick.addEventListener('click', () => codexOpenAudioFilePicker('选择或录制一段音频后自动转写'));
+ audioPick.addEventListener('click', () => codexOpenAudioFilePicker('请选择语音备忘录/录音文件,选完会自动转写'));
}
const audioFile = document.getElementById('codex-audio-file');
if (audioFile && !audioFile.dataset.bound) {
diff --git a/Tools/Figma/TH1MainMenuStructure/MAIN_MENU_LOGIC.md b/Tools/Figma/TH1MainMenuStructure/MAIN_MENU_LOGIC.md
new file mode 100644
index 000000000..23b6a5568
--- /dev/null
+++ b/Tools/Figma/TH1MainMenuStructure/MAIN_MENU_LOGIC.md
@@ -0,0 +1,77 @@
+# TH1 Main Menu Logic Summary
+
+## Entry
+
+`UIManager.OnGameStart()` publishes `ShowUIOutsideMenu`.
+
+`UIEventManagerBinder.HandleShowUIOutsideMenu()` receives it and calls:
+
+`UIOutsideManager.StartNewTask(new UIOutsideTask(ViewControllerManager.UIOutsideMenuController, evt))`
+
+`UIOutsideManager.StartNewTask()` closes the previous current Outside task, assigns the new task, and opens the controller.
+
+`UIOutsideMenuController.OnOpen()` calls `WindowScript.SetContent(evt)` and hides the startup fallback image once.
+
+## Main Menu Setup
+
+`UIOutsideMenuView.SetContent()` performs the runtime setup:
+
+- Randomizes `ButtonList` icons by selecting unique `GiantType` values from the implemented force/chess combinations.
+- Binds click listeners for every menu button.
+- Hides `ResumeButton` by default, then shows it only when `GameArchiveManager.Instance.HasQuickResumeArchive(NetMode.Single)` is true.
+- Sets `QQHintVersion` from `ConfigManager.Instance.VersionCfg.CurVersionInfo.FullVersion`.
+- Configures the Traditional Chinese Discord hint.
+- Plays the `Main` BGM through `AudioManager.Instance.PlayMusic("Main", 1f, 2f, true)`.
+
+## Button Flow
+
+| Button | Runtime action |
+| --- | --- |
+| `StartButton` | Publishes `ShowUIOutsideSelect`. |
+| `ResumeButton` | Loads the quick single-player archive, opens loading UI, calls `Main.Instance.ResumeMatch(record, preread)`, then hides menu/all outside UI. |
+| `MultiplayButton` | Publishes `ShowUIOutsideMultiplay`. |
+| `LibraryButton` | Publishes `ShowUIOutsideLibrary`. |
+| `HistoryButton` | Publishes `ShowUIOutsideHistory`. |
+| `TutorButton` | Publishes `ShowUIOutsideTutor`. |
+| `StoryButton` | Publishes `ShowUIOutsideStory`; binder opens the independent Story controller through `UIOutsideManager.OpenStory()`. |
+| `TransReportButton` | Publishes `ShowUIGlobalBugReport`. |
+| `QuestionnaireButton` | Found or runtime-created, then publishes `ShowUIOutsideQuestionnaire`. |
+| `AnnouncementButton` | Opens `AnnouncementPanel` locally. |
+| `AboutButton` | Opens `AboutPanel` locally. |
+| `SettingButton` | Opens `SettingPanel` locally. |
+| `ShutdownButton` | Opens `ShutdownPanel` locally. |
+
+## Local Child Panels
+
+`AnnouncementPanel`:
+
+- Close by close button or blocking area.
+- Builds version tabs from `ConfigManager.Instance.VersionCfg.Versions`.
+- Shows selected version content through multilingual text.
+
+`AboutPanel`:
+
+- Displays current version.
+- Binds external links: Bilibili, website, X, YouTube, Discord.
+- Builds staff rows from `Table.Instance.StaffDataAssets.StaffList`.
+
+`SettingPanel`:
+
+- Initializes `LanguageOptionGroup` to the current language.
+- Treats ZH, TDZH, JP, EN, KR as primary language options.
+- Uses option index `5` for `MoreLanguageModule`.
+- `ManageButton` hides the local setting panel and publishes `ShowUIOutsideMod`.
+
+`ShutdownPanel`:
+
+- Cancel and block close the panel.
+- Confirm calls `Application.Quit()` in build or stops Play Mode in editor.
+
+## Important UI Layer Behavior
+
+Most Outside screens use `UIOutsideManager.StartNewTask()`, so opening another normal Outside screen closes the previous one.
+
+Loading is separate from the current Outside task.
+
+Story, Invited, and Wiki are independent controllers in `UIOutsideManager` and do not follow the ordinary single current-task replacement path.
+
diff --git a/Tools/Figma/TH1MainMenuStructure/README.md b/Tools/Figma/TH1MainMenuStructure/README.md
new file mode 100644
index 000000000..978cd9639
--- /dev/null
+++ b/Tools/Figma/TH1MainMenuStructure/README.md
@@ -0,0 +1,32 @@
+# TH1 Main Menu Figma Structure Plugin
+
+This is a local Figma development plugin that recreates the current TH1 main menu structure as editable Figma frames.
+
+## How to run
+
+1. Open Figma Desktop.
+2. Create or open a blank design file.
+3. Go to `Plugins > Development > Import plugin from manifest...`.
+4. Select:
+
+ `C:\TH1\TH1\Tools\Figma\TH1MainMenuStructure\manifest.json`
+
+5. Run `Plugins > Development > TH1 Main Menu Structure`.
+
+The plugin creates two frames:
+
+- `TH1 Main Menu - Current Unity Structure`
+- `TH1 Main Menu - Logic Flow`
+
+## Scope
+
+The generated Figma file is a structural restoration, not a visual redesign. It uses placeholder shapes and labels instead of importing Unity texture assets.
+
+The structure is based on:
+
+- `Unity/Assets/Scripts/TH1_UI/View/Outside/UIOutsideMenuView.cs`
+- `Unity/Assets/Scripts/TH1_UI/Controller/Outside/UIOutsideMenuController.cs`
+- `Unity/Assets/Scripts/TH1_Core/Events/UIEventManagerBinder.cs`
+- `Unity/Assets/Scripts/TH1_UI/Core/UIOutsideManager.cs`
+- `Unity/Assets/BundleResources/Prefab/UI/Outside/UIOutsideMenu.prefab`
+
diff --git a/Tools/Figma/TH1MainMenuStructure/code.js b/Tools/Figma/TH1MainMenuStructure/code.js
new file mode 100644
index 000000000..1f07ef20b
--- /dev/null
+++ b/Tools/Figma/TH1MainMenuStructure/code.js
@@ -0,0 +1,265 @@
+async function main() {
+ await figma.loadFontAsync({ family: "Inter", style: "Regular" });
+ await figma.loadFontAsync({ family: "Inter", style: "Medium" });
+ await figma.loadFontAsync({ family: "Inter", style: "Bold" });
+
+ const page = figma.currentPage;
+ page.name = "TH1 Main Menu Reverse Map";
+
+ const frames = page.children.filter((node) =>
+ node.name === "TH1 Main Menu - Current Unity Structure" ||
+ node.name === "TH1 Main Menu - Logic Flow"
+ );
+ for (const frame of frames) frame.remove();
+
+ const layout = createFrame(page, "TH1 Main Menu - Current Unity Structure", 0, 0, 1920, 1080, "#101319");
+ const flow = createFrame(page, "TH1 Main Menu - Logic Flow", 2040, 0, 1920, 1080, "#0f1419");
+
+ drawMenuLayout(layout);
+ drawLogicFlow(flow);
+
+ figma.viewport.scrollAndZoomIntoView([layout, flow]);
+ figma.closePlugin("Created TH1 main menu structure and logic flow frames.");
+}
+
+function drawMenuLayout(parent) {
+ addRect(parent, "Background image: Background / MenuPreview", 0, 0, 1920, 1080, "#171d24", 0);
+ addRect(parent, "dim overlay", 0, 0, 1920, 1080, "#000000", 0, 0.18);
+
+ addText(parent, "title", "TH1 Main Menu - current Unity structure", 32, 28, 820, 36, 26, "#f5f0d7", "Bold");
+ addText(parent, "source note",
+ "Sources: UIOutsideMenuView.cs, UIOutsideMenuController.cs, UIEventManagerBinder.cs, UIOutsideManager.cs, UIOutsideMenu.prefab",
+ 32, 66, 1220, 28, 14, "#aab3bd", "Regular");
+
+ addRect(parent, "GameLogo placeholder", 883, -21, 100, 100, "#d9c07a", 10, 0.95);
+ addText(parent, "GameLogo text", "GameLogo", 871, 84, 140, 24, 13, "#d9c07a", "Medium", "CENTER");
+
+ addRect(parent, "Logo image placeholder", 195, 467, 1651, 390, "#2b3542", 12, 0.68, "#d9c07a");
+ addText(parent, "Logo label", "Logo image node\nanchored center: x=60, y=-122, size=1651x390", 275, 586, 600, 76, 30, "#f4e8be", "Bold");
+
+ addRect(parent, "ButtonRow container", 620, 250, 761, 341, "#1b2430", 8, 0.82, "#6e8bb8");
+ addText(parent, "ButtonRow label", "ButtonRow - six MenuButton prefab instances", 648, 264, 520, 22, 16, "#b9c8df", "Medium");
+
+ const buttons = [
+ ["StartButton", "Start / 开始", "ShowUIOutsideSelect", 650, 310],
+ ["MultiplayButton", "Multiplay / 多人", "ShowUIOutsideMultiplay", 1030, 310],
+ ["ResumeButton", "Resume / 继续", "visible only when quick resume exists", 650, 420],
+ ["LibraryButton", "Library / 图鉴", "ShowUIOutsideLibrary", 1030, 420],
+ ["HistoryButton", "History / 历史", "ShowUIOutsideHistory", 650, 530],
+ ["TutorButton", "Tutor / 教程", "ShowUIOutsideTutor", 1030, 530]
+ ];
+ for (const [name, label, event, x, y] of buttons) {
+ addButton(parent, name, label, event, x, y, 300, 82);
+ }
+
+ addButton(parent, "StoryButton", "Story / 序章", "ShowUIOutsideStory", 26, 747, 322, 105, "#4d3844");
+ addButton(parent, "TransReport", "Translation / bug feedback", "ShowUIGlobalBugReport", 786, 564, 338, 54, "#314658");
+ addButton(parent, "Questionnaire", "Questionnaire / 问卷", "Created or found at runtime, then ShowUIOutsideQuestionnaire", 786, 628, 338, 54, "#314658");
+
+ addIconButton(parent, "AnnouncementButton / BoardingButton", "updates", 372, 862);
+ addIconButton(parent, "AboutButton", "about", 1825, 789);
+ addIconButton(parent, "SettingButton", "settings", 1825, 887);
+ addIconButton(parent, "ShutdownButton", "quit", 1825, 985);
+
+ addPanel(parent, "SteamHint", "Early access / DEMO copy\nPrefab text: 当前是抢先体验版本", 26, 566, 391, 171);
+ addPanel(parent, "QQHint / DiscordHint", "Community copy\nZH: QQ group\nTDZH: Discord URL becomes clickable", 26, 886, 392, 179);
+
+ const runtime = createGroupLabel(parent, "Runtime behavior", 1368, 144, 500, 384);
+ const notes = [
+ "SetContent() binds all button listeners each open.",
+ "ButtonList icons are randomized from GiantType sprites.",
+ "ResumeButton is hidden unless GameArchiveManager.HasQuickResumeArchive(NetMode.Single).",
+ "AudioManager.PlayMusic(\"Main\", 1f, 2f, true) starts menu BGM.",
+ "Traditional Chinese Discord hint opens external URL on click.",
+ "Startup fallback image is hidden once the menu opens."
+ ];
+ let noteY = 194;
+ for (const note of notes) {
+ addText(parent, "runtime note", "- " + note, 1392, noteY, 450, 34, 14, "#d8dfeb", "Regular");
+ noteY += 48;
+ }
+
+ addModal(parent, "AnnouncementPanel", "VersionButtonList + VersionContent\nOpens locally from AnnouncementButton", 1260, 650, "#293647");
+ addModal(parent, "AboutPanel", "VersionLabel + external links + StaffArea\nBilibili / Website / X / YouTube / Discord", 1480, 650, "#293647");
+ addModal(parent, "SettingPanel", "LanguageOptionGroup\nMoreLanguageModule + ManageButton -> UIOutsideMod", 1260, 840, "#293647");
+ addModal(parent, "ShutdownPanel", "CancelButton / CheckButton / BlockButton\nConfirm calls Application.Quit()", 1480, 840, "#293647");
+
+ addText(parent, "implementation note",
+ "This frame is a structural restoration. It intentionally uses placeholders instead of copying Unity texture assets into Figma.",
+ 32, 1030, 1180, 24, 14, "#9aa6b2", "Regular");
+}
+
+function drawLogicFlow(parent) {
+ addText(parent, "title", "TH1 Main Menu - logic flow", 32, 28, 760, 36, 26, "#f5f0d7", "Bold");
+ addText(parent, "source note",
+ "Main menu is an OutsideUI task. One current Outside task is replaced by StartNewTask(), while loading and some overlays are independent.",
+ 32, 66, 1220, 28, 14, "#aab3bd", "Regular");
+
+ const init = [
+ ["UIManager.OnGameStart()", "publishes ShowUIOutsideMenu"],
+ ["UIEventManagerBinder", "HandleShowUIOutsideMenu"],
+ ["UIOutsideManager", "StartNewTask(UIOutsideMenuController)"],
+ ["UIOutsideMenuController.OnOpen()", "WindowScript.SetContent(evt)"],
+ ["UIOutsideMenuView.SetContent()", "bind buttons, version text, Discord hint, BGM"]
+ ];
+
+ let y = 132;
+ for (let i = 0; i < init.length; i++) {
+ addFlowCard(parent, init[i][0], init[i][1], 72 + i * 350, y, 290, 100, "#223044");
+ if (i < init.length - 1) addArrowText(parent, "->", 372 + i * 350, y + 36);
+ }
+
+ addSectionLabel(parent, "Button event map", 72, 300);
+ const events = [
+ ["StartButton", "ShowUIOutsideSelect", "Outside task replaced by select screen."],
+ ["ResumeButton", "ShowUIOutsideLoading -> Main.ResumeMatch -> HideUIOutsideAll", "Only visible when quick single-player archive exists."],
+ ["MultiplayButton", "ShowUIOutsideMultiplay", "Opens Steam lobby / room flow."],
+ ["LibraryButton", "ShowUIOutsideLibrary", "Library screen can later open Select."],
+ ["HistoryButton", "ShowUIOutsideHistory", "History screen can later open Select."],
+ ["TutorButton", "ShowUIOutsideTutor", "Tutorial selection screen."],
+ ["StoryButton", "ShowUIOutsideStory", "Uses UIOutsideManager.OpenStory(), independent of current task."],
+ ["TransReport", "ShowUIGlobalBugReport", "GlobalTop overlay, does not replace menu task."],
+ ["Questionnaire", "ShowUIOutsideQuestionnaire", "Runtime-created if prefab child is absent."],
+ ["AnnouncementButton", "AnnouncementPanel.Open()", "Local child panel, version list from VersionCfg."],
+ ["AboutButton", "AboutPanel.Open()", "Local child panel, external links and staff list."],
+ ["SettingButton", "SettingPanel.Open()", "Language options; ManageButton opens UIOutsideMod."],
+ ["ShutdownButton", "ShutdownPanel.Open()", "Confirm exits game or stops editor play mode."]
+ ];
+
+ let col = 0;
+ let row = 0;
+ for (const [button, event, note] of events) {
+ const x = 72 + col * 455;
+ const yy = 340 + row * 112;
+ addEventCard(parent, button, event, note, x, yy);
+ row++;
+ if (row === 6) {
+ row = 0;
+ col++;
+ }
+ }
+
+ addSectionLabel(parent, "OutsideUI replacement rules", 72, 1030);
+ addText(parent, "outside rules",
+ "StartNewTask closes the previous Outside task before opening the new one. Loading uses a separate task. Story, Invited, and Wiki are independent controllers in UIOutsideManager.",
+ 350, 1028, 1300, 32, 15, "#d8dfeb", "Regular");
+}
+
+function addButton(parent, name, label, event, x, y, w, h, fill) {
+ const color = fill || "#34415a";
+ addRect(parent, name, x, y, w, h, color, 8, 0.94, "#e2c576");
+ addRect(parent, name + " icon mask", x + 14, y + 10, 64, h - 20, "#202733", 6, 1, "#6f7e92");
+ addText(parent, name + " label", label, x + 92, y + 13, w - 110, 27, 18, "#fff2c3", "Bold");
+ addText(parent, name + " event", event, x + 92, y + 43, w - 110, h - 47, 12, "#c2ccd9", "Regular");
+}
+
+function addIconButton(parent, name, label, x, y) {
+ addRect(parent, name, x, y, 64, 64, "#222c37", 32, 1, "#d2bd72");
+ addText(parent, name + " glyph", label, x - 12, y + 22, 88, 18, 11, "#f5e6b6", "Medium", "CENTER");
+}
+
+function addPanel(parent, name, text, x, y, w, h) {
+ addRect(parent, name, x, y, w, h, "#1c2630", 8, 0.92, "#52687d");
+ addText(parent, name + " title", name, x + 18, y + 16, w - 36, 24, 17, "#f4e8be", "Bold");
+ addText(parent, name + " text", text, x + 18, y + 52, w - 36, h - 64, 14, "#c7d0dc", "Regular");
+}
+
+function addModal(parent, name, text, x, y, fill) {
+ addRect(parent, name, x, y, 180, 140, fill, 8, 0.96, "#e2c576");
+ addText(parent, name + " title", name, x + 14, y + 14, 152, 22, 15, "#fff2c3", "Bold");
+ addText(parent, name + " body", text, x + 14, y + 42, 152, 86, 11, "#d8dfeb", "Regular");
+}
+
+function addFlowCard(parent, title, body, x, y, w, h, fill) {
+ addRect(parent, title, x, y, w, h, fill, 8, 0.97, "#637a94");
+ addText(parent, title + " title", title, x + 16, y + 16, w - 32, 24, 16, "#fff2c3", "Bold");
+ addText(parent, title + " body", body, x + 16, y + 48, w - 32, 38, 13, "#d8dfeb", "Regular");
+}
+
+function addEventCard(parent, button, event, note, x, y) {
+ addRect(parent, button, x, y, 410, 92, "#1d2835", 8, 0.96, "#40576d");
+ addText(parent, button + " title", button, x + 14, y + 12, 150, 22, 14, "#f5e6b6", "Bold");
+ addText(parent, button + " event", event, x + 168, y + 12, 228, 26, 12, "#9fd1ff", "Medium");
+ addText(parent, button + " note", note, x + 14, y + 44, 382, 34, 11, "#c7d0dc", "Regular");
+}
+
+function addSectionLabel(parent, text, x, y) {
+ addText(parent, text + " label", text, x, y, 260, 26, 18, "#f5e6b6", "Bold");
+ addRect(parent, text + " underline", x, y + 28, 220, 2, "#d2bd72", 0, 0.9);
+}
+
+function addArrowText(parent, text, x, y) {
+ addText(parent, "arrow", text, x, y, 48, 24, 20, "#d2bd72", "Bold", "CENTER");
+}
+
+function createGroupLabel(parent, title, x, y, w, h) {
+ addRect(parent, title, x, y, w, h, "#111821", 8, 0.78, "#40576d");
+ addText(parent, title + " title", title, x + 24, y + 22, w - 48, 28, 20, "#f5e6b6", "Bold");
+}
+
+function createFrame(parent, name, x, y, w, h, fill) {
+ const frame = figma.createFrame();
+ frame.name = name;
+ frame.x = x;
+ frame.y = y;
+ frame.resize(w, h);
+ frame.fills = [paint(fill)];
+ parent.appendChild(frame);
+ return frame;
+}
+
+function addRect(parent, name, x, y, w, h, fill, radius, opacity, stroke) {
+ const rect = figma.createRectangle();
+ rect.name = name;
+ rect.x = x;
+ rect.y = y;
+ rect.resize(w, h);
+ rect.cornerRadius = radius || 0;
+ rect.fills = [paint(fill, opacity == null ? 1 : opacity)];
+ if (stroke) {
+ rect.strokes = [paint(stroke)];
+ rect.strokeWeight = 1;
+ }
+ parent.appendChild(rect);
+ return rect;
+}
+
+function addText(parent, name, value, x, y, w, h, size, fill, style, align) {
+ const text = figma.createText();
+ text.name = name;
+ text.x = x;
+ text.y = y;
+ text.resize(w, h);
+ text.fontName = { family: "Inter", style: style || "Regular" };
+ text.fontSize = size;
+ text.lineHeight = { unit: "AUTO" };
+ text.textAlignHorizontal = align || "LEFT";
+ text.fills = [paint(fill)];
+ text.characters = value;
+ parent.appendChild(text);
+ return text;
+}
+
+function paint(hex, opacity) {
+ const rgb = hexToRgb(hex);
+ return {
+ type: "SOLID",
+ color: {
+ r: rgb.r / 255,
+ g: rgb.g / 255,
+ b: rgb.b / 255
+ },
+ opacity: opacity == null ? 1 : opacity
+ };
+}
+
+function hexToRgb(hex) {
+ const cleaned = hex.replace("#", "");
+ return {
+ r: parseInt(cleaned.slice(0, 2), 16),
+ g: parseInt(cleaned.slice(2, 4), 16),
+ b: parseInt(cleaned.slice(4, 6), 16)
+ };
+}
+
+main();
diff --git a/Tools/Figma/TH1MainMenuStructure/manifest.json b/Tools/Figma/TH1MainMenuStructure/manifest.json
new file mode 100644
index 000000000..038755e23
--- /dev/null
+++ b/Tools/Figma/TH1MainMenuStructure/manifest.json
@@ -0,0 +1,7 @@
+{
+ "name": "TH1 Main Menu Structure",
+ "id": "th1-main-menu-structure",
+ "api": "1.0.0",
+ "main": "code.js",
+ "editorType": ["figma"]
+}
diff --git a/Unity/Assets/BundleResources/DataAssets/QuestionnaireDataAssets.asset b/Unity/Assets/BundleResources/DataAssets/QuestionnaireDataAssets.asset
index 9ad9683c6..9f2a70dfd 100644
--- a/Unity/Assets/BundleResources/DataAssets/QuestionnaireDataAssets.asset
+++ b/Unity/Assets/BundleResources/DataAssets/QuestionnaireDataAssets.asset
@@ -12,341 +12,470 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 9f2a6e7435b94d158cb76d9fd201a10a, type: 3}
m_Name: QuestionnaireDataAssets
m_EditorClassIdentifier:
- DefaultQuestionnaireId: gameplay-feedback-2026-06
+ DefaultQuestionnaireId: gameplay-feedback-2026-06-v2
MenuButtonText: "\u4F53\u9A8C\u53CD\u9988"
QuestionnaireList:
- - QuestionnaireId: gameplay-feedback-2026-06
+ - QuestionnaireId: gameplay-feedback-2026-06-v2
+ Revision: 1
+ Status: 0
+ Priority: 100
+ StartAtUtc: ""
+ EndAtUtc: ""
+ AllowMultipleSubmissions: 1
+ MaxSubmissionCount: 0
+ MinSubmitIntervalHours: 0
+ CanViewHistory: 1
+ CanSubmitAfterExpired: 0
+ TargetRules:
+ VersionWhitelist: []
+ PlatformWhitelist: []
+ LanguageWhitelist: []
+ SteamOnly: 0
+ BetaOnly: 0
+ MinFinishedMatchCount: 0
Title: "TOHOTOPIA\u73A9\u6CD5\u4E0E\u7B56\u7565\u53CD\u9988\u95EE\u5377"
- Description: "\u611F\u8C22\u4F60\u613F\u610F\u7559\u4E0B\u53CD\u9988\u3002\u8FD9\u4EFD\u95EE\u5377\u4E3B\u8981\u60F3\u4E86\u89E3\u89C4\u5219\u7406\u89E3\u3001\u7B56\u7565\u6DF1\u5EA6\u3001\u9635\u8425\u5E73\u8861\u3001\u5355\u5C40\u8282\u594F\u548C\u64CD\u4F5C\u8D1F\u62C5\uFF0C\u5927\u7EA6\u9700\u89816-8\u5206\u949F\u3002"
+ Description: "\u611F\u8C22\u4F60\u613F\u610F\u7559\u4E0B\u53CD\u9988\u3002\u8FD9\u4EFD\u95EE\u5377\u4E3B\u8981\u60F3\u4E86\u89E3\u89C4\u5219\u7406\u89E3\u3001\u7B56\u7565\u6DF1\u5EA6\u3001\u9635\u8425\u4E0E\u82F1\u96C4\u5E73\u8861\u3001\u5355\u5C40\u8282\u594F\u3001\u64CD\u4F5C\u8D1F\u62C5\u548C\u591A\u8BED\u8A00\u4F53\u9A8C\uFF0C\u5927\u7EA6\u9700\u89818-10\u5206\u949F\u3002"
SubmitButtonText: "\u63D0\u4EA4"
ResubmitButtonText: "\u91CD\u65B0\u586B\u5199"
CloseButtonText: "\u5173\u95ED"
- SubmittedMessage: "\u5DF2\u63D0\u4EA4\u3002\u4F60\u53EF\u4EE5\u91CD\u65B0\u586B\u5199\uFF0C\u518D\u6B21\u63D0\u4EA4\u4F1A\u8986\u76D6\u672C\u5730\u4E0A\u4E00\u4EFD\u7B54\u5377\u3002"
+ SubmittedMessage: "已提交。你可以在填写记录中查看这次答卷,也可以再次填写并新增一条记录。"
UploadingMessage: "\u5DF2\u4FDD\u5B58\u672C\u5730\u7B54\u5377\uFF0C\u6B63\u5728\u4E0A\u4F20\u5230\u670D\u52A1\u5668..."
- UploadSuccessMessage: "\u5DF2\u63D0\u4EA4\u5E76\u4E0A\u4F20\u5230\u670D\u52A1\u5668\u3002\u4F60\u53EF\u4EE5\u91CD\u65B0\u586B\u5199\uFF0C\u518D\u6B21\u63D0\u4EA4\u4F1A\u8986\u76D6\u672C\u5730\u7B54\u5377\u5E76\u4E0A\u4F20\u65B0\u7684\u670D\u52A1\u5668\u8BB0\u5F55\u3002"
+ UploadSuccessMessage: "已提交并上传到服务器。你可以在填写记录中查看这次答卷,也可以再次填写并上传新的记录。"
UploadFailedMessage: "\u5DF2\u5728\u672C\u5730\u4FDD\u5B58\uFF0C\u4F46\u670D\u52A1\u5668\u4E0A\u4F20\u5931\u8D25\u3002\u8BF7\u786E\u8BA4 Steam \u5728\u7EBF\u540E\u7A0D\u540E\u91CD\u65B0\u63D0\u4EA4\u3002"
UploadAuthFailedMessage: "\u5DF2\u5728\u672C\u5730\u4FDD\u5B58\uFF0C\u4F46\u5F53\u524D\u65E0\u6CD5\u83B7\u53D6 Steam \u767B\u5F55\u4FE1\u606F\uFF0C\u6682\u65F6\u6CA1\u6709\u4E0A\u4F20\u5230\u670D\u52A1\u5668\u3002"
RequiredMessage: "\u8BF7\u5148\u5B8C\u6210\u5E26*\u7684\u5FC5\u7B54\u9898\u3002"
- RefillHintText: "\u53EF\u4EE5\u91CD\u65B0\u586B\u5199\uFF0C\u63D0\u4EA4\u540E\u4F1A\u8986\u76D6\u4E0A\u4E00\u6B21\u672C\u5730\u7B54\u5377\u3002"
+ RefillHintText: "已清空当前填写内容,提交后会新增一条填写记录。"
SaveFailedMessage: "\u672C\u5730\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002"
Questions:
- QuestionId: q01
QuestionType: 1
- Title: "\u4F60\u76EE\u524D\u7684\u603B\u6E38\u620F\u65F6\u957F\u5927\u7EA6\u662F\u591A\u5C11\uFF1F"
+ Title: "你目前的总游戏时长大约是多少?"
Hint: ""
Required: 1
MaxSelectCount: 0
Options:
- OptionId: under_1h
- Text: "1\u5C0F\u65F6\u4EE5\u5185"
+ Text: "1小时以内"
- OptionId: 1_3h
- Text: "1-3\u5C0F\u65F6"
+ Text: "1-3小时"
- OptionId: 3_10h
- Text: "3-10\u5C0F\u65F6"
+ Text: "3-10小时"
- OptionId: 10h_plus
- Text: "10\u5C0F\u65F6\u4EE5\u4E0A"
+ Text: "10小时以上"
- QuestionId: q02
QuestionType: 1
- Title: "\u4F60\u73B0\u5728\u6700\u4E3B\u8981\u600E\u4E48\u73A9\u8FD9\u4E2A\u6E38\u620F\uFF1F"
+ Title: "你现在最主要怎么玩这个游戏?"
Hint: ""
Required: 1
MaxSelectCount: 0
Options:
- OptionId: single_player_ai
- Text: "\u4E3B\u8981\u6253\u5355\u4EBAAI"
+ Text: "主要打单人AI"
- OptionId: multiplayer_friends
- Text: "\u4E3B\u8981\u548C\u670B\u53CB\u8054\u673A"
+ Text: "主要和朋友联机"
- OptionId: explore_factions
- Text: "\u4E3B\u8981\u8BD5\u9635\u8425/\u82F1\u96C4"
+ Text: "主要试阵营/英雄"
- OptionId: still_learning
- Text: "\u8FD8\u5728\u719F\u6089\u57FA\u7840\u89C4\u5219"
+ Text: "还在熟悉基础规则"
- QuestionId: q03
QuestionType: 1
- Title: "\u4F60\u5BF9\u7C7B\u4F3C4X/\u68CB\u76D8\u7B56\u7565\u6E38\u620F\u7684\u719F\u6089\u7A0B\u5EA6\u662F\uFF1F"
+ Title: "你当前主要使用哪种游戏语言?"
+ Hint: ""
+ Required: 1
+ MaxSelectCount: 0
+ Options:
+ - OptionId: zh_cn
+ Text: "简体中文"
+ - OptionId: zh_tw
+ Text: "繁体中文"
+ - OptionId: en
+ Text: "English"
+ - OptionId: ja
+ Text: "日本語"
+ - OptionId: ko
+ Text: "한국어"
+ - OptionId: other
+ Text: "Other"
+ - QuestionId: q04
+ QuestionType: 1
+ Title: "你对当前语言版本的文本质量满意吗?"
+ Hint: ""
+ Required: 1
+ MaxSelectCount: 0
+ Options:
+ - OptionId: satisfied
+ Text: "很满意,基本自然准确"
+ - OptionId: understandable
+ Text: "基本能理解,但有些句子别扭"
+ - OptionId: unclear
+ Text: "有不少误译或表达不清"
+ - OptionId: affects_rules
+ Text: "文本质量影响我理解规则"
+ - OptionId: not_sure
+ Text: "我没有注意或不好判断"
+ - QuestionId: q05
+ QuestionType: 2
+ Title: "你还希望游戏支持哪些语言?"
+ Hint: "可多选"
+ Required: 0
+ MaxSelectCount: 0
+ Options:
+ - OptionId: russian
+ Text: "Russian"
+ - OptionId: spanish
+ Text: "Spanish"
+ - OptionId: portuguese
+ Text: "Portuguese"
+ - OptionId: french
+ Text: "French"
+ - OptionId: german
+ Text: "German"
+ - OptionId: thai
+ Text: "Thai"
+ - OptionId: vietnamese
+ Text: "Vietnamese"
+ - OptionId: indonesian
+ Text: "Indonesian"
+ - OptionId: turkish
+ Text: "Turkish"
+ - OptionId: italian
+ Text: "Italian"
+ - OptionId: polish
+ Text: "Polish"
+ - OptionId: other
+ Text: "Other,请在最后开放题补充"
+ - OptionId: no_need
+ Text: "我不需要新增语言"
+ - QuestionId: q06
+ QuestionType: 1
+ Title: "你对类似4X/棋盘策略游戏的熟悉程度是?"
Hint: ""
Required: 1
MaxSelectCount: 0
Options:
- OptionId: new_to_strategy
- Text: "\u57FA\u672C\u662F\u65B0\u624B"
+ Text: "基本是新手"
- OptionId: played_some
- Text: "\u73A9\u8FC7\u4E00\u4E9B\u56DE\u5408/\u7B56\u7565\u6E38\u620F"
+ Text: "玩过一些回合或策略游戏"
- OptionId: polytopia_like
- Text: "\u719F\u6089Polytopia\u6216\u7C7B\u4F3C\u77ED\u5C40\u7B56\u7565\u6E38\u620F"
+ Text: "熟悉Polytopia或类似短局策略游戏"
- OptionId: civ_4x_veteran
- Text: "\u719F\u6089\u6587\u660E/4X/\u6218\u68CB\u7B49\u7B56\u7565\u6E38\u620F"
- - QuestionId: q04
+ Text: "熟悉文明、4X、战棋等策略游戏"
+ - QuestionId: q07
QuestionType: 1
- Title: "\u5728\u524D30\u5206\u949F\u5185\uFF0C\u4F60\u662F\u5426\u77E5\u9053\u81EA\u5DF1\u6BCF\u56DE\u5408\u5E94\u8BE5\u505A\u4EC0\u4E48\uFF1F"
+ Title: "你在开新局的前10回合,通常是否清楚自己应该优先做什么?"
Hint: ""
Required: 1
MaxSelectCount: 0
Options:
- - OptionId: clear
- Text: "\u5F88\u6E05\u695A"
- - OptionId: mostly_clear
- Text: "\u5927\u81F4\u80FD\u5224\u65AD"
- - OptionId: often_lost
- Text: "\u7ECF\u5E38\u4E0D\u77E5\u9053\u4E0B\u4E00\u6B65\u505A\u4EC0\u4E48"
- - OptionId: not_played_enough
- Text: "\u8FD8\u6CA1\u73A9\u5230\u8DB3\u591F\u5224\u65AD"
- - QuestionId: q05
+ - OptionId: clear_plan
+ Text: "很清楚,有固定开局思路"
+ - OptionId: adapt
+ Text: "大致清楚,但会看局势摸索"
+ - OptionId: unclear_priority
+ Text: "经常不知道优先扩张、科技还是出兵"
+ - OptionId: trial_or_guide
+ Text: "主要靠试错或看攻略/问别人"
+ - OptionId: not_enough_games
+ Text: "还没开过足够多局判断"
+ - QuestionId: q08
QuestionType: 2
- Title: "\u54EA\u4E9B\u89C4\u5219\u6216\u4FE1\u606F\u6700\u5BB9\u6613\u8BA9\u4F60\u56F0\u60D1\uFF1F"
- Hint: "\u53EF\u591A\u9009"
+ Title: "哪些规则或信息最容易让你困惑?"
+ Hint: "可多选"
Required: 0
MaxSelectCount: 0
Options:
- OptionId: damage_formula
- Text: "\u653B\u51FB/\u9632\u5FA1/\u8840\u91CF\u5982\u4F55\u51B3\u5B9A\u4F24\u5BB3"
+ Text: "攻击/防御/血量如何决定伤害"
- OptionId: action_points
- Text: "\u79FB\u52A8\u3001\u653B\u51FB\u3001\u6CBB\u7597\u7B49\u884C\u52A8\u70B9\u89C4\u5219"
+ Text: "移动、攻击、治疗等行动点规则"
- OptionId: tech_city_economy
- Text: "\u79D1\u6280\u3001\u57CE\u5E02\u5347\u7EA7\u548C\u8D44\u6E90\u6536\u76CA"
+ Text: "科技、城市升级和资源收益"
- OptionId: hero_skill_timing
- Text: "\u82F1\u96C4\u6280\u80FD\u89E6\u53D1\u6761\u4EF6\u548C\u6301\u7EED\u6548\u679C"
+ Text: "英雄技能触发条件和持续效果"
- OptionId: diplomacy_victory
- Text: "\u5916\u4EA4\u3001\u5BA3\u6218\u548C\u80DC\u5229\u6761\u4EF6"
+ Text: "外交、宣战和胜利条件"
- OptionId: terrain_building
- Text: "\u9053\u8DEF\u3001\u6865\u6881\u3001\u5730\u5F62\u548C\u5EFA\u7B51\u9650\u5236"
- - QuestionId: q06
+ Text: "道路、桥梁、地形和建筑限制"
+ - QuestionId: q09
QuestionType: 1
- Title: "\u5F53\u524D\u5355\u5C40\u8282\u594F\u5BF9\u4F60\u6765\u8BF4\u5982\u4F55\uFF1F"
+ Title: "当前单局节奏对你来说如何?"
Hint: ""
Required: 1
MaxSelectCount: 0
Options:
- OptionId: too_fast
- Text: "\u504F\u5FEB\uFF0C\u8FD8\u6CA1\u7406\u89E3\u5C31\u88AB\u6253\u5D29"
+ Text: "偏快,还没理解就被打崩"
- OptionId: good
- Text: "\u6BD4\u8F83\u5408\u9002"
+ Text: "比较合适"
- OptionId: slow
- Text: "\u504F\u6162\uFF0C\u7B49\u5F85\u548C\u91CD\u590D\u64CD\u4F5C\u591A"
+ Text: "偏慢,等待和重复操作多"
- OptionId: depends
- Text: "\u4E0D\u540C\u9636\u6BB5\u5DEE\u522B\u5F88\u5927"
- - QuestionId: q07
+ Text: "不同阶段差别很大"
+ - QuestionId: q10
QuestionType: 1
- Title: "\u4E00\u5C40\u5230\u4E2D\u540E\u671F\u65F6\uFF0C\u7B56\u7565\u9009\u62E9\u662F\u5426\u8FD8\u6709\u8DA3\uFF1F"
+ Title: "一局到中后期时,策略选择是否还有趣?"
Hint: ""
Required: 1
MaxSelectCount: 0
Options:
- OptionId: stays_interesting
- Text: "\u8D8A\u5230\u540E\u9762\u8D8A\u6709\u535A\u5F08"
+ Text: "越到后面越有博弈"
- OptionId: some_repetition
- Text: "\u6709\u8DA3\uFF0C\u4F46\u6709\u4E9B\u91CD\u590D"
+ Text: "有趣,但有些重复"
- OptionId: becomes_single_plan
- Text: "\u5BB9\u6613\u53D8\u6210\u56FA\u5B9A\u5957\u8DEF"
+ Text: "容易变成固定套路"
- OptionId: not_reached
- Text: "\u8FD8\u6CA1\u73A9\u5230\u8DB3\u591F\u5224\u65AD"
- - QuestionId: q08
+ Text: "还没玩到足够判断"
+ - QuestionId: q11
QuestionType: 2
- Title: "\u4F60\u89C9\u5F97\u5F53\u524D\u6700\u6709\u7B56\u7565\u4EF7\u503C\u7684\u51B3\u7B56\u662F\u54EA\u4E9B\uFF1F"
- Hint: "\u53EF\u591A\u9009"
+ Title: "你觉得当前最有策略价值的决策是哪些?"
+ Hint: "可多选"
Required: 0
MaxSelectCount: 0
Options:
- OptionId: expansion_timing
- Text: "\u63A2\u7D22\u3001\u5360\u57CE\u548C\u6269\u5F20\u65F6\u673A"
+ Text: "探索、占城和扩张时机"
- OptionId: tech_path
- Text: "\u79D1\u6280\u987A\u5E8F\u548C\u7ECF\u6D4E\u89C4\u5212"
+ Text: "科技顺序和经济规划"
- OptionId: hero_leveling
- Text: "\u82F1\u96C4\u9009\u62E9\u3001\u5347\u7EA7\u548C\u4EFB\u52A1"
+ Text: "英雄选择、升级和任务"
- OptionId: tactical_positioning
- Text: "\u6218\u6597\u7AD9\u4F4D\u3001\u5148\u624B\u548C\u53CD\u5236"
+ Text: "战斗站位、先手和反制"
- OptionId: faction_mechanics
- Text: "\u9635\u8425\u7279\u8272\u673A\u5236"
+ Text: "阵营特色机制"
- OptionId: diplomacy_timing
- Text: "\u5916\u4EA4\u3001\u7ED3\u76DF\u548C\u5BA3\u6218\u65F6\u673A"
- - QuestionId: q09
+ Text: "外交、结盟和宣战时机"
+ - QuestionId: q12
QuestionType: 2
- Title: "\u4F60\u89C9\u5F97\u54EA\u4E9B\u4E2D\u540E\u671F\u95EE\u9898\u6700\u660E\u663E\uFF1F"
- Hint: "\u53EF\u591A\u9009"
+ Title: "你觉得哪些中后期问题最明显?"
+ Hint: "可多选"
Required: 0
MaxSelectCount: 0
Options:
- OptionId: warship_cannon
- Text: "\u6218\u8230/\u70AE\u624B\u7B49\u8FDC\u7A0B\u706B\u529B\u8FC7\u4E8E\u4E3B\u5BFC"
+ Text: "战舰/炮手等远程火力过于主导"
- OptionId: hero_snowball
- Text: "\u82F1\u96C4\u6216\u91D1\u5E01\u5347\u7EA7\u6EDA\u96EA\u7403\u592A\u5FEB"
+ Text: "英雄或金币升级滚雪球太快"
- OptionId: tech_runs_out
- Text: "\u79D1\u6280\u3001\u6587\u5316\u6216\u5185\u653F\u540E\u671F\u76EE\u6807\u4E0D\u8DB3"
+ Text: "科技、文化或内政后期目标不足"
- OptionId: unit_spam
- Text: "\u7206\u5175/\u5806\u5355\u4F4D\u540E\u64CD\u4F5C\u91CF\u592A\u5927"
+ Text: "爆兵/堆单位后操作量太大"
- OptionId: ai_pressure
- Text: "AI\u5F3A\u5EA6\u6216\u5175\u529B\u4F53\u611F\u4E0D\u5408\u7406"
+ Text: "AI强度或兵力体感不合理"
- OptionId: not_reached
- Text: "\u8FD8\u6CA1\u73A9\u5230\u4E2D\u540E\u671F"
- - QuestionId: q10
+ Text: "还没玩到中后期"
+ - QuestionId: q13
QuestionType: 1
- Title: "\u6218\u6597\u7ED3\u679C\u548C\u4F24\u5BB3\u9884\u671F\u5BF9\u4F60\u6765\u8BF4\u662F\u5426\u8DB3\u591F\u6E05\u695A\uFF1F"
+ Title: "战斗结果和伤害预期对你来说是否足够清楚?"
Hint: ""
Required: 1
MaxSelectCount: 0
Options:
- OptionId: clear
- Text: "\u5F88\u6E05\u695A"
+ Text: "很清楚"
- OptionId: mostly_clear
- Text: "\u5927\u81F4\u6E05\u695A\uFF0C\u4F46\u5076\u5C14\u4F1A\u610F\u5916"
+ Text: "大致清楚,但偶尔会意外"
- OptionId: unclear
- Text: "\u7ECF\u5E38\u4E0D\u77E5\u9053\u4E3A\u4EC0\u4E48\u8FD9\u4E48\u4F24"
+ Text: "经常不知道为什么这么伤"
- OptionId: not_combat_focused
- Text: "\u6211\u5F88\u5C11\u5173\u6CE8\u8FD9\u4E2A"
- - QuestionId: q11
+ Text: "我很少关注这个"
+ - QuestionId: q14
QuestionType: 2
- Title: "\u54EA\u4E9B\u6218\u6597\u4FE1\u606F\u6700\u9700\u8981\u663E\u793A\u5F97\u66F4\u6E05\u695A\uFF1F"
- Hint: "\u53EF\u591A\u9009"
+ Title: "哪些战斗信息最需要显示得更清楚?"
+ Hint: "可多选"
Required: 0
MaxSelectCount: 0
Options:
- OptionId: combat_predict
- Text: "\u653B\u51FB\u540E\u53CC\u65B9\u5269\u4F59\u8840\u91CF"
+ Text: "攻击后双方剩余血量"
- OptionId: counterattack
- Text: "\u53CD\u51FB\u3001\u8FDE\u6740\u548C\u5148\u624B\u5224\u5B9A"
+ Text: "反击、连杀和先手判定"
- OptionId: terrain_defense
- Text: "\u5730\u5F62\u548C\u57CE\u9632\u5BF9\u9632\u5FA1\u7684\u5F71\u54CD"
+ Text: "地形和城防对防御的影响"
- OptionId: aoe_chain
- Text: "\u8303\u56F4\u4F24\u5BB3\u548C\u8FDE\u9501\u6548\u679C"
+ Text: "范围伤害和连锁效果"
- OptionId: skill_effects
- Text: "\u82F1\u96C4/\u5355\u4F4D\u6280\u80FD\u5BF9\u6218\u6597\u7684\u5F71\u54CD"
+ Text: "英雄/单位技能对战斗的影响"
- OptionId: threat_range
- Text: "\u654C\u65B9\u4E0B\u56DE\u5408\u53EF\u80FD\u6253\u5230\u7684\u8303\u56F4"
- - QuestionId: q12
+ Text: "敌方下回合可能打到的范围"
+ - QuestionId: q15
QuestionType: 1
- Title: "\u4F60\u89C9\u5F97\u9635\u8425\u4E4B\u95F4\u7684\u73A9\u6CD5\u5DEE\u5F02\u662F\u5426\u8DB3\u591F\u660E\u663E\uFF1F"
+ Title: "你觉得阵营之间的玩法差异是否足够明显?"
Hint: ""
Required: 1
MaxSelectCount: 0
Options:
- OptionId: very_distinct
- Text: "\u5F88\u660E\u663E\uFF0C\u6BCF\u4E2A\u9635\u8425\u90FD\u6709\u81EA\u5DF1\u7684\u6253\u6CD5"
+ Text: "很明显,每个阵营都有自己的打法"
- OptionId: some_distinct
- Text: "\u6709\u5DEE\u5F02\uFF0C\u4F46\u90E8\u5206\u9635\u8425\u8FD8\u662F\u50CF"
+ Text: "有差异,但部分阵营还是像"
- OptionId: mostly_same
- Text: "\u611F\u89C9\u5927\u591A\u90FD\u662F\u76F8\u4F3C\u5957\u8DEF"
+ Text: "感觉大多都是相似套路"
- OptionId: not_enough_factions
- Text: "\u73A9\u8FC7\u7684\u9635\u8425\u8FD8\u4E0D\u591F\u591A"
- - QuestionId: q13
+ Text: "玩过的阵营还不够多"
+ - QuestionId: q16
QuestionType: 1
- Title: "\u4F60\u89C9\u5F97\u5F53\u524D\u9635\u8425/\u82F1\u96C4\u5E73\u8861\u6700\u63A5\u8FD1\u54EA\u79CD\u60C5\u51B5\uFF1F"
+ Title: "你觉得当前阵营/英雄平衡最接近哪种情况?"
Hint: ""
Required: 1
MaxSelectCount: 0
Options:
- OptionId: mostly_fair
- Text: "\u5927\u4F53\u516C\u5E73"
+ Text: "大体公平"
- OptionId: new_factions_strong
- Text: "\u65B0\u9635\u8425/\u65B0\u82F1\u96C4\u660E\u663E\u66F4\u5F3A"
+ Text: "新阵营或新英雄明显更强"
- OptionId: old_factions_weak
- Text: "\u65E9\u671F\u9635\u8425\u6216\u666E\u901A\u5175\u79CD\u660E\u663E\u843D\u540E"
+ Text: "早期阵营或普通兵种明显落后"
- OptionId: specific_outliers
- Text: "\u5C11\u6570\u82F1\u96C4/\u6280\u80FD\u7279\u522B\u5931\u8861"
+ Text: "少数英雄或技能特别失衡"
- OptionId: no_judgement
- Text: "\u8FD8\u4E0D\u597D\u5224\u65AD"
- - QuestionId: q14
+ Text: "还不好判断"
+ - QuestionId: q17
QuestionType: 2
- Title: "\u5982\u679C\u4F60\u89C9\u5F97\u6709\u5931\u8861\u70B9\uFF0C\u4E3B\u8981\u662F\u54EA\u4E9B\uFF1F"
- Hint: "\u53EF\u591A\u9009\uFF1B\u6CA1\u611F\u89C9\u5230\u53EF\u8DF3\u8FC7"
+ Title: "哪些英雄最需要重新检查平衡?"
+ Hint: "最多选5项;如果选择“没有明显失衡英雄”或“玩过/遇到的英雄还不够多”,建议不要再选具体英雄"
Required: 0
- MaxSelectCount: 0
+ MaxSelectCount: 5
Options:
- - OptionId: koishi_reisen
- Text: "\u53E4\u660E\u5730\u604B/\u94C3\u4ED9\u7B49\u82F1\u96C4\u6E05\u573A\u6216\u53CD\u5236\u96BE"
- - OptionId: remilia_scarlet
- Text: "\u7EA2\u9B54\u9986\u6216\u65E7\u9635\u8425\u5F3A\u5EA6\u4E0D\u8DB3"
- - OptionId: viking_new
- Text: "\u7EF4\u4EAC/\u534E\u6247/\u5807\u5B50\u7B49\u65B0\u5185\u5BB9\u5F3A\u5EA6\u504F\u9AD8"
- - OptionId: hero_gold_level
- Text: "\u82F1\u96C4\u91D1\u5E01\u5347\u7EA7\u548C\u7ECF\u6D4E\u6EDA\u96EA\u7403"
- - OptionId: warship_cannon_knight
- Text: "\u6218\u8230\u3001\u70AE\u624B\u3001\u9A91\u58EB\u7B49\u5173\u952E\u5355\u4F4D"
- - OptionId: ai_bonus
- Text: "AI\u52A0\u6210\u6216\u9AD8\u96BE\u5EA6\u4F53\u611F"
- - QuestionId: q15
+ - OptionId: kaguya
+ Text: "辉夜"
+ - OptionId: reisen
+ Text: "铃仙"
+ - OptionId: tewi
+ Text: "帝"
+ - OptionId: eirin
+ Text: "永琳"
+ - OptionId: mokou
+ Text: "妹红"
+ - OptionId: remilia
+ Text: "蕾米莉亚"
+ - OptionId: patchouli
+ Text: "帕秋莉"
+ - OptionId: sakuya
+ Text: "咲夜"
+ - OptionId: flandre
+ Text: "芙兰朵露"
+ - OptionId: meiling
+ Text: "美铃"
+ - OptionId: kanako
+ Text: "神奈子"
+ - OptionId: suwako
+ Text: "诹访子"
+ - OptionId: sanae
+ Text: "早苗"
+ - OptionId: aya
+ Text: "文"
+ - OptionId: momiji
+ Text: "椛"
+ - OptionId: satori
+ Text: "觉"
+ - OptionId: koishi
+ Text: "恋"
+ - OptionId: utsuho
+ Text: "空"
+ - OptionId: yuugi
+ Text: "勇仪"
+ - OptionId: rin
+ Text: "燐"
+ - OptionId: reimu
+ Text: "灵梦"
+ - OptionId: sumireko
+ Text: "堇子"
+ - OptionId: kasen
+ Text: "华扇"
+ - OptionId: aunn
+ Text: "阿吽"
+ - OptionId: suika
+ Text: "萃香"
+ - OptionId: none
+ Text: "没有明显失衡英雄"
+ - OptionId: not_enough
+ Text: "玩过或遇到的英雄还不够多"
+ - QuestionId: q18
QuestionType: 2
- Title: "\u54EA\u4E9B\u64CD\u4F5C\u6700\u8BA9\u4F60\u611F\u5230\u8D1F\u62C5\u6216\u70E6\u8E81\uFF1F"
- Hint: "\u53EF\u591A\u9009"
+ Title: "哪些操作最让你感到负担或烦躁?"
+ Hint: "可多选"
Required: 0
MaxSelectCount: 0
Options:
- OptionId: many_units
- Text: "\u4E2D\u540E\u671F\u5355\u4F4D\u592A\u591A\uFF0C\u9010\u4E2A\u70B9\u5F88\u7D2F"
+ Text: "中后期单位太多,逐个点很累"
- OptionId: find_idle_units
- Text: "\u96BE\u4EE5\u627E\u5230\u672A\u884C\u52A8\u5355\u4F4D\u6216\u82F1\u96C4"
+ Text: "难以找到未行动单位或英雄"
- OptionId: misclick
- Text: "\u5BB9\u6613\u8BEF\u70B9\u79FB\u52A8/\u653B\u51FB/\u5360\u9886\u7B49\u4E0D\u53EF\u9006\u64CD\u4F5C"
+ Text: "容易误点移动、攻击、占领等不可逆操作"
- OptionId: pathing_camera
- Text: "\u62D6\u5730\u56FE\u3001\u9009\u4E2D\u5355\u4F4D\u6216\u81EA\u52A8\u955C\u5934\u4E0D\u987A"
+ Text: "拖地图、选中单位或自动镜头不顺"
- OptionId: city_unit_overlap
- Text: "\u57CE\u5E02\u4E0A\u6709\u5355\u4F4D\u65F6\u4EA4\u4E92\u4E0D\u65B9\u4FBF"
+ Text: "城市上有单位时交互不方便"
- OptionId: waiting_turns
- Text: "\u7B49\u5F85AI\u6216\u5176\u4ED6\u73A9\u5BB6\u56DE\u5408\u592A\u4E45"
- - QuestionId: q16
+ Text: "等待AI或其他玩家回合太久"
+ - QuestionId: q19
QuestionType: 2
- Title: "\u4F60\u6700\u5E0C\u671B\u4F18\u5148\u589E\u52A0\u54EA\u4E9B\u51CF\u8D1F\u529F\u80FD\uFF1F"
- Hint: "\u6700\u591A\u90093\u9879"
+ Title: "哪些操作体验你最希望继续优化?"
+ Hint: "最多选3项"
Required: 0
MaxSelectCount: 3
Options:
- - OptionId: next_idle_unit
- Text: "\u4E0B\u4E00\u4E2A\u672A\u884C\u52A8\u5355\u4F4D/\u672A\u5904\u7406\u5355\u4F4D\u63D0\u793A"
- - OptionId: undo_safe_actions
- Text: "\u672A\u63A2\u8DEF/\u672A\u6218\u6597\u65F6\u7684\u64A4\u9500"
- - OptionId: confirmation
- Text: "\u653B\u51FB\u53CB\u519B\u3001\u9996\u90FD\u64CD\u4F5C\u7B49\u4E8C\u6B21\u786E\u8BA4"
- - OptionId: unit_sleep_skip
- Text: "\u4F11\u7720/\u8DF3\u8FC7\u5355\u4F4D\u548C\u81EA\u52A8\u5FAA\u73AF"
+ - OptionId: idle_unit_hint
+ Text: "未行动单位提示仍不够明显"
+ - OptionId: skip_sleep
+ Text: "跳过或休眠单位的操作还不够顺"
+ - OptionId: undo_protection
+ Text: "撤销或误操作保护还不够可靠"
- OptionId: better_tooltips
- Text: "\u66F4\u8BE6\u7EC6\u7684\u6280\u80FD/\u79D1\u6280/\u5730\u5F62\u63D0\u793A"
+ Text: "技能、科技、地形说明不够清楚"
+ - OptionId: combat_prediction
+ Text: "战斗预测和威胁范围不够清楚"
- OptionId: faster_ai
- Text: "\u66F4\u5FEB\u7684AI\u56DE\u5408\u548C\u52A8\u753B\u901F\u5EA6\u9009\u9879"
- - QuestionId: q17
+ Text: "AI、动画、等待节奏仍然偏慢"
+ - OptionId: camera_selection
+ Text: "镜头、拖图、选中单位手感不顺"
+ - QuestionId: q20
QuestionType: 1
- Title: "\u76EE\u524D\u5730\u56FE\u751F\u6210\u548C\u5F00\u5C40\u516C\u5E73\u6027\u5BF9\u4F60\u6765\u8BF4\u5982\u4F55\uFF1F"
+ Title: "目前地图生成和开局公平性对你来说如何?"
Hint: ""
Required: 1
MaxSelectCount: 0
Options:
- OptionId: fair
- Text: "\u5927\u4F53\u516C\u5E73"
+ Text: "大体公平"
- OptionId: sometimes_bad
- Text: "\u5076\u5C14\u6709\u5F88\u5DEE\u7684\u51FA\u751F\u70B9\u6216\u5730\u5F62"
+ Text: "偶尔有很差的出生点或地形"
- OptionId: often_bad
- Text: "\u7ECF\u5E38\u56E0\u5730\u56FE\u5F71\u54CD\u5F88\u5927"
+ Text: "经常因地图影响很大"
- OptionId: not_notice
- Text: "\u6CA1\u6709\u660E\u663E\u611F\u53D7"
- - QuestionId: q18
+ Text: "没有明显感受"
+ - QuestionId: q21
QuestionType: 2
- Title: "\u5982\u679C\u4E0B\u4E2A\u7248\u672C\u4E3B\u8981\u4F18\u5316\u73A9\u6CD5\uFF0C\u4F60\u6700\u5E0C\u671B\u4F18\u5148\u505A\u54EA\u4E9B\uFF1F"
- Hint: "\u6700\u591A\u90093\u9879"
+ Title: "如果下个版本主要优化玩法,你最希望优先做哪些?"
+ Hint: "最多选3项"
Required: 1
MaxSelectCount: 3
Options:
- OptionId: tutorial_rules
- Text: "\u6559\u7A0B\u3001\u56FE\u9274\u548C\u89C4\u5219\u8BF4\u660E"
+ Text: "教程、图鉴和规则说明"
+ - OptionId: localization
+ Text: "翻译质量和多语言支持"
- OptionId: combat_readability
- Text: "\u6218\u6597\u9884\u6D4B\u3001\u4F24\u5BB3\u516C\u5F0F\u548C\u5A01\u80C1\u8303\u56F4"
+ Text: "战斗预测、伤害公式和威胁范围"
- OptionId: faction_balance
- Text: "\u9635\u8425/\u82F1\u96C4/\u5173\u952E\u5355\u4F4D\u5E73\u8861"
+ Text: "阵营、英雄、关键单位平衡"
- OptionId: economy_tech_depth
- Text: "\u7ECF\u6D4E\u3001\u79D1\u6280\u548C\u4E2D\u540E\u671F\u76EE\u6807"
+ Text: "经济、科技和中后期目标"
- OptionId: burden_reduction
- Text: "\u51CF\u5C11\u91CD\u590D\u64CD\u4F5C\u548C\u8BEF\u64CD\u4F5C"
+ Text: "减少重复操作和误操作"
- OptionId: map_generation
- Text: "\u5730\u56FE\u751F\u6210\u548C\u5F00\u5C40\u516C\u5E73"
+ Text: "地图生成和开局公平"
- OptionId: multiplayer_stability
- Text: "\u8054\u673A\u7A33\u5B9A\u6027\u548C\u591A\u4EBA\u4F53\u9A8C"
- - QuestionId: q19
+ Text: "联机稳定性和多人体验"
+ - OptionId: other
+ Text: "其他,请在开放反馈中补充"
+ - QuestionId: q22
QuestionType: 0
- Title: "\u8BF7\u5199\u4E00\u4E2A\u4F60\u89C9\u5F97\u6700\u503C\u5F97\u4FDD\u7559\u6216\u7EE7\u7EED\u5F3A\u5316\u7684\u73A9\u6CD5\u70B9\u3002"
- Hint: "\u6BD4\u5982\u67D0\u4E2A\u9635\u8425\u3001\u82F1\u96C4\u6280\u80FD\u3001\u6218\u6597\u535A\u5F08\u6216\u7ECF\u6D4E\u601D\u8DEF"
- Required: 0
- MaxSelectCount: 0
- Options: []
- - QuestionId: q20
- QuestionType: 0
- Title: "\u8BF7\u5199\u4E00\u4E2A\u6700\u5F71\u54CD\u4F60\u7EE7\u7EED\u73A9\u7684\u73A9\u6CD5\u95EE\u9898\u6216\u8D1F\u62C5\u3002"
- Hint: "\u8D8A\u5177\u4F53\u8D8A\u597D\uFF0C\u6BD4\u5982\u54EA\u4E2A\u9635\u8425/\u82F1\u96C4/\u9636\u6BB5/\u64CD\u4F5C\u8BA9\u4F60\u60F3\u9000\u51FA"
+ Title: "请补充:最值得保留的玩法点,以及最影响你继续玩的玩法问题或负担。"
+ Hint: "可以各写一句。比如某个阵营、英雄技能、战斗博弈、经济思路,或某个让你想退出的阶段、操作、英雄、翻译问题。"
Required: 0
MaxSelectCount: 0
Options: []
diff --git a/Unity/Assets/Scripts/TH1_Logic/Input/InputLogic.cs b/Unity/Assets/Scripts/TH1_Logic/Input/InputLogic.cs
index 4524da992..c3503b175 100644
--- a/Unity/Assets/Scripts/TH1_Logic/Input/InputLogic.cs
+++ b/Unity/Assets/Scripts/TH1_Logic/Input/InputLogic.cs
@@ -145,7 +145,7 @@ namespace Logic
}
var camera = Camera.main;
if (camera == null || Main.MapData?.MapConfig == null || Main.MapData.GridMap == null || Table.Instance == null) return;
- Vector3 mousePosition = camera.ScreenToWorldPoint(Input.mousePosition);
+ Vector3 mousePosition = camera.ScreenToWorldPoint(UIBlockCameraDrag.ClickScreenPosition);
Vector2Int cellPosition = Table.Instance.WorldToGrid(mousePosition);
if (cellPosition.x >= Main.MapData.MapConfig.Width || cellPosition.x < 0 || cellPosition.y < 0 ||
cellPosition.y >= Main.MapData.MapConfig.Height)
@@ -191,7 +191,7 @@ namespace Logic
}
var camera = Camera.main;
if (camera == null || Main.MapData?.MapConfig == null || Main.MapData.GridMap == null || Table.Instance == null) return;
- Vector3 mousePosition = camera.ScreenToWorldPoint(Input.mousePosition);
+ Vector3 mousePosition = camera.ScreenToWorldPoint(UIBlockCameraDrag.ClickScreenPosition);
Vector2Int cellPosition = Table.Instance.WorldToGrid(mousePosition);
if (cellPosition.x >= Main.MapData.MapConfig.Width || cellPosition.x < 0 || cellPosition.y < 0 ||
cellPosition.y >= Main.MapData.MapConfig.Height)
diff --git a/Unity/Assets/Scripts/TH1_UI/UIBlockCameraDrag.cs b/Unity/Assets/Scripts/TH1_UI/UIBlockCameraDrag.cs
index d67d0d6e1..2bbab23a1 100644
--- a/Unity/Assets/Scripts/TH1_UI/UIBlockCameraDrag.cs
+++ b/Unity/Assets/Scripts/TH1_UI/UIBlockCameraDrag.cs
@@ -6,10 +6,14 @@ using UnityEngine.EventSystems;
// 例如:UIMouseBlocker,或者就保持 UIBlockCameraDrag 也可以,只要功能上能理解
public class UIBlockCameraDrag : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler, IPointerDownHandler, IPointerUpHandler
{
+ private const float DragThresholdPixels = 10f;
+
public static bool IsPointerOnUI = false;
public static bool ShouldBlockDrag = false;
public static bool MoveEvent = false;
public static bool DownUpEvent = false;
+ public static Vector3 PointerDownScreenPosition;
+ public static Vector3 ClickScreenPosition;
public static Vector3 DragOrigin;
public static Vector3 MoveVector;
private static UIBlockCameraDrag _activeBlocker;
@@ -25,6 +29,8 @@ public class UIBlockCameraDrag : MonoBehaviour, IPointerEnterHandler, IPointerEx
ShouldBlockDrag = false;
MoveEvent = false;
DownUpEvent = false;
+ PointerDownScreenPosition = Vector3.zero;
+ ClickScreenPosition = Vector3.zero;
DragOrigin = Vector3.zero;
MoveVector = Vector3.zero;
_activeBlocker = null;
@@ -73,6 +79,8 @@ public class UIBlockCameraDrag : MonoBehaviour, IPointerEnterHandler, IPointerEx
_activeBlocker = this;
ShouldBlockDrag = true;
MoveEvent = false;
+ PointerDownScreenPosition = eventData.position;
+ ClickScreenPosition = PointerDownScreenPosition;
DragOrigin = GetMouseWorldPosition();
MoveVector = Vector3.zero;
}
@@ -83,7 +91,10 @@ public class UIBlockCameraDrag : MonoBehaviour, IPointerEnterHandler, IPointerEx
if (eventData.button == PointerEventData.InputButton.Left)
{
if ((_activeBlocker == this || _isPointerInside) && !MoveEvent && !BanClick)
+ {
+ ClickScreenPosition = PointerDownScreenPosition;
DownUpEvent = true;
+ }
IsPointerOnUI = _isPointerInside;
ClearDragState();
@@ -101,10 +112,25 @@ public class UIBlockCameraDrag : MonoBehaviour, IPointerEnterHandler, IPointerEx
{
if (ShouldBlockDrag)
{
- MoveVector = DragOrigin - GetMouseWorldPosition();
- if (MoveVector.magnitude > 0.01f)
+ if (!MoveEvent)
{
+ var screenDelta = (Vector2)Input.mousePosition - (Vector2)PointerDownScreenPosition;
+ if (screenDelta.magnitude <= DragThresholdPixels)
+ {
+ MoveVector = Vector3.zero;
+ return;
+ }
+
MoveEvent = true;
+ DragOrigin = GetMouseWorldPosition();
+ MoveVector = Vector3.zero;
+ return;
+ }
+
+ MoveVector = DragOrigin - GetMouseWorldPosition();
+ if (MoveVector.sqrMagnitude <= Mathf.Epsilon)
+ {
+ MoveVector = Vector3.zero;
}
}
}
diff --git a/Unity/Assets/Scripts/TH1_UI/View/Outside/UIOutsideMenuView.cs b/Unity/Assets/Scripts/TH1_UI/View/Outside/UIOutsideMenuView.cs
index 02093a60d..83e97d314 100644
--- a/Unity/Assets/Scripts/TH1_UI/View/Outside/UIOutsideMenuView.cs
+++ b/Unity/Assets/Scripts/TH1_UI/View/Outside/UIOutsideMenuView.cs
@@ -142,7 +142,7 @@ namespace TH1_UI.View.Outside
StoryButton.onClick.RemoveAllListeners();
StoryButton.onClick.AddListener(OnStoryClicked);
BindTransReportButton();
- HideQuestionnaireButton();
+ BindQuestionnaireButton();
SettingButton.onClick.RemoveAllListeners();
SettingButton.onClick.AddListener(OnSettingClicked);
@@ -282,6 +282,7 @@ namespace TH1_UI.View.Outside
}
if (QuestionnaireButton == null) return;
+ QuestionnaireButton.gameObject.SetActive(true);
RestoreButtonTargetAlpha(QuestionnaireButton);
SetQuestionnaireButtonText();
QuestionnaireButton.onClick.RemoveAllListeners();
diff --git a/Unity/Assets/Scripts/TH1_UI/View/Outside/UIOutsideQuestionnaireOptionMono.cs b/Unity/Assets/Scripts/TH1_UI/View/Outside/UIOutsideQuestionnaireOptionMono.cs
index b21cffcb2..eb4fc4751 100644
--- a/Unity/Assets/Scripts/TH1_UI/View/Outside/UIOutsideQuestionnaireOptionMono.cs
+++ b/Unity/Assets/Scripts/TH1_UI/View/Outside/UIOutsideQuestionnaireOptionMono.cs
@@ -39,6 +39,14 @@ namespace TH1_UI.View.Outside
Toggle?.SetIsOnWithoutNotify(value);
}
+ public void SetInteractable(bool value)
+ {
+ if (Toggle != null)
+ {
+ Toggle.interactable = value;
+ }
+ }
+
private void OnToggleChanged(bool value)
{
_onValueChanged?.Invoke(this, value);
diff --git a/Unity/Assets/Scripts/TH1_UI/View/Outside/UIOutsideQuestionnaireQuestionMono.cs b/Unity/Assets/Scripts/TH1_UI/View/Outside/UIOutsideQuestionnaireQuestionMono.cs
index 7506b40a4..fe081ef9b 100644
--- a/Unity/Assets/Scripts/TH1_UI/View/Outside/UIOutsideQuestionnaireQuestionMono.cs
+++ b/Unity/Assets/Scripts/TH1_UI/View/Outside/UIOutsideQuestionnaireQuestionMono.cs
@@ -103,6 +103,19 @@ namespace TH1_UI.View.Outside
_onAnswerChanged?.Invoke();
}
+ public void SetReadOnly(bool readOnly)
+ {
+ if (OpenInput != null)
+ {
+ OpenInput.interactable = !readOnly;
+ }
+
+ foreach (var item in _optionItems)
+ {
+ item.SetInteractable(!readOnly);
+ }
+ }
+
public bool HasAnswer()
{
if (QuestionInfo == null) return false;
diff --git a/Unity/Assets/Scripts/TH1_UI/View/Outside/UIOutsideQuestionnaireView.cs b/Unity/Assets/Scripts/TH1_UI/View/Outside/UIOutsideQuestionnaireView.cs
index 5a218055e..a9e98c7f3 100644
--- a/Unity/Assets/Scripts/TH1_UI/View/Outside/UIOutsideQuestionnaireView.cs
+++ b/Unity/Assets/Scripts/TH1_UI/View/Outside/UIOutsideQuestionnaireView.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.Linq;
using Logic.CrashSight;
@@ -37,10 +38,25 @@ namespace TH1_UI.View.Outside
public ViDelegateAssisstant.Dele OnBtnCloseClick;
private readonly List _questionItems = new List();
+ private readonly List _listItems = new List();
+
private QuestionnaireDataAssets _dataAssets;
private QuestionnaireInfo _questionnaireInfo;
private QuestionnaireAnswerSheet _currentSheet;
private bool _isUploading;
+ private bool _layoutBuilt;
+ private bool _showExpired;
+
+ private Transform _sidebarContent;
+ private Transform _rightBody;
+ private Transform _rightFooter;
+ private TextMeshProUGUI _expiredToggleText;
+ private TextMeshProUGUI _rightHeaderTitle;
+ private TextMeshProUGUI _rightHeaderSubtitle;
+ private Button _recordsButton;
+ private Button _startButton;
+ private TextMeshProUGUI _recordsButtonText;
+ private TextMeshProUGUI _startButtonText;
protected override void OnInit()
{
@@ -53,46 +69,25 @@ namespace TH1_UI.View.Outside
EnsureLayout();
LoadAssetsIfNeeded();
- _questionnaireInfo = null;
- if (_dataAssets != null)
- {
- if (!string.IsNullOrEmpty(evt.QuestionnaireId))
- {
- _dataAssets.GetQuestionnaireInfo(evt.QuestionnaireId, out _questionnaireInfo);
- }
-
- _questionnaireInfo ??= _dataAssets.GetDefaultQuestionnaire();
- }
-
- if (_questionnaireInfo == null)
- {
- Debug.LogError("[UIOutsideQuestionnaireView] QuestionnaireDataAssets is missing questionnaire data.");
- SetStatus(string.Empty);
- return;
- }
-
- TitleText.text = ResolveText(_questionnaireInfo.Title);
- DescriptionText.text = ResolveText(_questionnaireInfo.Description);
- SubmitButtonText.text = ResolveText(_questionnaireInfo.SubmitButtonText);
- RefillButtonText.text = ResolveText(_questionnaireInfo.ResubmitButtonText);
- CloseButtonText.text = ResolveText(_questionnaireInfo.CloseButtonText);
-
- SubmitButton.onClick.RemoveAllListeners();
- SubmitButton.onClick.AddListener(OnSubmitClicked);
- RefillButton.onClick.RemoveAllListeners();
- RefillButton.onClick.AddListener(OnRefillClicked);
CloseButton.onClick.RemoveAllListeners();
CloseButton.onClick.AddListener(OnCloseClicked);
SetUploading(false);
- BuildQuestions();
- _currentSheet = QuestionnaireAnswerStore.Instance.GetAnswerSheet(_questionnaireInfo.QuestionnaireId);
- ApplyPreviousSheet(_currentSheet);
+ var selected = ResolveInitialQuestionnaire(evt);
+ if (selected != null && selected.GetEffectiveStatus(DateTime.UtcNow) == QuestionnaireStatus.Expired)
+ {
+ _showExpired = true;
+ }
- var hasSubmitted = _currentSheet != null;
- RefillButton.gameObject.SetActive(hasSubmitted);
- var submittedMessage = ResolveSubmittedMessage(_currentSheet);
- SetStatus(hasSubmitted ? submittedMessage : string.Empty);
+ BuildQuestionnaireList();
+
+ if (selected == null)
+ {
+ ShowEmptyState("暂无可填写问卷");
+ return;
+ }
+
+ SelectQuestionnaire(selected);
}
public void OnCloseView()
@@ -110,7 +105,8 @@ namespace TH1_UI.View.Outside
return raw;
}
- public static TextMeshProUGUI CreateText(Transform parent, string objectName, string text, float fontSize, Color color, TextAlignmentOptions alignment)
+ public static TextMeshProUGUI CreateText(Transform parent, string objectName, string text, float fontSize,
+ Color color, TextAlignmentOptions alignment)
{
var go = new GameObject(objectName, typeof(RectTransform), typeof(TextMeshProUGUI));
go.transform.SetParent(parent, false);
@@ -142,23 +138,425 @@ namespace TH1_UI.View.Outside
}
}
- private void BuildQuestions()
+ private QuestionnaireInfo ResolveInitialQuestionnaire(ShowUIOutsideQuestionnaire evt)
{
- foreach (Transform child in QuestionListContent)
+ if (_dataAssets == null)
{
- Destroy(child.gameObject);
+ Debug.LogError("[UIOutsideQuestionnaireView] QuestionnaireDataAssets is missing.");
+ return null;
}
- _questionItems.Clear();
- if (_questionnaireInfo.Questions == null) return;
-
- for (var i = 0; i < _questionnaireInfo.Questions.Count; i++)
+ QuestionnaireInfo selected = null;
+ if (!string.IsNullOrEmpty(evt.QuestionnaireId))
{
- var question = _questionnaireInfo.Questions[i];
+ _dataAssets.GetQuestionnaireInfo(evt.QuestionnaireId, out selected);
+ }
+
+ selected ??= _dataAssets.GetDefaultQuestionnaire();
+ if (selected != null && selected.IsVisibleAt(DateTime.UtcNow)) return selected;
+
+ return GetVisibleQuestionnaires().FirstOrDefault();
+ }
+
+ private List GetVisibleQuestionnaires()
+ {
+ return _dataAssets != null
+ ? _dataAssets.GetVisibleQuestionnaires(DateTime.UtcNow)
+ : new List();
+ }
+
+ private void SelectQuestionnaire(QuestionnaireInfo info)
+ {
+ if (info == null)
+ {
+ ShowEmptyState("暂无可填写问卷");
+ return;
+ }
+
+ _questionnaireInfo = info;
+ _currentSheet = QuestionnaireAnswerStore.Instance.GetLatestAnswerSheet(info.QuestionnaireId);
+ if (info.GetEffectiveStatus(DateTime.UtcNow) == QuestionnaireStatus.Expired && !_showExpired)
+ {
+ _showExpired = true;
+ BuildQuestionnaireList();
+ }
+
+ UpdateListSelection();
+ ShowDetail();
+ }
+
+ private void BuildQuestionnaireList()
+ {
+ if (_sidebarContent == null) return;
+
+ DestroyChildren(_sidebarContent);
+ _listItems.Clear();
+
+ var items = GetVisibleQuestionnaires();
+ var featured = items.Where(item => item.GetEffectiveStatus(DateTime.UtcNow) == QuestionnaireStatus.Featured).ToList();
+ var longTerm = items.Where(item => item.GetEffectiveStatus(DateTime.UtcNow) == QuestionnaireStatus.LongTerm).ToList();
+ var expired = items.Where(item => item.GetEffectiveStatus(DateTime.UtcNow) == QuestionnaireStatus.Expired).ToList();
+
+ CreateSectionHeader(_sidebarContent, "当期问卷");
+ CreateQuestionnaireListItems(featured, "暂无当期问卷");
+ CreateDivider(_sidebarContent);
+
+ CreateSectionHeader(_sidebarContent, "长期问卷");
+ CreateQuestionnaireListItems(longTerm, "暂无长期问卷");
+ CreateDivider(_sidebarContent);
+
+ CreateExpiredHeader(expired.Count);
+ if (_showExpired)
+ {
+ CreateQuestionnaireListItems(expired, "暂无往期问卷");
+ }
+ else if (expired.Count > 0)
+ {
+ var hint = CreateText(_sidebarContent, "ExpiredHiddenHint", $"{expired.Count} 份往期问卷已收起", 17f,
+ new Color(0.62f, 0.66f, 0.64f, 1f), TextAlignmentOptions.Left);
+ hint.enableWordWrapping = true;
+ }
+
+ if (items.Count == 0)
+ {
+ var empty = CreateText(_sidebarContent, "EmptyList", "暂无可显示问卷", 18f,
+ new Color(0.74f, 0.76f, 0.72f, 1f), TextAlignmentOptions.Left);
+ empty.enableWordWrapping = true;
+ }
+ }
+
+ private void CreateQuestionnaireListItems(List infos, string emptyText)
+ {
+ if (infos == null || infos.Count == 0)
+ {
+ var empty = CreateText(_sidebarContent, "Empty", emptyText, 17f,
+ new Color(0.62f, 0.66f, 0.64f, 1f), TextAlignmentOptions.Left);
+ empty.enableWordWrapping = true;
+ return;
+ }
+
+ foreach (var info in infos)
+ {
+ CreateQuestionnaireListItem(info);
+ }
+ }
+
+ private void CreateQuestionnaireListItem(QuestionnaireInfo info)
+ {
+ var row = new GameObject("QuestionnaireItem", typeof(RectTransform), typeof(Image), typeof(Button),
+ typeof(VerticalLayoutGroup), typeof(LayoutElement));
+ row.transform.SetParent(_sidebarContent, false);
+
+ var image = row.GetComponent();
+ image.color = new Color(1f, 1f, 1f, 0.055f);
+
+ var layout = row.GetComponent();
+ layout.padding = new RectOffset(12, 12, 10, 10);
+ layout.spacing = 4f;
+ layout.childControlWidth = true;
+ layout.childControlHeight = true;
+ layout.childForceExpandWidth = true;
+ layout.childForceExpandHeight = false;
+
+ var layoutElement = row.GetComponent();
+ layoutElement.minHeight = 76f;
+ layoutElement.preferredHeight = 82f;
+
+ var button = row.GetComponent