From 3de3f1a2e20491f59f95adda4db1c3b3ba0c86c8 Mon Sep 17 00:00:00 2001 From: Ding Yuntian <1491671119@qq.com> Date: Tue, 28 Jul 2026 17:15:03 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0=E5=8C=96=E8=AF=AD=E8=A8=80=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Locales/Portuguese (Brazil) (pt-BR).asset | 25 +++++++++++++++++++ .../Portuguese (Brazil) (pt-BR).asset.meta | 8 ++++++ Assets/Language/Locales/Russian (ru).asset | 25 +++++++++++++++++++ .../Language/Locales/Russian (ru).asset.meta | 8 ++++++ Assets/Language/Locales/Spanish (es).asset | 25 +++++++++++++++++++ .../Language/Locales/Spanish (es).asset.meta | 8 ++++++ .../FP_Day1_begin/FP_Day1_begin.yarnproject | 7 ++++-- .../FP/FP_Day1_mid/FP_Day1_mid.yarnproject | 7 ++++-- .../FP_Day1_night/FP_Day1_night.yarnproject | 9 ++++--- .../FP_Day1_sleep/FP_Day1_sleep.yarnproject | 7 ++++-- .../FP_Day2_begin/FP_Day2_begin.yarnproject | 10 ++++++-- .../FP/FP_Day2_mid/FP_Day2_mid.yarnproject | 10 ++++++-- .../FP_Day2_night/FP_Day2_night.yarnproject | 10 ++++++-- .../FP_Day2_sleep/FP_Day2_sleep.yarnproject | 10 ++++++-- .../FP/FP_Day3_mid/FP_Day3_mid.yarnproject | 10 ++++++-- .../FP_Day3_night/FP_Day3_night.yarnproject | 10 ++++++-- .../FP/FP_Huoshan1/FP_Huoshan1.yarnproject | 10 ++++++-- .../Yarn/FP/FP_Peipei1/FP_Peipei1.yarnproject | 7 ++++-- .../Yarn/FP/FP_Peipei2/FP_Peipei2.yarnproject | 10 ++++++-- .../Yarn/FP/FP_Peipei3/FP_Peipei3.yarnproject | 10 ++++++-- .../FP/FP_Prologue/FP_Prologue.yarnproject | 7 ++++-- .../Yarn/FP/FP_Shitou1/FP_Shitou1.yarnproject | 12 ++++++--- .../Yarn/FP/FP_Shitou2/FP_Shitou2.yarnproject | 12 ++++++--- 23 files changed, 220 insertions(+), 37 deletions(-) create mode 100644 Assets/Language/Locales/Portuguese (Brazil) (pt-BR).asset create mode 100644 Assets/Language/Locales/Portuguese (Brazil) (pt-BR).asset.meta create mode 100644 Assets/Language/Locales/Russian (ru).asset create mode 100644 Assets/Language/Locales/Russian (ru).asset.meta create mode 100644 Assets/Language/Locales/Spanish (es).asset create mode 100644 Assets/Language/Locales/Spanish (es).asset.meta diff --git a/Assets/Language/Locales/Portuguese (Brazil) (pt-BR).asset b/Assets/Language/Locales/Portuguese (Brazil) (pt-BR).asset new file mode 100644 index 000000000..789b70372 --- /dev/null +++ b/Assets/Language/Locales/Portuguese (Brazil) (pt-BR).asset @@ -0,0 +1,25 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 1bb1838fe8befb0429646b938e757ff3, type: 3} + m_Name: Portuguese (Brazil) (pt-BR) + m_EditorClassIdentifier: + m_Identifier: + m_Code: pt-BR + m_Metadata: + m_Items: [] + m_LocaleName: Portuguese (Brazil) (pt-BR) + m_CustomFormatCultureCode: + m_UseCustomFormatter: 0 + m_SortOrder: 10000 + references: + version: 2 + RefIds: [] diff --git a/Assets/Language/Locales/Portuguese (Brazil) (pt-BR).asset.meta b/Assets/Language/Locales/Portuguese (Brazil) (pt-BR).asset.meta new file mode 100644 index 000000000..fec4a9603 --- /dev/null +++ b/Assets/Language/Locales/Portuguese (Brazil) (pt-BR).asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d0c46f4777ac92249a43143a15892fd2 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Language/Locales/Russian (ru).asset b/Assets/Language/Locales/Russian (ru).asset new file mode 100644 index 000000000..be0f031a0 --- /dev/null +++ b/Assets/Language/Locales/Russian (ru).asset @@ -0,0 +1,25 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 1bb1838fe8befb0429646b938e757ff3, type: 3} + m_Name: Russian (ru) + m_EditorClassIdentifier: + m_Identifier: + m_Code: ru + m_Metadata: + m_Items: [] + m_LocaleName: Russian (ru) + m_CustomFormatCultureCode: + m_UseCustomFormatter: 0 + m_SortOrder: 10000 + references: + version: 2 + RefIds: [] diff --git a/Assets/Language/Locales/Russian (ru).asset.meta b/Assets/Language/Locales/Russian (ru).asset.meta new file mode 100644 index 000000000..6edff392f --- /dev/null +++ b/Assets/Language/Locales/Russian (ru).asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9b1581a48f0ca11448dbddaf79c11252 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Language/Locales/Spanish (es).asset b/Assets/Language/Locales/Spanish (es).asset new file mode 100644 index 000000000..9dcc250c0 --- /dev/null +++ b/Assets/Language/Locales/Spanish (es).asset @@ -0,0 +1,25 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 1bb1838fe8befb0429646b938e757ff3, type: 3} + m_Name: Spanish (es) + m_EditorClassIdentifier: + m_Identifier: + m_Code: es + m_Metadata: + m_Items: [] + m_LocaleName: Spanish (es) + m_CustomFormatCultureCode: + m_UseCustomFormatter: 0 + m_SortOrder: 10000 + references: + version: 2 + RefIds: [] diff --git a/Assets/Language/Locales/Spanish (es).asset.meta b/Assets/Language/Locales/Spanish (es).asset.meta new file mode 100644 index 000000000..326e03433 --- /dev/null +++ b/Assets/Language/Locales/Spanish (es).asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3cadb0d5f8bf5e64599f52e139f7e3cc +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Yarn/FP/FP_Day1_begin/FP_Day1_begin.yarnproject b/Assets/Yarn/FP/FP_Day1_begin/FP_Day1_begin.yarnproject index 3cee600c4..3498884f1 100644 --- a/Assets/Yarn/FP/FP_Day1_begin/FP_Day1_begin.yarnproject +++ b/Assets/Yarn/FP/FP_Day1_begin/FP_Day1_begin.yarnproject @@ -12,8 +12,11 @@ }, "ja-JP": { "strings": "FP_Day1_begin-ja.csv" - } + }, + "ru": {}, + "pt-BR": {}, + "es": {} }, "baseLanguage": "zh-Hans", "compilerOptions": {} -} \ No newline at end of file +} diff --git a/Assets/Yarn/FP/FP_Day1_mid/FP_Day1_mid.yarnproject b/Assets/Yarn/FP/FP_Day1_mid/FP_Day1_mid.yarnproject index f037d3a69..bdb191ca7 100644 --- a/Assets/Yarn/FP/FP_Day1_mid/FP_Day1_mid.yarnproject +++ b/Assets/Yarn/FP/FP_Day1_mid/FP_Day1_mid.yarnproject @@ -12,8 +12,11 @@ }, "ja-JP": { "strings": "FP_Day1_mid-ja.csv" - } + }, + "ru": {}, + "pt-BR": {}, + "es": {} }, "baseLanguage": "zh-Hans", "compilerOptions": {} -} \ No newline at end of file +} diff --git a/Assets/Yarn/FP/FP_Day1_night/FP_Day1_night.yarnproject b/Assets/Yarn/FP/FP_Day1_night/FP_Day1_night.yarnproject index e68f15bad..89db6b075 100644 --- a/Assets/Yarn/FP/FP_Day1_night/FP_Day1_night.yarnproject +++ b/Assets/Yarn/FP/FP_Day1_night/FP_Day1_night.yarnproject @@ -10,10 +10,13 @@ "en": { "strings": "FP_Day1_night-en.csv" }, - "ja": { + "ja-JP": { "strings": "FP_Day1_night-ja.csv" - } + }, + "ru": {}, + "pt-BR": {}, + "es": {} }, "baseLanguage": "zh-Hans", "compilerOptions": {} -} \ No newline at end of file +} diff --git a/Assets/Yarn/FP/FP_Day1_sleep/FP_Day1_sleep.yarnproject b/Assets/Yarn/FP/FP_Day1_sleep/FP_Day1_sleep.yarnproject index 64df55bf3..f40e0ee79 100644 --- a/Assets/Yarn/FP/FP_Day1_sleep/FP_Day1_sleep.yarnproject +++ b/Assets/Yarn/FP/FP_Day1_sleep/FP_Day1_sleep.yarnproject @@ -12,8 +12,11 @@ }, "ja-JP": { "strings": "FP_Day1_sleep-ja.csv" - } + }, + "ru": {}, + "pt-BR": {}, + "es": {} }, "baseLanguage": "zh-Hans", "compilerOptions": {} -} \ No newline at end of file +} diff --git a/Assets/Yarn/FP/FP_Day2_begin/FP_Day2_begin.yarnproject b/Assets/Yarn/FP/FP_Day2_begin/FP_Day2_begin.yarnproject index 0de6e1d44..e7be334a0 100644 --- a/Assets/Yarn/FP/FP_Day2_begin/FP_Day2_begin.yarnproject +++ b/Assets/Yarn/FP/FP_Day2_begin/FP_Day2_begin.yarnproject @@ -6,7 +6,13 @@ "excludeFiles": [ "**/*~/*" ], - "localisation": {}, + "localisation": { + "en": {}, + "ja-JP": {}, + "ru": {}, + "pt-BR": {}, + "es": {} + }, "baseLanguage": "zh-Hans", "compilerOptions": {} -} \ No newline at end of file +} diff --git a/Assets/Yarn/FP/FP_Day2_mid/FP_Day2_mid.yarnproject b/Assets/Yarn/FP/FP_Day2_mid/FP_Day2_mid.yarnproject index 0de6e1d44..e7be334a0 100644 --- a/Assets/Yarn/FP/FP_Day2_mid/FP_Day2_mid.yarnproject +++ b/Assets/Yarn/FP/FP_Day2_mid/FP_Day2_mid.yarnproject @@ -6,7 +6,13 @@ "excludeFiles": [ "**/*~/*" ], - "localisation": {}, + "localisation": { + "en": {}, + "ja-JP": {}, + "ru": {}, + "pt-BR": {}, + "es": {} + }, "baseLanguage": "zh-Hans", "compilerOptions": {} -} \ No newline at end of file +} diff --git a/Assets/Yarn/FP/FP_Day2_night/FP_Day2_night.yarnproject b/Assets/Yarn/FP/FP_Day2_night/FP_Day2_night.yarnproject index 1b27abdbd..ccd9a3176 100644 --- a/Assets/Yarn/FP/FP_Day2_night/FP_Day2_night.yarnproject +++ b/Assets/Yarn/FP/FP_Day2_night/FP_Day2_night.yarnproject @@ -6,7 +6,13 @@ "excludeFiles": [ "**/*~/*" ], - "localisation": {}, + "localisation": { + "en": {}, + "ja-JP": {}, + "ru": {}, + "pt-BR": {}, + "es": {} + }, "baseLanguage": "zh-Hans", "compilerOptions": {} -} \ No newline at end of file +} diff --git a/Assets/Yarn/FP/FP_Day2_sleep/FP_Day2_sleep.yarnproject b/Assets/Yarn/FP/FP_Day2_sleep/FP_Day2_sleep.yarnproject index ed99bb63e..38f6182aa 100644 --- a/Assets/Yarn/FP/FP_Day2_sleep/FP_Day2_sleep.yarnproject +++ b/Assets/Yarn/FP/FP_Day2_sleep/FP_Day2_sleep.yarnproject @@ -6,7 +6,13 @@ "excludeFiles": [ "**/*~/*" ], - "localisation": {}, + "localisation": { + "en": {}, + "ja-JP": {}, + "ru": {}, + "pt-BR": {}, + "es": {} + }, "baseLanguage": "zh-Hans", "compilerOptions": {} -} \ No newline at end of file +} diff --git a/Assets/Yarn/FP/FP_Day3_mid/FP_Day3_mid.yarnproject b/Assets/Yarn/FP/FP_Day3_mid/FP_Day3_mid.yarnproject index 1b27abdbd..ccd9a3176 100644 --- a/Assets/Yarn/FP/FP_Day3_mid/FP_Day3_mid.yarnproject +++ b/Assets/Yarn/FP/FP_Day3_mid/FP_Day3_mid.yarnproject @@ -6,7 +6,13 @@ "excludeFiles": [ "**/*~/*" ], - "localisation": {}, + "localisation": { + "en": {}, + "ja-JP": {}, + "ru": {}, + "pt-BR": {}, + "es": {} + }, "baseLanguage": "zh-Hans", "compilerOptions": {} -} \ No newline at end of file +} diff --git a/Assets/Yarn/FP/FP_Day3_night/FP_Day3_night.yarnproject b/Assets/Yarn/FP/FP_Day3_night/FP_Day3_night.yarnproject index 1b27abdbd..ccd9a3176 100644 --- a/Assets/Yarn/FP/FP_Day3_night/FP_Day3_night.yarnproject +++ b/Assets/Yarn/FP/FP_Day3_night/FP_Day3_night.yarnproject @@ -6,7 +6,13 @@ "excludeFiles": [ "**/*~/*" ], - "localisation": {}, + "localisation": { + "en": {}, + "ja-JP": {}, + "ru": {}, + "pt-BR": {}, + "es": {} + }, "baseLanguage": "zh-Hans", "compilerOptions": {} -} \ No newline at end of file +} diff --git a/Assets/Yarn/FP/FP_Huoshan1/FP_Huoshan1.yarnproject b/Assets/Yarn/FP/FP_Huoshan1/FP_Huoshan1.yarnproject index 3ab20a4f2..9f8cbc668 100644 --- a/Assets/Yarn/FP/FP_Huoshan1/FP_Huoshan1.yarnproject +++ b/Assets/Yarn/FP/FP_Huoshan1/FP_Huoshan1.yarnproject @@ -7,7 +7,13 @@ "**/*~/*", "./Samples/Yarn Spinner*/*" ], - "localisation": {}, + "localisation": { + "en": {}, + "ja-JP": {}, + "ru": {}, + "pt-BR": {}, + "es": {} + }, "baseLanguage": "zh-Hans", "compilerOptions": {} -} \ No newline at end of file +} diff --git a/Assets/Yarn/FP/FP_Peipei1/FP_Peipei1.yarnproject b/Assets/Yarn/FP/FP_Peipei1/FP_Peipei1.yarnproject index 94d755571..2db5d3327 100644 --- a/Assets/Yarn/FP/FP_Peipei1/FP_Peipei1.yarnproject +++ b/Assets/Yarn/FP/FP_Peipei1/FP_Peipei1.yarnproject @@ -13,8 +13,11 @@ }, "ja-JP": { "strings": "FP_Peipei1-ja.csv" - } + }, + "ru": {}, + "pt-BR": {}, + "es": {} }, "baseLanguage": "zh-Hans", "compilerOptions": {} -} \ No newline at end of file +} diff --git a/Assets/Yarn/FP/FP_Peipei2/FP_Peipei2.yarnproject b/Assets/Yarn/FP/FP_Peipei2/FP_Peipei2.yarnproject index 1b27abdbd..ccd9a3176 100644 --- a/Assets/Yarn/FP/FP_Peipei2/FP_Peipei2.yarnproject +++ b/Assets/Yarn/FP/FP_Peipei2/FP_Peipei2.yarnproject @@ -6,7 +6,13 @@ "excludeFiles": [ "**/*~/*" ], - "localisation": {}, + "localisation": { + "en": {}, + "ja-JP": {}, + "ru": {}, + "pt-BR": {}, + "es": {} + }, "baseLanguage": "zh-Hans", "compilerOptions": {} -} \ No newline at end of file +} diff --git a/Assets/Yarn/FP/FP_Peipei3/FP_Peipei3.yarnproject b/Assets/Yarn/FP/FP_Peipei3/FP_Peipei3.yarnproject index 1b27abdbd..ccd9a3176 100644 --- a/Assets/Yarn/FP/FP_Peipei3/FP_Peipei3.yarnproject +++ b/Assets/Yarn/FP/FP_Peipei3/FP_Peipei3.yarnproject @@ -6,7 +6,13 @@ "excludeFiles": [ "**/*~/*" ], - "localisation": {}, + "localisation": { + "en": {}, + "ja-JP": {}, + "ru": {}, + "pt-BR": {}, + "es": {} + }, "baseLanguage": "zh-Hans", "compilerOptions": {} -} \ No newline at end of file +} diff --git a/Assets/Yarn/FP/FP_Prologue/FP_Prologue.yarnproject b/Assets/Yarn/FP/FP_Prologue/FP_Prologue.yarnproject index 4f48af953..6acfe5e9e 100644 --- a/Assets/Yarn/FP/FP_Prologue/FP_Prologue.yarnproject +++ b/Assets/Yarn/FP/FP_Prologue/FP_Prologue.yarnproject @@ -12,8 +12,11 @@ }, "ja-JP": { "strings": "FP_Prologue-ja.csv" - } + }, + "ru": {}, + "pt-BR": {}, + "es": {} }, "baseLanguage": "zh-Hans", "compilerOptions": {} -} \ No newline at end of file +} diff --git a/Assets/Yarn/FP/FP_Shitou1/FP_Shitou1.yarnproject b/Assets/Yarn/FP/FP_Shitou1/FP_Shitou1.yarnproject index c03630120..cf0ae3a8f 100644 --- a/Assets/Yarn/FP/FP_Shitou1/FP_Shitou1.yarnproject +++ b/Assets/Yarn/FP/FP_Shitou1/FP_Shitou1.yarnproject @@ -6,7 +6,13 @@ "excludeFiles": [ "**/*~/*" ], - "localisation": {}, - "baseLanguage": "zh-Hant", + "localisation": { + "en": {}, + "ja-JP": {}, + "ru": {}, + "pt-BR": {}, + "es": {} + }, + "baseLanguage": "zh-Hans", "compilerOptions": {} -} \ No newline at end of file +} diff --git a/Assets/Yarn/FP/FP_Shitou2/FP_Shitou2.yarnproject b/Assets/Yarn/FP/FP_Shitou2/FP_Shitou2.yarnproject index c03630120..cf0ae3a8f 100644 --- a/Assets/Yarn/FP/FP_Shitou2/FP_Shitou2.yarnproject +++ b/Assets/Yarn/FP/FP_Shitou2/FP_Shitou2.yarnproject @@ -6,7 +6,13 @@ "excludeFiles": [ "**/*~/*" ], - "localisation": {}, - "baseLanguage": "zh-Hant", + "localisation": { + "en": {}, + "ja-JP": {}, + "ru": {}, + "pt-BR": {}, + "es": {} + }, + "baseLanguage": "zh-Hans", "compilerOptions": {} -} \ No newline at end of file +} From ca5b714124bd5e105d4a437239322f9273e1d1ca Mon Sep 17 00:00:00 2001 From: Ding Yuntian <1491671119@qq.com> Date: Tue, 28 Jul 2026 19:13:53 +0800 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=E7=81=AB=E5=B1=B1=E7=8E=A9?= =?UTF-8?q?=E6=B3=95=E6=9C=AC=E5=9C=B0=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 仍有硬编码残留,待处理 --- .../AddressableAssetSettings.asset | 8 +- .../AssetGroups/Localization-Locales.asset | 18 + ...g-Tables-Portuguese (Brazil) (pt-BR).asset | 31 ++ ...es-Portuguese (Brazil) (pt-BR).asset.meta} | 5 +- ...alization-String-Tables-Russian (ru).asset | 31 ++ ...tion-String-Tables-Russian (ru).asset.meta | 8 + ...alization-String-Tables-Spanish (es).asset | 31 ++ ...tion-String-Tables-Spanish (es).asset.meta | 8 + ...zil) (pt-BR)_BundledAssetGroupSchema.asset | 45 ++ ...(pt-BR)_BundledAssetGroupSchema.asset.meta | 8 + ...il) (pt-BR)_ContentUpdateGroupSchema.asset | 16 + ...pt-BR)_ContentUpdateGroupSchema.asset.meta | 8 + ...Russian (ru)_BundledAssetGroupSchema.asset | 45 ++ ...an (ru)_BundledAssetGroupSchema.asset.meta | 8 + ...ussian (ru)_ContentUpdateGroupSchema.asset | 16 + ...n (ru)_ContentUpdateGroupSchema.asset.meta | 8 + ...Spanish (es)_BundledAssetGroupSchema.asset | 45 ++ ...sh (es)_BundledAssetGroupSchema.asset.meta | 8 + ...panish (es)_ContentUpdateGroupSchema.asset | 16 + ...h (es)_ContentUpdateGroupSchema.asset.meta | 8 + .../Huoshan/ExpressionLocalizationTests.cs | 249 ++++++++++ .../ExpressionLocalizationTests.cs.meta} | 2 +- .../ExpressionLocalizationValidator.cs | 292 ++++++++++++ .../ExpressionLocalizationValidator.cs.meta | 11 + .../Expression/ExpressionContentCatalog.asset | 28 ++ .../ExpressionContentCatalog.asset.meta | 8 + .../Prefabs/PunchTapeForMem.prefab | 4 +- .../Prefabs/PunchTapeForPrinter.prefab | 4 +- Assets/Language/ChapterInfo/ChapterInfo.asset | 3 + .../Language/ChapterInfo/ChapterInfo_es.asset | 23 + .../ChapterInfo/ChapterInfo_es.asset.meta | 8 + .../ChapterInfo/ChapterInfo_pt-BR.asset | 23 + .../ChapterInfo/ChapterInfo_pt-BR.asset.meta | 8 + .../Language/ChapterInfo/ChapterInfo_ru.asset | 23 + .../ChapterInfo/ChapterInfo_ru.asset.meta | 8 + .../Language/Params/Params Shared Data.asset | 112 +++++ Assets/Language/Params/Params_en.asset | 112 +++++ Assets/Language/Params/Params_es.asset | 23 + Assets/Language/Params/Params_es.asset.meta | 8 + Assets/Language/Params/Params_ja-JP.asset | 112 +++++ Assets/Language/Params/Params_pt-BR.asset | 23 + .../Language/Params/Params_pt-BR.asset.meta | 8 + Assets/Language/Params/Params_ru.asset | 23 + Assets/Language/Params/Params_ru.asset.meta | 8 + Assets/Language/Params/Params_zh-Hans.asset | 112 +++++ Assets/Language/UIText/UIText_en.asset | 8 +- Assets/Language/UIText/UIText_es.asset | 23 + Assets/Language/UIText/UIText_es.asset.meta | 8 + Assets/Language/UIText/UIText_ja-JP.asset | 8 +- Assets/Language/UIText/UIText_pt-BR.asset | 23 + .../Language/UIText/UIText_pt-BR.asset.meta | 8 + Assets/Language/UIText/UIText_ru.asset | 23 + Assets/Language/UIText/UIText_ru.asset.meta | 8 + Assets/Scenes/HuoShanFixScene.unity | 1 + .../AibisDream.Menus/AibisAssetMenus.cs | 1 + Assets/Scripts/AssetRefs/ConstRef.cs | 1 + .../Dialog System/AibisLineProvider.cs | 13 +- .../FixSystem/PunchTape/PunchTapeItem.cs | 20 +- Assets/Scripts/Framework/Config/ConfigUtil.cs | 25 +- Assets/Scripts/Framework/Config/Param.cs | 27 -- .../Framework/OtherKit/LocalizationKit.cs | 84 +++- .../Language/ExpressionContentCatalog.cs | 130 +++++ .../Language/ExpressionContentCatalog.cs.meta | 11 + .../HuoShan/Language/ExpressionManager.cs | 16 + .../Language/ExpressionTextTokenizer.cs | 108 +++++ .../Language/ExpressionTextTokenizer.cs.meta | 11 + .../Language/LanguageParticleManager.cs | 103 ++-- .../HuoShan/Language/LanguageYarnCommand.cs | 409 ++++++++++++++-- .../LogReleasePresentationController.cs | 49 +- Assets/StreamingAssets/Config/params.csv | 9 - Assets/Yarn/FP/FP_Huoshan1/Stage6.yarn | 78 +-- Docs/HuoShan/HuoshanStage6LocalizationPlan.md | 449 +++--------------- 72 files changed, 2634 insertions(+), 626 deletions(-) create mode 100644 Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Portuguese (Brazil) (pt-BR).asset rename Assets/{StreamingAssets/Config/params.csv.meta => AddressableAssetsData/AssetGroups/Localization-String-Tables-Portuguese (Brazil) (pt-BR).asset.meta} (52%) create mode 100644 Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Russian (ru).asset create mode 100644 Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Russian (ru).asset.meta create mode 100644 Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Spanish (es).asset create mode 100644 Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Spanish (es).asset.meta create mode 100644 Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Portuguese (Brazil) (pt-BR)_BundledAssetGroupSchema.asset create mode 100644 Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Portuguese (Brazil) (pt-BR)_BundledAssetGroupSchema.asset.meta create mode 100644 Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Portuguese (Brazil) (pt-BR)_ContentUpdateGroupSchema.asset create mode 100644 Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Portuguese (Brazil) (pt-BR)_ContentUpdateGroupSchema.asset.meta create mode 100644 Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Russian (ru)_BundledAssetGroupSchema.asset create mode 100644 Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Russian (ru)_BundledAssetGroupSchema.asset.meta create mode 100644 Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Russian (ru)_ContentUpdateGroupSchema.asset create mode 100644 Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Russian (ru)_ContentUpdateGroupSchema.asset.meta create mode 100644 Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Spanish (es)_BundledAssetGroupSchema.asset create mode 100644 Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Spanish (es)_BundledAssetGroupSchema.asset.meta create mode 100644 Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Spanish (es)_ContentUpdateGroupSchema.asset create mode 100644 Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Spanish (es)_ContentUpdateGroupSchema.asset.meta create mode 100644 Assets/Editor/Huoshan/ExpressionLocalizationTests.cs rename Assets/{Scripts/Framework/Config/Param.cs.meta => Editor/Huoshan/ExpressionLocalizationTests.cs.meta} (83%) create mode 100644 Assets/Editor/Huoshan/ExpressionLocalizationValidator.cs create mode 100644 Assets/Editor/Huoshan/ExpressionLocalizationValidator.cs.meta create mode 100644 Assets/GameContent/Feature_Huoshan/Expression/ExpressionContentCatalog.asset create mode 100644 Assets/GameContent/Feature_Huoshan/Expression/ExpressionContentCatalog.asset.meta create mode 100644 Assets/Language/ChapterInfo/ChapterInfo_es.asset create mode 100644 Assets/Language/ChapterInfo/ChapterInfo_es.asset.meta create mode 100644 Assets/Language/ChapterInfo/ChapterInfo_pt-BR.asset create mode 100644 Assets/Language/ChapterInfo/ChapterInfo_pt-BR.asset.meta create mode 100644 Assets/Language/ChapterInfo/ChapterInfo_ru.asset create mode 100644 Assets/Language/ChapterInfo/ChapterInfo_ru.asset.meta create mode 100644 Assets/Language/Params/Params_es.asset create mode 100644 Assets/Language/Params/Params_es.asset.meta create mode 100644 Assets/Language/Params/Params_pt-BR.asset create mode 100644 Assets/Language/Params/Params_pt-BR.asset.meta create mode 100644 Assets/Language/Params/Params_ru.asset create mode 100644 Assets/Language/Params/Params_ru.asset.meta create mode 100644 Assets/Language/UIText/UIText_es.asset create mode 100644 Assets/Language/UIText/UIText_es.asset.meta create mode 100644 Assets/Language/UIText/UIText_pt-BR.asset create mode 100644 Assets/Language/UIText/UIText_pt-BR.asset.meta create mode 100644 Assets/Language/UIText/UIText_ru.asset create mode 100644 Assets/Language/UIText/UIText_ru.asset.meta delete mode 100644 Assets/Scripts/Framework/Config/Param.cs create mode 100644 Assets/Scripts/MiniGame/HuoShan/Language/ExpressionContentCatalog.cs create mode 100644 Assets/Scripts/MiniGame/HuoShan/Language/ExpressionContentCatalog.cs.meta create mode 100644 Assets/Scripts/MiniGame/HuoShan/Language/ExpressionTextTokenizer.cs create mode 100644 Assets/Scripts/MiniGame/HuoShan/Language/ExpressionTextTokenizer.cs.meta delete mode 100644 Assets/StreamingAssets/Config/params.csv diff --git a/Assets/AddressableAssetsData/AddressableAssetSettings.asset b/Assets/AddressableAssetsData/AddressableAssetSettings.asset index 04056350e..a6039db1d 100644 --- a/Assets/AddressableAssetsData/AddressableAssetSettings.asset +++ b/Assets/AddressableAssetsData/AddressableAssetSettings.asset @@ -15,7 +15,7 @@ MonoBehaviour: m_DefaultGroup: 09546aca93a801441859af2a811fe53b m_currentHash: serializedVersion: 2 - Hash: e92b9b9776d0601449874b8c1b0ed1ae + Hash: ae2a22805ec7ff55174c38995aa3e54d m_OptimizeCatalogSize: 0 m_BuildRemoteCatalog: 0 m_BundleLocalCatalog: 0 @@ -68,6 +68,9 @@ MonoBehaviour: - {fileID: 11400000, guid: af94af36950b08d4f9a5037546bbe040, type: 2} - {fileID: 11400000, guid: 60fd2fd8c7094b242b01c63de65de9ea, type: 2} - {fileID: 11400000, guid: 7e4ab0de168458b43a21dc2f9e811ff5, type: 2} + - {fileID: 11400000, guid: 089fc86f714d6a94bafea3b117fbd639, type: 2} + - {fileID: 11400000, guid: 1c592abe924a856439461f3785ec53da, type: 2} + - {fileID: 11400000, guid: 26fd03652560e7f4abec092229fc38f2, type: 2} m_BuildSettings: m_CompileScriptsInVirtualMode: 0 m_CleanupStreamingAssetsAfterBuilds: 1 @@ -115,6 +118,9 @@ MonoBehaviour: - Locale-zh-Hans - SceneSO - Locale-ja-JP + - Locale-pt-BR + - Locale-ru + - Locale-es m_SchemaTemplates: [] m_GroupTemplateObjects: - {fileID: 11400000, guid: 0c158052680a3e94184c450d00291514, type: 2} diff --git a/Assets/AddressableAssetsData/AssetGroups/Localization-Locales.asset b/Assets/AddressableAssetsData/AssetGroups/Localization-Locales.asset index 7adacd092..57a1b8efd 100644 --- a/Assets/AddressableAssetsData/AssetGroups/Localization-Locales.asset +++ b/Assets/AddressableAssetsData/AssetGroups/Localization-Locales.asset @@ -35,6 +35,24 @@ MonoBehaviour: m_SerializedLabels: - Locale FlaggedDuringContentUpdateRestriction: 0 + - m_GUID: 9b1581a48f0ca11448dbddaf79c11252 + m_Address: Russian (ru) + m_ReadOnly: 1 + m_SerializedLabels: + - Locale + FlaggedDuringContentUpdateRestriction: 0 + - m_GUID: 3cadb0d5f8bf5e64599f52e139f7e3cc + m_Address: Spanish (es) + m_ReadOnly: 1 + m_SerializedLabels: + - Locale + FlaggedDuringContentUpdateRestriction: 0 + - m_GUID: d0c46f4777ac92249a43143a15892fd2 + m_Address: Portuguese (Brazil) (pt-BR) + m_ReadOnly: 1 + m_SerializedLabels: + - Locale + FlaggedDuringContentUpdateRestriction: 0 m_ReadOnly: 1 m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2} m_SchemaSet: diff --git a/Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Portuguese (Brazil) (pt-BR).asset b/Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Portuguese (Brazil) (pt-BR).asset new file mode 100644 index 000000000..11ff7620c --- /dev/null +++ b/Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Portuguese (Brazil) (pt-BR).asset @@ -0,0 +1,31 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: bbb281ee3bf0b054c82ac2347e9e782c, type: 3} + m_Name: Localization-String-Tables-Portuguese (Brazil) (pt-BR) + m_EditorClassIdentifier: + m_GroupName: Localization-String-Tables-Portuguese (Brazil) (pt-BR) + m_Data: + m_SerializedData: [] + m_GUID: 67ced675275774640b77e0ded8750379 + m_SerializeEntries: + - m_GUID: 63300a11309a7b34bbf7a345b123cab8 + m_Address: ChapterInfo_pt-BR + m_ReadOnly: 1 + m_SerializedLabels: + - Locale-pt-BR + FlaggedDuringContentUpdateRestriction: 0 + m_ReadOnly: 1 + m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2} + m_SchemaSet: + m_Schemas: + - {fileID: 11400000, guid: b430e9b782d3119489b903dce4915542, type: 2} + - {fileID: 11400000, guid: 70748601b27158b47810538f6ed1c013, type: 2} diff --git a/Assets/StreamingAssets/Config/params.csv.meta b/Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Portuguese (Brazil) (pt-BR).asset.meta similarity index 52% rename from Assets/StreamingAssets/Config/params.csv.meta rename to Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Portuguese (Brazil) (pt-BR).asset.meta index dd48c8466..c2ec71056 100644 --- a/Assets/StreamingAssets/Config/params.csv.meta +++ b/Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Portuguese (Brazil) (pt-BR).asset.meta @@ -1,7 +1,8 @@ fileFormatVersion: 2 -guid: e721272cd05718a4d8872d0f3e3f5b8c -DefaultImporter: +guid: 089fc86f714d6a94bafea3b117fbd639 +NativeFormatImporter: externalObjects: {} + mainObjectFileID: 11400000 userData: assetBundleName: assetBundleVariant: diff --git a/Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Russian (ru).asset b/Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Russian (ru).asset new file mode 100644 index 000000000..e9d766346 --- /dev/null +++ b/Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Russian (ru).asset @@ -0,0 +1,31 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: bbb281ee3bf0b054c82ac2347e9e782c, type: 3} + m_Name: Localization-String-Tables-Russian (ru) + m_EditorClassIdentifier: + m_GroupName: Localization-String-Tables-Russian (ru) + m_Data: + m_SerializedData: [] + m_GUID: c1e47d7d0d831ac4b881f19aa2bf17e0 + m_SerializeEntries: + - m_GUID: c69caec2066296840887d7e0c2eff52e + m_Address: ChapterInfo_ru + m_ReadOnly: 1 + m_SerializedLabels: + - Locale-ru + FlaggedDuringContentUpdateRestriction: 0 + m_ReadOnly: 1 + m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2} + m_SchemaSet: + m_Schemas: + - {fileID: 11400000, guid: b251ae14d0b273843b5674d239daa31c, type: 2} + - {fileID: 11400000, guid: 808fc72235ececa47aa55960a94a9a9a, type: 2} diff --git a/Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Russian (ru).asset.meta b/Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Russian (ru).asset.meta new file mode 100644 index 000000000..0ed488165 --- /dev/null +++ b/Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Russian (ru).asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1c592abe924a856439461f3785ec53da +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Spanish (es).asset b/Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Spanish (es).asset new file mode 100644 index 000000000..48bafd2a8 --- /dev/null +++ b/Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Spanish (es).asset @@ -0,0 +1,31 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: bbb281ee3bf0b054c82ac2347e9e782c, type: 3} + m_Name: Localization-String-Tables-Spanish (es) + m_EditorClassIdentifier: + m_GroupName: Localization-String-Tables-Spanish (es) + m_Data: + m_SerializedData: [] + m_GUID: c8471b00dabdd224ab9c17d71d951cbc + m_SerializeEntries: + - m_GUID: 728457f9c2ef69c489530047d495eca5 + m_Address: ChapterInfo_es + m_ReadOnly: 1 + m_SerializedLabels: + - Locale-es + FlaggedDuringContentUpdateRestriction: 0 + m_ReadOnly: 1 + m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2} + m_SchemaSet: + m_Schemas: + - {fileID: 11400000, guid: 3ddf5f47a1b265049825b193e7e93fbd, type: 2} + - {fileID: 11400000, guid: 3e5e5c2400a39814994a9fcb8eaf49a6, type: 2} diff --git a/Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Spanish (es).asset.meta b/Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Spanish (es).asset.meta new file mode 100644 index 000000000..af75d0792 --- /dev/null +++ b/Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Spanish (es).asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 26fd03652560e7f4abec092229fc38f2 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Portuguese (Brazil) (pt-BR)_BundledAssetGroupSchema.asset b/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Portuguese (Brazil) (pt-BR)_BundledAssetGroupSchema.asset new file mode 100644 index 000000000..304ca64d2 --- /dev/null +++ b/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Portuguese (Brazil) (pt-BR)_BundledAssetGroupSchema.asset @@ -0,0 +1,45 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e5d17a21594effb4e9591490b009e7aa, type: 3} + m_Name: Localization-String-Tables-Portuguese (Brazil) (pt-BR)_BundledAssetGroupSchema + m_EditorClassIdentifier: + m_Group: {fileID: 11400000, guid: 089fc86f714d6a94bafea3b117fbd639, type: 2} + m_InternalBundleIdMode: 1 + m_Compression: 1 + m_IncludeAddressInCatalog: 1 + m_IncludeGUIDInCatalog: 1 + m_IncludeLabelsInCatalog: 1 + m_InternalIdNamingMode: 0 + m_CacheClearBehavior: 0 + m_IncludeInBuild: 1 + m_BundledAssetProviderType: + m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider + m_ForceUniqueProvider: 0 + m_UseAssetBundleCache: 1 + m_UseAssetBundleCrc: 1 + m_UseAssetBundleCrcForCachedBundles: 1 + m_UseUWRForLocalBundles: 0 + m_Timeout: 0 + m_ChunkedTransfer: 0 + m_RedirectLimit: -1 + m_RetryCount: 0 + m_BuildPath: + m_Id: 2ae6a7549097a9b4caefe44b61b1fcc5 + m_LoadPath: + m_Id: 631ec5c677ab8084fb9be9b9ee39c6ef + m_BundleMode: 0 + m_AssetBundleProviderType: + m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider + m_BundleNaming: 1 + m_AssetLoadMode: 0 diff --git a/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Portuguese (Brazil) (pt-BR)_BundledAssetGroupSchema.asset.meta b/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Portuguese (Brazil) (pt-BR)_BundledAssetGroupSchema.asset.meta new file mode 100644 index 000000000..484e449d1 --- /dev/null +++ b/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Portuguese (Brazil) (pt-BR)_BundledAssetGroupSchema.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b430e9b782d3119489b903dce4915542 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Portuguese (Brazil) (pt-BR)_ContentUpdateGroupSchema.asset b/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Portuguese (Brazil) (pt-BR)_ContentUpdateGroupSchema.asset new file mode 100644 index 000000000..9eda8227e --- /dev/null +++ b/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Portuguese (Brazil) (pt-BR)_ContentUpdateGroupSchema.asset @@ -0,0 +1,16 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5834b5087d578d24c926ce20cd31e6d6, type: 3} + m_Name: Localization-String-Tables-Portuguese (Brazil) (pt-BR)_ContentUpdateGroupSchema + m_EditorClassIdentifier: + m_Group: {fileID: 11400000, guid: 089fc86f714d6a94bafea3b117fbd639, type: 2} + m_StaticContent: 0 diff --git a/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Portuguese (Brazil) (pt-BR)_ContentUpdateGroupSchema.asset.meta b/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Portuguese (Brazil) (pt-BR)_ContentUpdateGroupSchema.asset.meta new file mode 100644 index 000000000..3365724bd --- /dev/null +++ b/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Portuguese (Brazil) (pt-BR)_ContentUpdateGroupSchema.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 70748601b27158b47810538f6ed1c013 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Russian (ru)_BundledAssetGroupSchema.asset b/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Russian (ru)_BundledAssetGroupSchema.asset new file mode 100644 index 000000000..4245288db --- /dev/null +++ b/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Russian (ru)_BundledAssetGroupSchema.asset @@ -0,0 +1,45 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e5d17a21594effb4e9591490b009e7aa, type: 3} + m_Name: Localization-String-Tables-Russian (ru)_BundledAssetGroupSchema + m_EditorClassIdentifier: + m_Group: {fileID: 11400000, guid: 1c592abe924a856439461f3785ec53da, type: 2} + m_InternalBundleIdMode: 1 + m_Compression: 1 + m_IncludeAddressInCatalog: 1 + m_IncludeGUIDInCatalog: 1 + m_IncludeLabelsInCatalog: 1 + m_InternalIdNamingMode: 0 + m_CacheClearBehavior: 0 + m_IncludeInBuild: 1 + m_BundledAssetProviderType: + m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider + m_ForceUniqueProvider: 0 + m_UseAssetBundleCache: 1 + m_UseAssetBundleCrc: 1 + m_UseAssetBundleCrcForCachedBundles: 1 + m_UseUWRForLocalBundles: 0 + m_Timeout: 0 + m_ChunkedTransfer: 0 + m_RedirectLimit: -1 + m_RetryCount: 0 + m_BuildPath: + m_Id: 2ae6a7549097a9b4caefe44b61b1fcc5 + m_LoadPath: + m_Id: 631ec5c677ab8084fb9be9b9ee39c6ef + m_BundleMode: 0 + m_AssetBundleProviderType: + m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider + m_BundleNaming: 1 + m_AssetLoadMode: 0 diff --git a/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Russian (ru)_BundledAssetGroupSchema.asset.meta b/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Russian (ru)_BundledAssetGroupSchema.asset.meta new file mode 100644 index 000000000..1438e4a66 --- /dev/null +++ b/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Russian (ru)_BundledAssetGroupSchema.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b251ae14d0b273843b5674d239daa31c +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Russian (ru)_ContentUpdateGroupSchema.asset b/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Russian (ru)_ContentUpdateGroupSchema.asset new file mode 100644 index 000000000..e09ed0469 --- /dev/null +++ b/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Russian (ru)_ContentUpdateGroupSchema.asset @@ -0,0 +1,16 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5834b5087d578d24c926ce20cd31e6d6, type: 3} + m_Name: Localization-String-Tables-Russian (ru)_ContentUpdateGroupSchema + m_EditorClassIdentifier: + m_Group: {fileID: 11400000, guid: 1c592abe924a856439461f3785ec53da, type: 2} + m_StaticContent: 0 diff --git a/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Russian (ru)_ContentUpdateGroupSchema.asset.meta b/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Russian (ru)_ContentUpdateGroupSchema.asset.meta new file mode 100644 index 000000000..4910de7bc --- /dev/null +++ b/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Russian (ru)_ContentUpdateGroupSchema.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 808fc72235ececa47aa55960a94a9a9a +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Spanish (es)_BundledAssetGroupSchema.asset b/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Spanish (es)_BundledAssetGroupSchema.asset new file mode 100644 index 000000000..94eac2b41 --- /dev/null +++ b/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Spanish (es)_BundledAssetGroupSchema.asset @@ -0,0 +1,45 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e5d17a21594effb4e9591490b009e7aa, type: 3} + m_Name: Localization-String-Tables-Spanish (es)_BundledAssetGroupSchema + m_EditorClassIdentifier: + m_Group: {fileID: 11400000, guid: 26fd03652560e7f4abec092229fc38f2, type: 2} + m_InternalBundleIdMode: 1 + m_Compression: 1 + m_IncludeAddressInCatalog: 1 + m_IncludeGUIDInCatalog: 1 + m_IncludeLabelsInCatalog: 1 + m_InternalIdNamingMode: 0 + m_CacheClearBehavior: 0 + m_IncludeInBuild: 1 + m_BundledAssetProviderType: + m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider + m_ForceUniqueProvider: 0 + m_UseAssetBundleCache: 1 + m_UseAssetBundleCrc: 1 + m_UseAssetBundleCrcForCachedBundles: 1 + m_UseUWRForLocalBundles: 0 + m_Timeout: 0 + m_ChunkedTransfer: 0 + m_RedirectLimit: -1 + m_RetryCount: 0 + m_BuildPath: + m_Id: 2ae6a7549097a9b4caefe44b61b1fcc5 + m_LoadPath: + m_Id: 631ec5c677ab8084fb9be9b9ee39c6ef + m_BundleMode: 0 + m_AssetBundleProviderType: + m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider + m_BundleNaming: 1 + m_AssetLoadMode: 0 diff --git a/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Spanish (es)_BundledAssetGroupSchema.asset.meta b/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Spanish (es)_BundledAssetGroupSchema.asset.meta new file mode 100644 index 000000000..529d6ebde --- /dev/null +++ b/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Spanish (es)_BundledAssetGroupSchema.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3ddf5f47a1b265049825b193e7e93fbd +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Spanish (es)_ContentUpdateGroupSchema.asset b/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Spanish (es)_ContentUpdateGroupSchema.asset new file mode 100644 index 000000000..1fa89db33 --- /dev/null +++ b/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Spanish (es)_ContentUpdateGroupSchema.asset @@ -0,0 +1,16 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5834b5087d578d24c926ce20cd31e6d6, type: 3} + m_Name: Localization-String-Tables-Spanish (es)_ContentUpdateGroupSchema + m_EditorClassIdentifier: + m_Group: {fileID: 11400000, guid: 26fd03652560e7f4abec092229fc38f2, type: 2} + m_StaticContent: 0 diff --git a/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Spanish (es)_ContentUpdateGroupSchema.asset.meta b/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Spanish (es)_ContentUpdateGroupSchema.asset.meta new file mode 100644 index 000000000..eed3bb436 --- /dev/null +++ b/Assets/AddressableAssetsData/AssetGroups/Schemas/Localization-String-Tables-Spanish (es)_ContentUpdateGroupSchema.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3e5e5c2400a39814994a9fcb8eaf49a6 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/Huoshan/ExpressionLocalizationTests.cs b/Assets/Editor/Huoshan/ExpressionLocalizationTests.cs new file mode 100644 index 000000000..3059607fb --- /dev/null +++ b/Assets/Editor/Huoshan/ExpressionLocalizationTests.cs @@ -0,0 +1,249 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using AibisDream.Framework; +using AibisDream.MiniGame.Language; +using AibisDream.Utility; +using NUnit.Framework; +using UnityEditor; +using UnityEditor.Localization; +using UnityEngine; +using UnityEngine.Localization; +using UnityEngine.Localization.Settings; +using UnityEngine.Localization.Tables; +using UnityEngine.TestTools; + +namespace AibisDream.EditorTests.Huoshan +{ + public sealed class ExpressionLocalizationTests + { + private const string CatalogPath = + "Assets/GameContent/Feature_Huoshan/Expression/ExpressionContentCatalog.asset"; + + [TestCase(null, false)] + [TestCase("", false)] + [TestCase("plain", false)] + [TestCase("L10N.key", false)] + [TestCase("l10n.key", true)] + public void LocalizedParamPrefix_IsStrict(string value, bool expected) + { + Assert.That(LocalizationKit.IsLocalizedParam(value), Is.EqualTo(expected)); + } + + [Test] + public void LocalizedParamKey_StripsOnlyKnownPrefix() + { + Assert.That(LocalizationKit.GetL10NParamKey("l10n.hs.exp.log1.target"), + Is.EqualTo("hs.exp.log1.target")); + Assert.That(LocalizationKit.GetL10NParamKey("raw"), Is.EqualTo("raw")); + } + + [UnityTest] + public IEnumerator LocalizeParamAsync_UsesExplicitLocaleAndDoesNotFallback() + { + Locale chinese = LocalizationEditorSettings.GetLocale( + new LocaleIdentifier("zh-Hans")); + Locale english = LocalizationEditorSettings.GetLocale( + new LocaleIdentifier("en")); + Assert.That(chinese, Is.Not.Null); + Assert.That(english, Is.Not.Null); + + Locale previous = LocalizationSettings.SelectedLocale; + LocalizationSettings.SelectedLocale = english; + Task chineseTask = LocalizationKit.LocalizeParamAsync( + "l10n.hs.exp.log1.target", + chinese); + Task englishTask = LocalizationKit.LocalizeParamAsync( + "l10n.hs.exp.log1.target", + english); + + while (!chineseTask.IsCompleted || !englishTask.IsCompleted) + yield return null; + + LocalizationSettings.SelectedLocale = previous; + Assert.That(chineseTask.Result, Is.EqualTo("我就是个笑话")); + Assert.That(englishTask.Result, Is.Empty); + } + + [UnityTest] + public IEnumerator LocalizeParamAsync_ReturnsRawTextAndMissingKeyMarker() + { + Task rawTask = LocalizationKit.LocalizeParamAsync("raw text"); + while (!rawTask.IsCompleted) + yield return null; + Assert.That(rawTask.Result, Is.EqualTo("raw text")); + + Locale chinese = LocalizationEditorSettings.GetLocale( + new LocaleIdentifier("zh-Hans")); + LogAssert.Expect( + LogType.Error, + new Regex(@"Params 缺少条目.*Key=hs\.exp\.missing.*Locale=zh-Hans")); + Task missingTask = LocalizationKit.LocalizeParamAsync( + "l10n.hs.exp.missing", + chinese); + while (!missingTask.IsCompleted) + yield return null; + Assert.That(missingTask.Result, Is.EqualTo("⟦hs.exp.missing⟧")); + } + + [Test] + public void LegacyCommonParams_ArePresentInUnityParams() + { + StringTableCollection collection = + LocalizationEditorSettings.GetStringTableCollection(ConstRef.ParamsTable); + Assert.That(collection, Is.Not.Null); + + string[] legacyKeys = { "天空", "海报", "植物", "朋友", "情绪", "记忆", "逻辑" }; + foreach (string key in legacyKeys) + Assert.That(collection.SharedData.GetEntry(key), Is.Not.Null, key); + + Assert.That(AssetDatabase.LoadAssetAtPath( + "Assets/StreamingAssets/Config/params.csv"), Is.Null); + } + + [Test] + public void ExpressionCatalog_IsValidAndCaseSensitive() + { + ExpressionContentCatalog catalog = + AssetDatabase.LoadAssetAtPath(CatalogPath); + Assert.That(catalog, Is.Not.Null); + Assert.That(catalog.GetValidationErrors(), Is.Empty); + Assert.That(catalog.ActorLoopReference, Is.EqualTo("l10n.hs.exp.actor.loop")); + Assert.That(catalog.Rounds.Select(round => round.Id), + Is.EqualTo(new[] { "log1", "log2", "log3" })); + + Assert.That(catalog.TryGetRound("log1", out ExpressionRoundDefinition log1), Is.True); + Assert.That(log1.NonTargetParticleCount, Is.EqualTo(16)); + Assert.That(catalog.TryGetRound("LOG1", out _), Is.False); + Assert.That(catalog.TryGetRound("missing", out _), Is.False); + } + + [Test] + public void ExpressionCatalog_RejectsDuplicateIdsAndNonLocalizedReferences() + { + ExpressionContentCatalog catalog = + ScriptableObject.CreateInstance(); + try + { + SetPrivateField(catalog, "actorLoopReference", "raw"); + var first = new ExpressionRoundDefinition(); + var second = new ExpressionRoundDefinition(); + ConfigureRound(first, "log1", "l10n.target", "l10n.tokens", 1); + ConfigureRound(second, "log1", "raw", "l10n.tokens", 1); + SetPrivateField( + catalog, + "rounds", + new List { first, second }); + + string errors = string.Join("\n", catalog.GetValidationErrors()); + Assert.That(errors, Does.Contain("Actor Loop")); + Assert.That(errors, Does.Contain("重复")); + Assert.That(errors, Does.Contain("l10n.*")); + } + finally + { + Object.DestroyImmediate(catalog); + } + } + + [Test] + public void TokenParser_TrimsDropsEmptyAndRejectsFullwidthSeparator() + { + Assert.That( + LanguageYarnCommand.TryParseExpressionTokens( + " 哈哈 | | 嘿嘿|呵呵 ", + out var tokens), + Is.True); + Assert.That(tokens, Is.EqualTo(new[] { "哈哈", "嘿嘿", "呵呵" })); + + LogAssert.Expect(LogType.Error, new Regex("全角分隔符")); + Assert.That( + LanguageYarnCommand.TryParseExpressionTokens("哈哈|嘿嘿", out _), + Is.False); + Assert.That( + LanguageYarnCommand.TryParseExpressionTokens(" | | ", out _), + Is.False); + } + + [Test] + public void UnicodeTokenizer_PreservesTextElementsAndDropsWhitespace() + { + string combining = "e\u0301"; + var elements = + ExpressionTextTokenizer.GetVisibleElements($"中 A あ {combining} 😀。"); + + Assert.That( + elements, + Is.EqualTo(new[] { "中", "A", "あ", combining, "😀", "。" })); + } + + [Test] + public void UnicodeLayout_PreservesWordGapWithoutWhitespaceParticle() + { + ExpressionTextTokenizer.Layout layout = + ExpressionTextTokenizer.BuildLayout("A B", 1f, 0.6f); + + Assert.That(layout.VisibleElements, Is.EqualTo(new[] { "A", "B" })); + Assert.That(layout.Offsets[0], Is.EqualTo(-0.8f).Within(0.0001f)); + Assert.That(layout.Offsets[1], Is.EqualTo(0.8f).Within(0.0001f)); + } + + [Test] + public void UnicodeSequenceMatch_UsesWholeTextElements() + { + var source = ExpressionTextTokenizer.GetVisibleElements("A😀e\u0301。"); + var fragment = ExpressionTextTokenizer.GetVisibleElements("😀e\u0301"); + Assert.That(ExpressionTextTokenizer.FindVisibleSequence(source, fragment), + Is.EqualTo(1)); + Assert.That( + ExpressionTextTokenizer.FindVisibleSequence( + source, + ExpressionTextTokenizer.GetVisibleElements("😀x")), + Is.EqualTo(-1)); + } + + [Test] + public void ExpressContentValidation_HasNoBlockingErrors() + { + var issues = ExpressionLocalizationValidator.Validate(); + string errors = string.Join( + "\n", + issues + .Where(issue => + issue.Severity == ExpressionLocalizationIssueSeverity.Error) + .Select(issue => issue.Message)); + + Assert.That(errors, Is.Empty); + Assert.That( + issues.Any(issue => + issue.Severity == ExpressionLocalizationIssueSeverity.Warning), + Is.True, + "英日空条目应作为待翻译警告保留。"); + } + + private static void ConfigureRound( + ExpressionRoundDefinition round, + string id, + string target, + string tokens, + int nonTargetCount) + { + SetPrivateField(round, "id", id); + SetPrivateField(round, "targetReference", target); + SetPrivateField(round, "tokenReference", tokens); + SetPrivateField(round, "nonTargetParticleCount", nonTargetCount); + } + + private static void SetPrivateField(object target, string fieldName, object value) + { + FieldInfo field = target.GetType().GetField( + fieldName, + BindingFlags.Instance | BindingFlags.NonPublic); + Assert.That(field, Is.Not.Null, fieldName); + field.SetValue(target, value); + } + } +} diff --git a/Assets/Scripts/Framework/Config/Param.cs.meta b/Assets/Editor/Huoshan/ExpressionLocalizationTests.cs.meta similarity index 83% rename from Assets/Scripts/Framework/Config/Param.cs.meta rename to Assets/Editor/Huoshan/ExpressionLocalizationTests.cs.meta index 3420c0a8f..b5f63475d 100644 --- a/Assets/Scripts/Framework/Config/Param.cs.meta +++ b/Assets/Editor/Huoshan/ExpressionLocalizationTests.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: c12e0d2c486f4ea6a5d4f70878b6e7c1 +guid: d6b8797332ac4876b66e381ea3c98723 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Editor/Huoshan/ExpressionLocalizationValidator.cs b/Assets/Editor/Huoshan/ExpressionLocalizationValidator.cs new file mode 100644 index 000000000..511b2c175 --- /dev/null +++ b/Assets/Editor/Huoshan/ExpressionLocalizationValidator.cs @@ -0,0 +1,292 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using AibisDream.Framework; +using AibisDream.MiniGame.Language; +using AibisDream.Utility; +using UnityEditor; +using UnityEditor.Localization; +using UnityEngine; +using UnityEngine.Localization; +using UnityEngine.Localization.Tables; + +namespace AibisDream.EditorTests.Huoshan +{ + public enum ExpressionLocalizationIssueSeverity + { + Warning, + Error + } + + public readonly struct ExpressionLocalizationIssue + { + public ExpressionLocalizationIssue( + ExpressionLocalizationIssueSeverity severity, + string message) + { + Severity = severity; + Message = message; + } + + public ExpressionLocalizationIssueSeverity Severity { get; } + public string Message { get; } + } + + /// + /// 校验 Stage6 Express 命令、轮次 Catalog 与 Params String Table 的引用关系。 + /// 英日空值是待翻译警告,不阻断内容校验。 + /// + public static class ExpressionLocalizationValidator + { + private const string Stage6Path = "Assets/Yarn/FP/FP_Huoshan1/Stage6.yarn"; + private const string CatalogPath = + "Assets/GameContent/Feature_Huoshan/Expression/ExpressionContentCatalog.asset"; + + private static readonly Regex YarnCommandRegex = + new(@"^\s*<<(?[A-Za-z0-9_]+)(?.*?)>>", RegexOptions.Compiled); + + private static readonly Regex QuotedArgumentRegex = + new("\"(?(?:\\\\.|[^\"])*)\"", RegexOptions.Compiled); + + private static readonly Regex L10NRegex = + new(@"l10n\.(?[A-Za-z0-9._-]+)", RegexOptions.Compiled); + + private static readonly Regex ChineseRegex = + new(@"[\u3400-\u9FFF]", RegexOptions.Compiled); + + private static readonly Dictionary VisibleArgumentIndices = + new(StringComparer.Ordinal) + { + ["start_expression"] = new[] { 0, 1 }, + ["expression_lie_begin"] = new[] { 0 }, + ["expression_truth_lie_flicker"] = new[] { 0, 1 }, + ["expression_lie_morph_truth"] = new[] { 0, 1 }, + ["expression_truth_leak"] = new[] { 0 }, + ["expression_lie_shuffle"] = new[] { 0 }, + ["expression_actor_flash"] = new[] { 0 }, + ["expression_actor_flash_in"] = new[] { 0 }, + ["expression_flash_in"] = new[] { 0 }, + ["expression_actor_truth_flash"] = new[] { 0 }, + ["expression_truth_flash"] = new[] { 0 }, + ["expression_actor_word_hit"] = new[] { 0 }, + ["expression_word_hit"] = new[] { 0 }, + ["expression_actor_slot"] = new[] { 0, 1 }, + ["expression_actor_scroll"] = new[] { 0 }, + ["expression_truth_attack"] = new[] { 0 }, + ["expression_truth_destabilize"] = new[] { 0 }, + ["expression_truth_settle"] = new[] { 0 } + }; + + [MenuItem("Tools/AIBIS/Huoshan/Validate Express Localization")] + public static void ValidateFromMenu() + { + IReadOnlyList issues = Validate(); + int errorCount = issues.Count(issue => + issue.Severity == ExpressionLocalizationIssueSeverity.Error); + int warningCount = issues.Count - errorCount; + + foreach (ExpressionLocalizationIssue issue in issues) + { + if (issue.Severity == ExpressionLocalizationIssueSeverity.Error) + Debug.LogError($"[ExpressL10N] {issue.Message}"); + else + Debug.LogWarning($"[ExpressL10N] {issue.Message}"); + } + + if (errorCount == 0) + { + Debug.Log( + $"[ExpressL10N] 校验通过。errors=0, pending translations={warningCount}"); + } + } + + public static IReadOnlyList Validate() + { + var issues = new List(); + var referencedKeys = new HashSet(StringComparer.Ordinal); + + ValidateStage6(issues, referencedKeys); + ValidateCatalog(issues, referencedKeys); + ValidateParams(issues, referencedKeys); + return issues; + } + + private static void ValidateStage6( + List issues, + HashSet referencedKeys) + { + if (!File.Exists(Stage6Path)) + { + AddError(issues, $"找不到 Stage6 Yarn:{Stage6Path}"); + return; + } + + string[] lines = File.ReadAllLines(Stage6Path); + for (int lineIndex = 0; lineIndex < lines.Length; lineIndex++) + { + string line = lines[lineIndex]; + if (line.TrimStart().StartsWith("//", StringComparison.Ordinal)) + continue; + + foreach (Match keyMatch in L10NRegex.Matches(line)) + referencedKeys.Add(keyMatch.Groups["key"].Value); + + Match commandMatch = YarnCommandRegex.Match(line); + if (!commandMatch.Success) + continue; + + string command = commandMatch.Groups["command"].Value; + if (!VisibleArgumentIndices.TryGetValue(command, out int[] visibleIndices)) + continue; + + string[] arguments = QuotedArgumentRegex + .Matches(commandMatch.Groups["arguments"].Value) + .Cast() + .Select(match => match.Groups["value"].Value) + .ToArray(); + + foreach (int argumentIndex in visibleIndices) + { + if (argumentIndex >= arguments.Length) + continue; + + string value = arguments[argumentIndex]; + if (!LocalizationKit.IsLocalizedParam(value) && ChineseRegex.IsMatch(value)) + { + AddError( + issues, + $"{Stage6Path}:{lineIndex + 1} 的 {command} 玩家文字参数仍为中文硬编码:{value}"); + } + } + } + } + + private static void ValidateCatalog( + List issues, + HashSet referencedKeys) + { + ExpressionContentCatalog catalog = + AssetDatabase.LoadAssetAtPath(CatalogPath); + if (catalog == null) + { + AddError(issues, $"找不到轮次 Catalog:{CatalogPath}"); + return; + } + + foreach (string validationError in catalog.GetValidationErrors()) + AddError(issues, validationError); + + AddCatalogReference(catalog.ActorLoopReference, referencedKeys); + foreach (ExpressionRoundDefinition round in catalog.Rounds) + { + if (round == null) + continue; + AddCatalogReference(round.TargetReference, referencedKeys); + AddCatalogReference(round.TokenReference, referencedKeys); + } + } + + private static void ValidateParams( + List issues, + HashSet referencedKeys) + { + StringTableCollection collection = + LocalizationEditorSettings.GetStringTableCollection(ConstRef.ParamsTable); + if (collection == null) + { + AddError(issues, $"找不到 String Table Collection:{ConstRef.ParamsTable}"); + return; + } + + StringTable chinese = + collection.GetTable(new LocaleIdentifier("zh-Hans")) as StringTable; + StringTable english = + collection.GetTable(new LocaleIdentifier("en")) as StringTable; + StringTable japanese = + collection.GetTable(new LocaleIdentifier("ja-JP")) as StringTable; + + foreach (string key in referencedKeys.OrderBy(value => value, StringComparer.Ordinal)) + { + if (collection.SharedData.GetEntry(key) == null) + { + AddError(issues, $"Params Shared Data 缺少 Key:{key}"); + continue; + } + + StringTableEntry chineseEntry = chinese?.GetEntry(key); + if (string.IsNullOrWhiteSpace(chineseEntry?.LocalizedValue)) + { + AddError(issues, $"Params 中文值为空:{key}"); + continue; + } + + if (key.EndsWith(".tokens", StringComparison.Ordinal) || + string.Equals(key, "hs.exp.actor.loop", StringComparison.Ordinal)) + { + ValidatePipeList(issues, key, chineseEntry.LocalizedValue); + } + + AddPendingTranslation(issues, english, "en", key); + AddPendingTranslation(issues, japanese, "ja-JP", key); + } + } + + private static void ValidatePipeList( + List issues, + string key, + string value) + { + if (value.Contains('|')) + { + AddError(issues, $"Params/{key} 使用了全角分隔符“|”,必须使用半角“|”。"); + return; + } + + bool hasToken = value + .Split('|') + .Any(token => !string.IsNullOrWhiteSpace(token)); + if (!hasToken) + AddError(issues, $"Params/{key} 没有有效项。"); + } + + private static void AddPendingTranslation( + List issues, + StringTable table, + string localeCode, + string key) + { + StringTableEntry entry = table?.GetEntry(key); + if (entry == null) + { + AddError(issues, $"Params/{key} 缺少 {localeCode} 条目。"); + return; + } + + if (string.IsNullOrEmpty(entry.LocalizedValue)) + { + issues.Add(new ExpressionLocalizationIssue( + ExpressionLocalizationIssueSeverity.Warning, + $"待翻译:Params/{key}, Locale={localeCode}")); + } + } + + private static void AddCatalogReference( + string reference, + HashSet referencedKeys) + { + if (LocalizationKit.IsLocalizedParam(reference)) + referencedKeys.Add(LocalizationKit.GetL10NParamKey(reference)); + } + + private static void AddError( + List issues, + string message) + { + issues.Add(new ExpressionLocalizationIssue( + ExpressionLocalizationIssueSeverity.Error, + message)); + } + } +} diff --git a/Assets/Editor/Huoshan/ExpressionLocalizationValidator.cs.meta b/Assets/Editor/Huoshan/ExpressionLocalizationValidator.cs.meta new file mode 100644 index 000000000..9d63c8a1c --- /dev/null +++ b/Assets/Editor/Huoshan/ExpressionLocalizationValidator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5c602299e38f4bf4899abfbcbed84d77 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameContent/Feature_Huoshan/Expression/ExpressionContentCatalog.asset b/Assets/GameContent/Feature_Huoshan/Expression/ExpressionContentCatalog.asset new file mode 100644 index 000000000..174b2a156 --- /dev/null +++ b/Assets/GameContent/Feature_Huoshan/Expression/ExpressionContentCatalog.asset @@ -0,0 +1,28 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a86a72005fba4c99a59962fb9e92df83, type: 3} + m_Name: ExpressionContentCatalog + m_EditorClassIdentifier: + actorLoopReference: l10n.hs.exp.actor.loop + rounds: + - id: log1 + targetReference: l10n.hs.exp.log1.target + tokenReference: l10n.hs.exp.log1.tokens + nonTargetParticleCount: 16 + - id: log2 + targetReference: l10n.hs.exp.log2.target + tokenReference: l10n.hs.exp.log2.tokens + nonTargetParticleCount: 28 + - id: log3 + targetReference: l10n.hs.exp.log3.target + tokenReference: l10n.hs.exp.log3.tokens + nonTargetParticleCount: 38 diff --git a/Assets/GameContent/Feature_Huoshan/Expression/ExpressionContentCatalog.asset.meta b/Assets/GameContent/Feature_Huoshan/Expression/ExpressionContentCatalog.asset.meta new file mode 100644 index 000000000..0b42c70d1 --- /dev/null +++ b/Assets/GameContent/Feature_Huoshan/Expression/ExpressionContentCatalog.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b2d94952a18d4d178421691ab103f0fd +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameContent/Feature_Memory/Prefabs/PunchTapeForMem.prefab b/Assets/GameContent/Feature_Memory/Prefabs/PunchTapeForMem.prefab index 86eb6b864..8c12f2af3 100644 --- a/Assets/GameContent/Feature_Memory/Prefabs/PunchTapeForMem.prefab +++ b/Assets/GameContent/Feature_Memory/Prefabs/PunchTapeForMem.prefab @@ -197,7 +197,7 @@ RectTransform: m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 1} m_AnchorMax: {x: 0, y: 1} - m_AnchoredPosition: {x: 107.3, y: -4} + m_AnchoredPosition: {x: 107.3, y: -8} m_SizeDelta: {x: 106, y: 20} m_Pivot: {x: 0, y: 1} --- !u!222 &6236610923486505318 @@ -756,7 +756,7 @@ RectTransform: m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 1} m_AnchorMax: {x: 0, y: 1} - m_AnchoredPosition: {x: 107.29999, y: -25} + m_AnchoredPosition: {x: 107.29999, y: -22.6} m_SizeDelta: {x: 96, y: 20} m_Pivot: {x: 0, y: 1} --- !u!222 &6685312199246720920 diff --git a/Assets/GameContent/Feature_Memory/Prefabs/PunchTapeForPrinter.prefab b/Assets/GameContent/Feature_Memory/Prefabs/PunchTapeForPrinter.prefab index d3fed0711..8d9093f8f 100644 --- a/Assets/GameContent/Feature_Memory/Prefabs/PunchTapeForPrinter.prefab +++ b/Assets/GameContent/Feature_Memory/Prefabs/PunchTapeForPrinter.prefab @@ -280,7 +280,7 @@ RectTransform: m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 1} m_AnchorMax: {x: 0, y: 1} - m_AnchoredPosition: {x: 132.9, y: -5} + m_AnchoredPosition: {x: 132.9, y: -9} m_SizeDelta: {x: 132.5, y: 25} m_Pivot: {x: 0, y: 1} --- !u!222 &3142962418811844898 @@ -743,7 +743,7 @@ RectTransform: m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 1} m_AnchorMax: {x: 0, y: 1} - m_AnchoredPosition: {x: 132.90002, y: -31.25} + m_AnchoredPosition: {x: 132.90002, y: -29.9} m_SizeDelta: {x: 120, y: 25} m_Pivot: {x: 0, y: 1} --- !u!222 &2054489642493287172 diff --git a/Assets/Language/ChapterInfo/ChapterInfo.asset b/Assets/Language/ChapterInfo/ChapterInfo.asset index 38ef7b835..5dc963898 100644 --- a/Assets/Language/ChapterInfo/ChapterInfo.asset +++ b/Assets/Language/ChapterInfo/ChapterInfo.asset @@ -17,6 +17,9 @@ MonoBehaviour: - {fileID: 11400000, guid: 8906df9235e52fb41b11fe8776a77032, type: 2} - {fileID: 11400000, guid: bdf8ad79a9f707b45805e23c25ccdadb, type: 2} - {fileID: 11400000, guid: 9d8accfdeabc8034c986233920a17184, type: 2} + - {fileID: 11400000, guid: 63300a11309a7b34bbf7a345b123cab8, type: 2} + - {fileID: 11400000, guid: c69caec2066296840887d7e0c2eff52e, type: 2} + - {fileID: 11400000, guid: 728457f9c2ef69c489530047d495eca5, type: 2} m_Extensions: [] m_Group: String Table references: diff --git a/Assets/Language/ChapterInfo/ChapterInfo_es.asset b/Assets/Language/ChapterInfo/ChapterInfo_es.asset new file mode 100644 index 000000000..3eec10dae --- /dev/null +++ b/Assets/Language/ChapterInfo/ChapterInfo_es.asset @@ -0,0 +1,23 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e9620f8c34305754d8cc9a7e49e852d9, type: 3} + m_Name: ChapterInfo_es + m_EditorClassIdentifier: + m_LocaleId: + m_Code: es + m_SharedData: {fileID: 11400000, guid: 7ea8c68fdc624de4c86434963c06ec55, type: 2} + m_Metadata: + m_Items: [] + m_TableData: [] + references: + version: 2 + RefIds: [] diff --git a/Assets/Language/ChapterInfo/ChapterInfo_es.asset.meta b/Assets/Language/ChapterInfo/ChapterInfo_es.asset.meta new file mode 100644 index 000000000..c8c3b6e74 --- /dev/null +++ b/Assets/Language/ChapterInfo/ChapterInfo_es.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 728457f9c2ef69c489530047d495eca5 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Language/ChapterInfo/ChapterInfo_pt-BR.asset b/Assets/Language/ChapterInfo/ChapterInfo_pt-BR.asset new file mode 100644 index 000000000..3768452df --- /dev/null +++ b/Assets/Language/ChapterInfo/ChapterInfo_pt-BR.asset @@ -0,0 +1,23 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e9620f8c34305754d8cc9a7e49e852d9, type: 3} + m_Name: ChapterInfo_pt-BR + m_EditorClassIdentifier: + m_LocaleId: + m_Code: pt-BR + m_SharedData: {fileID: 11400000, guid: 7ea8c68fdc624de4c86434963c06ec55, type: 2} + m_Metadata: + m_Items: [] + m_TableData: [] + references: + version: 2 + RefIds: [] diff --git a/Assets/Language/ChapterInfo/ChapterInfo_pt-BR.asset.meta b/Assets/Language/ChapterInfo/ChapterInfo_pt-BR.asset.meta new file mode 100644 index 000000000..47854f9d2 --- /dev/null +++ b/Assets/Language/ChapterInfo/ChapterInfo_pt-BR.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 63300a11309a7b34bbf7a345b123cab8 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Language/ChapterInfo/ChapterInfo_ru.asset b/Assets/Language/ChapterInfo/ChapterInfo_ru.asset new file mode 100644 index 000000000..89512aae4 --- /dev/null +++ b/Assets/Language/ChapterInfo/ChapterInfo_ru.asset @@ -0,0 +1,23 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e9620f8c34305754d8cc9a7e49e852d9, type: 3} + m_Name: ChapterInfo_ru + m_EditorClassIdentifier: + m_LocaleId: + m_Code: ru + m_SharedData: {fileID: 11400000, guid: 7ea8c68fdc624de4c86434963c06ec55, type: 2} + m_Metadata: + m_Items: [] + m_TableData: [] + references: + version: 2 + RefIds: [] diff --git a/Assets/Language/ChapterInfo/ChapterInfo_ru.asset.meta b/Assets/Language/ChapterInfo/ChapterInfo_ru.asset.meta new file mode 100644 index 000000000..29f7fb121 --- /dev/null +++ b/Assets/Language/ChapterInfo/ChapterInfo_ru.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c69caec2066296840887d7e0c2eff52e +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Language/Params/Params Shared Data.asset b/Assets/Language/Params/Params Shared Data.asset index 63613a326..6698a0112 100644 --- a/Assets/Language/Params/Params Shared Data.asset +++ b/Assets/Language/Params/Params Shared Data.asset @@ -43,6 +43,118 @@ MonoBehaviour: m_Key: "\u903B\u8F91" m_Metadata: m_Items: [] + - m_Id: 200000000000001001 + m_Key: hs.exp.log1.target + m_Metadata: + m_Items: [] + - m_Id: 200000000000001002 + m_Key: hs.exp.log1.tokens + m_Metadata: + m_Items: [] + - m_Id: 200000000000001003 + m_Key: hs.exp.log2.target + m_Metadata: + m_Items: [] + - m_Id: 200000000000001004 + m_Key: hs.exp.log2.tokens + m_Metadata: + m_Items: [] + - m_Id: 200000000000001005 + m_Key: hs.exp.log3.target + m_Metadata: + m_Items: [] + - m_Id: 200000000000001006 + m_Key: hs.exp.log3.tokens + m_Metadata: + m_Items: [] + - m_Id: 200000000000001007 + m_Key: hs.exp.actor.loop + m_Metadata: + m_Items: [] + - m_Id: 200000000000001008 + m_Key: hs.exp.atk.why + m_Metadata: + m_Items: [] + - m_Id: 200000000000001009 + m_Key: hs.exp.atk.doubt + m_Metadata: + m_Items: [] + - m_Id: 200000000000001010 + m_Key: hs.exp.atk.last + m_Metadata: + m_Items: [] + - m_Id: 200000000000001011 + m_Key: hs.exp.atk.weather + m_Metadata: + m_Items: [] + - m_Id: 200000000000001012 + m_Key: hs.exp.atk.unsold + m_Metadata: + m_Items: [] + - m_Id: 200000000000001013 + m_Key: hs.exp.atk.explain + m_Metadata: + m_Items: [] + - m_Id: 200000000000001014 + m_Key: hs.exp.atk.refuse + m_Metadata: + m_Items: [] + - m_Id: 200000000000001015 + m_Key: hs.exp.atk.trust + m_Metadata: + m_Items: [] + - m_Id: 200000000000001016 + m_Key: hs.exp.atk.enough + m_Metadata: + m_Items: [] + - m_Id: 200000000000001017 + m_Key: hs.exp.atk.damn + m_Metadata: + m_Items: [] + - m_Id: 200000000000001018 + m_Key: hs.exp.atk.useless + m_Metadata: + m_Items: [] + - m_Id: 200000000000001019 + m_Key: hs.exp.atk.prove + m_Metadata: + m_Items: [] + - m_Id: 200000000000001020 + m_Key: hs.exp.atk.nothing + m_Metadata: + m_Items: [] + - m_Id: 200000000000001021 + m_Key: hs.exp.atk.catchup + m_Metadata: + m_Items: [] + - m_Id: 200000000000001022 + m_Key: hs.exp.atk.yingli + m_Metadata: + m_Items: [] + - m_Id: 200000000000001023 + m_Key: hs.exp.atk.courage + m_Metadata: + m_Items: [] + - m_Id: 200000000000001024 + m_Key: hs.exp.atk.say + m_Metadata: + m_Items: [] + - m_Id: 200000000000001025 + m_Key: hs.exp.atk.trash + m_Metadata: + m_Items: [] + - m_Id: 200000000000001026 + m_Key: hs.exp.atk.hopeless + m_Metadata: + m_Items: [] + - m_Id: 200000000000001027 + m_Key: hs.exp.atk.why_loop + m_Metadata: + m_Items: [] + - m_Id: 200000000000001028 + m_Key: hs.exp.final.win + m_Metadata: + m_Items: [] m_Metadata: m_Items: [] m_KeyGenerator: diff --git a/Assets/Language/Params/Params_en.asset b/Assets/Language/Params/Params_en.asset index 216cba05f..535805882 100644 --- a/Assets/Language/Params/Params_en.asset +++ b/Assets/Language/Params/Params_en.asset @@ -46,6 +46,118 @@ MonoBehaviour: m_Localized: Logic m_Metadata: m_Items: [] + - m_Id: 200000000000001001 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001002 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001003 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001004 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001005 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001006 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001007 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001008 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001009 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001010 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001011 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001012 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001013 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001014 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001015 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001016 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001017 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001018 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001019 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001020 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001021 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001022 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001023 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001024 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001025 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001026 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001027 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001028 + m_Localized: + m_Metadata: + m_Items: [] references: version: 2 RefIds: [] diff --git a/Assets/Language/Params/Params_es.asset b/Assets/Language/Params/Params_es.asset new file mode 100644 index 000000000..caedbf494 --- /dev/null +++ b/Assets/Language/Params/Params_es.asset @@ -0,0 +1,23 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e9620f8c34305754d8cc9a7e49e852d9, type: 3} + m_Name: Params_es + m_EditorClassIdentifier: + m_LocaleId: + m_Code: es + m_SharedData: {fileID: 11400000, guid: 4a2d391471bd9cd439150c52659e9846, type: 2} + m_Metadata: + m_Items: [] + m_TableData: [] + references: + version: 2 + RefIds: [] diff --git a/Assets/Language/Params/Params_es.asset.meta b/Assets/Language/Params/Params_es.asset.meta new file mode 100644 index 000000000..c30129b64 --- /dev/null +++ b/Assets/Language/Params/Params_es.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1edf9254b97981643bdecd6d22ed1150 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Language/Params/Params_ja-JP.asset b/Assets/Language/Params/Params_ja-JP.asset index 08a89565e..ead8fda35 100644 --- a/Assets/Language/Params/Params_ja-JP.asset +++ b/Assets/Language/Params/Params_ja-JP.asset @@ -46,6 +46,118 @@ MonoBehaviour: m_Localized: "\u8AD6\u7406" m_Metadata: m_Items: [] + - m_Id: 200000000000001001 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001002 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001003 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001004 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001005 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001006 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001007 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001008 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001009 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001010 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001011 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001012 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001013 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001014 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001015 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001016 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001017 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001018 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001019 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001020 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001021 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001022 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001023 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001024 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001025 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001026 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001027 + m_Localized: + m_Metadata: + m_Items: [] + - m_Id: 200000000000001028 + m_Localized: + m_Metadata: + m_Items: [] references: version: 2 RefIds: [] diff --git a/Assets/Language/Params/Params_pt-BR.asset b/Assets/Language/Params/Params_pt-BR.asset new file mode 100644 index 000000000..c0628fea0 --- /dev/null +++ b/Assets/Language/Params/Params_pt-BR.asset @@ -0,0 +1,23 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e9620f8c34305754d8cc9a7e49e852d9, type: 3} + m_Name: Params_pt-BR + m_EditorClassIdentifier: + m_LocaleId: + m_Code: pt-BR + m_SharedData: {fileID: 11400000, guid: 4a2d391471bd9cd439150c52659e9846, type: 2} + m_Metadata: + m_Items: [] + m_TableData: [] + references: + version: 2 + RefIds: [] diff --git a/Assets/Language/Params/Params_pt-BR.asset.meta b/Assets/Language/Params/Params_pt-BR.asset.meta new file mode 100644 index 000000000..64ec76054 --- /dev/null +++ b/Assets/Language/Params/Params_pt-BR.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6ad3c9298dbc3b642b1251ffc306eaa6 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Language/Params/Params_ru.asset b/Assets/Language/Params/Params_ru.asset new file mode 100644 index 000000000..aa0e7a050 --- /dev/null +++ b/Assets/Language/Params/Params_ru.asset @@ -0,0 +1,23 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e9620f8c34305754d8cc9a7e49e852d9, type: 3} + m_Name: Params_ru + m_EditorClassIdentifier: + m_LocaleId: + m_Code: ru + m_SharedData: {fileID: 11400000, guid: 4a2d391471bd9cd439150c52659e9846, type: 2} + m_Metadata: + m_Items: [] + m_TableData: [] + references: + version: 2 + RefIds: [] diff --git a/Assets/Language/Params/Params_ru.asset.meta b/Assets/Language/Params/Params_ru.asset.meta new file mode 100644 index 000000000..9fa0dc533 --- /dev/null +++ b/Assets/Language/Params/Params_ru.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b91f4281442d2b54f92fbecb5bd56426 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Language/Params/Params_zh-Hans.asset b/Assets/Language/Params/Params_zh-Hans.asset index e8c3963bd..6f3544b82 100644 --- a/Assets/Language/Params/Params_zh-Hans.asset +++ b/Assets/Language/Params/Params_zh-Hans.asset @@ -46,6 +46,118 @@ MonoBehaviour: m_Localized: "\u903B\u8F91" m_Metadata: m_Items: [] + - m_Id: 200000000000001001 + m_Localized: "\u6211\u5C31\u662F\u4E2A\u7B11\u8BDD" + m_Metadata: + m_Items: [] + - m_Id: 200000000000001002 + m_Localized: "\u54C8\u54C8|\u563F\u563F|\u5475\u5475" + m_Metadata: + m_Items: [] + - m_Id: 200000000000001003 + m_Localized: "\u4E0D\u8981\u79BB\u5F00\u6211" + m_Metadata: + m_Items: [] + - m_Id: 200000000000001004 + m_Localized: "\u54C8\u54C8\u54C8|\u563F\u563F\u563F|\u5475\u5475\u5475|\u7B11\u6B7B\u4E86|\u597D\u597D\u7B11" + m_Metadata: + m_Items: [] + - m_Id: 200000000000001005 + m_Localized: "\u4E3A\u4EC0\u4E48\u4E0D\u76F8\u4FE1\u6211" + m_Metadata: + m_Items: [] + - m_Id: 200000000000001006 + m_Localized: "\u54C8\u54C8\u54C8\u54C8|\u7B11\u6B7B\u6211\u4E86|\u592A\u597D\u7B11\u4E86|\u7EF7\u4E0D\u4F4F\u4E86|\u7B11\u4E0D\u6D3B\u4E86|\u771F\u7684\u592A\u641E\u7B11" + m_Metadata: + m_Items: [] + - m_Id: 200000000000001007 + m_Localized: "\u6CA1\u95EE\u9898|\u5187\u95EE\u9898|\u5E3D\u95EE\u9898" + m_Metadata: + m_Items: [] + - m_Id: 200000000000001008 + m_Localized: "\u4E3A\u4EC0\u4E48" + m_Metadata: + m_Items: [] + - m_Id: 200000000000001009 + m_Localized: "\u4E0D\u76F8\u4FE1\u6211" + m_Metadata: + m_Items: [] + - m_Id: 200000000000001010 + m_Localized: "\u6211\u660E\u767D\u6211\u57AB\u5E95" + m_Metadata: + m_Items: [] + - m_Id: 200000000000001011 + m_Localized: "\u5929\u6C14\u771F\u7684\u4E0D\u884C" + m_Metadata: + m_Items: [] + - m_Id: 200000000000001012 + m_Localized: "\u5356\u4E0D\u51FA\u53BB" + m_Metadata: + m_Items: [] + - m_Id: 200000000000001013 + m_Localized: "\u52AA\u529B\u89E3\u91CA" + m_Metadata: + m_Items: [] + - m_Id: 200000000000001014 + m_Localized: "\u4E0D\u613F\u610F\u76F8\u4FE1\u6211" + m_Metadata: + m_Items: [] + - m_Id: 200000000000001015 + m_Localized: "\u4E00\u70B9\u4FE1\u4EFB" + m_Metadata: + m_Items: [] + - m_Id: 200000000000001016 + m_Localized: "\u53D7\u591F\u4E86" + m_Metadata: + m_Items: [] + - m_Id: 200000000000001017 + m_Localized: "\u771F\u8BE5\u6B7B" + m_Metadata: + m_Items: [] + - m_Id: 200000000000001018 + m_Localized: "\u771F\u6CA1\u7528" + m_Metadata: + m_Items: [] + - m_Id: 200000000000001019 + m_Localized: "\u8BC1\u660E\u81EA\u5DF1" + m_Metadata: + m_Items: [] + - m_Id: 200000000000001020 + m_Localized: "\u4EC0\u4E48\u90FD\u505A\u4E0D\u5230" + m_Metadata: + m_Items: [] + - m_Id: 200000000000001021 + m_Localized: "\u8FFD\u4E0A\u4F60" + m_Metadata: + m_Items: [] + - m_Id: 200000000000001022 + m_Localized: "\u5E2E\u5230\u82F1\u7406" + m_Metadata: + m_Items: [] + - m_Id: 200000000000001023 + m_Localized: "\u6CA1\u6709\u52C7\u6C14" + m_Metadata: + m_Items: [] + - m_Id: 200000000000001024 + m_Localized: "\u8BF4\u51FA\u6765" + m_Metadata: + m_Items: [] + - m_Id: 200000000000001025 + m_Localized: "\u5E9F\u7269" + m_Metadata: + m_Items: [] + - m_Id: 200000000000001026 + m_Localized: "\u70C2\u6CE5\u6276\u4E0D\u4E0A\u5899" + m_Metadata: + m_Items: [] + - m_Id: 200000000000001027 + m_Localized: "\u4E3A\u4EC0\u4E48\u4E3A\u4EC0\u4E48" + m_Metadata: + m_Items: [] + - m_Id: 200000000000001028 + m_Localized: "\u6211\u60F3\u8D62\u4E00\u6B21" + m_Metadata: + m_Items: [] references: version: 2 RefIds: [] diff --git a/Assets/Language/UIText/UIText_en.asset b/Assets/Language/UIText/UIText_en.asset index db990ed77..647465939 100644 --- a/Assets/Language/UIText/UIText_en.asset +++ b/Assets/Language/UIText/UIText_en.asset @@ -258,19 +258,19 @@ MonoBehaviour: m_Metadata: m_Items: [] - m_Id: 200000000000000001 - m_Localized: "\u76EE\u6807\uFF1A\u5265\u79BB\u7EA2\u8272\u7684\u5E72\u6270\u8BED\u8A00\uFF0C\u805A\u96C6\u84DD\u8272\u7684\u771F\u5B9E\u8868\u8FBE" + m_Localized: m_Metadata: m_Items: [] - m_Id: 200000000000000002 - m_Localized: "\u8F93\u51FA" + m_Localized: m_Metadata: m_Items: [] - m_Id: 200000000000000003 - m_Localized: "{0}% \u8BED\u8A00\u6574\u5408\u5B8C\u6210\u5EA6" + m_Localized: m_Metadata: m_Items: [] - m_Id: 200000000000000004 - m_Localized: "{0} \u5904\u5F02\u5E38\u601D\u7EEA\u4ECD\u5728\u5E72\u6270\u8868\u8FBE" + m_Localized: m_Metadata: m_Items: [] references: diff --git a/Assets/Language/UIText/UIText_es.asset b/Assets/Language/UIText/UIText_es.asset new file mode 100644 index 000000000..2908ae0c0 --- /dev/null +++ b/Assets/Language/UIText/UIText_es.asset @@ -0,0 +1,23 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e9620f8c34305754d8cc9a7e49e852d9, type: 3} + m_Name: UIText_es + m_EditorClassIdentifier: + m_LocaleId: + m_Code: es + m_SharedData: {fileID: 11400000, guid: 994c69bf4b2403043bc9562f2d24ee56, type: 2} + m_Metadata: + m_Items: [] + m_TableData: [] + references: + version: 2 + RefIds: [] diff --git a/Assets/Language/UIText/UIText_es.asset.meta b/Assets/Language/UIText/UIText_es.asset.meta new file mode 100644 index 000000000..22e3e9853 --- /dev/null +++ b/Assets/Language/UIText/UIText_es.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 380c27123212d66468aa1f3ed3bf72ea +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Language/UIText/UIText_ja-JP.asset b/Assets/Language/UIText/UIText_ja-JP.asset index 147bce71f..70b897290 100644 --- a/Assets/Language/UIText/UIText_ja-JP.asset +++ b/Assets/Language/UIText/UIText_ja-JP.asset @@ -247,19 +247,19 @@ MonoBehaviour: m_Metadata: m_Items: [] - m_Id: 200000000000000001 - m_Localized: "\u76EE\u6807\uFF1A\u5265\u79BB\u7EA2\u8272\u7684\u5E72\u6270\u8BED\u8A00\uFF0C\u805A\u96C6\u84DD\u8272\u7684\u771F\u5B9E\u8868\u8FBE" + m_Localized: m_Metadata: m_Items: [] - m_Id: 200000000000000002 - m_Localized: "\u8F93\u51FA" + m_Localized: m_Metadata: m_Items: [] - m_Id: 200000000000000003 - m_Localized: "{0}% \u8BED\u8A00\u6574\u5408\u5B8C\u6210\u5EA6" + m_Localized: m_Metadata: m_Items: [] - m_Id: 200000000000000004 - m_Localized: "{0} \u5904\u5F02\u5E38\u601D\u7EEA\u4ECD\u5728\u5E72\u6270\u8868\u8FBE" + m_Localized: m_Metadata: m_Items: [] references: diff --git a/Assets/Language/UIText/UIText_pt-BR.asset b/Assets/Language/UIText/UIText_pt-BR.asset new file mode 100644 index 000000000..0876510d6 --- /dev/null +++ b/Assets/Language/UIText/UIText_pt-BR.asset @@ -0,0 +1,23 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e9620f8c34305754d8cc9a7e49e852d9, type: 3} + m_Name: UIText_pt-BR + m_EditorClassIdentifier: + m_LocaleId: + m_Code: pt-BR + m_SharedData: {fileID: 11400000, guid: 994c69bf4b2403043bc9562f2d24ee56, type: 2} + m_Metadata: + m_Items: [] + m_TableData: [] + references: + version: 2 + RefIds: [] diff --git a/Assets/Language/UIText/UIText_pt-BR.asset.meta b/Assets/Language/UIText/UIText_pt-BR.asset.meta new file mode 100644 index 000000000..32d4a2d7b --- /dev/null +++ b/Assets/Language/UIText/UIText_pt-BR.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7d496f91e240bb143b62a6239c817d09 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Language/UIText/UIText_ru.asset b/Assets/Language/UIText/UIText_ru.asset new file mode 100644 index 000000000..48581a69d --- /dev/null +++ b/Assets/Language/UIText/UIText_ru.asset @@ -0,0 +1,23 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e9620f8c34305754d8cc9a7e49e852d9, type: 3} + m_Name: UIText_ru + m_EditorClassIdentifier: + m_LocaleId: + m_Code: ru + m_SharedData: {fileID: 11400000, guid: 994c69bf4b2403043bc9562f2d24ee56, type: 2} + m_Metadata: + m_Items: [] + m_TableData: [] + references: + version: 2 + RefIds: [] diff --git a/Assets/Language/UIText/UIText_ru.asset.meta b/Assets/Language/UIText/UIText_ru.asset.meta new file mode 100644 index 000000000..6c8b5b353 --- /dev/null +++ b/Assets/Language/UIText/UIText_ru.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f679069e455200841a92c27186383940 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scenes/HuoShanFixScene.unity b/Assets/Scenes/HuoShanFixScene.unity index ce31dc270..0f71c9d1c 100644 --- a/Assets/Scenes/HuoShanFixScene.unity +++ b/Assets/Scenes/HuoShanFixScene.unity @@ -16127,6 +16127,7 @@ MonoBehaviour: expressionView: {fileID: 5162511683866016917} logReleasePresentation: {fileID: 2138889543} screenPresentation: {fileID: 2138889544} + contentCatalog: {fileID: 11400000, guid: b2d94952a18d4d178421691ab103f0fd, type: 2} defaultAnxietyPhrases: - "\u6211\u597D\u6015" - "\u600E\u4E48\u529E" diff --git a/Assets/Scripts/AibisDream.Menus/AibisAssetMenus.cs b/Assets/Scripts/AibisDream.Menus/AibisAssetMenus.cs index 044cf2672..b35fbb864 100644 --- a/Assets/Scripts/AibisDream.Menus/AibisAssetMenus.cs +++ b/Assets/Scripts/AibisDream.Menus/AibisAssetMenus.cs @@ -17,6 +17,7 @@ namespace AibisDream public const string FixWhackMoleData = Root + "/维修·打地鼠/关卡数据"; public const string HuoShanEmotionWave = Root + "/火山/情绪波配置"; public const string HuoShanWaveform = Root + "/火山/波形配置"; + public const string HuoShanExpressionContent = Root + "/火山/表达内容配置"; public const string BlockPuzzleValidator = Root + "/方块拼图/验证器"; public const string MemoryPunchTapeCatalog = Root + "/记忆/打孔带目录"; } diff --git a/Assets/Scripts/AssetRefs/ConstRef.cs b/Assets/Scripts/AssetRefs/ConstRef.cs index b3a306507..e5f75494e 100644 --- a/Assets/Scripts/AssetRefs/ConstRef.cs +++ b/Assets/Scripts/AssetRefs/ConstRef.cs @@ -52,6 +52,7 @@ namespace AibisDream.Utility public const string UITextTable = "UIText"; public const string ChapterInfoTable = "ChapterInfo"; + public const string ParamsTable = "Params"; #endregion diff --git a/Assets/Scripts/Dialog System/AibisLineProvider.cs b/Assets/Scripts/Dialog System/AibisLineProvider.cs index 825e9baef..9c6041335 100644 --- a/Assets/Scripts/Dialog System/AibisLineProvider.cs +++ b/Assets/Scripts/Dialog System/AibisLineProvider.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Threading; +using System.Threading.Tasks; using AibisDream.Framework; using AibisDream.Utility; using UnityEngine; @@ -79,10 +80,16 @@ namespace AibisDream return LocalizedLine.InvalidLine; } - // 对 substitutions 做本地化解析(保留旧 LocalisedLineProvider 的能力) - for (int i = 0; i < line.Substitutions.Length; i++) + // 对 substitutions 并行做本地化解析(保留旧 LocalisedLineProvider 的能力)。 + if (line.Substitutions.Length > 0) { - line.Substitutions[i] = LocalizationKit.LocalizeParam(line.Substitutions[i]); + var localizationTasks = new Task[line.Substitutions.Length]; + for (int i = 0; i < line.Substitutions.Length; i++) + localizationTasks[i] = LocalizationKit.LocalizeParamAsync(line.Substitutions[i]); + + string[] localizedSubstitutions = await Task.WhenAll(localizationTasks); + for (int i = 0; i < localizedSubstitutions.Length; i++) + line.Substitutions[i] = localizedSubstitutions[i]; } // 先展开项目级语义标记,再保留旧 LocalisedLineProvider 的文本转义处理。 diff --git a/Assets/Scripts/FixSystem/PunchTape/PunchTapeItem.cs b/Assets/Scripts/FixSystem/PunchTape/PunchTapeItem.cs index 933555f62..6cf245206 100644 --- a/Assets/Scripts/FixSystem/PunchTape/PunchTapeItem.cs +++ b/Assets/Scripts/FixSystem/PunchTape/PunchTapeItem.cs @@ -25,6 +25,7 @@ namespace AibisDream.FixSystem private PunchTape punchTapeData; private PunchTapeDefinition punchTapeDefinition; + private int localizationRefreshVersion; private void OnEnable() { @@ -34,6 +35,7 @@ namespace AibisDream.FixSystem private void OnDisable() { LocalizationSettings.SelectedLocaleChanged -= HandleSelectedLocaleChanged; + localizationRefreshVersion++; } public void Setup(PunchTape punchTape, PunchTapeDefinition definition) @@ -57,8 +59,7 @@ namespace AibisDream.FixSystem if (nameText != null) { - RefreshLocalizedName(LocalizationKit.GetLocaleKind( - LocalizationSettings.SelectedLocale?.Identifier.Code)); + RefreshLocalizedName(LocalizationSettings.SelectedLocale); } memoryIndexLabelText?.RefreshString(); @@ -73,17 +74,26 @@ namespace AibisDream.FixSystem private void HandleSelectedLocaleChanged(Locale locale) { - RefreshLocalizedName(LocalizationKit.GetLocaleKind(locale?.Identifier.Code)); + RefreshLocalizedName(locale); } - private void RefreshLocalizedName(LocaleKind localeKind) + private async void RefreshLocalizedName(Locale locale) { if (nameText == null || punchTapeDefinition == null) return; + int version = ++localizationRefreshVersion; + string localizedName = await LocalizationKit.LocalizeParamAsync( + punchTapeDefinition.DisplayNameParam, + locale); + if (version != localizationRefreshVersion || + nameText == null || + punchTapeDefinition == null) + return; + nameText.StringReference.Arguments = new object[] { - LocalizationKit.LocalizeParam(punchTapeDefinition.DisplayNameParam, localeKind) + localizedName }; nameText.RefreshString(); } diff --git a/Assets/Scripts/Framework/Config/ConfigUtil.cs b/Assets/Scripts/Framework/Config/ConfigUtil.cs index 50372eeea..2bc332505 100644 --- a/Assets/Scripts/Framework/Config/ConfigUtil.cs +++ b/Assets/Scripts/Framework/Config/ConfigUtil.cs @@ -13,10 +13,8 @@ namespace AibisDream.Kit public class ConfigUtil : SingletonBase { private const string CharacterConfigPath = "/Config/character.csv"; - private const string ParamConfigPath = "/Config/params.csv"; private Dictionary _characters; - private Dictionary _params; /// /// 用于单例模式 @@ -41,7 +39,6 @@ namespace AibisDream.Kit public override void OnSingletonInit() { InitCharacterConfig(); - InitParamConfig(); } private void InitCharacterConfig() @@ -52,15 +49,6 @@ namespace AibisDream.Kit .ToDictionary(item => item.Key, item => item); } - private void InitParamConfig() - { - var paramList = CsvUtil.ReadAsBean(Application.streamingAssetsPath + ParamConfigPath); - _params = paramList - .Where(item => !string.IsNullOrWhiteSpace(item.Key)) - .ToDictionary(item => item.Key, item => item); - } - - /// /// 按key获取角色预设 /// @@ -72,17 +60,6 @@ namespace AibisDream.Kit return _characters.TryGetValue(key, out character); } - /// - /// 按key获取参数本地化配置 - /// - /// key - /// 参数配置 - /// 是否能获取 - public bool TryGetParam(string key, out Param param) - { - return _params.TryGetValue(key, out param); - } - /// /// 按类型读取Json配置 /// @@ -176,4 +153,4 @@ namespace AibisDream.Kit return fileNames; } } -} \ No newline at end of file +} diff --git a/Assets/Scripts/Framework/Config/Param.cs b/Assets/Scripts/Framework/Config/Param.cs deleted file mode 100644 index 021cfca82..000000000 --- a/Assets/Scripts/Framework/Config/Param.cs +++ /dev/null @@ -1,27 +0,0 @@ -using AibisDream.Framework; - -namespace AibisDream.Kit -{ - public class Param - { - public string Key { get; set; } - public string Cn { get; set; } - public string En { get; set; } - public string Ja { get; set; } - - public string LocalizeByKind(LocaleKind kind) - { - switch (kind) - { - case LocaleKind.En: - return string.IsNullOrWhiteSpace(En) ? Key : En; - case LocaleKind.Ja: - if (!string.IsNullOrWhiteSpace(Ja)) return Ja; - return string.IsNullOrWhiteSpace(Cn) ? Key : Cn; - case LocaleKind.Cn: - default: - return string.IsNullOrWhiteSpace(Cn) ? Key : Cn; - } - } - } -} diff --git a/Assets/Scripts/Framework/OtherKit/LocalizationKit.cs b/Assets/Scripts/Framework/OtherKit/LocalizationKit.cs index 76ad11c66..789e3e46a 100644 --- a/Assets/Scripts/Framework/OtherKit/LocalizationKit.cs +++ b/Assets/Scripts/Framework/OtherKit/LocalizationKit.cs @@ -1,5 +1,7 @@ using System.Collections; -using AibisDream.Kit; +using System; +using System.Threading.Tasks; +using AibisDream.Utility; using UnityEngine; using UnityEngine.Localization; using UnityEngine.Localization.Settings; @@ -127,39 +129,85 @@ namespace AibisDream.Framework } /// - /// 本地化参数 + /// 判断字符串是否为 l10n 参数引用。 /// - /// 参数名 - /// - public static string LocalizeParam(string param) + public static bool IsLocalizedParam(string value) { - return LocalizeParam(param, CurrentLocaleKind); + return !string.IsNullOrEmpty(value) && + value.StartsWith(LocalizationPrefix, StringComparison.Ordinal); } /// - /// 按指定语言列解析本地化参数,供需要响应 Locale 切换的非 Yarn UI 使用。 + /// 从 Params String Table 异步解析 l10n 参数。非 l10n 参数原样返回。 + /// 当前 Locale 条目为空时保留空值,不使用任何 Locale 回退。 /// - public static string LocalizeParam(string param, LocaleKind localeKind) + public static async Task LocalizeParamAsync(string param, Locale locale = null) { - // 非本地化参数直接返回 - if (string.IsNullOrEmpty(param)) return param; - if (!param.StartsWith(LocalizationPrefix)) return param; + if (!IsLocalizedParam(param)) + return param; - var localizationKey = param[LocalizationPrefix.Length..]; - if (ConfigUtil.Instance.TryGetParam(localizationKey, out var value)) + string localizationKey = GetL10NParamKey(param); + if (string.IsNullOrEmpty(localizationKey)) { - return value.LocalizeByKind(localeKind); + Debug.LogError("[LocalizationKit] l10n 参数缺少 Entry Key。"); + return "⟦empty⟧"; } - // 如果找不到,则使用原始值 - Debug.LogWarning($"Variable Key '{localizationKey}' not found in params csv."); - return localizationKey; + try + { + await LocalizationSettings.InitializationOperation.Task; + Locale resolvedLocale = locale != null ? locale : LocalizationSettings.SelectedLocale; + var operation = LocalizationSettings.StringDatabase.GetTableEntryAsync( + ConstRef.ParamsTable, + localizationKey, + resolvedLocale, + FallbackBehavior.DontUseFallback); + var result = await operation.Task; + if (result.Entry != null) + return result.Entry.LocalizedValue ?? string.Empty; + + string localeCode = resolvedLocale != null + ? resolvedLocale.Identifier.Code + : ""; + Debug.LogError( + $"[LocalizationKit] Params 缺少条目:Table={ConstRef.ParamsTable}, " + + $"Key={localizationKey}, Locale={localeCode}。"); + return $"⟦{localizationKey}⟧"; + } + catch (Exception exception) + { + string localeCode = locale != null + ? locale.Identifier.Code + : LocalizationSettings.SelectedLocale?.Identifier.Code ?? ""; + Debug.LogError( + $"[LocalizationKit] Params 查询失败:Table={ConstRef.ParamsTable}, " + + $"Key={localizationKey}, Locale={localeCode}。\n{exception}"); + return $"⟦{localizationKey}⟧"; + } + } + + /// + /// 协程版 l10n 参数解析,供 Yarn Command 和 MonoBehaviour 使用。 + /// + public static IEnumerator LocalizeParam( + string param, + Locale locale, + Action completed) + { + Task task = LocalizeParamAsync(param, locale); + while (!task.IsCompleted) + yield return null; + + string result = task.Status == TaskStatus.RanToCompletion + ? task.Result + : $"⟦{GetL10NParamKey(param)}⟧"; + completed?.Invoke(result); } public static string GetL10NParamKey(string key) { // 本地化参数则去除前缀,非本地化参数直接返回 - return key.StartsWith(LocalizationPrefix) ? key[LocalizationPrefix.Length..] : key; + return IsLocalizedParam(key) ? key[LocalizationPrefix.Length..] : key; } /// diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionContentCatalog.cs b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionContentCatalog.cs new file mode 100644 index 000000000..aa6f9f8d5 --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionContentCatalog.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using AibisDream.Framework; +using UnityEngine; + +namespace AibisDream.MiniGame.Language +{ + [Serializable] + public sealed class ExpressionRoundDefinition + { + [SerializeField] private string id; + [SerializeField] private string targetReference; + [SerializeField] private string tokenReference; + [SerializeField, Min(0)] private int nonTargetParticleCount; + + public string Id => id; + public string TargetReference => targetReference; + public string TokenReference => tokenReference; + public int NonTargetParticleCount => nonTargetParticleCount; + + public bool TryValidate(out string error) + { + if (string.IsNullOrWhiteSpace(id)) + { + error = "轮次 ID 不能为空。"; + return false; + } + + if (!LocalizationKit.IsLocalizedParam(targetReference)) + { + error = $"轮次 '{id}' 的目标文字必须配置为 l10n.* 引用。"; + return false; + } + + if (!LocalizationKit.IsLocalizedParam(tokenReference)) + { + error = $"轮次 '{id}' 的干扰 token 必须配置为 l10n.* 引用。"; + return false; + } + + if (nonTargetParticleCount < 0) + { + error = $"轮次 '{id}' 的非目标粒子数量不能为负数。"; + return false; + } + + error = null; + return true; + } + } + + [CreateAssetMenu( + fileName = "ExpressionContentCatalog", + menuName = AibisAssetMenus.HuoShanExpressionContent)] + public sealed class ExpressionContentCatalog : ScriptableObject + { + [SerializeField] private string actorLoopReference; + [SerializeField] private List rounds = new(); + + private Dictionary roundById; + + public string ActorLoopReference => actorLoopReference; + public IReadOnlyList Rounds => rounds; + + public bool TryGetRound(string roundId, out ExpressionRoundDefinition definition) + { + EnsureLookup(); + if (string.IsNullOrWhiteSpace(roundId)) + { + definition = null; + return false; + } + + return roundById.TryGetValue(roundId, out definition); + } + + public List GetValidationErrors() + { + var errors = new List(); + if (!LocalizationKit.IsLocalizedParam(actorLoopReference)) + errors.Add("Actor Loop 必须配置为 l10n.* 引用。"); + + var ids = new HashSet(StringComparer.Ordinal); + if (rounds == null || rounds.Count == 0) + { + errors.Add("Catalog 中没有表达轮次。"); + return errors; + } + + for (int i = 0; i < rounds.Count; i++) + { + ExpressionRoundDefinition round = rounds[i]; + if (round == null) + { + errors.Add($"rounds[{i}] 为空。"); + continue; + } + + if (!round.TryValidate(out string error)) + errors.Add(error); + if (!string.IsNullOrWhiteSpace(round.Id) && !ids.Add(round.Id)) + errors.Add($"存在重复轮次 ID:'{round.Id}'。"); + } + + return errors; + } + + private void EnsureLookup() + { + if (roundById != null) + return; + + roundById = new Dictionary(StringComparer.Ordinal); + if (rounds == null) + return; + + foreach (ExpressionRoundDefinition round in rounds) + { + if (round == null || string.IsNullOrWhiteSpace(round.Id) || roundById.ContainsKey(round.Id)) + continue; + roundById.Add(round.Id, round); + } + } + + private void OnValidate() + { + roundById = null; + } + } +} diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionContentCatalog.cs.meta b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionContentCatalog.cs.meta new file mode 100644 index 000000000..afcdaa8e8 --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionContentCatalog.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a86a72005fba4c99a59962fb9e92df83 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs index 6bdd898f3..8a3322ce0 100644 --- a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs +++ b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs @@ -4,6 +4,7 @@ using UnityEngine; using AibisDream.FixSystem; using AibisDream.MiniGame.Language; using System.Collections.Generic; +using UnityEngine.Localization; namespace AibisDream { @@ -17,6 +18,7 @@ namespace AibisDream [SerializeField] private GameObject expressionView; [SerializeField] private LogReleasePresentationController logReleasePresentation; [SerializeField] private ExpressionScreenPresentationController screenPresentation; + [SerializeField] private ExpressionContentCatalog contentCatalog; [Header("默认配置")] [SerializeField] private List defaultAnxietyPhrases = new List @@ -54,6 +56,18 @@ namespace AibisDream [SerializeField, Range(0.3f, 1f)] private float screenStressCap = 0.85f; public LogReleasePresentationController LogReleasePresentation => logReleasePresentation; + public ExpressionContentCatalog ContentCatalog => contentCatalog; + public Locale ActiveExpressionLocale { get; private set; } + + public Locale GetExpressionLocale(Locale fallback) + { + return ActiveExpressionLocale != null ? ActiveExpressionLocale : fallback; + } + + public void SetActiveExpressionLocale(Locale locale) + { + ActiveExpressionLocale = locale; + } /// 当前屏幕 Glitch 档位(0–1),由 expression_screen_glitch 持续保持。 public float ScreenGlitchLevel => screenGlitchIntensity; @@ -274,6 +288,7 @@ namespace AibisDream /// public void CloseView() { + ActiveExpressionLocale = null; screenPresentation?.ResetImmediate(); ResetScreenGlitchState(); ApplyScreenGlitchProperties(); @@ -941,6 +956,7 @@ namespace AibisDream private void OnDisable() { + ActiveExpressionLocale = null; ResetScreenGlitchState(); ApplyScreenGlitchProperties(); diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionTextTokenizer.cs b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionTextTokenizer.cs new file mode 100644 index 000000000..e01958d45 --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionTextTokenizer.cs @@ -0,0 +1,108 @@ +using System.Collections.Generic; +using System.Globalization; + +namespace AibisDream.MiniGame.Language +{ + /// + /// 将玩家可见文字按 Unicode 文本元素拆分。空白保留在布局中,但不生成粒子。 + /// 随机粒子字符池不使用本类,维持现有行为。 + /// + public static class ExpressionTextTokenizer + { + public readonly struct Layout + { + public Layout(List visibleElements, List offsets) + { + VisibleElements = visibleElements; + Offsets = offsets; + } + + public IReadOnlyList VisibleElements { get; } + public IReadOnlyList Offsets { get; } + public int Count => VisibleElements?.Count ?? 0; + } + + public static List GetVisibleElements(string text) + { + var result = new List(); + if (string.IsNullOrEmpty(text)) + return result; + + TextElementEnumerator enumerator = StringInfo.GetTextElementEnumerator(text); + while (enumerator.MoveNext()) + { + string element = enumerator.GetTextElement(); + if (!string.IsNullOrWhiteSpace(element)) + result.Add(element); + } + + return result; + } + + public static Layout BuildLayout( + string text, + float characterSpacing, + float whitespaceSpacingMultiplier) + { + var visible = new List(); + var positions = new List(); + if (string.IsNullOrEmpty(text)) + return new Layout(visible, positions); + + float spacing = characterSpacing > 0f ? characterSpacing : 1f; + float whitespaceAdvance = spacing * whitespaceSpacingMultiplier; + float cursor = 0f; + TextElementEnumerator enumerator = StringInfo.GetTextElementEnumerator(text); + while (enumerator.MoveNext()) + { + string element = enumerator.GetTextElement(); + if (string.IsNullOrWhiteSpace(element)) + { + if (visible.Count > 0) + cursor += whitespaceAdvance; + continue; + } + + visible.Add(element); + positions.Add(cursor); + cursor += spacing; + } + + if (positions.Count > 0) + { + float center = (positions[0] + positions[positions.Count - 1]) * 0.5f; + for (int i = 0; i < positions.Count; i++) + positions[i] -= center; + } + + return new Layout(visible, positions); + } + + public static int FindVisibleSequence( + IReadOnlyList source, + IReadOnlyList fragment) + { + if (source == null || fragment == null || fragment.Count == 0 || fragment.Count > source.Count) + return -1; + + int lastStart = source.Count - fragment.Count; + for (int start = 0; start <= lastStart; start++) + { + bool matches = true; + for (int i = 0; i < fragment.Count; i++) + { + if (!string.Equals(source[start + i], fragment[i], System.StringComparison.Ordinal)) + { + matches = false; + break; + } + } + + if (matches) + return start; + } + + return -1; + } + } +} diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionTextTokenizer.cs.meta b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionTextTokenizer.cs.meta new file mode 100644 index 000000000..ed272d648 --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionTextTokenizer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f8ba4ca26b9b46649c0da7161dc9c756 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs index 4939637b4..b3d8ed0f0 100644 --- a/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs +++ b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs @@ -93,6 +93,7 @@ namespace AibisDream.MiniGame.Language [SerializeField] private float floatingTextFontSize = 3f; // 背景浮动文字大小 [SerializeField] private bool targetParticleBold = true; // 目标粒子是否加粗 [SerializeField] private bool nonTargetParticleBold = false; // 非目标粒子是否加粗 + [SerializeField, Range(0.1f, 2f)] private float whitespaceSpacingMultiplier = 0.6f; [Header("非目标粒子干扰效果")] [SerializeField, Range(0f, 0.1f)] private float nonTargetShakeIntensity = 0.02f; // 非目标粒子抖动强度 @@ -354,10 +355,18 @@ namespace AibisDream.MiniGame.Language CaptureDefaultCandidateCountFromInspector(); string useSentence = string.IsNullOrEmpty(sentence) ? "这是一个测试示例" : sentence; + int visibleTargetCount = ExpressionTextTokenizer.GetVisibleElements(useSentence).Count; + if (visibleTargetCount == 0) + { + Debug.LogError("[LanguageParticleManager] 目标句没有可显示的 Unicode 文本元素;初始化已中止。"); + return; + } if (nonTargetParticleTotal >= 0) { - candidateCount = Mathf.Max(useSentence.Length + nonTargetParticleTotal, useSentence.Length); + candidateCount = Mathf.Max( + visibleTargetCount + nonTargetParticleTotal, + visibleTargetCount); } else { @@ -409,7 +418,7 @@ namespace AibisDream.MiniGame.Language TextParticle.SetAnxietyPhrases(anxietyPhrases); // 根据目标句子更新目标粒子数量 - redParticleCount = Mathf.Min(targetSentence.Length, candidateCount); + redParticleCount = Mathf.Min(visibleTargetCount, candidateCount); // 重新初始化游戏状态并播放入场动画 ResetGame(true); @@ -1247,7 +1256,11 @@ namespace AibisDream.MiniGame.Language SetStatusUIVisibility(true, 0f); orderedRedParticles = GetOrderedRedParticles(); - int displayLength = Mathf.Min(orderedRedParticles.Count, targetSentence.Length); + ExpressionTextTokenizer.Layout layout = ExpressionTextTokenizer.BuildLayout( + targetSentence, + 0.9f, + whitespaceSpacingMultiplier); + int displayLength = Mathf.Min(orderedRedParticles.Count, layout.Count); if (displayLength < orderedRedParticles.Count) { @@ -1256,20 +1269,17 @@ namespace AibisDream.MiniGame.Language for (int i = 0; i < displayLength; i++) { - finalTargetCharacters.Add(targetSentence[i].ToString()); + finalTargetCharacters.Add(layout.VisibleElements[i]); } Vector3 displayCenter = worldCanvas != null ? worldCanvas.transform.position : Vector3.zero; if (finalTargetCharacters.Count > 0) { - float spacing = 0.9f; - float totalWidth = spacing * Mathf.Max(0, finalTargetCharacters.Count - 1); - float startX = -totalWidth / 2f; finalTargetPositions.Clear(); Bounds clip = GetCompletionClipBounds(); for (int i = 0; i < finalTargetCharacters.Count; i++) { - Vector3 pos = displayCenter + new Vector3(startX + i * spacing, 0f, 0f); + Vector3 pos = displayCenter + new Vector3(layout.Offsets[i], 0f, 0f); finalTargetPositions.Add(ClampPositionToBounds(pos, clip)); } } @@ -2151,6 +2161,7 @@ namespace AibisDream.MiniGame.Language List particles = GetPresentationPhraseParticles(); if (!ValidatePresentationPhrase(text, particles, "真话失稳")) yield break; + List characters = ExpressionTextTokenizer.GetVisibleElements(text); Color unstableColor = truthAttackColor; if (!string.IsNullOrWhiteSpace(colorHex)) @@ -2169,8 +2180,8 @@ namespace AibisDream.MiniGame.Language float totalDuration = Mathf.Max(0.12f, duration); float resolvedShakeAmplitude = Mathf.Max(0f, shakeAmplitude); float resolvedShakeSpeed = Mathf.Max(0f, shakeSpeed); - List destinations = CalculatePresentationPhrasePositions(text.Length); - UpdateFinalPresentationPhrase(text, destinations); + List destinations = CalculatePresentationPhrasePositions(text); + UpdateFinalPresentationPhrase(characters, destinations); for (int i = 0; i < particles.Count; i++) { @@ -2197,13 +2208,13 @@ namespace AibisDream.MiniGame.Language particle.resolvedColorProgress = 1f; particle.calmProgress = 1f; - if (i >= text.Length) + if (i >= characters.Count) { particle.alpha = 0f; continue; } - particle.ForceSetCharacter(text[i].ToString()); + particle.ForceSetCharacter(characters[i]); particle.alpha = 1f; particle.SetFocusInterference( unstableColor, @@ -2221,7 +2232,7 @@ namespace AibisDream.MiniGame.Language yield return new WaitForSeconds(totalDuration); - for (int i = 0; i < text.Length && i < particles.Count; i++) + for (int i = 0; i < characters.Count && i < particles.Count; i++) { CandidateParticle particle = particles[i]; if (particle == null) @@ -2239,6 +2250,7 @@ namespace AibisDream.MiniGame.Language List particles = GetPresentationPhraseParticles(); if (!ValidatePresentationPhrase(text, particles, "真话攻击")) yield break; + List characters = ExpressionTextTokenizer.GetVisibleElements(text); PreparePresentationPhraseControl(); @@ -2247,8 +2259,8 @@ namespace AibisDream.MiniGame.Language float travelDuration = totalDuration * 0.68f; float settleDuration = Mathf.Max(0.04f, totalDuration - travelDuration); int directionSign = truthAttackSequenceIndex++ % 2 == 0 ? -1 : 1; - List destinations = CalculatePresentationPhrasePositions(text.Length); - UpdateFinalPresentationPhrase(text, destinations); + List destinations = CalculatePresentationPhrasePositions(text); + UpdateFinalPresentationPhrase(characters, destinations); for (int i = 0; i < particles.Count; i++) { @@ -2276,7 +2288,7 @@ namespace AibisDream.MiniGame.Language particle.resolvedColorProgress = 1f; particle.calmProgress = 1f; - if (i >= text.Length) + if (i >= characters.Count) { DOTween.To( () => particle.alpha, @@ -2288,7 +2300,7 @@ namespace AibisDream.MiniGame.Language continue; } - particle.ForceSetCharacter(text[i].ToString()); + particle.ForceSetCharacter(characters[i]); particle.alpha = 1f; particle.focusInterferenceProgress = 1f; @@ -2325,7 +2337,7 @@ namespace AibisDream.MiniGame.Language yield return new WaitForSeconds(totalDuration); - for (int i = 0; i < text.Length && i < particles.Count; i++) + for (int i = 0; i < characters.Count && i < particles.Count; i++) { CandidateParticle particle = particles[i]; if (particle == null) @@ -2344,6 +2356,7 @@ namespace AibisDream.MiniGame.Language List particles = GetPresentationPhraseParticles(); if (!ValidatePresentationPhrase(text, particles, "真话稳定")) yield break; + List characters = ExpressionTextTokenizer.GetVisibleElements(text); PreparePresentationPhraseControl(); @@ -2384,8 +2397,8 @@ namespace AibisDream.MiniGame.Language yield return new WaitForSeconds(breakDuration); - List destinations = CalculatePresentationPhrasePositions(text.Length); - UpdateFinalPresentationPhrase(text, destinations); + List destinations = CalculatePresentationPhrasePositions(text); + UpdateFinalPresentationPhrase(characters, destinations); for (int i = 0; i < particles.Count; i++) { CandidateParticle particle = particles[i]; @@ -2410,14 +2423,14 @@ namespace AibisDream.MiniGame.Language particle.resolvedColorProgress = 1f; particle.calmProgress = 1f; - if (i >= text.Length) + if (i >= characters.Count) { particle.alpha = 0f; particle.ClearFocusInterference(); continue; } - particle.ForceSetCharacter(text[i].ToString()); + particle.ForceSetCharacter(characters[i]); particle.SetFocusInterference(truthAttackColor, 0f, 0f); particle.focusInterferenceProgress = 1f; particle.alpha = 0.15f; @@ -2453,8 +2466,8 @@ namespace AibisDream.MiniGame.Language if (particle == null) continue; particle.ClearFocusInterference(); - particle.alpha = i < text.Length ? 1f : 0f; - if (i < text.Length) + particle.alpha = i < characters.Count ? 1f : 0f; + if (i < characters.Count) { particle.transform.position = destinations[i]; particle.transform.localScale = Vector3.one * focusZoomScale; @@ -2494,10 +2507,17 @@ namespace AibisDream.MiniGame.Language return false; } - if (text.Length > particles.Count) + int requiredParticles = ExpressionTextTokenizer.GetVisibleElements(text).Count; + if (requiredParticles == 0) + { + Debug.LogWarning($"[LanguageParticleManager] {context}没有可显示的文本元素;已安全跳过。"); + return false; + } + + if (requiredParticles > particles.Count) { Debug.LogWarning( - $"[LanguageParticleManager] {context}“{text}”需要 {text.Length} 个目标粒子," + + $"[LanguageParticleManager] {context}“{text}”需要 {requiredParticles} 个目标粒子," + $"当前只有 {particles.Count} 个;为避免截断已跳过。"); return false; } @@ -2521,27 +2541,32 @@ namespace AibisDream.MiniGame.Language connectionRenderer.TargetAlphaScale = 0f; } - private List CalculatePresentationPhrasePositions(int characterCount) + private List CalculatePresentationPhrasePositions(string text) { - var positions = new List(Mathf.Max(0, characterCount)); - Vector3 center = worldCanvas != null ? worldCanvas.transform.position : transform.position; float spacing = Mathf.Max(0.1f, truthAttackCharacterSpacing); - float startX = -spacing * Mathf.Max(0, characterCount - 1) * 0.5f; + ExpressionTextTokenizer.Layout layout = ExpressionTextTokenizer.BuildLayout( + text, + spacing, + whitespaceSpacingMultiplier); + var positions = new List(layout.Count); + Vector3 center = worldCanvas != null ? worldCanvas.transform.position : transform.position; Bounds clip = GetCompletionClipBounds(); - for (int i = 0; i < characterCount; i++) + for (int i = 0; i < layout.Count; i++) { - Vector3 position = center + new Vector3(startX + i * spacing, 0f, 0f); + Vector3 position = center + new Vector3(layout.Offsets[i], 0f, 0f); positions.Add(ClampPositionToBounds(position, clip)); } return positions; } - private void UpdateFinalPresentationPhrase(string text, List positions) + private void UpdateFinalPresentationPhrase( + IReadOnlyList characters, + List positions) { finalTargetCharacters.Clear(); finalTargetPositions.Clear(); - for (int i = 0; i < text.Length; i++) - finalTargetCharacters.Add(text[i].ToString()); + for (int i = 0; i < characters.Count; i++) + finalTargetCharacters.Add(characters[i]); finalTargetPositions.AddRange(positions); } @@ -2579,7 +2604,9 @@ namespace AibisDream.MiniGame.Language if (string.IsNullOrEmpty(fragment) || orderedRedParticles.Count == 0) yield break; - int startIndex = targetSentence.IndexOf(fragment, System.StringComparison.Ordinal); + List targetElements = ExpressionTextTokenizer.GetVisibleElements(targetSentence); + List fragmentElements = ExpressionTextTokenizer.GetVisibleElements(fragment); + int startIndex = ExpressionTextTokenizer.FindVisibleSequence(targetElements, fragmentElements); if (startIndex < 0) { Debug.LogWarning($"[LanguageParticleManager] 真话泄漏片段“{fragment}”不在目标句“{targetSentence}”中。"); @@ -2587,12 +2614,12 @@ namespace AibisDream.MiniGame.Language } var leaked = new List(); - int endExclusive = Mathf.Min(startIndex + fragment.Length, orderedRedParticles.Count); + int endExclusive = Mathf.Min(startIndex + fragmentElements.Count, orderedRedParticles.Count); for (int i = startIndex; i < endExclusive; i++) { CandidateParticle particle = orderedRedParticles[i]; if (particle == null) continue; - particle.ForceSetCharacter(targetSentence[i].ToString()); + particle.ForceSetCharacter(targetElements[i]); particle.isStatic = true; particle.alpha = 1f; leaked.Add(particle); diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/LanguageYarnCommand.cs b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageYarnCommand.cs index c393ecaaf..8487124fb 100644 --- a/Assets/Scripts/MiniGame/HuoShan/Language/LanguageYarnCommand.cs +++ b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageYarnCommand.cs @@ -1,7 +1,11 @@ using System.Collections; using System.Collections.Generic; +using System.Threading.Tasks; using AibisDream.FixSystem; using AibisDream; +using AibisDream.Framework; +using UnityEngine.Localization; +using UnityEngine.Localization.Settings; using Yarn.Unity; namespace AibisDream.MiniGame.Language @@ -13,6 +17,66 @@ namespace AibisDream.MiniGame.Language { private static AibisDream.ExpressionManager ExpressionManager => FixSystemCenter.SystemDic.Get(); + private static Locale GetCommandLocale() + { + Locale current = LocalizationSettings.SelectedLocale; + return ExpressionManager != null + ? ExpressionManager.GetExpressionLocale(current) + : current; + } + + private static IEnumerator ResolveTextReferences( + Locale locale, + System.Action completed, + params string[] references) + { + var tasks = new Task[references.Length]; + for (int i = 0; i < references.Length; i++) + tasks[i] = LocalizationKit.LocalizeParamAsync(references[i], locale); + + Task all = Task.WhenAll(tasks); + while (!all.IsCompleted) + yield return null; + + completed?.Invoke(all.Status == TaskStatus.RanToCompletion + ? all.Result + : System.Array.Empty()); + } + + private static bool ValidateRequiredText(string value, string command, string parameter) + { + if (!string.IsNullOrWhiteSpace(value)) + return true; + + UnityEngine.Debug.LogError( + $"[LanguageYarnCommand] {command} 的玩家可见参数 {parameter} 解析为空;命令已安全结束。"); + return false; + } + + public static bool TryParseExpressionTokens(string value, out List tokens) + { + tokens = new List(); + if (string.IsNullOrWhiteSpace(value)) + return false; + + if (value.Contains("|")) + { + UnityEngine.Debug.LogError( + "[LanguageYarnCommand] 干扰 token 使用了全角分隔符“|”;请改用半角“|”。"); + return false; + } + + string[] parts = value.Split('|'); + foreach (string part in parts) + { + string token = part.Trim(); + if (!string.IsNullOrEmpty(token)) + tokens.Add(token); + } + + return tokens.Count > 0; + } + private static bool TryGetPresentation(out LogReleasePresentationController presentation) { presentation = ExpressionManager != null @@ -30,8 +94,8 @@ namespace AibisDream.MiniGame.Language /// 启动表达粒子系统(在打开视图后调用),等粒子字入场完成即可返回(可交互);panel 动效并行不阻塞。 /// 流程:若有上一轮则先淡出字与连线 → 再淡入火山释放 log → 再启动新一轮并等粒子就绪。 /// 注意:Yarn Spinner 2.x 对同名 重载支持不可靠,只保留一个注册入口,用可选参数区分 3/4 参调用。 - /// <> - /// <> + /// <> + /// <> /// (第四项为「非目标」候选粒子总数,候选池 = 目标句字数 + 该值;不写第四项则用 Inspector 的 Candidate Count) /// /// 干扰短句(笑话等),用 | 分隔,用于非目标粒子字符池,如:"哈哈|嘿嘿|呵呵" @@ -60,21 +124,103 @@ namespace AibisDream.MiniGame.Language yield break; } - string[] phrases = null; - if (!string.IsNullOrEmpty(anxietyPhrasesStr)) + Locale locale = LocalizationSettings.SelectedLocale; + string[] localized = null; + yield return ResolveTextReferences( + locale, + values => localized = values, + anxietyPhrasesStr, + targetSentence); + if (localized == null || localized.Length != 2 || + !ValidateRequiredText(localized[0], "start_expression", nameof(anxietyPhrasesStr)) || + !ValidateRequiredText(localized[1], "start_expression", nameof(targetSentence))) + yield break; + + if (!TryParseExpressionTokens(localized[0], out List phrases)) { - phrases = anxietyPhrasesStr.Split('|'); - for (int i = 0; i < phrases.Length; i++) - { - phrases[i] = phrases[i].Trim(); - } + UnityEngine.Debug.LogError( + "[LanguageYarnCommand] start_expression 没有有效的干扰 token;命令已安全结束。"); + yield break; } // 先清理上一轮演出,再淡出旧文字/连线并恢复 Log 操作界面。 yield return ExpressionManager.PreparePresentationForNextRound(); yield return ExpressionManager.FadeOutParticlesBeforeNewRound(0f); yield return ExpressionManager.EnsureReleaseLogFadedIn(1f); - ExpressionManager.StartSystem(phrases, targetSentence, completionNode, nonTargetParticleTotal); + ExpressionManager.SetActiveExpressionLocale(locale); + ExpressionManager.StartSystem( + phrases, + localized[1], + completionNode, + nonTargetParticleTotal); + yield return ExpressionManager.WaitUntilExpressionFlowReady(); + } + + /// + /// 使用 ExpressionContentCatalog 中的轮次配置启动表达系统。 + /// <> + /// + [YarnCommand("start_expression_round")] + public static IEnumerator StartExpressionRound(string roundId, string completionNode = "") + { + if (ExpressionManager == null) + { + UnityEngine.Debug.LogError("ExpressionManager 未找到!请确保场景中有 ExpressionManager 组件。"); + yield break; + } + + ExpressionContentCatalog catalog = ExpressionManager.ContentCatalog; + if (catalog == null) + { + UnityEngine.Debug.LogError( + "[LanguageYarnCommand] ExpressionContentCatalog 未配置;无法启动表达轮次。"); + yield break; + } + + List catalogErrors = catalog.GetValidationErrors(); + if (catalogErrors.Count > 0) + { + UnityEngine.Debug.LogError( + "[LanguageYarnCommand] ExpressionContentCatalog 数据无效;无法启动表达轮次。\n" + + string.Join("\n", catalogErrors)); + yield break; + } + + if (!catalog.TryGetRound(roundId, out ExpressionRoundDefinition round)) + { + UnityEngine.Debug.LogError( + $"[LanguageYarnCommand] ExpressionContentCatalog 中不存在轮次 '{roundId}'。"); + yield break; + } + + Locale locale = LocalizationSettings.SelectedLocale; + string[] localized = null; + yield return ResolveTextReferences( + locale, + values => localized = values, + round.TokenReference, + round.TargetReference); + if (localized == null || localized.Length != 2 || + !ValidateRequiredText(localized[0], "start_expression_round", nameof(round.TokenReference)) || + !ValidateRequiredText(localized[1], "start_expression_round", nameof(round.TargetReference))) + yield break; + + if (!TryParseExpressionTokens(localized[0], out List phrases)) + { + UnityEngine.Debug.LogError( + $"[LanguageYarnCommand] 表达轮次 '{roundId}' 没有有效的干扰 token。"); + yield break; + } + + yield return ExpressionManager.PreparePresentationForNextRound(); + yield return ExpressionManager.FadeOutParticlesBeforeNewRound(0f); + yield return ExpressionManager.EnsureReleaseLogFadedIn(1f); + ExpressionManager.SetActiveExpressionLocale(locale); + ExpressionManager.StartSystem( + phrases, + localized[1], + completionNode ?? string.Empty, + round.NonTargetParticleCount); yield return ExpressionManager.WaitUntilExpressionFlowReady(); } @@ -241,8 +387,17 @@ namespace AibisDream.MiniGame.Language UnityEngine.Debug.LogError("ExpressionManager 未找到!"); yield break; } + string[] localized = null; + yield return ResolveTextReferences( + GetCommandLocale(), + values => localized = values, + keyword); + if (localized == null || localized.Length != 1 || + !ValidateRequiredText(localized[0], "expression_lie_begin", nameof(keyword))) + yield break; + yield return ExpressionManager.StartCoroutine( - ExpressionManager.BeginExpressionLie(keyword, preset)); + ExpressionManager.BeginExpressionLie(localized[0], preset)); } [YarnCommand("expression_lie_break")] @@ -385,24 +540,37 @@ namespace AibisDream.MiniGame.Language /// /// 结尾真假快速闪切(非阻塞,节奏由同长度 wait 控制):真心话与谎话交替独占屏幕, /// 两者的屏幕底色(电视头背景)不同,越到后面切得越快,结束定格在真话帧。 - /// <> + /// <> /// [YarnCommand("expression_truth_lie_flicker")] - public static void ExpressionTruthLieFlicker( + public static IEnumerator ExpressionTruthLieFlicker( string liePhrases, string truthPhrase, float duration, float interval = 0.24f, float fontSize = 12f) { - if (TryGetPresentation(out LogReleasePresentationController presentation)) - presentation.StartTruthLieFlicker(liePhrases, truthPhrase, duration, interval, fontSize); + if (!TryGetPresentation(out LogReleasePresentationController presentation)) + yield break; + + string[] localized = null; + yield return ResolveTextReferences( + GetCommandLocale(), + values => localized = values, + liePhrases, + truthPhrase); + if (localized == null || localized.Length != 2 || + !ValidateRequiredText(localized[0], "expression_truth_lie_flicker", nameof(liePhrases)) || + !ValidateRequiredText(localized[1], "expression_truth_lie_flicker", nameof(truthPhrase))) + yield break; + + presentation.StartTruthLieFlicker(localized[0], localized[1], duration, interval, fontSize); } /// /// 定格的谎话被 glitch 翻转成真话(阻塞):大字乱跳中长度从谎话渐变到真话、真假底色抢闪, /// 最后落在暖色真话。紧接 expression_memory_cut + expression_truth_snap 收尾。 - /// <> + /// <> /// [YarnCommand("expression_lie_morph_truth")] public static IEnumerator ExpressionLieMorphTruth( @@ -412,8 +580,19 @@ namespace AibisDream.MiniGame.Language { if (!TryGetPresentation(out LogReleasePresentationController presentation)) yield break; + string[] localized = null; + yield return ResolveTextReferences( + GetCommandLocale(), + values => localized = values, + lieText, + truthText); + if (localized == null || localized.Length != 2 || + !ValidateRequiredText(localized[0], "expression_lie_morph_truth", nameof(lieText)) || + !ValidateRequiredText(localized[1], "expression_lie_morph_truth", nameof(truthText))) + yield break; + yield return ExpressionManager.StartCoroutine( - presentation.MorphLieToTruth(lieText, truthText, duration)); + presentation.MorphLieToTruth(localized[0], localized[1], duration)); } [YarnCommand("expression_truth_leak")] @@ -421,24 +600,44 @@ namespace AibisDream.MiniGame.Language { if (!TryGetPresentation(out LogReleasePresentationController presentation)) yield break; + string[] localized = null; + yield return ResolveTextReferences( + GetCommandLocale(), + values => localized = values, + fragment); + if (localized == null || localized.Length != 1 || + !ValidateRequiredText(localized[0], "expression_truth_leak", nameof(fragment))) + yield break; + yield return ExpressionManager.StartCoroutine( - presentation.LeakTruthFragment(fragment, duration)); + presentation.LeakTruthFragment(localized[0], duration)); } [YarnCommand("expression_lie_shuffle")] - public static void ExpressionLieShuffle( + public static IEnumerator ExpressionLieShuffle( string characterPool, float duration, float interval) { - if (TryGetPresentation(out LogReleasePresentationController presentation)) - presentation.StartLieCharacterShuffle(characterPool, duration, interval); + if (!TryGetPresentation(out LogReleasePresentationController presentation)) + yield break; + + string[] localized = null; + yield return ResolveTextReferences( + GetCommandLocale(), + values => localized = values, + characterPool); + if (localized == null || localized.Length != 1 || + !ValidateRequiredText(localized[0], "expression_lie_shuffle", nameof(characterPool))) + yield break; + + presentation.StartLieCharacterShuffle(localized[0], duration, interval); } /// /// 演员大字闪现(阻塞):抖动的粒子字瞬间隐藏,屏幕中央出现白色大字(红青色散双影+急速缩放), /// 停顿 holdDuration 秒后始终恢复粒子字抖动;结尾定格请使用 expression_actor_flash_in。 - /// <> + /// <> /// [YarnCommand("expression_actor_flash")] public static IEnumerator ExpressionActorFlash( @@ -454,9 +653,19 @@ namespace AibisDream.MiniGame.Language { if (!TryGetPresentation(out LogReleasePresentationController presentation)) yield break; + + string[] localized = null; + yield return ResolveTextReferences( + GetCommandLocale(), + values => localized = values, + text); + if (localized == null || localized.Length != 1 || + !ValidateRequiredText(localized[0], "expression_actor_flash", nameof(text))) + yield break; + yield return ExpressionManager.StartCoroutine( presentation.FlashActorLie( - text, + localized[0], holdDuration, fontSize, settledScale, @@ -470,7 +679,7 @@ namespace AibisDream.MiniGame.Language /// /// 演员大字入场并保持(阻塞):入场表现与 expression_actor_flash 相同,但不执行退场, /// 适合在段落结尾定格。后续清场命令仍会正常移除它。 - /// <> + /// <> /// [YarnCommand("expression_actor_flash_in")] public static IEnumerator ExpressionActorFlashIn( @@ -486,9 +695,19 @@ namespace AibisDream.MiniGame.Language { if (!TryGetPresentation(out LogReleasePresentationController presentation)) yield break; + + string[] localized = null; + yield return ResolveTextReferences( + GetCommandLocale(), + values => localized = values, + text); + if (localized == null || localized.Length != 1 || + !ValidateRequiredText(localized[0], "expression_actor_flash_in", nameof(text))) + yield break; + yield return ExpressionManager.StartCoroutine( presentation.FlashActorLie( - text, + localized[0], holdDuration, fontSize, settledScale, @@ -505,10 +724,36 @@ namespace AibisDream.MiniGame.Language /// <> /// [YarnCommand("expression_actor_flash_loop_start")] - public static void ExpressionActorFlashLoopStart() + public static IEnumerator ExpressionActorFlashLoopStart() { - if (TryGetPresentation(out LogReleasePresentationController presentation)) - presentation.StartActorFlashLoop(); + if (!TryGetPresentation(out LogReleasePresentationController presentation)) + yield break; + + ExpressionContentCatalog catalog = ExpressionManager.ContentCatalog; + if (catalog == null || !LocalizationKit.IsLocalizedParam(catalog.ActorLoopReference)) + { + UnityEngine.Debug.LogError( + "[LanguageYarnCommand] Actor Loop 本地化引用未配置;命令已安全结束。"); + yield break; + } + + string[] localized = null; + yield return ResolveTextReferences( + GetCommandLocale(), + values => localized = values, + catalog.ActorLoopReference); + if (localized == null || localized.Length != 1 || + !ValidateRequiredText(localized[0], "expression_actor_flash_loop_start", "ActorLoopReference")) + yield break; + + if (!TryParseExpressionTokens(localized[0], out List phrases)) + { + UnityEngine.Debug.LogError( + "[LanguageYarnCommand] Actor Loop 短句列表无效;命令已安全结束。"); + yield break; + } + + presentation.StartActorFlashLoop(phrases); } /// @@ -525,7 +770,7 @@ namespace AibisDream.MiniGame.Language /// /// 真话闪现(阻塞):谎话大字之间,暖色完整真话带细微抖动挣扎浮现一拍; /// "被掐断"由紧随其后的 expression_glitch_pulse 表现。不推进谎话冲击等级。 - /// <> + /// <> /// [YarnCommand("expression_actor_truth_flash")] public static IEnumerator ExpressionActorTruthFlash( @@ -537,25 +782,52 @@ namespace AibisDream.MiniGame.Language { if (!TryGetPresentation(out LogReleasePresentationController presentation)) yield break; + + string[] localized = null; + yield return ResolveTextReferences( + GetCommandLocale(), + values => localized = values, + text); + if (localized == null || localized.Length != 1 || + !ValidateRequiredText(localized[0], "expression_actor_truth_flash", nameof(text))) + yield break; + yield return ExpressionManager.StartCoroutine( - presentation.FlashActorTruth(text, holdDuration, fontSize, alpha, jitter)); + presentation.FlashActorTruth(localized[0], holdDuration, fontSize, alpha, jitter)); } /// /// 老虎机换字(非阻塞,节拍由 Yarn wait 控制):聚焦字在 duration 内只从 charPool 里疯狂换字。 /// truthPool 非空时其字符也混入换字池,并以真话暖色显示。 - /// <> + /// <> /// [YarnCommand("expression_actor_slot")] - public static void ExpressionActorSlot( + public static IEnumerator ExpressionActorSlot( string charPool, float duration, float interval = 0.05f, float fontSize = 14f, string truthPool = "") { - if (TryGetPresentation(out LogReleasePresentationController presentation)) - presentation.StartActorSlotMachine(charPool, duration, interval, fontSize, truthPool); + if (!TryGetPresentation(out LogReleasePresentationController presentation)) + yield break; + + string[] localized = null; + yield return ResolveTextReferences( + GetCommandLocale(), + values => localized = values, + charPool, + truthPool); + if (localized == null || localized.Length != 2 || + !ValidateRequiredText(localized[0], "expression_actor_slot", nameof(charPool))) + yield break; + + presentation.StartActorSlotMachine( + localized[0], + duration, + interval, + fontSize, + localized[1] ?? string.Empty); } /// @@ -583,7 +855,7 @@ namespace AibisDream.MiniGame.Language /// /// 单字/短词满屏重击,并等待该拍结束。 - /// <> + /// <> /// [YarnCommand("expression_actor_word_hit")] public static IEnumerator ExpressionActorWordHit( @@ -594,13 +866,23 @@ namespace AibisDream.MiniGame.Language { if (!TryGetPresentation(out LogReleasePresentationController presentation)) yield break; + + string[] localized = null; + yield return ResolveTextReferences( + GetCommandLocale(), + values => localized = values, + text); + if (localized == null || localized.Length != 1 || + !ValidateRequiredText(localized[0], "expression_actor_word_hit", nameof(text))) + yield break; + yield return ExpressionManager.StartCoroutine( - presentation.PlayActorWordHit(text, holdDuration, fontSize, punchScale)); + presentation.PlayActorWordHit(localized[0], holdDuration, fontSize, punchScale)); } /// /// 单列词组严格裁在表情屏内,像字幕一样无缝向下滚动。 - /// <> + /// <> /// [YarnCommand("expression_actor_scroll")] public static IEnumerator ExpressionActorScroll( @@ -612,9 +894,19 @@ namespace AibisDream.MiniGame.Language { if (!TryGetPresentation(out LogReleasePresentationController presentation)) yield break; + + string[] localized = null; + yield return ResolveTextReferences( + GetCommandLocale(), + values => localized = values, + phraseList); + if (localized == null || localized.Length != 1 || + !ValidateRequiredText(localized[0], "expression_actor_scroll", nameof(phraseList))) + yield break; + yield return ExpressionManager.StartCoroutine( presentation.ScrollActorLies( - phraseList, + localized[0], scrollDuration, loopDuration, holdDuration, @@ -742,7 +1034,7 @@ namespace AibisDream.MiniGame.Language /// /// 用现有目标粒子完整显示一个红色攻击词,并等待确定性的冲击动作结束。 /// 超过目标粒子数量时会安全跳过,不会截断。 - /// <> + /// <> /// [YarnCommand("expression_truth_attack")] public static IEnumerator ExpressionTruthAttack( @@ -753,17 +1045,26 @@ namespace AibisDream.MiniGame.Language if (!TryGetPresentation(out LogReleasePresentationController presentation)) yield break; + string[] localized = null; + yield return ResolveTextReferences( + GetCommandLocale(), + values => localized = values, + text); + if (localized == null || localized.Length != 1 || + !ValidateRequiredText(localized[0], "expression_truth_attack", nameof(text))) + yield break; + // 红屏武装时每个攻击词都把屏幕顶一下(punch),力度跟着这个词的 impact 走。 // 写在这里而不是 Yarn 里逐条加 pulse:攻击列表经常重调,两边手写必然脱节。 ExpressionManager.PunchScreenForAttack(impact); yield return ExpressionManager.StartCoroutine( - presentation.PlayTruthAttack(text, duration, impact)); + presentation.PlayTruthAttack(localized[0], duration, impact)); } /// /// Keep an already resolved truth phrase in place while it gradually becomes unstable and red. - /// <> + /// <> /// [YarnCommand("expression_truth_destabilize")] public static IEnumerator ExpressionTruthDestabilize( @@ -775,9 +1076,19 @@ namespace AibisDream.MiniGame.Language { if (!TryGetPresentation(out LogReleasePresentationController presentation)) yield break; + + string[] localized = null; + yield return ResolveTextReferences( + GetCommandLocale(), + values => localized = values, + text); + if (localized == null || localized.Length != 1 || + !ValidateRequiredText(localized[0], "expression_truth_destabilize", nameof(text))) + yield break; + yield return ExpressionManager.StartCoroutine( presentation.DestabilizeTruthPhrase( - text, + localized[0], duration, shakeAmplitude, shakeSpeed, @@ -786,7 +1097,7 @@ namespace AibisDream.MiniGame.Language /// /// 将当前攻击词重组为固定青绿色真话,并等待稳定完成。 - /// <> + /// <> /// [YarnCommand("expression_truth_settle")] public static IEnumerator ExpressionTruthSettle( @@ -795,8 +1106,18 @@ namespace AibisDream.MiniGame.Language { if (!TryGetPresentation(out LogReleasePresentationController presentation)) yield break; + + string[] localized = null; + yield return ResolveTextReferences( + GetCommandLocale(), + values => localized = values, + text); + if (localized == null || localized.Length != 1 || + !ValidateRequiredText(localized[0], "expression_truth_settle", nameof(text))) + yield break; + yield return ExpressionManager.StartCoroutine( - presentation.SettleTruthPhrase(text, duration)); + presentation.SettleTruthPhrase(localized[0], duration)); } [YarnCommand("expression_truth_blackout")] diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/LogReleasePresentationController.cs b/Assets/Scripts/MiniGame/HuoShan/Language/LogReleasePresentationController.cs index 7543401dc..623578141 100644 --- a/Assets/Scripts/MiniGame/HuoShan/Language/LogReleasePresentationController.cs +++ b/Assets/Scripts/MiniGame/HuoShan/Language/LogReleasePresentationController.cs @@ -808,14 +808,19 @@ namespace AibisDream.MiniGame.Language } } - public void StartActorFlashLoop() + public void StartActorFlashLoop(IReadOnlyList phrases) { EnsureRuntimeObjects(); if (!ExpectState(nameof(StartActorFlashLoop), PresentationState.Memory)) return; + if (phrases == null || phrases.Count == 0) + { + Debug.LogError("[LogReleasePresentation] Actor Flash Loop 没有有效短句;已安全跳过。"); + return; + } StopActorFlashLoopImmediate(true); - actorFlashLoopRoutine = StartCoroutine(ActorFlashLoopRoutine()); + actorFlashLoopRoutine = StartCoroutine(ActorFlashLoopRoutine(phrases)); } public void StopActorFlashLoop() @@ -823,27 +828,33 @@ namespace AibisDream.MiniGame.Language StopActorFlashLoopImmediate(true); } - private IEnumerator ActorFlashLoopRoutine() + private IEnumerator ActorFlashLoopRoutine(IReadOnlyList phrases) { while (state == PresentationState.Memory) { - yield return FlashActorLie( - "没问题", 0.25f, 10.5f, 1.1f, 1.45f, 0.008f, 0.06f, 0.50f, 0f); - if (state != PresentationState.Memory) - break; - yield return new WaitForSeconds(0.3f); + for (int i = 0; i < phrases.Count && state == PresentationState.Memory; i++) + { + int style = i % 3; + float hold = style == 2 ? 0.15f : 0.25f; + float fontSize = style == 0 ? 10.5f : style == 1 ? 11.2f : 12f; + float punchScale = style == 0 ? 1.45f : style == 1 ? 1.85f : 2.20f; + float chromaDistance = style == 2 ? 0.010f : 0.008f; + float chromaAlpha = style == 2 ? 0.08f : 0.06f; + float impact = style == 0 ? 0.50f : style == 1 ? 0.72f : 1f; - yield return FlashActorLie( - "冇问题", 0.25f, 11.2f, 1.1f, 1.85f, 0.008f, 0.06f, 0.72f, 0f); - if (state != PresentationState.Memory) - break; - yield return new WaitForSeconds(0.3f); - - yield return FlashActorLie( - "帽问题", 0.15f, 12.0f, 1.1f, 2.20f, 0.010f, 0.08f, 1.00f, 0f); - if (state != PresentationState.Memory) - break; - yield return new WaitForSeconds(0.3f); + yield return FlashActorLie( + phrases[i], + hold, + fontSize, + 1.1f, + punchScale, + chromaDistance, + chromaAlpha, + impact, + 0f); + if (state == PresentationState.Memory) + yield return new WaitForSeconds(0.3f); + } } actorFlashLoopRoutine = null; diff --git a/Assets/StreamingAssets/Config/params.csv b/Assets/StreamingAssets/Config/params.csv deleted file mode 100644 index 2f40c4aa3..000000000 --- a/Assets/StreamingAssets/Config/params.csv +++ /dev/null @@ -1,9 +0,0 @@ -键,中文,英文,日文 -Key,Cn,En,Ja -天空,天空,Sky,そら -海报,海报,Poster,ポスター -植物,植物,Plant,植物 -朋友,朋友,Friend,友達 -情绪,情绪,Emo,情緒 -记忆,记忆,Memory,記憶 -逻辑,逻辑,Logic,論理 diff --git a/Assets/Yarn/FP/FP_Huoshan1/Stage6.yarn b/Assets/Yarn/FP/FP_Huoshan1/Stage6.yarn index 6b875a86d..b73ec5dfd 100644 --- a/Assets/Yarn/FP/FP_Huoshan1/Stage6.yarn +++ b/Assets/Yarn/FP/FP_Huoshan1/Stage6.yarn @@ -71,7 +71,7 @@ me: 这一条看起来……是一个关于焦虑的记录。 #line:046ab93d Task: 第 1 条log:左键排斥、右键吸引,连接目标并分离干扰 #line:07a299c <> <> -<> +<> === @@ -131,8 +131,8 @@ position: 900,2100 // JumpDevTool 专用:从 Stage6就绪存档恢复后,跳过粒子玩法直接预览 LOG1 演出 <> <> -<> -// <> +<> +// <> === @@ -243,7 +243,7 @@ Task: 第 2 条log:左键排斥、右键吸引,连接目标并分离干扰 # <> <> <> -<> +<> === @@ -297,7 +297,7 @@ position: 900,2550 // JumpDevTool 专用:从 Stage6就绪存档恢复后,跳过粒子玩法直接预览 LOG2 演出 <> <> -<> +<> === @@ -399,7 +399,7 @@ Task: 第 3 条log:左键排斥、右键吸引,连接目标并分离干扰 # <> <> <> -<> +<> === @@ -462,7 +462,7 @@ position: 900,3000 // JumpDevTool 专用:从 Stage6就绪存档恢复后,跳过粒子玩法直接预览 LOG3 演出 <> <> -<> +<> === @@ -507,7 +507,7 @@ hs: …… #line:085bda6 <> <> hs: 不要那样看我…… #line:00e7b75 -<> +<> <> === @@ -528,69 +528,69 @@ me: 等会…其他的log好像有异常传入…… #line:0d3cc39 // 节奏不是一句一格。四种拍子交替:**空**(留白)/ **连**(挤在一起吐出来)/ // **顿**(狠话之后的静默)/ **急**(越来越快)。均匀推进会把最狠的几句读平。 // —— 空:起手两记,各自留白 -<> +<> hs: 为什么#auto_next #line:059ef58 <> -<> +<> hs: 不相信我? #line:06810ae <> // —— 连:辩解一口气倒出来,中间不喘 -<> +<> hs: 我明白我垫底, #line:00248ff -<> +<> hs: 可是这周天气真的不行! #line:0ffc958 -<> +<> hs: 其它销售……也是一顶都卖不出去! #line:0d2c25f // —— 顿:辩解说完,发现没人信,卡壳一下 <> -<> +<> hs: 为什么我都这么努力解释了,#auto_next #line:0ebd252 -<> +<> hs: 你还是不愿意相信我? #line:0328a7c -<> +<> hs: 到底它们是你的员工,还是我?#auto_next #line:0a915ef hs: 你对我从来没有一点信任…… #line:0b69892 // —— 顿:最长的一次静默,攒住再爆 <> -<> +<> hs: 我真的受够了! #line:0312421 hs: 总是被你当狗一样训,#auto_next #line:00412cb -<> +<> <> -<> +<> <> -<> +<> <> -<> +<> hs: 路斯,我它码真该死! #line:03678e3 -<> +<> hs: 我真没用! #line:093977a <> -<> +<> hs: 你不知道我有多想证明自己,#auto_next #line:0555cab -<> +<> hs: 可是我什么都做不到…… #line:0cf70a3 <> // —— 急:一句比一句挤,往最狠的那句冲 -<> +<> hs: 我没办法追上你,#auto_next #line:0deff66 -<> +<> hs: 没办法帮到英理,#auto_next #line:0c2e7b8 -<> +<> hs: 甚至连自己被羞辱了都没有勇气#auto_next #line:00ab3a7 -<> +<> hs: 说出来,#auto_next #line:0ad49b4 // —— 顿:最狠那句前面一定要有空 <> -<> +<> hs: 我就是个废物…… #line:079cc40 -<> +<> <> -<> +<> <> -<> +<> <> -<> +<> hs: 我它码就是个烂泥扶不上墙的废物!! #line:0e8a4da // —— 空:吼到底之后整个泄下来,转成气音 <> @@ -602,15 +602,15 @@ hs: 就这么想赢一次…… #line:08aece0 <> // 五连击必须**错开**:原本五条挤在同一帧,五记 punch 会合成一记,读不出连击。 // 间隔越来越短 = 越捶越急。 -<> +<> <> -<> +<> <> -<> +<> <> -<> +<> <> -<> +<> <> <> <> @@ -622,7 +622,7 @@ hs: 就这么想赢一次…… #line:08aece0 <> <> hs: 我真的好想… #line:02fff1d -<> +<> <> <> me: …… #line:04c2999 diff --git a/Docs/HuoShan/HuoshanStage6LocalizationPlan.md b/Docs/HuoShan/HuoshanStage6LocalizationPlan.md index ba0098b19..7d5183d27 100644 --- a/Docs/HuoShan/HuoshanStage6LocalizationPlan.md +++ b/Docs/HuoShan/HuoshanStage6LocalizationPlan.md @@ -1,412 +1,93 @@ -# 火山 Stage6 本地化完整实施计划 +# 火山 Stage6 / ExpressSystem 本地化方案 -## 1. 目标与边界 +## 当前决策 -Stage6 继续使用项目现有的两套本地化入口: +ExpressSystem 的玩家可见玩法文字统一通过 `l10n.*` 引用,并以 Unity +Localization 的 `Params` String Table Collection 作为唯一数据源。 -- 固定 UI:Unity Localization 的现有 `UIText` String Table Collection。 -- Stage6 玩法及演出内容:新增独立的 `HuoshanStage6` String Table Collection。 -- Yarn 普通对白:继续沿用项目 Yarn Spinner 本地化体系,但本轮不启用 `FP_Huoshan1` 的整章对白翻译。 -- `params.csv` 继续只用于 `l10n.*` 通用短语替换,不承载 Stage6 玩法文本。 +- `l10n.xxx` 查询 `Params/xxx`。 +- 查询使用 `DontUseFallback`,不回退中文或其他 Locale。 +- 中文表保存源数据;英文、日文表保留同一套 Key,未翻译值留空。 +- Key 不存在时显示 `⟦key⟧` 并记录包含 Table、Key、Locale 的错误。 +- Express 必填值解析为空时安全终止命令,不启动粒子或释放演出。 +- `StreamingAssets/Config/params.csv` 已退出运行链路。 +- 固定 UI 继续使用 `UIText`;普通 Yarn 对白、Task、教学和角色名不迁入 + `Params`。 -本轮纳入: +## 轮次配置 -- 表达小游戏目标、输出、整合率、干扰数量等固定 UI。 -- 三轮语言粒子的目标句和干扰 token。 -- LOG3 释放演出中的攻击词、稳定词、失稳词等动态文字。 -- 相关程序接口、Yarn 命令协议和使用文档。 -- 中、英、日三种语言数据;英日文首版标记为待母语审校。 +三轮正式内容由 `ExpressionContentCatalog` 管理,场景中的 +`ExpressionManager` 持有 Catalog 引用: -本轮明确暂缓: +| Round ID | Target | Tokens | 非目标粒子数 | +|---|---|---|---:| +| `log1` | `l10n.hs.exp.log1.target` | `l10n.hs.exp.log1.tokens` | 16 | +| `log2` | `l10n.hs.exp.log2.target` | `l10n.hs.exp.log2.tokens` | 28 | +| `log3` | `l10n.hs.exp.log3.target` | `l10n.hs.exp.log3.tokens` | 38 | -- 普通角色对白。 -- Task 面板中的“释放表达模块的阻塞 log”和 LOG1/2/3 任务提示。 -- “左键排斥、右键吸引”等 Yarn 操作教学。 -- Yarn line tag 和 Yarn 本地化 CSV。 -- 调试日志、Inspector Tooltip、对象名、节点名等开发文本。 +Actor Loop 使用 `l10n.hs.exp.actor.loop`。Round ID 大小写敏感;重复 ID、 +未知 ID、空引用、非 `l10n.*` 引用或解析后的空值均视为数据错误。 -因此,本轮完成后核心小游戏及释放演出可以多语言运行,但任务面板与操作教学仍会显示中文,等待之后随对白本地化统一处理。 - -## 2. 本地化数据结构 - -### 2.1 固定 UI:`UIText` - -继续使用现有键,不新建重复条目: - -| Key | 用途 | 中文示例 | 英文草稿 | 日文草稿 | -|---|---|---|---|---| -| `huoshan_expression_objective` | 小游戏目标 | 目标:分离红色干扰,连接蓝色真心 | OBJECTIVE: ISOLATE RED INTERFERENCE. CONNECT THE BLUE TRUTH. | 目標:赤い干渉を分離し、青い本音をつなげる | -| `huoshan_expression_output` | 输出按钮 | 输出 | OUTPUT | 出力 | -| `huoshan_expression_integration` | 整合率 | 语言整合率:{0}% | LANGUAGE INTEGRATION: {0}% | 言語統合率:{0}% | -| `huoshan_expression_interference` | 干扰数量 | 干扰思绪:{0} | INTERFERENCE SIGNALS: {0} | 干渉思考:{0} | - -要求: - -- 保留 `{0}` 参数,程序继续通过格式化参数填入数值。 -- 英文和日文目前的中文占位必须替换。 -- 固定 UI 继续使用现有 `LocalizeStringEvent` 或 `StringDatabase` 绑定。 -- 切换 Locale 时立即刷新这些固定 UI。 - -### 2.2 玩法内容:`HuoshanStage6` - -新增 String Table Collection: - -```text -HuoshanStage6 -├─ zh-Hans -├─ en -└─ ja-JP -``` - -键名只表达语义,不包含语言、行号或 Yarn 节点名。 - -#### 语言粒子数据 - -```text -round.log1.target -round.log1.interference_tokens - -round.log2.target -round.log2.interference_tokens - -round.log3.target -round.log3.interference_tokens -``` - -中文源数据: - -| Key | 中文值 | -|---|---| -| `round.log1.target` | 我就是个笑话 | -| `round.log1.interference_tokens` | 哈哈\|嘿嘿\|呵呵 | -| `round.log2.target` | 不要离开我 | -| `round.log2.interference_tokens` | 哈哈哈\|嘿嘿嘿\|呵呵呵\|笑死了\|好好笑 | -| `round.log3.target` | 不要那样看我 | -| `round.log3.interference_tokens` | 哈哈哈哈\|笑死我了\|太好笑了\|绷不住了\|笑不活了\|真的太搞笑 | - -`interference_tokens` 的表值使用 `|` 分隔。解析规则: - -- 按 `|` 拆分。 -- 对每项执行 Trim。 -- 丢弃空项。 -- 至少保留一个有效 token,否则视为数据错误。 -- token 由本地化人员针对语言重新创作,不要求与中文逐字对应。 -- 英文示例:`HA|HAHA|LOL|JK`。 -- 日文示例:`ハハ|笑|冗談|ウケる`。 - -#### LOG3 释放演出数据 - -按语义建立以下键;重复演出复用同一个键: - -```text -release.attack.why -release.attack.dont_believe_me -release.attack.i_know_im_last -release.attack.bad_weather -release.attack.cannot_sell -release.attack.try_to_explain -release.attack.refuse_to_believe -release.attack.a_little_trust -release.attack.had_enough -release.attack.damn -release.attack.useless -release.attack.prove_myself -release.attack.cannot_do_anything -release.attack.catch_up -release.attack.help_yingli -release.attack.no_courage -release.attack.say_it -release.attack.trash -release.attack.hopeless -release.attack.why_loop - -release.settle.want_to_win_once -release.destabilize.dont_look_at_me -``` - -映射规则: - -| 中文文本 | Key | -|---|---| -| 为什么 | `release.attack.why` | -| 不相信我 | `release.attack.dont_believe_me` | -| 我明白我垫底 | `release.attack.i_know_im_last` | -| 天气真的不行 | `release.attack.bad_weather` | -| 卖不出去 | `release.attack.cannot_sell` | -| 努力解释 | `release.attack.try_to_explain` | -| 不愿意相信我 | `release.attack.refuse_to_believe` | -| 一点信任 | `release.attack.a_little_trust` | -| 受够了 | `release.attack.had_enough` | -| 真该死 | `release.attack.damn` | -| 真没用 | `release.attack.useless` | -| 证明自己 | `release.attack.prove_myself` | -| 什么都做不到 | `release.attack.cannot_do_anything` | -| 追上你 | `release.attack.catch_up` | -| 帮到英理 | `release.attack.help_yingli` | -| 没有勇气 | `release.attack.no_courage` | -| 说出来 | `release.attack.say_it` | -| 废物 | `release.attack.trash` | -| 烂泥扶不上墙 | `release.attack.hopeless` | -| 为什么为什么 | `release.attack.why_loop` | -| 我想赢一次 | `release.settle.want_to_win_once` | -| 不要那样看我 | `release.destabilize.dont_look_at_me` | - -Yarn 中重复出现的“受够了”“废物”“为什么为什么”继续保持原来的重复次数和演出参数,只复用同一条本地化键,不能因去重而改变演出节奏。 - -未来如果 Stage6 使用 `expression_system_message` 或其他会显示字符串的释放命令,也必须新增对应的 `release.*` 键,不能直接写玩家可见文本。 - -### 2.3 表格元数据 - -英日文第一版可以直接投入开发测试,但每个非中文条目增加统一备注: - -```text -DRAFT / NEEDS NATIVE REVIEW -Context: HuoShan Stage6 expression minigame -``` - -释放演出条目应额外注明: - -- 显示形式,例如瞬间闪现、连续攻击或最终稳定文字。 -- 建议长度。 -- 是否允许创译。 -- 重复次数由 Yarn 控制,本地化值本身不要手工重复。 - -## 3. 程序接口和数据流 - -### 3.1 本地化引用协议 - -Yarn 命令参数支持两种输入: - -```text -@h6. 从 HuoshanStage6 String Table 解析 -普通字符串 按原始字符串使用,保持旧内容兼容 -``` - -示例: +正式 Yarn 使用: ```yarn -<> - -<> -<> -<> +<> ``` -其中: +旧 `start_expression` 继续兼容原始字符串,也允许将前两个参数写为 +`l10n.*`。`completionNode`、`memoryKey`、`preset`、颜色、Timeline 名称及 +数值参数均为结构参数,不做本地化解析。 -- 前两个 `start_expression` 参数是本地化内容。 -- 完成节点 `LOG1梳理完成` 是 Yarn 流程标识,不做本地化。 -- 粒子数量、持续时间、颜色、Timeline、memory preset 等均为结构参数,不做本地化。 +## Locale 生命周期 -### 3.2 建议公共数据结构 +启动轮次时捕获当前 Locale,目标句和 token 并行解析;校验通过后才初始化 +粒子系统。该 Locale 快照会保留到下一轮开始或 ExpressSystem 关闭,当前轮 +后续释放演出都使用同一快照。没有活动轮次时,演出命令使用命令调用时的 +当前 Locale。 -```csharp -public readonly struct Stage6TextReference -{ - public string RawValue { get; } - public bool IsLocalized { get; } - public string EntryKey { get; } -} +这可避免切换语言时旧异步请求覆盖新一轮内容。`PunchTapeItem` 也通过请求 +版本号避免旧名称刷新覆盖新 Locale。 -public sealed class Stage6RoundText -{ - public string TargetText { get; init; } - public IReadOnlyList InterferenceTokens { get; init; } -} +## Key 约定 -public sealed class Stage6LocalizationContext -{ - public const string TableName = "HuoshanStage6"; - public const string ReferencePrefix = "@h6."; +Express 使用短前缀 `hs.exp.*`: - public Locale RoundLocale { get; private set; } +- 三轮目标和 token:`hs.exp.log{1..3}.target`、`.tokens` +- Actor Loop:`hs.exp.actor.loop` +- LOG3 攻击词:`hs.exp.atk.*` +- 最终稳定文字:`hs.exp.final.win` - public IEnumerator BeginRound(Locale locale); +重复演出复用同一 Key,但 Yarn 中仍保留原有调用次数、等待、数值、顺序和 +`#line:` 标签。 - public IEnumerator Resolve( - string textReference, - Action completed); +## token 与 Unicode - public IEnumerator Resolve( - string textReference, - Locale locale, - Action completed); +干扰 token 只接受半角 `|`: - public IEnumerator LoadRound( - string roundId, - Locale locale, - Action completed); -} -``` +- 按 `|` 拆分; +- Trim 并删除空项; +- 全空时中止轮次; +- 全角 `|` 是数据错误。 -职责划分: +正式目标句和释放演出文字按 `StringInfo` 的 Unicode 文本元素处理。汉字、 +假名、拉丁字母、组合字符、代理字符和标点均保持完整;空白不生成粒子,但 +最终排列保留词间距。目标粒子数量、最终排列、攻击词长度校验和 truth leak +匹配使用同一套文本元素结果。 -- `Stage6TextReference`:判断参数是本地化引用还是兼容用原始字符串。 -- `Stage6LocalizationContext`:统一访问 `HuoshanStage6`,避免每个 Yarn 命令各写一套解析逻辑。 -- `Stage6RoundText`:向粒子系统提供已经解析完成的数据;粒子系统不直接访问 String Table。 -- `LanguageYarnCommand`:在启动小游戏前等待本地化解析完成,再把纯文本数据交给 `LanguageParticleManager`。 -- Log 释放相关 Yarn Command:显示前通过同一 resolver 解析字符串,再交给 `LogReleasePresentationController`。 -- `LanguageParticleManager` 和 `LogReleasePresentationController` 最终只接收已解析的文本,不关心键、表名或 Locale。 +本次明确不修改 `TextParticle` 的随机字符池、`chineseChars`、 +`chineseWords`、目标粒子入场阶段的随机字符来源,以及背景/干扰粒子的字符 +池策略。 -### 3.3 解析和错误处理 +## 内容校验 -解析规则必须集中实现: +编辑器校验覆盖: -1. 参数不以 `@h6.` 开头时,直接返回原字符串。 -2. 参数以 `@h6.` 开头时,取后半部分作为 Entry Key。 -3. Entry Key 为空时记录错误,并显示可识别的错误占位。 -4. 优先获取指定 Locale 的条目。 -5. 当前语言缺失时回退到 `zh-Hans` 的同名条目。 -6. 中文源条目也缺失时: - - 输出明确错误日志,包含 Table、Key、Locale 和调用命令。 - - 玩家画面显示 `⟦缺失的键名⟧`。 - - 不得直接显示 `@h6.xxx`,也不能静默返回空字符串。 -7. `interference_tokens` 解析后为空时终止该次 `start_expression`,输出数据错误,避免粒子生成阶段产生难以定位的异常。 +- Stage6 正式 Express 文字参数不得残留中文硬编码; +- Yarn 和 Catalog 中引用的 `l10n.*` Key 必须存在于 `Params` Shared Data; +- 中文值必须非空; +- 英文、日文空值只报告待翻译,不导致校验失败; +- `.tokens` 至少包含一个有效项,且不能使用全角 `|`; +- completion node、preset、memory key、颜色、Timeline 等结构参数不误报。 -### 3.4 Locale 切换行为 - -固定 UI: - -- `objective`、`output`、`integration`、`interference` 监听 `SelectedLocaleChanged`。 -- 切换语言后立即刷新。 -- 当前数值必须保留,只替换格式模板。 - -语言粒子: - -- 每次 `start_expression` 开始时捕获当前 Locale。 -- 同一轮的目标句、目标粒子和干扰 token 全部使用该快照。 -- 轮次进行中切换 Locale,不重建、不替换现有粒子。 -- 下一次 `start_expression` 使用新的 Locale。 -- 异步解析结束前不得开始生成粒子。 - -LOG 释放命令: - -- 每条命令执行时解析其文字参数。 -- 如果该命令属于仍在进行的表达轮次,沿用该轮的 Locale 快照。 -- 没有活动轮次时使用命令执行时的当前 Locale。 -- 单次命令解析完成后再启动对应演出,避免先显示 key、后替换文字。 - -### 3.5 Unicode 与粒子拆分 - -目标句不能按 UTF-16 `char` 或 `string.Length` 拆分,必须使用 Unicode 文本元素/字素簇。 - -规则: - -- 中文汉字、日文假名、拉丁字母、组合音标均按完整文本元素处理。 -- 英文空格不生成粒子,但在成功连接后的排版中形成单词间距。 -- 标点是否生成粒子沿用当前玩法规则,但不能被拆成代理项。 -- 有效目标粒子数量、完成判断和候选粒子数量都基于同一套文本元素结果。 -- 干扰 token 是本地化短词;生成粒子时再按同样的文本元素规则取字符。 -- 不使用英文 `string.Length` 直接决定粒子数量。 - -## 4. 具体改动安排 - -### 4.1 Unity Localization 资源 - -- 补齐 `UIText` 中四个现有火山 UI 键的英日文。 -- 新建 `HuoshanStage6` Collection 及中、英、日三张表。 -- 将 Collection 按项目现有 Localization Addressables 方式纳入构建。 -- 配置英日文字体或 TMP fallback,覆盖拉丁字符、日文假名、常用标点。 - -### 4.2 语言粒子系统 - -- `LanguageYarnCommand` 接入统一的 `@h6.` resolver。 -- `start_expression` 在调用 `LanguageParticleManager` 前解析目标句和 token。 -- `LanguageParticleManager` 接收 `Stage6RoundText` 或等价的纯数据参数。 -- 保留现有原始字符串调用方式,避免其他测试场景和旧 Yarn 立即失效。 -- 将固定 UI 的中文常量仅作为开发兜底,不作为正式语言数据来源。 - -### 4.3 LOG 释放演出 - -- 所有接受玩家可见字符串的释放命令接入相同 resolver。 -- 将 Stage6 中的攻击、settle、destabilize 参数改为 `@h6.` 引用。 -- 不修改数值演出参数、调用顺序和重复次数。 -- `LogReleasePresentationController` 保持表现层职责,只接收最终显示文本。 - -### 4.4 Yarn 内容 - -- 仅替换本地化范围内的 Yarn 命令参数。 -- 不添加或修改 `#line:` 标签。 -- 不配置 `FP_Huoshan1.yarnproject` 的整章 localization。 -- 不生成 Stage6 Yarn 翻译 CSV。 -- 被注释的示例命令不需要建立正式本地化条目;若未来启用,必须先补键。 -- Task 行、鼠标教学和普通对白保持原样。 - -### 4.5 Prefab、图片与文档 - -- `ExpressManager.prefab` 当前没有确认的运行时玩家文本,也不是 Stage6 正式文本数据源,本轮不向其中写多语言文案。 -- `ExpressionScreen_NoText.png` 已确认无烘焙文字,不制作多语言 Sprite。 -- 场景或 Prefab 中诸如“输出”的序列化 TMP 文本只作为编辑器预览;运行时必须由 `UIText` 覆盖。 -- 更新 `LogReleaseYarnCommands.md`: - - 说明 `@h6.` 引用格式。 - - 列出哪些参数可本地化、哪些是结构参数。 - - 提供 attack、settle、destabilize 示例。 - - 记录回退与错误显示规则。 -- 同步更新语言系统说明文档中的 `start_expression` 示例,展示本地化键和旧原始字符串两种写法。 - -## 5. 测试与验收 - -### 5.1 数据完整性测试 - -- `HuoshanStage6` 的中、英、日表拥有相同键集合。 -- 中文源条目不得为空。 -- 英日文不得残留中文占位;允许通过备注标记待审校。 -- 所有 `interference_tokens` 至少包含一个有效 token。 -- 所有 Stage6 中实际执行的本地化命令参数都能解析。 -- 结构参数没有被误当成本地化键。 -- 扫描 Stage6 后,剩余硬编码玩家文本必须全部属于已声明的暂缓范围。 - -### 5.2 编辑模式测试 - -- `@h6.key` 能解析指定 Locale。 -- 普通字符串保持原值。 -- 缺少目标语言时回退中文。 -- 三种语言都缺失时显示 `⟦key⟧` 并记录错误。 -- token 能正确 Trim 和过滤空项。 -- Unicode 文本元素拆分能处理中文、英文、日文和组合字符。 -- 英文空格不生成粒子,但结果排版保留词间距。 -- 重复攻击词引用同一 Key 时仍能按 Yarn 调用次数重复播放。 - -### 5.3 PlayMode/场景验收 - -分别使用 `zh-Hans`、`en`、`ja-JP` 运行 LOG1、LOG2、LOG3: - -- 固定 UI 显示正确,格式参数正确。 -- 每轮目标句和干扰 token 使用对应语言。 -- 粒子连接、排斥、吸引和完成判断不受文本长度影响。 -- LOG3 attack、settle、destabilize 全部显示对应语言。 -- 演出调用顺序、节奏、颜色和持续时间与改造前一致。 -- 中途切换语言时,固定 UI 立即更新,当前轮粒子不变,下一轮使用新语言。 -- 字体无缺字方框。 -- 英文长文本和日文文本没有溢出、异常换行或关键画面遮挡。 - -### 5.4 回归验收 - -- 使用旧的原始字符串调用 `start_expression` 仍然可以运行。 -- `languageTest` 等测试内容不因新增 resolver 失效。 -- Yarn 编译无错误。 -- Stage6 的完成节点跳转保持不变。 -- Task 完成逻辑和 Yarn line ID 不受影响。 -- Addressables/Localization 构建包含新增表格。 -- 在 Unity Editor 中完成最终视觉检查;当前无法通过 Unity MCP 自动进行场景级检查。 - -## 6. 交付判定 - -满足以下条件后视为本轮完成: - -- Stage6 固定小游戏 UI 已具备中英日数据。 -- 三轮粒子内容和全部正式 LOG 释放文本均从 `HuoshanStage6` 获取。 -- Yarn 中相关硬编码文本已替换为 `@h6.` 引用。 -- 原始字符串调用保持兼容。 -- Locale 快照、回退、缺键和 Unicode 拆分规则均有测试覆盖。 -- 文档足以让后续策划直接新增本地化键并在 Yarn 命令中使用。 -- 任务提示、操作教学和普通对白仍明确列为后续 Yarn 本地化工作,不被误报为已完成。 +运行时和 EditMode 测试同时覆盖原始字符串、空值不回退、缺 Key 占位、 +显式 Locale、轮次配置、token 解析和 Unicode 文本元素行为。 From a48e4babd41751ca450674ad729253924c57cdca Mon Sep 17 00:00:00 2001 From: Ding Yuntian <1491671119@qq.com> Date: Tue, 28 Jul 2026 21:34:42 +0800 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20=E7=A1=AC=E7=BC=96=E7=A0=81?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...g-Tables-Portuguese (Brazil) (pt-BR).asset | 12 + ...alization-String-Tables-Russian (ru).asset | 12 + ...alization-String-Tables-Spanish (es).asset | 12 + .../Huoshan/ExpressionLocalizationTests.cs | 119 +++++++- .../ExpressionLocalizationValidator.cs | 87 +++++- .../Huoshan/LogReleasePresentationTests.cs | 18 +- Assets/Font/Assets/方正黑体简体 SDF.asset | 4 +- .../Expression/ExpressionContentCatalog.asset | 1 - .../Language/Params/Params Shared Data.asset | 8 +- Assets/Language/Params/Params.asset | 3 + Assets/Language/Params/Params_en.asset | 8 +- Assets/Language/Params/Params_es.asset | 6 +- Assets/Language/Params/Params_ja-JP.asset | 8 +- Assets/Language/Params/Params_pt-BR.asset | 6 +- Assets/Language/Params/Params_ru.asset | 6 +- Assets/Language/Params/Params_zh-Hans.asset | 8 +- .../Language/UIText/UIText Shared Data.asset | 36 --- Assets/Language/UIText/UIText.asset | 3 + Assets/Language/UIText/UIText_en.asset | 63 +--- Assets/Language/UIText/UIText_ja-JP.asset | 44 +-- Assets/Language/UIText/UIText_zh-Hans.asset | 48 +-- Assets/Prefabs/LanguageParticleSystem.prefab | 2 - Assets/Scenes/HuoShanFixScene.unity | 12 +- Assets/Scripts/AssetRefs/ConstRef.cs | 1 + .../Framework/OtherKit/LocalizationKit.cs | 16 + .../HuoShan/Language/CandidateParticle.cs | 4 +- .../Language/ExpressionContentCatalog.cs | 5 - .../HuoShan/Language/ExpressionManager.cs | 74 +++-- .../Language/ExpressionTextTokenizer.cs | 15 +- .../Language/LanguageParticleManager.cs | 85 ++++-- .../HuoShan/Language/LanguageYarnCommand.cs | 160 ++++------ .../LogReleasePresentationController.cs | 276 +----------------- .../MiniGame/HuoShan/Language/QuickStart.md | 4 +- .../MiniGame/HuoShan/Language/README.md | 13 +- .../MiniGame/HuoShan/Language/TextParticle.cs | 80 +++-- Docs/HuoShan/HuoshanStage6LocalizationPlan.md | 27 +- Docs/HuoShan/LogReleaseYarnCommands.md | 4 - 37 files changed, 558 insertions(+), 732 deletions(-) diff --git a/Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Portuguese (Brazil) (pt-BR).asset b/Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Portuguese (Brazil) (pt-BR).asset index 11ff7620c..61ec7e31b 100644 --- a/Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Portuguese (Brazil) (pt-BR).asset +++ b/Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Portuguese (Brazil) (pt-BR).asset @@ -23,6 +23,18 @@ MonoBehaviour: m_SerializedLabels: - Locale-pt-BR FlaggedDuringContentUpdateRestriction: 0 + - m_GUID: 6ad3c9298dbc3b642b1251ffc306eaa6 + m_Address: Params_pt-BR + m_ReadOnly: 1 + m_SerializedLabels: + - Locale-pt-BR + FlaggedDuringContentUpdateRestriction: 0 + - m_GUID: 7d496f91e240bb143b62a6239c817d09 + m_Address: UIText_pt-BR + m_ReadOnly: 1 + m_SerializedLabels: + - Locale-pt-BR + FlaggedDuringContentUpdateRestriction: 0 m_ReadOnly: 1 m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2} m_SchemaSet: diff --git a/Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Russian (ru).asset b/Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Russian (ru).asset index e9d766346..7b69f4afa 100644 --- a/Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Russian (ru).asset +++ b/Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Russian (ru).asset @@ -23,6 +23,18 @@ MonoBehaviour: m_SerializedLabels: - Locale-ru FlaggedDuringContentUpdateRestriction: 0 + - m_GUID: b91f4281442d2b54f92fbecb5bd56426 + m_Address: Params_ru + m_ReadOnly: 1 + m_SerializedLabels: + - Locale-ru + FlaggedDuringContentUpdateRestriction: 0 + - m_GUID: f679069e455200841a92c27186383940 + m_Address: UIText_ru + m_ReadOnly: 1 + m_SerializedLabels: + - Locale-ru + FlaggedDuringContentUpdateRestriction: 0 m_ReadOnly: 1 m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2} m_SchemaSet: diff --git a/Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Spanish (es).asset b/Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Spanish (es).asset index 48bafd2a8..584d3089c 100644 --- a/Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Spanish (es).asset +++ b/Assets/AddressableAssetsData/AssetGroups/Localization-String-Tables-Spanish (es).asset @@ -23,6 +23,18 @@ MonoBehaviour: m_SerializedLabels: - Locale-es FlaggedDuringContentUpdateRestriction: 0 + - m_GUID: 1edf9254b97981643bdecd6d22ed1150 + m_Address: Params_es + m_ReadOnly: 1 + m_SerializedLabels: + - Locale-es + FlaggedDuringContentUpdateRestriction: 0 + - m_GUID: 380c27123212d66468aa1f3ed3bf72ea + m_Address: UIText_es + m_ReadOnly: 1 + m_SerializedLabels: + - Locale-es + FlaggedDuringContentUpdateRestriction: 0 m_ReadOnly: 1 m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2} m_SchemaSet: diff --git a/Assets/Editor/Huoshan/ExpressionLocalizationTests.cs b/Assets/Editor/Huoshan/ExpressionLocalizationTests.cs index 3059607fb..529e06503 100644 --- a/Assets/Editor/Huoshan/ExpressionLocalizationTests.cs +++ b/Assets/Editor/Huoshan/ExpressionLocalizationTests.cs @@ -41,6 +41,26 @@ namespace AibisDream.EditorTests.Huoshan Assert.That(LocalizationKit.GetL10NParamKey("raw"), Is.EqualTo("raw")); } + [Test] + public void MissingParamResult_RequiresMatchingLocalizedReference() + { + Assert.That( + LocalizationKit.IsMissingParamResult( + "l10n.hs.exp.pool", + "⟦hs.exp.pool⟧"), + Is.True); + Assert.That( + LocalizationKit.IsMissingParamResult( + "l10n.hs.exp.pool", + "⟦another.key⟧"), + Is.False); + Assert.That( + LocalizationKit.IsMissingParamResult( + "raw", + "⟦raw⟧"), + Is.False); + } + [UnityTest] public IEnumerator LocalizeParamAsync_UsesExplicitLocaleAndDoesNotFallback() { @@ -111,7 +131,6 @@ namespace AibisDream.EditorTests.Huoshan AssetDatabase.LoadAssetAtPath(CatalogPath); Assert.That(catalog, Is.Not.Null); Assert.That(catalog.GetValidationErrors(), Is.Empty); - Assert.That(catalog.ActorLoopReference, Is.EqualTo("l10n.hs.exp.actor.loop")); Assert.That(catalog.Rounds.Select(round => round.Id), Is.EqualTo(new[] { "log1", "log2", "log3" })); @@ -128,7 +147,6 @@ namespace AibisDream.EditorTests.Huoshan ScriptableObject.CreateInstance(); try { - SetPrivateField(catalog, "actorLoopReference", "raw"); var first = new ExpressionRoundDefinition(); var second = new ExpressionRoundDefinition(); ConfigureRound(first, "log1", "l10n.target", "l10n.tokens", 1); @@ -139,7 +157,6 @@ namespace AibisDream.EditorTests.Huoshan new List { first, second }); string errors = string.Join("\n", catalog.GetValidationErrors()); - Assert.That(errors, Does.Contain("Actor Loop")); Assert.That(errors, Does.Contain("重复")); Assert.That(errors, Does.Contain("l10n.*")); } @@ -168,6 +185,100 @@ namespace AibisDream.EditorTests.Huoshan Is.False); } + [Test] + public void CharacterPoolParser_PreservesUnicodeDuplicatesAndRejectsSeparators() + { + string combining = "e\u0301"; + Assert.That( + LanguageYarnCommand.TryParseExpressionPool( + $" 中 中 あ {combining} 😀。 ", + out List pool), + Is.True); + Assert.That( + pool, + Is.EqualTo(new[] { "中", "中", "あ", combining, "😀", "。" })); + Assert.That( + LanguageYarnCommand.TryParseExpressionPool("中|あ", out _), + Is.False); + Assert.That( + LanguageYarnCommand.TryParseExpressionPool("中|あ", out _), + Is.False); + Assert.That( + LanguageYarnCommand.TryParseExpressionPool(" \t ", out _), + Is.False); + } + + [Test] + public void Params_ContainsLocalizedExpressPoolForEveryLocale() + { + StringTableCollection collection = + LocalizationEditorSettings.GetStringTableCollection(ConstRef.ParamsTable); + Assert.That(collection, Is.Not.Null); + + string key = + LocalizationKit.GetL10NParamKey(ConstRef.ExpressParticlePoolParam); + Assert.That(collection.SharedData.GetEntry(key), Is.Not.Null); + + string[] localeCodes = { "zh-Hans", "en", "ja-JP", "es", "ru", "pt-BR" }; + foreach (string localeCode in localeCodes) + { + StringTable table = + collection.GetTable(new LocaleIdentifier(localeCode)) as StringTable; + Assert.That(table, Is.Not.Null, localeCode); + Assert.That(table.GetEntry(key), Is.Not.Null, localeCode); + } + + StringTable chinese = + collection.GetTable(new LocaleIdentifier("zh-Hans")) as StringTable; + Assert.That( + ExpressionTextTokenizer.GetVisibleElements( + chinese.GetEntry(key).LocalizedValue), + Is.Not.Empty); + + foreach (string localeCode in localeCodes.Skip(1)) + { + StringTable table = + collection.GetTable(new LocaleIdentifier(localeCode)) as StringTable; + Assert.That(table.GetEntry(key).LocalizedValue, Is.Empty, localeCode); + } + } + + [Test] + public void TextParticle_UsesConfiguredRoleAndOverridePoolsWithoutFirstFrameFallback() + { + var gameObject = new GameObject("TextParticlePoolTest"); + try + { + CandidateParticle particle = gameObject.AddComponent(); + Assert.That(particle.currentChar, Is.Empty); + + string combining = "e\u0301"; + particle.SetCharacterPools( + new[] { "😀" }, + new[] { combining }); + + particle.originalIsRed = true; + particle.InitializeRandomCharacter(); + Assert.That(particle.currentChar, Is.EqualTo("😀")); + + particle.originalIsRed = false; + particle.InitializeRandomCharacter(); + Assert.That(particle.currentChar, Is.EqualTo(combining)); + + particle.SetOverrideCharacterPool(new[] { "。" }); + particle.InitializeRandomCharacter(); + Assert.That(particle.currentChar, Is.EqualTo("。")); + + particle.SetOverrideCharacterPool(null); + particle.InitializeRandomCharacter(); + Assert.That(particle.currentChar, Is.EqualTo(combining)); + } + finally + { + Object.DestroyImmediate(gameObject); + } + } + [Test] public void UnicodeTokenizer_PreservesTextElementsAndDropsWhitespace() { @@ -221,7 +332,7 @@ namespace AibisDream.EditorTests.Huoshan issues.Any(issue => issue.Severity == ExpressionLocalizationIssueSeverity.Warning), Is.True, - "英日空条目应作为待翻译警告保留。"); + "未翻译 Locale 的空条目应作为待翻译警告保留。"); } private static void ConfigureRound( diff --git a/Assets/Editor/Huoshan/ExpressionLocalizationValidator.cs b/Assets/Editor/Huoshan/ExpressionLocalizationValidator.cs index 511b2c175..9c657f7e0 100644 --- a/Assets/Editor/Huoshan/ExpressionLocalizationValidator.cs +++ b/Assets/Editor/Huoshan/ExpressionLocalizationValidator.cs @@ -36,7 +36,7 @@ namespace AibisDream.EditorTests.Huoshan /// /// 校验 Stage6 Express 命令、轮次 Catalog 与 Params String Table 的引用关系。 - /// 英日空值是待翻译警告,不阻断内容校验。 + /// 未翻译 Locale 的空值是待翻译警告,不阻断内容校验。 /// public static class ExpressionLocalizationValidator { @@ -60,7 +60,6 @@ namespace AibisDream.EditorTests.Huoshan new(StringComparer.Ordinal) { ["start_expression"] = new[] { 0, 1 }, - ["expression_lie_begin"] = new[] { 0 }, ["expression_truth_lie_flicker"] = new[] { 0, 1 }, ["expression_lie_morph_truth"] = new[] { 0, 1 }, ["expression_truth_leak"] = new[] { 0 }, @@ -109,6 +108,8 @@ namespace AibisDream.EditorTests.Huoshan ValidateStage6(issues, referencedKeys); ValidateCatalog(issues, referencedKeys); + referencedKeys.Add( + LocalizationKit.GetL10NParamKey(ConstRef.ExpressParticlePoolParam)); ValidateParams(issues, referencedKeys); return issues; } @@ -178,7 +179,6 @@ namespace AibisDream.EditorTests.Huoshan foreach (string validationError in catalog.GetValidationErrors()) AddError(issues, validationError); - AddCatalogReference(catalog.ActorLoopReference, referencedKeys); foreach (ExpressionRoundDefinition round in catalog.Rounds) { if (round == null) @@ -206,6 +206,14 @@ namespace AibisDream.EditorTests.Huoshan collection.GetTable(new LocaleIdentifier("en")) as StringTable; StringTable japanese = collection.GetTable(new LocaleIdentifier("ja-JP")) as StringTable; + StringTable spanish = + collection.GetTable(new LocaleIdentifier("es")) as StringTable; + StringTable russian = + collection.GetTable(new LocaleIdentifier("ru")) as StringTable; + StringTable portuguese = + collection.GetTable(new LocaleIdentifier("pt-BR")) as StringTable; + string poolKey = + LocalizationKit.GetL10NParamKey(ConstRef.ExpressParticlePoolParam); foreach (string key in referencedKeys.OrderBy(value => value, StringComparer.Ordinal)) { @@ -222,14 +230,51 @@ namespace AibisDream.EditorTests.Huoshan continue; } - if (key.EndsWith(".tokens", StringComparison.Ordinal) || - string.Equals(key, "hs.exp.actor.loop", StringComparison.Ordinal)) + if (key.EndsWith(".tokens", StringComparison.Ordinal)) { ValidatePipeList(issues, key, chineseEntry.LocalizedValue); } - AddPendingTranslation(issues, english, "en", key); - AddPendingTranslation(issues, japanese, "ja-JP", key); + if (string.Equals(key, poolKey, StringComparison.Ordinal)) + { + ValidateCharacterPool( + issues, + key, + "zh-Hans", + chineseEntry.LocalizedValue); + AddPendingPoolTranslation(issues, english, "en", key); + AddPendingPoolTranslation(issues, japanese, "ja-JP", key); + AddPendingPoolTranslation(issues, spanish, "es", key); + AddPendingPoolTranslation(issues, russian, "ru", key); + AddPendingPoolTranslation(issues, portuguese, "pt-BR", key); + } + else + { + AddPendingTranslation(issues, english, "en", key); + AddPendingTranslation(issues, japanese, "ja-JP", key); + } + } + } + + private static void ValidateCharacterPool( + List issues, + string key, + string localeCode, + string value) + { + if (value.Contains('|') || value.Contains('|')) + { + AddError( + issues, + $"Params/{key}, Locale={localeCode} 的字符池不能包含“|”或“|”。"); + return; + } + + if (!LanguageYarnCommand.TryParseExpressionPool(value, out _)) + { + AddError( + issues, + $"Params/{key}, Locale={localeCode} 没有有效的 Unicode 文本元素。"); } } @@ -272,6 +317,34 @@ namespace AibisDream.EditorTests.Huoshan } } + private static void AddPendingPoolTranslation( + List issues, + StringTable table, + string localeCode, + string key) + { + StringTableEntry entry = table?.GetEntry(key); + if (entry == null) + { + AddError(issues, $"Params/{key} 缺少 {localeCode} 条目。"); + return; + } + + if (string.IsNullOrEmpty(entry.LocalizedValue)) + { + issues.Add(new ExpressionLocalizationIssue( + ExpressionLocalizationIssueSeverity.Warning, + $"待翻译:Params/{key}, Locale={localeCode}")); + return; + } + + ValidateCharacterPool( + issues, + key, + localeCode, + entry.LocalizedValue); + } + private static void AddCatalogReference( string reference, HashSet referencedKeys) diff --git a/Assets/Editor/Huoshan/LogReleasePresentationTests.cs b/Assets/Editor/Huoshan/LogReleasePresentationTests.cs index 8c316f3d7..5ad2a5228 100644 --- a/Assets/Editor/Huoshan/LogReleasePresentationTests.cs +++ b/Assets/Editor/Huoshan/LogReleasePresentationTests.cs @@ -105,17 +105,12 @@ namespace AibisDream.EditorTests.Huoshan Assert.That(lastCompletion, Is.EqualTo(totalDuration).Within(0.0001f)); } - [TestCase("LightPreset", 0.10f, 0.35f, 0.25f, 0.25f, 0.70f, 0.15f)] - [TestCase("MediumPreset", 0.20f, 0.55f, 0.50f, 0.35f, 0.90f, 0.30f)] - [TestCase("HeavyPreset", 0.35f, 0.80f, 0.85f, 0.50f, 1.10f, 0.50f)] + [TestCase("LightPreset", 0.10f)] + [TestCase("MediumPreset", 0.20f)] + [TestCase("HeavyPreset", 0.35f)] public void PresentationPreset_UsesApprovedValues( string presetFieldName, - float baseDistortion, - float liePause, - float breakPeak, - float breakDuration, - float truthDuration, - float truthPeripheral) + float baseDistortion) { const BindingFlags flags = BindingFlags.Static | BindingFlags.NonPublic; FieldInfo presetField = typeof(LogReleasePresentationController).GetField(presetFieldName, flags); @@ -124,11 +119,6 @@ namespace AibisDream.EditorTests.Huoshan Type presetType = preset.GetType(); AssertPresetField(presetType, preset, "BaseDistortion", baseDistortion); - AssertPresetField(presetType, preset, "LiePause", liePause); - AssertPresetField(presetType, preset, "BreakPeak", breakPeak); - AssertPresetField(presetType, preset, "BreakDuration", breakDuration); - AssertPresetField(presetType, preset, "TruthDuration", truthDuration); - AssertPresetField(presetType, preset, "TruthPeripheral", truthPeripheral); } [Test] diff --git a/Assets/Font/Assets/方正黑体简体 SDF.asset b/Assets/Font/Assets/方正黑体简体 SDF.asset index d1eef57a0..3799d5b43 100644 --- a/Assets/Font/Assets/方正黑体简体 SDF.asset +++ b/Assets/Font/Assets/方正黑体简体 SDF.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5afb8210c7b9dde3cfe36dadfb239feebe6a4a7f076ff4b7b6979dfc04f1456c -size 8566586 +oid sha256:b22f4332d3d76690e085b3ecc0516e1cfc0d65a9a2fc53c33ce3d08ac48963a8 +size 8568631 diff --git a/Assets/GameContent/Feature_Huoshan/Expression/ExpressionContentCatalog.asset b/Assets/GameContent/Feature_Huoshan/Expression/ExpressionContentCatalog.asset index 174b2a156..452d1dfd9 100644 --- a/Assets/GameContent/Feature_Huoshan/Expression/ExpressionContentCatalog.asset +++ b/Assets/GameContent/Feature_Huoshan/Expression/ExpressionContentCatalog.asset @@ -12,7 +12,6 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: a86a72005fba4c99a59962fb9e92df83, type: 3} m_Name: ExpressionContentCatalog m_EditorClassIdentifier: - actorLoopReference: l10n.hs.exp.actor.loop rounds: - id: log1 targetReference: l10n.hs.exp.log1.target diff --git a/Assets/Language/Params/Params Shared Data.asset b/Assets/Language/Params/Params Shared Data.asset index 6698a0112..1100a2da7 100644 --- a/Assets/Language/Params/Params Shared Data.asset +++ b/Assets/Language/Params/Params Shared Data.asset @@ -67,10 +67,6 @@ MonoBehaviour: m_Key: hs.exp.log3.tokens m_Metadata: m_Items: [] - - m_Id: 200000000000001007 - m_Key: hs.exp.actor.loop - m_Metadata: - m_Items: [] - m_Id: 200000000000001008 m_Key: hs.exp.atk.why m_Metadata: @@ -155,6 +151,10 @@ MonoBehaviour: m_Key: hs.exp.final.win m_Metadata: m_Items: [] + - m_Id: 200000000000001029 + m_Key: hs.exp.pool + m_Metadata: + m_Items: [] m_Metadata: m_Items: [] m_KeyGenerator: diff --git a/Assets/Language/Params/Params.asset b/Assets/Language/Params/Params.asset index 428ec4545..a29689969 100644 --- a/Assets/Language/Params/Params.asset +++ b/Assets/Language/Params/Params.asset @@ -17,6 +17,9 @@ MonoBehaviour: - {fileID: 11400000, guid: 9248cb179e7aafa4c903f1fc54b41ef7, type: 2} - {fileID: 11400000, guid: aeb318c895d57014b89d5540c6baff95, type: 2} - {fileID: 11400000, guid: 9ed23d1d910a1a147ba5c316a87cbad4, type: 2} + - {fileID: 11400000, guid: 6ad3c9298dbc3b642b1251ffc306eaa6, type: 2} + - {fileID: 11400000, guid: b91f4281442d2b54f92fbecb5bd56426, type: 2} + - {fileID: 11400000, guid: 1edf9254b97981643bdecd6d22ed1150, type: 2} m_Extensions: [] m_Group: String Table references: diff --git a/Assets/Language/Params/Params_en.asset b/Assets/Language/Params/Params_en.asset index 535805882..b087b7c69 100644 --- a/Assets/Language/Params/Params_en.asset +++ b/Assets/Language/Params/Params_en.asset @@ -70,10 +70,6 @@ MonoBehaviour: m_Localized: m_Metadata: m_Items: [] - - m_Id: 200000000000001007 - m_Localized: - m_Metadata: - m_Items: [] - m_Id: 200000000000001008 m_Localized: m_Metadata: @@ -158,6 +154,10 @@ MonoBehaviour: m_Localized: m_Metadata: m_Items: [] + - m_Id: 200000000000001029 + m_Localized: + m_Metadata: + m_Items: [] references: version: 2 RefIds: [] diff --git a/Assets/Language/Params/Params_es.asset b/Assets/Language/Params/Params_es.asset index caedbf494..5a8f4f83c 100644 --- a/Assets/Language/Params/Params_es.asset +++ b/Assets/Language/Params/Params_es.asset @@ -17,7 +17,11 @@ MonoBehaviour: m_SharedData: {fileID: 11400000, guid: 4a2d391471bd9cd439150c52659e9846, type: 2} m_Metadata: m_Items: [] - m_TableData: [] + m_TableData: + - m_Id: 200000000000001029 + m_Localized: + m_Metadata: + m_Items: [] references: version: 2 RefIds: [] diff --git a/Assets/Language/Params/Params_ja-JP.asset b/Assets/Language/Params/Params_ja-JP.asset index ead8fda35..f95e06640 100644 --- a/Assets/Language/Params/Params_ja-JP.asset +++ b/Assets/Language/Params/Params_ja-JP.asset @@ -70,10 +70,6 @@ MonoBehaviour: m_Localized: m_Metadata: m_Items: [] - - m_Id: 200000000000001007 - m_Localized: - m_Metadata: - m_Items: [] - m_Id: 200000000000001008 m_Localized: m_Metadata: @@ -158,6 +154,10 @@ MonoBehaviour: m_Localized: m_Metadata: m_Items: [] + - m_Id: 200000000000001029 + m_Localized: + m_Metadata: + m_Items: [] references: version: 2 RefIds: [] diff --git a/Assets/Language/Params/Params_pt-BR.asset b/Assets/Language/Params/Params_pt-BR.asset index c0628fea0..d2960d65b 100644 --- a/Assets/Language/Params/Params_pt-BR.asset +++ b/Assets/Language/Params/Params_pt-BR.asset @@ -17,7 +17,11 @@ MonoBehaviour: m_SharedData: {fileID: 11400000, guid: 4a2d391471bd9cd439150c52659e9846, type: 2} m_Metadata: m_Items: [] - m_TableData: [] + m_TableData: + - m_Id: 200000000000001029 + m_Localized: + m_Metadata: + m_Items: [] references: version: 2 RefIds: [] diff --git a/Assets/Language/Params/Params_ru.asset b/Assets/Language/Params/Params_ru.asset index aa0e7a050..815b39a08 100644 --- a/Assets/Language/Params/Params_ru.asset +++ b/Assets/Language/Params/Params_ru.asset @@ -17,7 +17,11 @@ MonoBehaviour: m_SharedData: {fileID: 11400000, guid: 4a2d391471bd9cd439150c52659e9846, type: 2} m_Metadata: m_Items: [] - m_TableData: [] + m_TableData: + - m_Id: 200000000000001029 + m_Localized: + m_Metadata: + m_Items: [] references: version: 2 RefIds: [] diff --git a/Assets/Language/Params/Params_zh-Hans.asset b/Assets/Language/Params/Params_zh-Hans.asset index 6f3544b82..7b31cbd11 100644 --- a/Assets/Language/Params/Params_zh-Hans.asset +++ b/Assets/Language/Params/Params_zh-Hans.asset @@ -70,10 +70,6 @@ MonoBehaviour: m_Localized: "\u54C8\u54C8\u54C8\u54C8|\u7B11\u6B7B\u6211\u4E86|\u592A\u597D\u7B11\u4E86|\u7EF7\u4E0D\u4F4F\u4E86|\u7B11\u4E0D\u6D3B\u4E86|\u771F\u7684\u592A\u641E\u7B11" m_Metadata: m_Items: [] - - m_Id: 200000000000001007 - m_Localized: "\u6CA1\u95EE\u9898|\u5187\u95EE\u9898|\u5E3D\u95EE\u9898" - m_Metadata: - m_Items: [] - m_Id: 200000000000001008 m_Localized: "\u4E3A\u4EC0\u4E48" m_Metadata: @@ -158,6 +154,10 @@ MonoBehaviour: m_Localized: "\u6211\u60F3\u8D62\u4E00\u6B21" m_Metadata: m_Items: [] + - m_Id: 200000000000001029 + m_Localized: 的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞 + m_Metadata: + m_Items: [] references: version: 2 RefIds: [] diff --git a/Assets/Language/UIText/UIText Shared Data.asset b/Assets/Language/UIText/UIText Shared Data.asset index d92107e9f..ada202421 100644 --- a/Assets/Language/UIText/UIText Shared Data.asset +++ b/Assets/Language/UIText/UIText Shared Data.asset @@ -19,10 +19,6 @@ MonoBehaviour: m_Key: start_newGame m_Metadata: m_Items: [] - - m_Id: 22296654778368 - m_Key: start_title - m_Metadata: - m_Items: [] - m_Id: 22296671555584 m_Key: start_selcetLevel m_Metadata: @@ -31,38 +27,10 @@ MonoBehaviour: m_Key: start_quit m_Metadata: m_Items: [] - - m_Id: 22296675749889 - m_Key: start_survey - m_Metadata: - m_Items: [] - - m_Id: 22296675749890 - m_Key: community - m_Metadata: - m_Items: [] - - m_Id: 22296675749891 - m_Key: record_title - m_Metadata: - m_Items: [] - m_Id: 26730768109568 m_Key: record_continue m_Metadata: m_Items: [] - - m_Id: 22296675749892 - m_Key: main_setting - m_Metadata: - m_Items: [] - - m_Id: 22296675749893 - m_Key: main_record - m_Metadata: - m_Items: [] - - m_Id: 22296675749894 - m_Key: main_auto - m_Metadata: - m_Items: [] - - m_Id: 22296675749895 - m_Key: main_fast - m_Metadata: - m_Items: [] - m_Id: 22296675749896 m_Key: setting_title m_Metadata: @@ -119,10 +87,6 @@ MonoBehaviour: m_Key: setting_textSpeed_fast m_Metadata: m_Items: [] - - m_Id: 752706112196608 - m_Key: save_warning - m_Metadata: - m_Items: [] - m_Id: 753317281009664 m_Key: save_return m_Metadata: diff --git a/Assets/Language/UIText/UIText.asset b/Assets/Language/UIText/UIText.asset index 0f692344b..c0a5be18b 100644 --- a/Assets/Language/UIText/UIText.asset +++ b/Assets/Language/UIText/UIText.asset @@ -17,6 +17,9 @@ MonoBehaviour: - {fileID: 11400000, guid: f3e7269fc46911b4dbe7ea36aa77aa90, type: 2} - {fileID: 11400000, guid: e6ecffb64cf71b44bbc44ebf958029cc, type: 2} - {fileID: 11400000, guid: 7c757424270c91e43bf45198f69c983a, type: 2} + - {fileID: 11400000, guid: 7d496f91e240bb143b62a6239c817d09, type: 2} + - {fileID: 11400000, guid: f679069e455200841a92c27186383940, type: 2} + - {fileID: 11400000, guid: 380c27123212d66468aa1f3ed3bf72ea, type: 2} m_Extensions: [] m_Group: String Table references: diff --git a/Assets/Language/UIText/UIText_en.asset b/Assets/Language/UIText/UIText_en.asset index 647465939..620119da2 100644 --- a/Assets/Language/UIText/UIText_en.asset +++ b/Assets/Language/UIText/UIText_en.asset @@ -16,18 +16,12 @@ MonoBehaviour: m_Code: en m_SharedData: {fileID: 11400000, guid: 994c69bf4b2403043bc9562f2d24ee56, type: 2} m_Metadata: - m_Items: - - rid: 7917695930343620616 + m_Items: [] m_TableData: - m_Id: 3350325104640 m_Localized: New Game m_Metadata: m_Items: [] - - m_Id: 22296654778368 - m_Localized: All Our\nBroken Parts - m_Metadata: - m_Items: - - rid: 7917695930343620616 - m_Id: 22296671555584 m_Localized: Chapter Select m_Metadata: @@ -40,34 +34,6 @@ MonoBehaviour: m_Localized: Exit m_Metadata: m_Items: [] - - m_Id: 22296675749889 - m_Localized: Questionnaire - m_Metadata: - m_Items: [] - - m_Id: 22296675749890 - m_Localized: Discord - m_Metadata: - m_Items: [] - - m_Id: 22296675749891 - m_Localized: Medical Records - m_Metadata: - m_Items: [] - - m_Id: 22296675749892 - m_Localized: Settings - m_Metadata: - m_Items: [] - - m_Id: 22296675749893 - m_Localized: Log - m_Metadata: - m_Items: [] - - m_Id: 22296675749894 - m_Localized: Auto - m_Metadata: - m_Items: [] - - m_Id: 22296675749895 - m_Localized: Fast Forward - m_Metadata: - m_Items: [] - m_Id: 22296675749896 m_Localized: m_Metadata: @@ -128,19 +94,6 @@ MonoBehaviour: m_Localized: Fast m_Metadata: m_Items: [] - - m_Id: 752706112196608 - m_Localized: 'This is a temporary feature prepared for testing purposes. - - - If - this is your first playthrough, we recommend going back and clicking Start - Game to begin. - - - If you wish to select a chapter, please restart the - game before using this feature, as bugs may otherwise occur.' - m_Metadata: - m_Items: [] - m_Id: 753317281009664 m_Localized: Back m_Metadata: @@ -258,25 +211,21 @@ MonoBehaviour: m_Metadata: m_Items: [] - m_Id: 200000000000000001 - m_Localized: + m_Localized: m_Metadata: m_Items: [] - m_Id: 200000000000000002 - m_Localized: + m_Localized: m_Metadata: m_Items: [] - m_Id: 200000000000000003 - m_Localized: + m_Localized: m_Metadata: m_Items: [] - m_Id: 200000000000000004 - m_Localized: + m_Localized: m_Metadata: m_Items: [] references: version: 2 - RefIds: - - rid: 7917695930343620616 - type: {class: SmartFormatTag, ns: UnityEngine.Localization.Metadata, asm: Unity.Localization} - data: - m_Entries: 0010305847140000 + RefIds: [] diff --git a/Assets/Language/UIText/UIText_ja-JP.asset b/Assets/Language/UIText/UIText_ja-JP.asset index 70b897290..6e40791e4 100644 --- a/Assets/Language/UIText/UIText_ja-JP.asset +++ b/Assets/Language/UIText/UIText_ja-JP.asset @@ -22,10 +22,6 @@ MonoBehaviour: m_Localized: "\u30CB\u30E5\u30FC\u30B2\u30FC\u30E0" m_Metadata: m_Items: [] - - m_Id: 22296654778368 - m_Localized: "\u611B\u3068\u30ED\u30DC\u30C3\u30C8\n\u4FEE\u7406\u6280\u8853" - m_Metadata: - m_Items: [] - m_Id: 22296671555584 m_Localized: "\u30C1\u30E3\u30D7\u30BF\u30FC\u9078\u629E" m_Metadata: @@ -38,38 +34,10 @@ MonoBehaviour: m_Localized: "\u7D42\u4E86" m_Metadata: m_Items: [] - - m_Id: 22296675749889 - m_Localized: "\u30A2\u30F3\u30B1\u30FC\u30C8" - m_Metadata: - m_Items: [] - - m_Id: 22296675749890 - m_Localized: Discord - m_Metadata: - m_Items: [] - - m_Id: 22296675749891 - m_Localized: "\u30AB\u30EB\u30C6" - m_Metadata: - m_Items: [] - m_Id: 26730768109568 m_Localized: "\u623B\u308B" m_Metadata: m_Items: [] - - m_Id: 22296675749892 - m_Localized: "\u8A2D\u5B9A" - m_Metadata: - m_Items: [] - - m_Id: 22296675749893 - m_Localized: "\u30D0\u30C3\u30AF\u30ED\u30B0" - m_Metadata: - m_Items: [] - - m_Id: 22296675749894 - m_Localized: "\u81EA\u52D5" - m_Metadata: - m_Items: [] - - m_Id: 22296675749895 - m_Localized: "\u65E9\u9001\u308A" - m_Metadata: - m_Items: [] - m_Id: 22296675749896 m_Localized: "<\u8A2D\u5B9A>" m_Metadata: @@ -126,10 +94,6 @@ MonoBehaviour: m_Localized: "\u9AD8\u901F" m_Metadata: m_Items: [] - - m_Id: 752706112196608 - m_Localized: "\u3053\u308C\u306F\u30C6\u30B9\u30C8\u7528\u306E\u4E00\u6642\u7684\u306A\u6A5F\u80FD\u3067\u3059\n\n\u521D\u3081\u3066\u30D7\u30EC\u30A4\u3059\u308B\u5834\u5408\u306F\u3001\u623B\u3063\u3066\u300C\u30B2\u30FC\u30E0\u30B9\u30BF\u30FC\u30C8\u300D\u304B\u3089\u59CB\u3081\u308B\u3053\u3068\u3092\u304A\u52E7\u3081\u3057\u307E\u3059\n\n\u30C1\u30E3\u30D7\u30BF\u30FC\u9078\u629E\u3092\u4F7F\u7528\u3057\u305F\u3044\u65B9\u306F\u3001\u30B2\u30FC\u30E0\u3092\u518D\u8D77\u52D5\u3057\u3066\u304B\u3089\u3054\u5229\u7528\u304F\u3060\u3055\u3044\u3002\u30D0\u30B0\u304C\u767A\u751F\u3059\u308B\u6050\u308C\u304C\u3042\u308A\u307E\u3059" - m_Metadata: - m_Items: [] - m_Id: 753317281009664 m_Localized: "\u623B\u308B" m_Metadata: @@ -247,19 +211,19 @@ MonoBehaviour: m_Metadata: m_Items: [] - m_Id: 200000000000000001 - m_Localized: + m_Localized: m_Metadata: m_Items: [] - m_Id: 200000000000000002 - m_Localized: + m_Localized: m_Metadata: m_Items: [] - m_Id: 200000000000000003 - m_Localized: + m_Localized: m_Metadata: m_Items: [] - m_Id: 200000000000000004 - m_Localized: + m_Localized: m_Metadata: m_Items: [] references: diff --git a/Assets/Language/UIText/UIText_zh-Hans.asset b/Assets/Language/UIText/UIText_zh-Hans.asset index c606c3421..2a28371a4 100644 --- a/Assets/Language/UIText/UIText_zh-Hans.asset +++ b/Assets/Language/UIText/UIText_zh-Hans.asset @@ -16,18 +16,12 @@ MonoBehaviour: m_Code: zh-Hans m_SharedData: {fileID: 11400000, guid: 994c69bf4b2403043bc9562f2d24ee56, type: 2} m_Metadata: - m_Items: - - rid: 7917695939544875419 + m_Items: [] m_TableData: - m_Id: 3350325104640 m_Localized: "\u65B0\u6E38\u620F" m_Metadata: m_Items: [] - - m_Id: 22296654778368 - m_Localized: "\u7231\u4E0E\u673A\u5668\u4EBA\\n\u7EF4\u4FEE\u6280\u672F" - m_Metadata: - m_Items: - - rid: 7917695939544875419 - m_Id: 22296671555584 m_Localized: "\u9009\u62E9\u7AE0\u8282" m_Metadata: @@ -40,38 +34,10 @@ MonoBehaviour: m_Localized: "\u9000\u51FA" m_Metadata: m_Items: [] - - m_Id: 22296675749889 - m_Localized: "\u95EE\u5377" - m_Metadata: - m_Items: [] - - m_Id: 22296675749890 - m_Localized: 325268983 - m_Metadata: - m_Items: [] - - m_Id: 22296675749891 - m_Localized: "\u8BCA\u7597\u8BB0\u5F55" - m_Metadata: - m_Items: [] - m_Id: 26730768109568 m_Localized: "\u8FD4\u56DE" m_Metadata: m_Items: [] - - m_Id: 22296675749892 - m_Localized: "\u8BBE\u7F6E" - m_Metadata: - m_Items: [] - - m_Id: 22296675749893 - m_Localized: "\u8BB0\u5F55" - m_Metadata: - m_Items: [] - - m_Id: 22296675749894 - m_Localized: "\u81EA\u52A8" - m_Metadata: - m_Items: [] - - m_Id: 22296675749895 - m_Localized: "\u5FEB\u8FDB" - m_Metadata: - m_Items: [] - m_Id: 22296675749896 m_Localized: "<\u8BBE\u7F6E>" m_Metadata: @@ -128,12 +94,6 @@ MonoBehaviour: m_Localized: "\u5FEB\u901F" m_Metadata: m_Items: [] - - m_Id: 752706112196608 - m_Localized: "\u8FD9\u662F\u4E00\u4E2A\u4E3A\u4FBF\u4E8E\u6D4B\u8BD5\u51C6\u5907\u7684\u4E34\u65F6\u529F\u80FD\n - \n \u5982\u679C\u9996\u6B21\u6E38\u73A9\uFF0C\u63A8\u8350\u60A8\u8FD4\u56DE\u5E76\u70B9\u51FB\u5F00\u59CB\u6E38\u620F\u76F4\u63A5\u5F00\u59CB\n - \n \u63A8\u8350\u60F3\u8981\u9009\u5173\u7684\u73A9\u5BB6\u91CD\u542F\u6E38\u620F\u540E\u518D\u4F7F\u7528\u672C\u529F\u80FD,\u5426\u5219\u53EF\u80FD\u51FA\u73B0bug" - m_Metadata: - m_Items: [] - m_Id: 753317281009664 m_Localized: "\u8FD4\u56DE" m_Metadata: @@ -268,8 +228,4 @@ MonoBehaviour: m_Items: [] references: version: 2 - RefIds: - - rid: 7917695939544875419 - type: {class: SmartFormatTag, ns: UnityEngine.Localization.Metadata, asm: Unity.Localization} - data: - m_Entries: 0010305847140000 + RefIds: [] diff --git a/Assets/Prefabs/LanguageParticleSystem.prefab b/Assets/Prefabs/LanguageParticleSystem.prefab index c2f20a167..09dc87aa2 100644 --- a/Assets/Prefabs/LanguageParticleSystem.prefab +++ b/Assets/Prefabs/LanguageParticleSystem.prefab @@ -173,8 +173,6 @@ MonoBehaviour: redParticleCount: 8 connectionDistance: 2 textMargin: 1 - anxietyPhrases: [] - targetSentence: "\u522B\u8FC7\u6765\u6211\u611F\u89C9\u5BB3\u6015" completionDialogNode: waveformAmplitude: 0.3 interactionRadius: 0.5 diff --git a/Assets/Scenes/HuoShanFixScene.unity b/Assets/Scenes/HuoShanFixScene.unity index 0f71c9d1c..c500f550b 100644 --- a/Assets/Scenes/HuoShanFixScene.unity +++ b/Assets/Scenes/HuoShanFixScene.unity @@ -16128,13 +16128,6 @@ MonoBehaviour: logReleasePresentation: {fileID: 2138889543} screenPresentation: {fileID: 2138889544} contentCatalog: {fileID: 11400000, guid: b2d94952a18d4d178421691ab103f0fd, type: 2} - defaultAnxietyPhrases: - - "\u6211\u597D\u6015" - - "\u600E\u4E48\u529E" - - "\u4E0D\u884C" - - "\u597D\u96BE" - - "\u505A\u4E0D\u5230" - defaultTargetSentence: "\u8FD9\u662F\u4E00\u4E2A\u6D4B\u8BD5\u793A\u4F8B" screenOverlayRenderer: {fileID: 2063129674} volcanoOverlayRenderer: {fileID: 1944146357} spriteOverlayFadeInAlpha: 1 @@ -16218,7 +16211,6 @@ MonoBehaviour: memoryExitDuration: 0.45 faceDipRatio: 0.015 lieColor: {r: 0.66, g: 0.94, b: 1, a: 1} - errorColor: {r: 1, g: 0.34, b: 0.18, a: 1} truthWarmColor: {r: 1, g: 1, b: 1, a: 1} truthAccentColor: {r: 1, g: 0.84, b: 0.6, a: 1} truthBackdropColor: {r: 0.3, g: 0.2, b: 0.1, a: 0.78} @@ -18471,8 +18463,6 @@ MonoBehaviour: redParticleCount: 8 connectionDistance: 1.8 textMargin: 0.45 - anxietyPhrases: [] - targetSentence: "\u522B\u8FC7\u6765\u6211\u611F\u89C9\u5BB3\u6015" completionDialogNode: waveformAmplitude: 0.3 interactionRadius: 0.5 @@ -18576,7 +18566,7 @@ MonoBehaviour: m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_text: "\u8F93\u51FA" + m_text: m_isRightToLeft: 0 m_fontAsset: {fileID: 11400000, guid: f242fb3dde1933640859f1c54591c9c8, type: 2} m_sharedMaterial: {fileID: -6659093157667844055, guid: f242fb3dde1933640859f1c54591c9c8, type: 2} diff --git a/Assets/Scripts/AssetRefs/ConstRef.cs b/Assets/Scripts/AssetRefs/ConstRef.cs index e5f75494e..1031b6bd9 100644 --- a/Assets/Scripts/AssetRefs/ConstRef.cs +++ b/Assets/Scripts/AssetRefs/ConstRef.cs @@ -53,6 +53,7 @@ namespace AibisDream.Utility public const string UITextTable = "UIText"; public const string ChapterInfoTable = "ChapterInfo"; public const string ParamsTable = "Params"; + public const string ExpressParticlePoolParam = "l10n.hs.exp.pool"; #endregion diff --git a/Assets/Scripts/Framework/OtherKit/LocalizationKit.cs b/Assets/Scripts/Framework/OtherKit/LocalizationKit.cs index 789e3e46a..4c65938f8 100644 --- a/Assets/Scripts/Framework/OtherKit/LocalizationKit.cs +++ b/Assets/Scripts/Framework/OtherKit/LocalizationKit.cs @@ -210,6 +210,22 @@ namespace AibisDream.Framework return IsLocalizedParam(key) ? key[LocalizationPrefix.Length..] : key; } + /// + /// 判断解析结果是否是当前 l10n. 引用对应的缺失标记。 + /// 非本地化原始文本即使使用相同括号格式也不会被误判。 + /// + public static bool IsMissingParamResult(string source, string localizedValue) + { + if (!IsLocalizedParam(source)) + return false; + + string key = GetL10NParamKey(source); + string marker = string.IsNullOrEmpty(key) + ? "⟦empty⟧" + : $"⟦{key}⟧"; + return string.Equals(localizedValue, marker, StringComparison.Ordinal); + } + /// /// 确保本地化系统已初始化 /// diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/CandidateParticle.cs b/Assets/Scripts/MiniGame/HuoShan/Language/CandidateParticle.cs index ad6a6aaa2..e5f3d6f7e 100644 --- a/Assets/Scripts/MiniGame/HuoShan/Language/CandidateParticle.cs +++ b/Assets/Scripts/MiniGame/HuoShan/Language/CandidateParticle.cs @@ -230,7 +230,7 @@ namespace AibisDream.MiniGame.Language } /// - /// 目标粒子:chineseChars 随机单字;非目标粒子:从笑话/干扰短句字符中随机。 + /// 目标粒子:从本轮本地化默认池随机;非目标粒子:从笑话/干扰短句字符中随机。 /// 使用 IsTarget 判断,与红蓝颜色解绑。 /// protected override string GetNextDisplayChar() @@ -264,7 +264,7 @@ namespace AibisDream.MiniGame.Language private void UpdateFocusInterferenceCharacters() { if (focusInterferenceProgress <= 0f || - string.IsNullOrEmpty(overrideCharPool) || + !HasOverrideCharacterPool || (!isStatic && !isCalmed)) { return; diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionContentCatalog.cs b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionContentCatalog.cs index aa6f9f8d5..61afbce82 100644 --- a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionContentCatalog.cs +++ b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionContentCatalog.cs @@ -54,12 +54,10 @@ namespace AibisDream.MiniGame.Language menuName = AibisAssetMenus.HuoShanExpressionContent)] public sealed class ExpressionContentCatalog : ScriptableObject { - [SerializeField] private string actorLoopReference; [SerializeField] private List rounds = new(); private Dictionary roundById; - public string ActorLoopReference => actorLoopReference; public IReadOnlyList Rounds => rounds; public bool TryGetRound(string roundId, out ExpressionRoundDefinition definition) @@ -77,9 +75,6 @@ namespace AibisDream.MiniGame.Language public List GetValidationErrors() { var errors = new List(); - if (!LocalizationKit.IsLocalizedParam(actorLoopReference)) - errors.Add("Actor Loop 必须配置为 l10n.* 引用。"); - var ids = new HashSet(StringComparer.Ordinal); if (rounds == null || rounds.Count == 0) { diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs index 8a3322ce0..4951855a6 100644 --- a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs +++ b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs @@ -20,15 +20,6 @@ namespace AibisDream [SerializeField] private ExpressionScreenPresentationController screenPresentation; [SerializeField] private ExpressionContentCatalog contentCatalog; - [Header("默认配置")] - [SerializeField] private List defaultAnxietyPhrases = new List - { - "哈哈", - "嘿嘿", - "呵呵" - }; - [SerializeField] private string defaultTargetSentence = "这是一个测试示例"; - [Header("Sprite 淡入淡出")] [SerializeField] private SpriteRenderer screenOverlayRenderer; [SerializeField] private SpriteRenderer volcanoOverlayRenderer; @@ -162,20 +153,37 @@ namespace AibisDream /// /// 干扰短句列表(笑话等,非目标粒子从中随机取字符) /// 目标句子 + /// 当前 Locale 的默认随机字符池 /// 完成时触发的对话节点名称(可选) /// 非目标候选粒子总数;<0 时使用 Inspector 默认候选池大小 - public void StartSystem(List anxietyPhrases, string targetSentence, string completionNodeName = null, int nonTargetParticleTotal = -1) + public void StartSystem( + List anxietyPhrases, + string targetSentence, + IReadOnlyList defaultCharacterPool, + string completionNodeName = null, + int nonTargetParticleTotal = -1) { HideSpriteOverlaysForMinigame(); if (particleManager != null) { particleManager.SetPresentationController(logReleasePresentation); - // 使用传入的配置,如果没有则使用默认配置 - var phrases = anxietyPhrases ?? defaultAnxietyPhrases; - var sentence = targetSentence ?? defaultTargetSentence; - - particleManager.InitializeSystem(phrases, sentence, completionNodeName, nonTargetParticleTotal); + if (anxietyPhrases == null || anxietyPhrases.Count == 0 || + string.IsNullOrWhiteSpace(targetSentence) || + defaultCharacterPool == null || + defaultCharacterPool.Count == 0) + { + Debug.LogError( + "[ExpressionManager] 启动参数缺少目标文字、干扰 token 或默认字符池;系统未启动。"); + return; + } + + particleManager.InitializeSystem( + anxietyPhrases, + targetSentence, + defaultCharacterPool, + completionNodeName, + nonTargetParticleTotal); } } @@ -211,10 +219,20 @@ namespace AibisDream /// /// 启动粒子系统(字符串数组版本,方便 Yarn 调用) /// - public void StartSystem(string[] anxietyPhrases, string targetSentence, string completionNodeName = null, int nonTargetParticleTotal = -1) + public void StartSystem( + string[] anxietyPhrases, + string targetSentence, + IReadOnlyList defaultCharacterPool, + string completionNodeName = null, + int nonTargetParticleTotal = -1) { List phrasesList = anxietyPhrases != null ? new List(anxietyPhrases) : null; - StartSystem(phrasesList, targetSentence, completionNodeName, nonTargetParticleTotal); + StartSystem( + phrasesList, + targetSentence, + defaultCharacterPool, + completionNodeName, + nonTargetParticleTotal); } /// @@ -921,28 +939,6 @@ namespace AibisDream targetOpacity); } - public IEnumerator BeginExpressionLie(string keyword, string preset) - { - if (logReleasePresentation == null) - { - Debug.LogError("[ExpressionManager] LogReleasePresentationController 未配置;谎话命令安全结束。"); - yield break; - } - yield return logReleasePresentation.BeginLie(keyword, preset); - } - - public IEnumerator BreakExpressionLie() - { - if (logReleasePresentation == null) yield break; - yield return logReleasePresentation.BreakLie(); - } - - public IEnumerator RevealExpressionTruth() - { - if (logReleasePresentation == null) yield break; - yield return logReleasePresentation.RevealTruth(); - } - public IEnumerator EndExpressionMemory() { if (logReleasePresentation == null) yield break; diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionTextTokenizer.cs b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionTextTokenizer.cs index e01958d45..a0712b059 100644 --- a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionTextTokenizer.cs +++ b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionTextTokenizer.cs @@ -4,8 +4,8 @@ using System.Globalization; namespace AibisDream.MiniGame.Language { /// - /// 将玩家可见文字按 Unicode 文本元素拆分。空白保留在布局中,但不生成粒子。 - /// 随机粒子字符池不使用本类,维持现有行为。 + /// 将玩家可见文字按 Unicode 文本元素拆分。 + /// 空白保留在布局中但不生成粒子;随机字符池直接忽略空白。 /// public static class ExpressionTextTokenizer { @@ -39,6 +39,17 @@ namespace AibisDream.MiniGame.Language return result; } + public static List BuildCharacterPool(IEnumerable texts) + { + var result = new List(); + if (texts == null) + return result; + + foreach (string text in texts) + result.AddRange(GetVisibleElements(text)); + return result; + } + public static Layout BuildLayout( string text, float characterSpacing, diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs index b3d8ed0f0..475100b1b 100644 --- a/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs +++ b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs @@ -64,8 +64,10 @@ namespace AibisDream.MiniGame.Language [SerializeField] private float textMargin = 1f; [Header("游戏配置")] - [SerializeField] private List anxietyPhrases = new List(); // 干扰短句(笑话等,用于非目标粒子,Yarn 会覆盖) - [SerializeField] private string targetSentence = "这是一个测试示例"; // 目标句子 + private List anxietyPhrases = new List(); + private string targetSentence = string.Empty; + private IReadOnlyList defaultCharacterPool = new List().AsReadOnly(); + private IReadOnlyList interferenceCharacterPool = new List().AsReadOnly(); [SerializeField] private string completionDialogNode = ""; // 完成时触发的对话节点 [Header("波形参数")] @@ -346,15 +348,39 @@ namespace AibisDream.MiniGame.Language /// /// 焦虑短句列表 /// 目标句子 + /// 当前 Locale 的默认随机字符池 /// 完成时触发的对话节点 /// /// 非目标候选粒子总数(蓝/干扰侧可交互粒子数量)。≥0 时候选池大小 = 目标句字数 + 该值;<0 时使用 Inspector 的 Candidate Count。 /// - public void InitializeSystem(List phrases, string sentence, string dialogNode = null, int nonTargetParticleTotal = -1) + public void InitializeSystem( + List phrases, + string sentence, + IReadOnlyList localizedDefaultPool, + string dialogNode = null, + int nonTargetParticleTotal = -1) { CaptureDefaultCandidateCountFromInspector(); - string useSentence = string.IsNullOrEmpty(sentence) ? "这是一个测试示例" : sentence; + if (phrases == null || phrases.Count == 0 || string.IsNullOrWhiteSpace(sentence)) + { + Debug.LogError( + "[LanguageParticleManager] 目标文字或干扰 token 为空;初始化已中止。"); + return; + } + + List resolvedDefaultPool = + ExpressionTextTokenizer.BuildCharacterPool(localizedDefaultPool); + List resolvedInterferencePool = + ExpressionTextTokenizer.BuildCharacterPool(phrases); + if (resolvedDefaultPool.Count == 0 || resolvedInterferencePool.Count == 0) + { + Debug.LogError( + "[LanguageParticleManager] 默认字符池或干扰字符池没有有效的 Unicode 文本元素;初始化已中止。"); + return; + } + + string useSentence = sentence; int visibleTargetCount = ExpressionTextTokenizer.GetVisibleElements(useSentence).Count; if (visibleTargetCount == 0) { @@ -373,6 +399,13 @@ namespace AibisDream.MiniGame.Language candidateCount = defaultCandidateCountFromInspector; } + // 在创建或复用粒子前先提交本轮不可变快照,防止首帧或上一轮字符泄漏。 + anxietyPhrases = new List(phrases); + targetSentence = useSentence; + completionDialogNode = dialogNode ?? ""; + defaultCharacterPool = resolvedDefaultPool.AsReadOnly(); + interferenceCharacterPool = resolvedInterferencePool.AsReadOnly(); + // 如果是第一次初始化,先执行基础初始化 if (!isInitialized) { @@ -407,16 +440,9 @@ namespace AibisDream.MiniGame.Language else { EnsureCandidatePoolSize(candidateCount); + ApplyCharacterPoolsToParticles(); } - // 设置游戏配置 - anxietyPhrases = phrases ?? new List(); - targetSentence = useSentence; - completionDialogNode = dialogNode ?? ""; - - // 设置全局焦虑短句配置 - TextParticle.SetAnxietyPhrases(anxietyPhrases); - // 根据目标句子更新目标粒子数量 redParticleCount = Mathf.Min(visibleTargetCount, candidateCount); @@ -700,6 +726,7 @@ namespace AibisDream.MiniGame.Language Vector3 pos = GetRandomPositionInBounds(); GameObject obj = CreateParticleObject(pos, $"FloatingParticle_{i}"); FloatingTextParticle particle = obj.AddComponent(); + particle.SetCharacterPools(defaultCharacterPool, interferenceCharacterPool); particle.SetFont(chineseFontAsset, chineseFontMaterial); particle.SetColor(floatingTextColor); particle.SetFontSize(floatingTextFontSize); @@ -744,6 +771,7 @@ namespace AibisDream.MiniGame.Language Vector3 pos = GetRandomPositionInBounds(); GameObject obj = CreateParticleObject(pos, $"CandidateParticle_{index}"); CandidateParticle particle = obj.AddComponent(); + particle.SetCharacterPools(defaultCharacterPool, interferenceCharacterPool); particle.SetFont(chineseFontAsset, chineseFontMaterial); particle.SetColors(targetParticleColor, nonTargetParticleColor, nonTargetParticleColor, targetCalmColor); particle.SetNonTargetMarquee( @@ -760,6 +788,22 @@ namespace AibisDream.MiniGame.Language candidateParticles.Add(particle); } + private void ApplyCharacterPoolsToParticles() + { + foreach (CandidateParticle particle in candidateParticles) + particle?.SetCharacterPools(defaultCharacterPool, interferenceCharacterPool); + foreach (FloatingTextParticle particle in floatingParticles) + particle?.SetCharacterPools(defaultCharacterPool, interferenceCharacterPool); + } + + private void RefreshAllRandomCharacters() + { + foreach (CandidateParticle particle in candidateParticles) + particle?.InitializeRandomCharacter(); + foreach (FloatingTextParticle particle in floatingParticles) + particle?.InitializeRandomCharacter(); + } + /// /// 增删候选粒子以匹配目标数量(用于 Yarn 动态指定非目标粒子总数后复用同一视图) /// @@ -2016,6 +2060,8 @@ namespace AibisDream.MiniGame.Language Debug.LogWarning("[LanguageParticleManager] 老虎机换字字符池为空;已安全跳过。"); return; } + IReadOnlyList poolElements = + ExpressionTextTokenizer.GetVisibleElements(pool).AsReadOnly(); float changeInterval = Mathf.Max(0.02f, interval); foreach (TargetFocusVisual visual in focusVisuals.Values) @@ -2023,7 +2069,7 @@ namespace AibisDream.MiniGame.Language if (visual?.particle == null) continue; - visual.particle.SetOverrideCharPool(pool); + visual.particle.SetOverrideCharacterPool(poolElements); visual.particle.SetChangeInterval(changeInterval); visual.particle.ResetChangeTimer(changeInterval); visual.particle.isStatic = false; @@ -2040,8 +2086,12 @@ namespace AibisDream.MiniGame.Language { if (string.IsNullOrEmpty(charPool)) return null; - string cleaned = charPool.Replace(" ", string.Empty).Replace("|", string.Empty); - return cleaned.Length == 0 ? null : cleaned; + string withoutSeparators = charPool + .Replace("|", string.Empty) + .Replace("|", string.Empty); + List elements = + ExpressionTextTokenizer.GetVisibleElements(withoutSeparators); + return elements.Count == 0 ? null : string.Concat(elements); } public void StopSlotMachineShuffle() @@ -2053,7 +2103,7 @@ namespace AibisDream.MiniGame.Language } foreach (CandidateParticle particle in targetParticles) - particle?.SetOverrideCharPool(null); + particle?.SetOverrideCharacterPool(null); } private IEnumerator EndSlotMachineShuffleAfter(float duration) @@ -2061,7 +2111,7 @@ namespace AibisDream.MiniGame.Language yield return new WaitForSeconds(duration); slotMachineRoutine = null; foreach (CandidateParticle particle in targetParticles) - particle?.SetOverrideCharPool(null); + particle?.SetOverrideCharacterPool(null); } public void PrepareTruthReleaseFromLie() @@ -3151,6 +3201,7 @@ namespace AibisDream.MiniGame.Language RebuildScreenClipMaterials(); SelectRedParticles(); + RefreshAllRandomCharacters(); UpdateStatusUI(); if (playEntranceEffect) { diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/LanguageYarnCommand.cs b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageYarnCommand.cs index 8487124fb..77f54b34a 100644 --- a/Assets/Scripts/MiniGame/HuoShan/Language/LanguageYarnCommand.cs +++ b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageYarnCommand.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using AibisDream.FixSystem; using AibisDream; using AibisDream.Framework; +using AibisDream.Utility; using UnityEngine.Localization; using UnityEngine.Localization.Settings; using Yarn.Unity; @@ -38,9 +39,19 @@ namespace AibisDream.MiniGame.Language while (!all.IsCompleted) yield return null; - completed?.Invoke(all.Status == TaskStatus.RanToCompletion - ? all.Result - : System.Array.Empty()); + if (all.Status != TaskStatus.RanToCompletion) + { + completed?.Invoke(System.Array.Empty()); + yield break; + } + + string[] values = all.Result; + for (int i = 0; i < values.Length; i++) + { + if (LocalizationKit.IsMissingParamResult(references[i], values[i])) + values[i] = null; + } + completed?.Invoke(values); } private static bool ValidateRequiredText(string value, string command, string parameter) @@ -77,6 +88,22 @@ namespace AibisDream.MiniGame.Language return tokens.Count > 0; } + public static bool TryParseExpressionPool( + string value, + out List characters) + { + characters = new List(); + if (string.IsNullOrWhiteSpace(value) || + value.Contains('|') || + value.Contains('|')) + { + return false; + } + + characters = ExpressionTextTokenizer.GetVisibleElements(value); + return characters.Count > 0; + } + private static bool TryGetPresentation(out LogReleasePresentationController presentation) { presentation = ExpressionManager != null @@ -130,10 +157,12 @@ namespace AibisDream.MiniGame.Language locale, values => localized = values, anxietyPhrasesStr, - targetSentence); - if (localized == null || localized.Length != 2 || + targetSentence, + ConstRef.ExpressParticlePoolParam); + if (localized == null || localized.Length != 3 || !ValidateRequiredText(localized[0], "start_expression", nameof(anxietyPhrasesStr)) || - !ValidateRequiredText(localized[1], "start_expression", nameof(targetSentence))) + !ValidateRequiredText(localized[1], "start_expression", nameof(targetSentence)) || + !ValidateRequiredText(localized[2], "start_expression", "defaultCharacterPool")) yield break; if (!TryParseExpressionTokens(localized[0], out List phrases)) @@ -142,6 +171,12 @@ namespace AibisDream.MiniGame.Language "[LanguageYarnCommand] start_expression 没有有效的干扰 token;命令已安全结束。"); yield break; } + if (!TryParseExpressionPool(localized[2], out List defaultCharacterPool)) + { + UnityEngine.Debug.LogError( + "[LanguageYarnCommand] start_expression 的默认字符池无效;命令已安全结束。"); + yield break; + } // 先清理上一轮演出,再淡出旧文字/连线并恢复 Log 操作界面。 yield return ExpressionManager.PreparePresentationForNextRound(); @@ -151,6 +186,7 @@ namespace AibisDream.MiniGame.Language ExpressionManager.StartSystem( phrases, localized[1], + defaultCharacterPool, completionNode, nonTargetParticleTotal); yield return ExpressionManager.WaitUntilExpressionFlowReady(); @@ -199,10 +235,12 @@ namespace AibisDream.MiniGame.Language locale, values => localized = values, round.TokenReference, - round.TargetReference); - if (localized == null || localized.Length != 2 || + round.TargetReference, + ConstRef.ExpressParticlePoolParam); + if (localized == null || localized.Length != 3 || !ValidateRequiredText(localized[0], "start_expression_round", nameof(round.TokenReference)) || - !ValidateRequiredText(localized[1], "start_expression_round", nameof(round.TargetReference))) + !ValidateRequiredText(localized[1], "start_expression_round", nameof(round.TargetReference)) || + !ValidateRequiredText(localized[2], "start_expression_round", "defaultCharacterPool")) yield break; if (!TryParseExpressionTokens(localized[0], out List phrases)) @@ -211,6 +249,12 @@ namespace AibisDream.MiniGame.Language $"[LanguageYarnCommand] 表达轮次 '{roundId}' 没有有效的干扰 token。"); yield break; } + if (!TryParseExpressionPool(localized[2], out List defaultCharacterPool)) + { + UnityEngine.Debug.LogError( + $"[LanguageYarnCommand] 表达轮次 '{roundId}' 的默认字符池无效。"); + yield break; + } yield return ExpressionManager.PreparePresentationForNextRound(); yield return ExpressionManager.FadeOutParticlesBeforeNewRound(0f); @@ -219,6 +263,7 @@ namespace AibisDream.MiniGame.Language ExpressionManager.StartSystem( phrases, localized[1], + defaultCharacterPool, completionNode ?? string.Empty, round.NonTargetParticleCount); yield return ExpressionManager.WaitUntilExpressionFlowReady(); @@ -379,49 +424,6 @@ namespace AibisDream.MiniGame.Language presentation.BeginFaceEntry(duration)); } - [YarnCommand("expression_lie_begin")] - public static IEnumerator ExpressionLieBegin(string keyword, string preset) - { - if (ExpressionManager == null) - { - UnityEngine.Debug.LogError("ExpressionManager 未找到!"); - yield break; - } - string[] localized = null; - yield return ResolveTextReferences( - GetCommandLocale(), - values => localized = values, - keyword); - if (localized == null || localized.Length != 1 || - !ValidateRequiredText(localized[0], "expression_lie_begin", nameof(keyword))) - yield break; - - yield return ExpressionManager.StartCoroutine( - ExpressionManager.BeginExpressionLie(localized[0], preset)); - } - - [YarnCommand("expression_lie_break")] - public static IEnumerator ExpressionLieBreak() - { - if (ExpressionManager == null) - { - UnityEngine.Debug.LogError("ExpressionManager 未找到!"); - yield break; - } - yield return ExpressionManager.StartCoroutine(ExpressionManager.BreakExpressionLie()); - } - - [YarnCommand("expression_truth_reveal")] - public static IEnumerator ExpressionTruthReveal() - { - if (ExpressionManager == null) - { - UnityEngine.Debug.LogError("ExpressionManager 未找到!"); - yield break; - } - yield return ExpressionManager.StartCoroutine(ExpressionManager.RevealExpressionTruth()); - } - // 以下命令是一动作一命令。非 IEnumerator 命令只启动效果,节拍由 Yarn 的 wait 控制。 [YarnCommand("expression_lie_prepare")] @@ -540,7 +542,7 @@ namespace AibisDream.MiniGame.Language /// /// 结尾真假快速闪切(非阻塞,节奏由同长度 wait 控制):真心话与谎话交替独占屏幕, /// 两者的屏幕底色(电视头背景)不同,越到后面切得越快,结束定格在真话帧。 - /// <> + /// <> /// [YarnCommand("expression_truth_lie_flicker")] public static IEnumerator ExpressionTruthLieFlicker( @@ -719,54 +721,6 @@ namespace AibisDream.MiniGame.Language true)); } - /// - /// 非阻塞启动“没问题 → 冇问题 → 帽问题”三拍无限闪现循环。 - /// <> - /// - [YarnCommand("expression_actor_flash_loop_start")] - public static IEnumerator ExpressionActorFlashLoopStart() - { - if (!TryGetPresentation(out LogReleasePresentationController presentation)) - yield break; - - ExpressionContentCatalog catalog = ExpressionManager.ContentCatalog; - if (catalog == null || !LocalizationKit.IsLocalizedParam(catalog.ActorLoopReference)) - { - UnityEngine.Debug.LogError( - "[LanguageYarnCommand] Actor Loop 本地化引用未配置;命令已安全结束。"); - yield break; - } - - string[] localized = null; - yield return ResolveTextReferences( - GetCommandLocale(), - values => localized = values, - catalog.ActorLoopReference); - if (localized == null || localized.Length != 1 || - !ValidateRequiredText(localized[0], "expression_actor_flash_loop_start", "ActorLoopReference")) - yield break; - - if (!TryParseExpressionTokens(localized[0], out List phrases)) - { - UnityEngine.Debug.LogError( - "[LanguageYarnCommand] Actor Loop 短句列表无效;命令已安全结束。"); - yield break; - } - - presentation.StartActorFlashLoop(phrases); - } - - /// - /// 立即停止无限闪现循环、清除当前大字,并恢复晃动粒子字。 - /// <> - /// - [YarnCommand("expression_actor_flash_loop_stop")] - public static void ExpressionActorFlashLoopStop() - { - if (TryGetPresentation(out LogReleasePresentationController presentation)) - presentation.StopActorFlashLoop(); - } - /// /// 真话闪现(阻塞):谎话大字之间,暖色完整真话带细微抖动挣扎浮现一拍; /// "被掐断"由紧随其后的 expression_glitch_pulse 表现。不推进谎话冲击等级。 @@ -799,7 +753,7 @@ namespace AibisDream.MiniGame.Language /// /// 老虎机换字(非阻塞,节拍由 Yarn wait 控制):聚焦字在 duration 内只从 charPool 里疯狂换字。 /// truthPool 非空时其字符也混入换字池,并以真话暖色显示。 - /// <> + /// <> /// [YarnCommand("expression_actor_slot")] public static IEnumerator ExpressionActorSlot( @@ -882,7 +836,7 @@ namespace AibisDream.MiniGame.Language /// /// 单列词组严格裁在表情屏内,像字幕一样无缝向下滚动。 - /// <> + /// <> /// [YarnCommand("expression_actor_scroll")] public static IEnumerator ExpressionActorScroll( diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/LogReleasePresentationController.cs b/Assets/Scripts/MiniGame/HuoShan/Language/LogReleasePresentationController.cs index 623578141..73ef9e9bb 100644 --- a/Assets/Scripts/MiniGame/HuoShan/Language/LogReleasePresentationController.cs +++ b/Assets/Scripts/MiniGame/HuoShan/Language/LogReleasePresentationController.cs @@ -38,34 +38,19 @@ namespace AibisDream.MiniGame.Language { public readonly string Name; public readonly float BaseDistortion; - public readonly float LiePause; - public readonly float BreakPeak; - public readonly float BreakDuration; - public readonly float TruthDuration; - public readonly float TruthPeripheral; public Preset( string name, - float baseDistortion, - float liePause, - float breakPeak, - float breakDuration, - float truthDuration, - float truthPeripheral) + float baseDistortion) { Name = name; BaseDistortion = baseDistortion; - LiePause = liePause; - BreakPeak = breakPeak; - BreakDuration = breakDuration; - TruthDuration = truthDuration; - TruthPeripheral = truthPeripheral; } } - private static readonly Preset LightPreset = new Preset("Light", 0.10f, 0.35f, 0.25f, 0.25f, 0.70f, 0.15f); - private static readonly Preset MediumPreset = new Preset("Medium", 0.20f, 0.55f, 0.50f, 0.35f, 0.90f, 0.30f); - private static readonly Preset HeavyPreset = new Preset("Heavy", 0.35f, 0.80f, 0.85f, 0.50f, 1.10f, 0.50f); + private static readonly Preset LightPreset = new Preset("Light", 0.10f); + private static readonly Preset MediumPreset = new Preset("Medium", 0.20f); + private static readonly Preset HeavyPreset = new Preset("Heavy", 0.35f); [Header("Scene References")] [SerializeField] private LanguageParticleManager particleManager; @@ -96,7 +81,6 @@ namespace AibisDream.MiniGame.Language [Header("Screen Styling")] [SerializeField] private Color lieColor = new Color(0.66f, 0.94f, 1f, 1f); - [SerializeField] private Color errorColor = new Color(1f, 0.34f, 0.18f, 1f); [SerializeField] private Color truthWarmColor = new Color(1f, 1f, 1f, 1f); [Tooltip("爆发段真话闪现与老虎机真话字符的暖色;需与谎话的冷白明显区分。")] [SerializeField] private Color truthAccentColor = new Color(1f, 0.84f, 0.6f, 1f); @@ -118,7 +102,6 @@ namespace AibisDream.MiniGame.Language private Preset currentPreset = LightPreset; private MemoryKind currentMemoryKind = MemoryKind.Office; private string currentLieKeyword = string.Empty; - private string currentLeakFragment = string.Empty; private Transform runtimeRoot; private SpriteRenderer memoryRendererA; @@ -129,7 +112,6 @@ namespace AibisDream.MiniGame.Language private SpriteMask screenSpriteMask; private TextMeshPro lieText; private TextMeshPro lieGhostText; - private TextMeshPro systemText; private TextMeshPro actorFlashText; private TextMeshPro actorFlashChromaRed; private TextMeshPro actorFlashChromaCyan; @@ -141,7 +123,6 @@ namespace AibisDream.MiniGame.Language private string truthWarmColorHex; private TMPRectClipper lieClipper; private TMPRectClipper lieGhostClipper; - private TMPRectClipper systemClipper; private TMPRectClipper actorFlashClipper; private TMPRectClipper actorFlashChromaRedClipper; private TMPRectClipper actorFlashChromaCyanClipper; @@ -150,7 +131,6 @@ namespace AibisDream.MiniGame.Language private bool actorFlashActive; private int actorFlashCount; private Coroutine actorOverdriveRoutine; - private Coroutine actorFlashLoopRoutine; private Texture2D runtimeWhiteTexture; private Sprite runtimeWhiteSprite; private MaterialPropertyBlock memoryBlockA; @@ -254,7 +234,6 @@ namespace AibisDream.MiniGame.Language TweenMemoryOpacity(memoryRendererB, 0f, duration); FadeText(lieText, 0f, duration); FadeText(lieGhostText, 0f, duration); - FadeText(systemText, 0f, duration); FadeText(actorFlashText, 0f, duration); FadeText(actorFlashChromaRed, 0f, duration); FadeText(actorFlashChromaCyan, 0f, duration); @@ -337,7 +316,6 @@ namespace AibisDream.MiniGame.Language currentPreset = preset; currentMemoryKind = kind; - currentLeakFragment = GetLeakFragment(kind); _ = horizontalSpeed; actorFlashCount = 0; memoryImpact = 0f; @@ -398,84 +376,6 @@ namespace AibisDream.MiniGame.Language Mathf.Max(0.01f, duration)); } - public IEnumerator BeginLie(string keyword, string presetName) - { - EnsureRuntimeObjects(); - if (!TryResolvePreset(presetName, out Preset requestedPreset)) - { - Debug.LogError($"[LogReleasePresentation] 未知演出预设“{presetName}”;命令安全结束。"); - yield break; - } - - if (!string.Equals(requestedPreset.Name, currentPreset.Name, StringComparison.OrdinalIgnoreCase)) - { - Debug.LogWarning( - $"[LogReleasePresentation] expression_lie_begin 的预设 {requestedPreset.Name} " + - $"与当前记忆 {currentPreset.Name} 不一致,将沿用当前记忆预设。"); - } - - PrepareLieVisuals(0.25f, 0.005f); - SqueezeTruthCharacters( - 0.22f, - 0.055f + currentPreset.BreakPeak * 0.065f, - 0.28f); - ShowLieKeyword(keyword, 0.22f, 14f); - PlayScreenScanline(0.32f, currentMemoryKind == MemoryKind.Sunset); - if (currentMemoryKind == MemoryKind.PrivateOffice) - ShowSystemMessage("输出正常", "normal", 0.12f); - else - HideSystemMessageImmediate(); - yield return new WaitForSeconds(0.25f); - } - - public IEnumerator BreakLie() - { - EnsureRuntimeObjects(); - if (!ExpectState(nameof(BreakLie), PresentationState.LieHolding)) - yield break; - - yield return new WaitForSeconds(currentPreset.LiePause); - BeginLieCracking(0.48f); - PlayScreenScanline(0.22f, true); - PlayScreenDim(0.28f, 0.06f, 0.12f); - PlayMemoryJolt(0.08f, 0.24f, 0.11f); - yield return LeakTruthFragment(currentLeakFragment, 0.24f); - - if (currentMemoryKind == MemoryKind.PrivateOffice) - { - HideSystemMessage(0.01f); - yield return new WaitForSeconds(0.08f); - ShowSystemMessage("输出与原始 log 不一致", "error", 0.08f); - yield return PulseGlitch(HeavyPreset.BreakPeak, 0.12f, 0.18f); - } - } - - public IEnumerator RevealTruth() - { - EnsureRuntimeObjects(); - if (!ExpectState( - nameof(RevealTruth), - PresentationState.LieCracking, - PresentationState.LieHolding)) - { - yield break; - } - - DOTween.Kill(this); - yield return HoldTruthBlackout(0.055f, 0.42f); - SplitLieVisual(currentPreset.BreakDuration, 0.18f, 1.85f, 0.42f); - PlayTruthFlash(0.62f, 0.045f, 0.025f, 0.16f); - PlayMemoryBrightnessPulse(0.16f, 0.06f, 0.20f); - PlayMemoryTear(currentPreset.BreakPeak, currentPreset.BreakDuration); - PlayGlitchPulse(currentPreset.BreakPeak, 0.12f, currentPreset.BreakDuration); - PlayFaceDip(currentPreset.BreakDuration, faceDipRatio); - ReleaseTruthCharacters(0.08f); - yield return ResolveTruthCharacters( - currentPreset.TruthDuration, - currentPreset.TruthPeripheral, - 0.42f); - } - public void PrepareLieVisuals(float freezeDuration, float noiseTarget) { EnsureRuntimeObjects(); @@ -560,42 +460,6 @@ namespace AibisDream.MiniGame.Language PlayScanline(Mathf.Max(0.01f, duration), stalled); } - public void ShowSystemMessage(string value, string style, float fadeDuration) - { - EnsureRuntimeObjects(); - if (!ExpectState( - nameof(ShowSystemMessage), - PresentationState.FaceEntering, - PresentationState.Memory, - PresentationState.LieHolding, - PresentationState.LieCracking)) - return; - Color color; - if (string.Equals(style, "error", StringComparison.OrdinalIgnoreCase)) - { - color = errorColor; - } - else - { - if (!string.Equals(style, "normal", StringComparison.OrdinalIgnoreCase)) - Debug.LogWarning($"[LogReleasePresentation] 未知系统提示样式“{style}”,改用 normal。"); - color = new Color(0.58f, 0.78f, 0.82f, 1f); - } - SetSystemMessage(value ?? string.Empty, color, Mathf.Max(0.01f, fadeDuration)); - } - - public void HideSystemMessage(float fadeDuration) - { - if (systemText == null || !systemText.gameObject.activeSelf) - return; - - systemText.DOKill(); - systemText.DOFade(0f, Mathf.Max(0.01f, fadeDuration)) - .SetEase(Ease.OutQuad) - .SetTarget(this) - .OnComplete(HideSystemMessageImmediate); - } - public void BeginLieCracking(float lieAlpha) { if (!ExpectState( @@ -654,9 +518,9 @@ namespace AibisDream.MiniGame.Language PresentationState.LieCracking)) yield break; - currentLeakFragment = fragment ?? string.Empty; + string resolvedFragment = fragment ?? string.Empty; yield return ShuffleLieCharactersRoutine( - currentLeakFragment, + resolvedFragment, Mathf.Max(0.01f, duration), 0.045f); } @@ -688,7 +552,6 @@ namespace AibisDream.MiniGame.Language return; DOTween.Kill(this); - StopActorFlashLoopImmediate(false); StopActorOverdriveImmediate(); memoryPushSpeed = 0f; memoryPushProgress = 0f; @@ -808,73 +671,6 @@ namespace AibisDream.MiniGame.Language } } - public void StartActorFlashLoop(IReadOnlyList phrases) - { - EnsureRuntimeObjects(); - if (!ExpectState(nameof(StartActorFlashLoop), PresentationState.Memory)) - return; - if (phrases == null || phrases.Count == 0) - { - Debug.LogError("[LogReleasePresentation] Actor Flash Loop 没有有效短句;已安全跳过。"); - return; - } - - StopActorFlashLoopImmediate(true); - actorFlashLoopRoutine = StartCoroutine(ActorFlashLoopRoutine(phrases)); - } - - public void StopActorFlashLoop() - { - StopActorFlashLoopImmediate(true); - } - - private IEnumerator ActorFlashLoopRoutine(IReadOnlyList phrases) - { - while (state == PresentationState.Memory) - { - for (int i = 0; i < phrases.Count && state == PresentationState.Memory; i++) - { - int style = i % 3; - float hold = style == 2 ? 0.15f : 0.25f; - float fontSize = style == 0 ? 10.5f : style == 1 ? 11.2f : 12f; - float punchScale = style == 0 ? 1.45f : style == 1 ? 1.85f : 2.20f; - float chromaDistance = style == 2 ? 0.010f : 0.008f; - float chromaAlpha = style == 2 ? 0.08f : 0.06f; - float impact = style == 0 ? 0.50f : style == 1 ? 0.72f : 1f; - - yield return FlashActorLie( - phrases[i], - hold, - fontSize, - 1.1f, - punchScale, - chromaDistance, - chromaAlpha, - impact, - 0f); - if (state == PresentationState.Memory) - yield return new WaitForSeconds(0.3f); - } - } - - actorFlashLoopRoutine = null; - } - - private void StopActorFlashLoopImmediate(bool restoreParticles) - { - if (actorFlashLoopRoutine != null) - { - StopCoroutine(actorFlashLoopRoutine); - actorFlashLoopRoutine = null; - } - - actorFlashActive = false; - memoryImpact = 0f; - HideActorFlashVisualsImmediate(); - if (restoreParticles && state == PresentationState.Memory) - particleManager?.SetTargetParticlesAlpha(1f, 0f); - } - /// /// 真话闪现:谎话大字之间,暖色完整真话带着细微抖动挣扎浮现一拍。 /// 不推进谎话冲击等级、不累积记忆损伤;"被掐断"由紧随其后的 expression_glitch_pulse 表现。 @@ -1100,7 +896,6 @@ namespace AibisDream.MiniGame.Language state = PresentationState.TruthResolving; StopLieShuffleImmediate(true); HideLieLayersImmediate(); - HideSystemMessageImmediate(); StopScanline(); StopScreenDim(); @@ -1706,7 +1501,6 @@ namespace AibisDream.MiniGame.Language return; state = PresentationState.TruthResolving; - HideSystemMessage(0.15f); particleManager?.PrepareTruthReleaseFromLie(); particleManager?.ApplyPresentationResolvedVisuals(presentationResolvedColor); particleManager?.SetTargetParticlesAlpha(1f, Mathf.Max(0.01f, alphaDuration)); @@ -1752,7 +1546,6 @@ namespace AibisDream.MiniGame.Language } HideLieLayersImmediate(); - HideSystemMessageImmediate(); StopScanline(); StopScreenDim(); ReleaseScreenTextClipMaterials(); @@ -1790,7 +1583,6 @@ namespace AibisDream.MiniGame.Language TweenMemoryOpacity(memoryRendererA, 0f, memoryExitDuration); TweenMemoryOpacity(memoryRendererB, 0f, memoryExitDuration); HideLieLayersImmediate(); - HideSystemMessageImmediate(); StopScanline(); StopScreenDim(); SilenceNoiseOverlayIfPresent(); @@ -1815,7 +1607,6 @@ namespace AibisDream.MiniGame.Language { DOTween.Kill(this); StopAllCoroutines(); - actorFlashLoopRoutine = null; RestoreFaceSpriteImmediate(); memoryOpacityA = 0f; @@ -1824,7 +1615,6 @@ namespace AibisDream.MiniGame.Language DisableMemoryRenderer(memoryRendererB); activeMemoryRenderer = null; HideLieLayersImmediate(); - HideSystemMessageImmediate(); StopScanline(); StopScreenDim(); ReleaseScreenTextClipMaterials(); @@ -1847,7 +1637,6 @@ namespace AibisDream.MiniGame.Language particleManager?.RestoreTargetPresentationDefaults(); state = PresentationState.Idle; currentLieKeyword = string.Empty; - currentLeakFragment = string.Empty; memoryPushSpeed = 0f; memoryPushProgress = 0f; memoryZoom = 1f; @@ -2309,21 +2098,18 @@ namespace AibisDream.MiniGame.Language CreateScreenMask(); lieText = CreateScreenText("LieKeyword", 4.1f, screenTextSortingOrder); lieGhostText = CreateScreenText("LieKeywordGhost", 4.1f, screenTextSortingOrder - 1); - systemText = CreateScreenText("SystemPrompt", 1.35f, screenTextSortingOrder + 1); actorFlashText = CreateScreenText("ActorFlashKeyword", 10.5f, screenTextSortingOrder); actorFlashChromaRed = CreateScreenText("ActorFlashChromaRed", 10.5f, screenTextSortingOrder - 1); actorFlashChromaCyan = CreateScreenText("ActorFlashChromaCyan", 10.5f, screenTextSortingOrder - 1); truthFlashText = CreateScreenText("ActorTruthFlash", 8.5f, screenTextSortingOrder - 1); lieClipper = lieText.gameObject.AddComponent(); lieGhostClipper = lieGhostText.gameObject.AddComponent(); - systemClipper = systemText.gameObject.AddComponent(); actorFlashClipper = actorFlashText.gameObject.AddComponent(); actorFlashChromaRedClipper = actorFlashChromaRed.gameObject.AddComponent(); actorFlashChromaCyanClipper = actorFlashChromaCyan.gameObject.AddComponent(); truthFlashClipper = truthFlashText.gameObject.AddComponent(); lieClipper.Initialize(lieText, expressionScreenMaskRect); lieGhostClipper.Initialize(lieGhostText, expressionScreenMaskRect); - systemClipper.Initialize(systemText, expressionScreenMaskRect); actorFlashClipper.Initialize(actorFlashText, expressionScreenMaskRect); actorFlashChromaRedClipper.Initialize(actorFlashChromaRed, expressionScreenMaskRect); actorFlashChromaCyanClipper.Initialize(actorFlashChromaCyan, expressionScreenMaskRect); @@ -2437,15 +2223,12 @@ namespace AibisDream.MiniGame.Language PositionText(actorFlashChromaRed, center, screen.size); PositionText(actorFlashChromaCyan, center, screen.size); PositionText(truthFlashText, center, screen.size); - Vector3 systemPosition = center + Vector3.up * screen.extents.y * 0.70f; - PositionText(systemText, systemPosition, new Vector2(screen.size.x, screen.size.y * 0.22f)); } private void EnsureScreenTextClipMaterials() { lieClipper?.Initialize(lieText, expressionScreenMaskRect); lieGhostClipper?.Initialize(lieGhostText, expressionScreenMaskRect); - systemClipper?.Initialize(systemText, expressionScreenMaskRect); actorFlashClipper?.Initialize(actorFlashText, expressionScreenMaskRect); actorFlashChromaRedClipper?.Initialize(actorFlashChromaRed, expressionScreenMaskRect); actorFlashChromaCyanClipper?.Initialize(actorFlashChromaCyan, expressionScreenMaskRect); @@ -2458,7 +2241,6 @@ namespace AibisDream.MiniGame.Language { lieClipper?.ReleaseMaterial(); lieGhostClipper?.ReleaseMaterial(); - systemClipper?.ReleaseMaterial(); actorFlashClipper?.ReleaseMaterial(); actorFlashChromaRedClipper?.ReleaseMaterial(); actorFlashChromaCyanClipper?.ReleaseMaterial(); @@ -2751,22 +2533,6 @@ namespace AibisDream.MiniGame.Language screenDimRenderer.gameObject.SetActive(false); } - private void SetSystemMessage(string value, Color color, float fadeDuration) - { - SetTextActive(systemText, value, color); - SetTextAlpha(systemText, 0f); - FadeText(systemText, 1f, fadeDuration); - } - - private void HideSystemMessageImmediate() - { - if (systemText == null) - return; - systemText.DOKill(); - systemText.text = string.Empty; - systemText.gameObject.SetActive(false); - } - private void HideLieLayersImmediate() { StopLieShuffleImmediate(false); @@ -2777,7 +2543,6 @@ namespace AibisDream.MiniGame.Language private void HideActorFlashImmediate() { - StopActorFlashLoopImmediate(false); StopActorOverdriveImmediate(); StopTruthLieFlicker(); actorFlashActive = false; @@ -2885,25 +2650,6 @@ namespace AibisDream.MiniGame.Language } } - private static bool TryResolvePreset(string value, out Preset preset) - { - switch ((value ?? string.Empty).Trim().ToLowerInvariant()) - { - case "light": - preset = LightPreset; - return true; - case "medium": - preset = MediumPreset; - return true; - case "heavy": - preset = HeavyPreset; - return true; - default: - preset = LightPreset; - return false; - } - } - private static Preset GetPresetForMemory(MemoryKind kind) { return kind switch @@ -2914,16 +2660,6 @@ namespace AibisDream.MiniGame.Language }; } - private static string GetLeakFragment(MemoryKind kind) - { - return kind switch - { - MemoryKind.Office => "笑话", - MemoryKind.Sunset => "离开", - _ => "不要那样看我" - }; - } - private bool ExpectState(string command, params PresentationState[] allowed) { for (int i = 0; i < allowed.Length; i++) diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/QuickStart.md b/Assets/Scripts/MiniGame/HuoShan/Language/QuickStart.md index 74c63f1fe..51e3b856b 100644 --- a/Assets/Scripts/MiniGame/HuoShan/Language/QuickStart.md +++ b/Assets/Scripts/MiniGame/HuoShan/Language/QuickStart.md @@ -26,7 +26,7 @@ Floating Count: 100 Candidate Count: 50 Red Particle Count: 8 Connection Distance: 2 -Target Sentence: "别过来我感觉害怕" +Target Sentence: 由启动命令传入,不在 Inspector 中保存测试文案 Waveform Amplitude: 0.3 Interaction Radius: 0.5 ``` @@ -68,7 +68,7 @@ Interaction Radius: 0.5 1. Window → TextMeshPro → Font Asset Creator 2. 选择支持中文的字体(如:思源黑体、微软雅黑) 3. Character Set: Custom Characters -4. 粘贴`TextParticle.cs`中的`chineseChars`字符串 +4. 粘贴 `Params/hs.exp.pool` 及正式 Express 文案所需字符 5. Generate Font Atlas ### 看不到粒子? diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/README.md b/Assets/Scripts/MiniGame/HuoShan/Language/README.md index 7692eaf64..111880343 100644 --- a/Assets/Scripts/MiniGame/HuoShan/Language/README.md +++ b/Assets/Scripts/MiniGame/HuoShan/Language/README.md @@ -50,7 +50,7 @@ Language/ #### 游戏参数 - **Connection Distance**: 连接距离阈值(默认2,Unity单位) -- **Target Sentence**: 目标句子(默认"别过来我感觉害怕") +- **Target Sentence**: 目标句子(必须由启动命令传入) #### 波形参数 - **Waveform Amplitude**: 波形振幅(默认0.3) @@ -91,7 +91,7 @@ Language/ 完成第一阶段后: 1. 红色粒子自动排列成水平一行 2. 蓝色粒子淡出 -3. 红色粒子逐个揭示目标文字:"别过来我感觉害怕" +3. 红色粒子逐个揭示启动命令传入的目标文字 4. 文字出现剧烈抖动(焦虑效果) ### 第三阶段:波形交互 @@ -158,15 +158,12 @@ Language/ ### 添加新的文字内容 -修改`CandidateParticle.cs`中的字符集: -```csharp -protected static string chineseChars = "你的字符集..."; -protected static string[] chineseWords = { "词组1", "词组2", ... }; -``` +在 Unity Localization 的 `Params/hs.exp.pool` 中配置当前语言的随机字符池。 +字符池按 Unicode 文本元素拆分,忽略空白;重复字符可用于提高出现权重。 ### 修改目标句子 -在Manager的Inspector中修改`Target Sentence`字段 +修改 `ExpressionContentCatalog` 对应轮次引用的 `Params` 本地化条目。 ### 调整波形特性 diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/TextParticle.cs b/Assets/Scripts/MiniGame/HuoShan/Language/TextParticle.cs index 27dfc8bf2..b7e20de86 100644 --- a/Assets/Scripts/MiniGame/HuoShan/Language/TextParticle.cs +++ b/Assets/Scripts/MiniGame/HuoShan/Language/TextParticle.cs @@ -27,7 +27,11 @@ namespace AibisDream.MiniGame.Language protected float changeTimer; protected float changeInterval = 1f; - protected string overrideCharPool; + private static readonly IReadOnlyList EmptyCharacterPool = + new List().AsReadOnly(); + protected IReadOnlyList defaultCharacterPool = EmptyCharacterPool; + protected IReadOnlyList interferenceCharacterPool = EmptyCharacterPool; + protected IReadOnlyList overrideCharacterPool = EmptyCharacterPool; protected Bounds movementBounds; protected bool useCircularBounds = false; protected Vector3 boundsCenter; @@ -36,13 +40,6 @@ namespace AibisDream.MiniGame.Language private Material customFontMaterial; private TMPRectClipper screenClipper; - // 字符集 - protected static string chineseChars = "的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞"; - protected static string[] chineseWords = { "不安", "紧张", "焦虑", "担忧", "烦躁", "恐慌", "恐惧", "绝望", "痛苦", "悲伤", "愤怒", "孤独", "无助", "迷茫", "困惑", "压抑", "沉重", "疲惫", "空虚", "失落" }; - - // 动态配置的干扰短句(笑话/焦虑等,由 Yarn start_expression 传入,用于非目标粒子字符池) - protected static List configuredAnxietyPhrases = new List(); - protected virtual void Awake() { if (textMesh == null) @@ -85,7 +82,7 @@ namespace AibisDream.MiniGame.Language SyncLayerToChildren(); changeTimer = Random.Range(0.5f, 1.5f); - currentChar = GetNextDisplayChar(); + currentChar = string.Empty; UpdateText(); // TMP 重建 mesh / SubMesh 后再对齐一次 layer SyncLayerToChildren(); @@ -112,7 +109,7 @@ namespace AibisDream.MiniGame.Language // 边界检测 CheckBounds(); - // 字符变化:目标粒子用 chineseChars 随机,非目标粒子从干扰短句字符中随机 + // 字符变化:目标粒子用本轮默认池,非目标粒子从干扰短句字符池中随机 changeTimer -= Time.deltaTime; if (changeTimer <= 0) { @@ -209,7 +206,7 @@ namespace AibisDream.MiniGame.Language protected string GetRandomChar() { - return chineseChars[Random.Range(0, chineseChars.Length)].ToString(); + return GetRandomPoolElement(defaultCharacterPool); } /// @@ -218,22 +215,14 @@ namespace AibisDream.MiniGame.Language /// protected string GetRandomCharFromPhrases() { - if (configuredAnxietyPhrases == null || configuredAnxietyPhrases.Count == 0) + if (interferenceCharacterPool == null || interferenceCharacterPool.Count == 0) return GetRandomChar(); - var chars = new List(); - foreach (var phrase in configuredAnxietyPhrases) - { - if (string.IsNullOrEmpty(phrase)) continue; - foreach (char c in phrase) - chars.Add(c); - } - if (chars.Count == 0) return GetRandomChar(); - return chars[Random.Range(0, chars.Count)].ToString(); + return GetRandomPoolElement(interferenceCharacterPool); } /// /// 获取下一次要显示的字符。子类可重写以区分目标粒子与非目标粒子(与红蓝颜色解绑)。 - /// 默认返回 chineseChars 随机单字。 + /// 默认从本轮本地化字符池随机返回一个 Unicode 文本元素。 /// protected virtual string GetNextDisplayChar() { @@ -247,27 +236,60 @@ namespace AibisDream.MiniGame.Language /// public void SetOverrideCharPool(string pool) { - overrideCharPool = string.IsNullOrEmpty(pool) ? null : pool; + SetOverrideCharacterPool( + ExpressionTextTokenizer.GetVisibleElements(pool)); + } + + public void SetOverrideCharacterPool(IReadOnlyList pool) + { + overrideCharacterPool = pool != null && pool.Count > 0 + ? pool + : EmptyCharacterPool; } protected bool TryGetOverridePoolChar(out string character) { - if (string.IsNullOrEmpty(overrideCharPool)) + if (!HasOverrideCharacterPool) { character = null; return false; } - character = overrideCharPool[Random.Range(0, overrideCharPool.Length)].ToString(); + character = GetRandomPoolElement(overrideCharacterPool); return true; } - + /// - /// 设置全局干扰短句配置(笑话等,由 LanguageParticleManager 调用) + /// 设置本轮默认与干扰字符池。调用方负责传入不可变的本轮快照。 /// - public static void SetAnxietyPhrases(List phrases) + public void SetCharacterPools( + IReadOnlyList defaultPool, + IReadOnlyList interferencePool) { - configuredAnxietyPhrases = phrases ?? new List(); + defaultCharacterPool = defaultPool != null && defaultPool.Count > 0 + ? defaultPool + : EmptyCharacterPool; + interferenceCharacterPool = + interferencePool != null && interferencePool.Count > 0 + ? interferencePool + : EmptyCharacterPool; + } + + public void InitializeRandomCharacter() + { + currentChar = GetNextDisplayChar(); + UpdateText(); + changeTimer = Random.Range(0.5f, 1.5f); + } + + protected bool HasOverrideCharacterPool => + overrideCharacterPool != null && overrideCharacterPool.Count > 0; + + private static string GetRandomPoolElement(IReadOnlyList pool) + { + if (pool == null || pool.Count == 0) + return string.Empty; + return pool[Random.Range(0, pool.Count)]; } protected void UpdateText() diff --git a/Docs/HuoShan/HuoshanStage6LocalizationPlan.md b/Docs/HuoShan/HuoshanStage6LocalizationPlan.md index 7d5183d27..1c3dbef77 100644 --- a/Docs/HuoShan/HuoshanStage6LocalizationPlan.md +++ b/Docs/HuoShan/HuoshanStage6LocalizationPlan.md @@ -7,9 +7,9 @@ Localization 的 `Params` String Table Collection 作为唯一数据源。 - `l10n.xxx` 查询 `Params/xxx`。 - 查询使用 `DontUseFallback`,不回退中文或其他 Locale。 -- 中文表保存源数据;英文、日文表保留同一套 Key,未翻译值留空。 -- Key 不存在时显示 `⟦key⟧` 并记录包含 Table、Key、Locale 的错误。 -- Express 必填值解析为空时安全终止命令,不启动粒子或释放演出。 +- 中文表保存源数据;其他 Locale 表保留同一套 Key,未翻译值留空。 +- Key 不存在时返回 `⟦key⟧` 并记录包含 Table、Key、Locale 的错误。 +- Express 必填值解析为空或缺失标记时安全终止命令,不启动粒子或释放演出。 - `StreamingAssets/Config/params.csv` 已退出运行链路。 - 固定 UI 继续使用 `UIText`;普通 Yarn 对白、Task、教学和角色名不迁入 `Params`。 @@ -25,8 +25,8 @@ Localization 的 `Params` String Table Collection 作为唯一数据源。 | `log2` | `l10n.hs.exp.log2.target` | `l10n.hs.exp.log2.tokens` | 28 | | `log3` | `l10n.hs.exp.log3.target` | `l10n.hs.exp.log3.tokens` | 38 | -Actor Loop 使用 `l10n.hs.exp.actor.loop`。Round ID 大小写敏感;重复 ID、 -未知 ID、空引用、非 `l10n.*` 引用或解析后的空值均视为数据错误。 +Round ID 大小写敏感;重复 ID、未知 ID、空引用、非 `l10n.*` 引用或解析 +后的空值均视为数据错误。 正式 Yarn 使用: @@ -40,8 +40,8 @@ Actor Loop 使用 `l10n.hs.exp.actor.loop`。Round ID 大小写敏感;重复 I ## Locale 生命周期 -启动轮次时捕获当前 Locale,目标句和 token 并行解析;校验通过后才初始化 -粒子系统。该 Locale 快照会保留到下一轮开始或 ExpressSystem 关闭,当前轮 +启动轮次时捕获当前 Locale,目标句、token 和 `l10n.hs.exp.pool` 并行解析; +校验通过后才初始化粒子系统。该 Locale 快照会保留到下一轮开始或 ExpressSystem 关闭,当前轮 后续释放演出都使用同一快照。没有活动轮次时,演出命令使用命令调用时的 当前 Locale。 @@ -53,9 +53,9 @@ Actor Loop 使用 `l10n.hs.exp.actor.loop`。Round ID 大小写敏感;重复 I Express 使用短前缀 `hs.exp.*`: - 三轮目标和 token:`hs.exp.log{1..3}.target`、`.tokens` -- Actor Loop:`hs.exp.actor.loop` - LOG3 攻击词:`hs.exp.atk.*` - 最终稳定文字:`hs.exp.final.win` +- 全局随机字符池:`hs.exp.pool` 重复演出复用同一 Key,但 Yarn 中仍保留原有调用次数、等待、数值、顺序和 `#line:` 标签。 @@ -74,9 +74,10 @@ Express 使用短前缀 `hs.exp.*`: 最终排列保留词间距。目标粒子数量、最终排列、攻击词长度校验和 truth leak 匹配使用同一套文本元素结果。 -本次明确不修改 `TextParticle` 的随机字符池、`chineseChars`、 -`chineseWords`、目标粒子入场阶段的随机字符来源,以及背景/干扰粒子的字符 -池策略。 +随机字符池同样按 Unicode 文本元素处理。`hs.exp.pool` 是连续文本,不使用 +`|` 或全角 `|`,空白被忽略,重复元素保留为随机权重。目标粒子从该池取字, +非目标和背景浮动粒子继续从本轮 token 展开的干扰池取字;临时老虎机池结束 +后按粒子角色恢复。旧的 `chineseChars` 与 `chineseWords` 运行时常量均已删除。 ## 内容校验 @@ -85,8 +86,10 @@ Express 使用短前缀 `hs.exp.*`: - Stage6 正式 Express 文字参数不得残留中文硬编码; - Yarn 和 Catalog 中引用的 `l10n.*` Key 必须存在于 `Params` Shared Data; - 中文值必须非空; -- 英文、日文空值只报告待翻译,不导致校验失败; +- 未翻译 Locale 的空值只报告待翻译,不导致校验失败; - `.tokens` 至少包含一个有效项,且不能使用全角 `|`; +- `hs.exp.pool` 必须存在于所有 Params Locale 表;中文非空,非空译文不得 + 包含 `|`、`|` 或只有空白; - completion node、preset、memory key、颜色、Timeline 等结构参数不误报。 运行时和 EditMode 测试同时覆盖原始字符串、空值不回退、缺 Key 占位、 diff --git a/Docs/HuoShan/LogReleaseYarnCommands.md b/Docs/HuoShan/LogReleaseYarnCommands.md index 53eca90db..e386a7730 100644 --- a/Docs/HuoShan/LogReleaseYarnCommands.md +++ b/Docs/HuoShan/LogReleaseYarnCommands.md @@ -2,8 +2,6 @@ `Stage6.yarn` 已改为使用原子命令编排。除特别标记为“等待”的命令外,命令只启动效果并立即让 Yarn 继续;并行效果之间的节奏用 `<>` 调整。 -旧命令 `expression_lie_begin`、`expression_lie_break`、`expression_truth_reveal` 仍然保留,供其他节点兼容使用。Stage6 不再依赖它们。 - ## 火山脸与 LOG1 大字 | 命令 | 参数 | 行为 | @@ -11,8 +9,6 @@ | `expression_face_fade_in` | `duration` | 显式淡入火山脸并等待完成。三条 LOG 都在完成节点中调用,不再由粒子系统自动淡入。 | | `expression_actor_flash` | `"text" holdDuration` | 清晰的大字重击;连续调用会逐级增强,但每次结束都退回晃动粒子字。 | | `expression_actor_flash_in` | 与 `expression_actor_flash` 相同 | 阻塞播放相同的大字重击入场,但保持最后一帧、不退场;用于段落结尾定格。 | -| `expression_actor_flash_loop_start` | 无 | 非阻塞启动“没问题 → 冇问题 → 帽问题”三拍无限循环,参数固定为 LOG1 当前节奏。 | -| `expression_actor_flash_loop_stop` | 无 | 立即停止循环、清掉当前大字并恢复晃动粒子字。 | | `expression_actor_slot` | `"characterPool" duration interval fontSize "truthPool"` | 中央大字持续换字。`truthPool` 非空时其字符也混入换字池,并在主字上以真话暖色(`truthAccentColor`)显示——真假字符交替闪烁即"争夺"。命令不等待,节奏由紧随其后的同长度 `wait` 控制;结束后停在最后一帧。 | | `expression_actor_truth_flash` | `"text" holdDuration fontSize alpha jitter` | 真话闪现(等待):谎话大字之间,暖色完整真话带细微抖动挣扎浮现一拍。不推进谎话冲击等级、不累积记忆损伤;"被掐断"由紧随其后的 `expression_glitch_pulse` 表现。 | | `expression_actor_word_hit` | `"text" holdDuration` | 单字或短词独占屏幕重击,并等待该拍完成。 | From 8f2ee603d55e372b113686c32a7b34581daa2e34 Mon Sep 17 00:00:00 2001 From: Ding Yuntian <1491671119@qq.com> Date: Wed, 29 Jul 2026 13:00:59 +0800 Subject: [PATCH 4/7] =?UTF-8?q?feat:=20=E7=81=AB=E5=B1=B1=E8=A1=A8?= =?UTF-8?q?=E8=BE=BE=E7=BB=B4=E4=BF=AE=E7=B2=92=E5=AD=90=E5=AD=97=E7=AC=A6?= =?UTF-8?q?=E6=B1=A0=E6=8B=86=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Huoshan/ExpressionLocalizationTests.cs | 230 +++++++- .../ExpressionLocalizationValidator.cs | 277 +++++++--- .../ExpressionParticleLanguageProfile.asset | 69 +++ ...pressionParticleLanguageProfile.asset.meta | 8 + Assets/Scenes/HuoShanFixScene.unity | 1 + .../HuoShan/Language/CandidateParticle.cs | 6 +- .../HuoShan/Language/ConnectionRenderer.cs | 70 ++- .../HuoShan/Language/ExpressionManager.cs | 53 +- .../Language/ExpressionParticleGeometry.cs | 106 ++++ .../ExpressionParticleGeometry.cs.meta | 11 + .../ExpressionParticleLanguageProfile.cs | 163 ++++++ .../ExpressionParticleLanguageProfile.cs.meta | 11 + .../Language/ExpressionRoundTextSnapshot.cs | 85 +++ .../ExpressionRoundTextSnapshot.cs.meta | 11 + .../Language/ExpressionTextTokenizer.cs | 131 ++++- .../HuoShan/Language/FloatingTextParticle.cs | 3 +- .../Language/LanguageParticleManager.cs | 522 ++++++++++++------ .../HuoShan/Language/LanguageYarnCommand.cs | 60 +- .../MiniGame/HuoShan/Language/TextParticle.cs | 167 ++++-- Assets/Tests/Huoshan.meta | 8 + Assets/Tests/Huoshan/PlayMode.meta | 8 + .../AibisDream.Huoshan.Tests.PlayMode.asmdef | 19 + ...isDream.Huoshan.Tests.PlayMode.asmdef.meta | 7 + .../ExpressionParticlePlayModeTests.cs | 152 +++++ .../ExpressionParticlePlayModeTests.cs.meta | 11 + Docs/HuoShan/HuoshanStage6LocalizationPlan.md | 42 +- 26 files changed, 1830 insertions(+), 401 deletions(-) create mode 100644 Assets/GameContent/Feature_Huoshan/Expression/ExpressionParticleLanguageProfile.asset create mode 100644 Assets/GameContent/Feature_Huoshan/Expression/ExpressionParticleLanguageProfile.asset.meta create mode 100644 Assets/Scripts/MiniGame/HuoShan/Language/ExpressionParticleGeometry.cs create mode 100644 Assets/Scripts/MiniGame/HuoShan/Language/ExpressionParticleGeometry.cs.meta create mode 100644 Assets/Scripts/MiniGame/HuoShan/Language/ExpressionParticleLanguageProfile.cs create mode 100644 Assets/Scripts/MiniGame/HuoShan/Language/ExpressionParticleLanguageProfile.cs.meta create mode 100644 Assets/Scripts/MiniGame/HuoShan/Language/ExpressionRoundTextSnapshot.cs create mode 100644 Assets/Scripts/MiniGame/HuoShan/Language/ExpressionRoundTextSnapshot.cs.meta create mode 100644 Assets/Tests/Huoshan.meta create mode 100644 Assets/Tests/Huoshan/PlayMode.meta create mode 100644 Assets/Tests/Huoshan/PlayMode/AibisDream.Huoshan.Tests.PlayMode.asmdef create mode 100644 Assets/Tests/Huoshan/PlayMode/AibisDream.Huoshan.Tests.PlayMode.asmdef.meta create mode 100644 Assets/Tests/Huoshan/PlayMode/ExpressionParticlePlayModeTests.cs create mode 100644 Assets/Tests/Huoshan/PlayMode/ExpressionParticlePlayModeTests.cs.meta diff --git a/Assets/Editor/Huoshan/ExpressionLocalizationTests.cs b/Assets/Editor/Huoshan/ExpressionLocalizationTests.cs index 529e06503..2d6d0029a 100644 --- a/Assets/Editor/Huoshan/ExpressionLocalizationTests.cs +++ b/Assets/Editor/Huoshan/ExpressionLocalizationTests.cs @@ -22,6 +22,8 @@ namespace AibisDream.EditorTests.Huoshan { private const string CatalogPath = "Assets/GameContent/Feature_Huoshan/Expression/ExpressionContentCatalog.asset"; + private const string ParticleProfilePath = + "Assets/GameContent/Feature_Huoshan/Expression/ExpressionParticleLanguageProfile.asset"; [TestCase(null, false)] [TestCase("", false)] @@ -208,6 +210,210 @@ namespace AibisDream.EditorTests.Huoshan Is.False); } + [Test] + public void ParticleLanguageProfile_ResolvesExactPrefixAndFallbackModes() + { + ExpressionParticleLanguageProfile profile = + AssetDatabase.LoadAssetAtPath( + ParticleProfilePath); + Assert.That(profile, Is.Not.Null); + Assert.That(profile.GetValidationErrors(), Is.Empty); + + Assert.That( + profile.Resolve(new LocaleIdentifier("zh-Hans")).UnitMode, + Is.EqualTo(ExpressionParticleUnitMode.Grapheme)); + Assert.That( + profile.Resolve(new LocaleIdentifier("ja-JP")).UnitMode, + Is.EqualTo(ExpressionParticleUnitMode.Grapheme)); + + foreach (string localeCode in new[] { "en", "es", "ru", "pt-BR" }) + { + Assert.That( + profile.Resolve(new LocaleIdentifier(localeCode)).UnitMode, + Is.EqualTo(ExpressionParticleUnitMode.Word), + localeCode); + } + + Assert.That( + profile.Resolve(new LocaleIdentifier("en-US")).LocaleCode, + Is.EqualTo("en")); + + LogAssert.Expect( + LogType.Warning, + new Regex(@"Locale 'zz-ZZ'.*Grapheme")); + Assert.That( + profile.Resolve(new LocaleIdentifier("zz-ZZ")).UnitMode, + Is.EqualTo(ExpressionParticleUnitMode.Grapheme)); + } + + [Test] + public void WordTokenizer_UsesWhitespaceBoundariesAndKeepsPunctuation() + { + Assert.That( + ExpressionTextTokenizer.TokenizeText( + "I am not a joke", + ExpressionParticleUnitMode.Word), + Is.EqualTo(new[] { "I", "am", "not", "a", "joke" })); + Assert.That( + ExpressionTextTokenizer.TokenizeText( + "don't self-doubt ¿Por qué? веришь?", + ExpressionParticleUnitMode.Word), + Is.EqualTo(new[] + { + "don't", + "self-doubt", + "¿Por", + "qué?", + "веришь?" + })); + Assert.That( + ExpressionTextTokenizer.TokenizeText( + "one \t\r\n two", + ExpressionParticleUnitMode.Word), + Is.EqualTo(new[] { "one", "two" })); + } + + [Test] + public void GraphemeTokenizer_PreservesCjkKanaAndCombiningCharacters() + { + const string combining = "e\u0301"; + Assert.That( + ExpressionTextTokenizer.TokenizeText( + $"汉 あ {combining}", + ExpressionParticleUnitMode.Grapheme), + Is.EqualTo(new[] { "汉", "あ", combining })); + } + + [Test] + public void WordPool_PreservesDuplicateWeightsAndRejectsPipeSeparators() + { + Assert.That( + LanguageYarnCommand.TryParseExpressionPool( + "noise doubt noise", + ExpressionParticleUnitMode.Word, + out List pool), + Is.True); + Assert.That(pool, Is.EqualTo(new[] { "noise", "doubt", "noise" })); + Assert.That( + LanguageYarnCommand.TryParseExpressionPool( + "noise|doubt", + ExpressionParticleUnitMode.Word, + out _), + Is.False); + Assert.That( + LanguageYarnCommand.TryParseExpressionPool( + "noise|doubt", + ExpressionParticleUnitMode.Word, + out _), + Is.False); + } + + [Test] + public void RoundSnapshot_IsParsedOnceAndKeepsItsLocaleAndWordUnits() + { + ExpressionParticleLanguageProfile languageProfile = + AssetDatabase.LoadAssetAtPath( + ParticleProfilePath); + ExpressionParticleLocaleSettings english = + languageProfile.Resolve(new LocaleIdentifier("en")); + Assert.That( + ExpressionRoundTextSnapshot.TryCreate( + new LocaleIdentifier("en"), + english, + "I am not a joke", + new[] { "false alarm", "false alarm" }, + "noise doubt noise", + out ExpressionRoundTextSnapshot snapshot, + out string error), + Is.True, + error); + + Locale previous = LocalizationSettings.SelectedLocale; + try + { + LocalizationSettings.SelectedLocale = + LocalizationEditorSettings.GetLocale( + new LocaleIdentifier("zh-Hans")); + + Assert.That(snapshot.Locale.Code, Is.EqualTo("en")); + Assert.That( + snapshot.TargetUnits, + Is.EqualTo(new[] { "I", "am", "not", "a", "joke" })); + Assert.That( + snapshot.InterferencePoolUnits, + Is.EqualTo(new[] { "false", "alarm", "false", "alarm" })); + Assert.That( + snapshot.DefaultPoolUnits, + Is.EqualTo(new[] { "noise", "doubt", "noise" })); + } + finally + { + LocalizationSettings.SelectedLocale = previous; + } + } + + [Test] + public void WordProfile_ScalesConfiguredNonTargetCounts() + { + ExpressionParticleLanguageProfile profile = + AssetDatabase.LoadAssetAtPath( + ParticleProfilePath); + ExpressionParticleLocaleSettings english = + profile.Resolve(new LocaleIdentifier("en")); + + Assert.That(english.ScaleNonTargetCount(16), Is.EqualTo(10)); + Assert.That(english.ScaleNonTargetCount(28), Is.EqualTo(18)); + Assert.That(english.ScaleNonTargetCount(38), Is.EqualTo(25)); + } + + [Test] + public void ParticleGeometry_UsesVisualEdgeDistanceAndShortestSeparationAxis() + { + var left = new Bounds(Vector3.zero, new Vector3(4f, 1f, 0.01f)); + var right = new Bounds( + new Vector3(4.15f, 0f, 0f), + new Vector3(4f, 1f, 0.01f)); + + Assert.That( + ExpressionParticleGeometry.BoundsDistance(left, right), + Is.EqualTo(0.15f).Within(0.0001f)); + + right.center = new Vector3(3.8f, 0f, 0f); + Assert.That( + ExpressionParticleGeometry.TryGetSeparation( + left, + right, + 0.1f, + out Vector2 direction, + out float overlap), + Is.True); + Assert.That(direction, Is.EqualTo(Vector2.left)); + Assert.That(overlap, Is.EqualTo(0.3f).Within(0.0001f)); + } + + [Test] + public void TextParticle_VisualBoundsIncludesTheWholeWordForPointerDistance() + { + var gameObject = new GameObject("TextParticleBoundsTest"); + try + { + CandidateParticle particle = + gameObject.AddComponent(); + particle.ForceSetUnitText("self-doubt"); + Bounds bounds = particle.GetVisualWorldBounds(); + + Assert.That(bounds.size.x, Is.GreaterThan(0f)); + Assert.That( + particle.DistanceToVisualBounds( + new Vector2(bounds.max.x, bounds.center.y)), + Is.EqualTo(0f).Within(0.0001f)); + } + finally + { + Object.DestroyImmediate(gameObject); + } + } + [Test] public void Params_ContainsLocalizedExpressPoolForEveryLocale() { @@ -250,28 +456,28 @@ namespace AibisDream.EditorTests.Huoshan try { CandidateParticle particle = gameObject.AddComponent(); - Assert.That(particle.currentChar, Is.Empty); + Assert.That(particle.CurrentUnitText, Is.Null.Or.Empty); string combining = "e\u0301"; - particle.SetCharacterPools( + particle.SetUnitPools( new[] { "😀" }, new[] { combining }); particle.originalIsRed = true; - particle.InitializeRandomCharacter(); - Assert.That(particle.currentChar, Is.EqualTo("😀")); + particle.InitializeRandomUnit(); + Assert.That(particle.CurrentUnitText, Is.EqualTo("😀")); particle.originalIsRed = false; - particle.InitializeRandomCharacter(); - Assert.That(particle.currentChar, Is.EqualTo(combining)); + particle.InitializeRandomUnit(); + Assert.That(particle.CurrentUnitText, Is.EqualTo(combining)); - particle.SetOverrideCharacterPool(new[] { "。" }); - particle.InitializeRandomCharacter(); - Assert.That(particle.currentChar, Is.EqualTo("。")); + particle.SetOverrideUnitPool(new[] { "。" }); + particle.InitializeRandomUnit(); + Assert.That(particle.CurrentUnitText, Is.EqualTo("。")); - particle.SetOverrideCharacterPool(null); - particle.InitializeRandomCharacter(); - Assert.That(particle.currentChar, Is.EqualTo(combining)); + particle.SetOverrideUnitPool((IReadOnlyList)null); + particle.InitializeRandomUnit(); + Assert.That(particle.CurrentUnitText, Is.EqualTo(combining)); } finally { diff --git a/Assets/Editor/Huoshan/ExpressionLocalizationValidator.cs b/Assets/Editor/Huoshan/ExpressionLocalizationValidator.cs index 9c657f7e0..7e4ee7283 100644 --- a/Assets/Editor/Huoshan/ExpressionLocalizationValidator.cs +++ b/Assets/Editor/Huoshan/ExpressionLocalizationValidator.cs @@ -43,6 +43,18 @@ namespace AibisDream.EditorTests.Huoshan private const string Stage6Path = "Assets/Yarn/FP/FP_Huoshan1/Stage6.yarn"; private const string CatalogPath = "Assets/GameContent/Feature_Huoshan/Expression/ExpressionContentCatalog.asset"; + private const string ParticleProfilePath = + "Assets/GameContent/Feature_Huoshan/Expression/ExpressionParticleLanguageProfile.asset"; + + private static readonly string[] SupportedLocaleCodes = + { + "zh-Hans", + "en", + "ja-JP", + "es", + "ru", + "pt-BR" + }; private static readonly Regex YarnCommandRegex = new(@"^\s*<<(?[A-Za-z0-9_]+)(?.*?)>>", RegexOptions.Compiled); @@ -110,10 +122,66 @@ namespace AibisDream.EditorTests.Huoshan ValidateCatalog(issues, referencedKeys); referencedKeys.Add( LocalizationKit.GetL10NParamKey(ConstRef.ExpressParticlePoolParam)); - ValidateParams(issues, referencedKeys); + ExpressionParticleLanguageProfile particleProfile = + ValidateParticleProfile(issues); + ValidateParams(issues, referencedKeys, particleProfile); return issues; } + private static ExpressionParticleLanguageProfile ValidateParticleProfile( + List issues) + { + ExpressionParticleLanguageProfile profile = + AssetDatabase.LoadAssetAtPath( + ParticleProfilePath); + if (profile == null) + { + AddError(issues, $"找不到粒子语言 Profile:{ParticleProfilePath}"); + return null; + } + + foreach (string validationError in profile.GetValidationErrors()) + AddError(issues, validationError); + + foreach (string localeCode in SupportedLocaleCodes) + { + bool canResolve = profile.Locales.Any(settings => + { + if (settings == null || + string.IsNullOrWhiteSpace(settings.LocaleCode)) + { + return false; + } + + if (string.Equals( + settings.LocaleCode, + localeCode, + StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + string requestedLanguage = + localeCode.Split('-')[0]; + string configuredLanguage = + settings.LocaleCode.Split('-')[0]; + return string.Equals( + requestedLanguage, + configuredLanguage, + StringComparison.OrdinalIgnoreCase); + }); + + if (!canResolve) + { + AddError( + issues, + $"粒子语言 Profile 无法解析支持的 Locale:{localeCode}"); + } + } + + return profile; + } + private static void ValidateStage6( List issues, HashSet referencedKeys) @@ -190,7 +258,8 @@ namespace AibisDream.EditorTests.Huoshan private static void ValidateParams( List issues, - HashSet referencedKeys) + HashSet referencedKeys, + ExpressionParticleLanguageProfile particleProfile) { StringTableCollection collection = LocalizationEditorSettings.GetStringTableCollection(ConstRef.ParamsTable); @@ -200,18 +269,10 @@ namespace AibisDream.EditorTests.Huoshan return; } - StringTable chinese = - collection.GetTable(new LocaleIdentifier("zh-Hans")) as StringTable; - StringTable english = - collection.GetTable(new LocaleIdentifier("en")) as StringTable; - StringTable japanese = - collection.GetTable(new LocaleIdentifier("ja-JP")) as StringTable; - StringTable spanish = - collection.GetTable(new LocaleIdentifier("es")) as StringTable; - StringTable russian = - collection.GetTable(new LocaleIdentifier("ru")) as StringTable; - StringTable portuguese = - collection.GetTable(new LocaleIdentifier("pt-BR")) as StringTable; + var tables = SupportedLocaleCodes.ToDictionary( + code => code, + code => collection.GetTable(new LocaleIdentifier(code)) as StringTable, + StringComparer.OrdinalIgnoreCase); string poolKey = LocalizationKit.GetL10NParamKey(ConstRef.ExpressParticlePoolParam); @@ -223,128 +284,164 @@ namespace AibisDream.EditorTests.Huoshan continue; } - StringTableEntry chineseEntry = chinese?.GetEntry(key); + StringTableEntry chineseEntry = tables["zh-Hans"]?.GetEntry(key); if (string.IsNullOrWhiteSpace(chineseEntry?.LocalizedValue)) { AddError(issues, $"Params 中文值为空:{key}"); - continue; } - if (key.EndsWith(".tokens", StringComparison.Ordinal)) + foreach (string localeCode in SupportedLocaleCodes) { - ValidatePipeList(issues, key, chineseEntry.LocalizedValue); - } + StringTable table = tables[localeCode]; + StringTableEntry entry = table?.GetEntry(key); + if (entry == null) + { + if (string.Equals( + localeCode, + "zh-Hans", + StringComparison.Ordinal)) + { + AddError( + issues, + $"Params/{key} 缺少 {localeCode} 条目。"); + } + else + { + issues.Add(new ExpressionLocalizationIssue( + ExpressionLocalizationIssueSeverity.Warning, + $"待翻译:Params/{key}, Locale={localeCode}(缺少条目)")); + } + continue; + } - if (string.Equals(key, poolKey, StringComparison.Ordinal)) - { - ValidateCharacterPool( - issues, - key, - "zh-Hans", - chineseEntry.LocalizedValue); - AddPendingPoolTranslation(issues, english, "en", key); - AddPendingPoolTranslation(issues, japanese, "ja-JP", key); - AddPendingPoolTranslation(issues, spanish, "es", key); - AddPendingPoolTranslation(issues, russian, "ru", key); - AddPendingPoolTranslation(issues, portuguese, "pt-BR", key); - } - else - { - AddPendingTranslation(issues, english, "en", key); - AddPendingTranslation(issues, japanese, "ja-JP", key); + if (string.IsNullOrWhiteSpace(entry.LocalizedValue)) + { + if (string.Equals(localeCode, "zh-Hans", StringComparison.Ordinal)) + continue; + + issues.Add(new ExpressionLocalizationIssue( + ExpressionLocalizationIssueSeverity.Warning, + $"待翻译:Params/{key}, Locale={localeCode}")); + continue; + } + + ExpressionParticleLocaleSettings settings = + particleProfile != null + ? particleProfile.Resolve(new LocaleIdentifier(localeCode)) + : ExpressionParticleLocaleSettings.CreateFallback(); + + if (string.Equals(key, poolKey, StringComparison.Ordinal)) + { + ValidateParticlePool( + issues, + key, + localeCode, + entry.LocalizedValue, + settings.UnitMode); + } + else if (key.EndsWith(".tokens", StringComparison.Ordinal)) + { + ValidatePipeList( + issues, + key, + localeCode, + entry.LocalizedValue, + settings.UnitMode); + } + else + { + ValidateVisibleText( + issues, + key, + localeCode, + entry.LocalizedValue, + settings.UnitMode); + } } } } - private static void ValidateCharacterPool( + private static void ValidateVisibleText( List issues, string key, string localeCode, - string value) + string value, + ExpressionParticleUnitMode mode) + { + if (ExpressionTextTokenizer.TokenizeText(value, mode).Count == 0) + { + AddError( + issues, + $"Params/{key}, Locale={localeCode} 没有有效的粒子单位。"); + } + } + + private static void ValidateParticlePool( + List issues, + string key, + string localeCode, + string value, + ExpressionParticleUnitMode mode) { if (value.Contains('|') || value.Contains('|')) { AddError( issues, - $"Params/{key}, Locale={localeCode} 的字符池不能包含“|”或“|”。"); + $"Params/{key}, Locale={localeCode} 的粒子池不能包含“|”或“|”。"); return; } - if (!LanguageYarnCommand.TryParseExpressionPool(value, out _)) + if (!LanguageYarnCommand.TryParseExpressionPool(value, mode, out _)) { + string expected = + mode == ExpressionParticleUnitMode.Word + ? "空白分隔单词" + : "Unicode 文本元素"; AddError( issues, - $"Params/{key}, Locale={localeCode} 没有有效的 Unicode 文本元素。"); + $"Params/{key}, Locale={localeCode} 没有有效的{expected}。"); } } private static void ValidatePipeList( List issues, string key, - string value) + string localeCode, + string value, + ExpressionParticleUnitMode mode) { if (value.Contains('|')) { - AddError(issues, $"Params/{key} 使用了全角分隔符“|”,必须使用半角“|”。"); + AddError( + issues, + $"Params/{key}, Locale={localeCode} 使用了全角分隔符“|”,必须使用半角“|”。"); return; } - bool hasToken = value + string[] phrases = value .Split('|') - .Any(token => !string.IsNullOrWhiteSpace(token)); - if (!hasToken) - AddError(issues, $"Params/{key} 没有有效项。"); - } - - private static void AddPendingTranslation( - List issues, - StringTable table, - string localeCode, - string key) - { - StringTableEntry entry = table?.GetEntry(key); - if (entry == null) + .Where(token => !string.IsNullOrWhiteSpace(token)) + .ToArray(); + if (phrases.Length == 0) { - AddError(issues, $"Params/{key} 缺少 {localeCode} 条目。"); + AddError( + issues, + $"Params/{key}, Locale={localeCode} 没有有效项。"); return; } - if (string.IsNullOrEmpty(entry.LocalizedValue)) + foreach (string phrase in phrases) { - issues.Add(new ExpressionLocalizationIssue( - ExpressionLocalizationIssueSeverity.Warning, - $"待翻译:Params/{key}, Locale={localeCode}")); + if (ExpressionTextTokenizer.TokenizeText(phrase, mode).Count == 0) + { + AddError( + issues, + $"Params/{key}, Locale={localeCode} 包含没有有效粒子单位的短句。"); + return; + } } } - private static void AddPendingPoolTranslation( - List issues, - StringTable table, - string localeCode, - string key) - { - StringTableEntry entry = table?.GetEntry(key); - if (entry == null) - { - AddError(issues, $"Params/{key} 缺少 {localeCode} 条目。"); - return; - } - - if (string.IsNullOrEmpty(entry.LocalizedValue)) - { - issues.Add(new ExpressionLocalizationIssue( - ExpressionLocalizationIssueSeverity.Warning, - $"待翻译:Params/{key}, Locale={localeCode}")); - return; - } - - ValidateCharacterPool( - issues, - key, - localeCode, - entry.LocalizedValue); - } - private static void AddCatalogReference( string reference, HashSet referencedKeys) diff --git a/Assets/GameContent/Feature_Huoshan/Expression/ExpressionParticleLanguageProfile.asset b/Assets/GameContent/Feature_Huoshan/Expression/ExpressionParticleLanguageProfile.asset new file mode 100644 index 000000000..0c137ad5d --- /dev/null +++ b/Assets/GameContent/Feature_Huoshan/Expression/ExpressionParticleLanguageProfile.asset @@ -0,0 +1,69 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 3a7f6c18f2184d09b551e103c67268a1, type: 3} + m_Name: ExpressionParticleLanguageProfile + m_EditorClassIdentifier: + locales: + - localeCode: zh-Hans + unitMode: 0 + targetFontScale: 1 + nonTargetFontScale: 1 + floatingFontScale: 1 + nonTargetCountScale: 1 + wordSpacing: 0.18 + visualEdgeConnectionDistance: 0 + minimumReadablePhraseScale: 0.65 + - localeCode: ja-JP + unitMode: 0 + targetFontScale: 1 + nonTargetFontScale: 1 + floatingFontScale: 1 + nonTargetCountScale: 1 + wordSpacing: 0.18 + visualEdgeConnectionDistance: 0 + minimumReadablePhraseScale: 0.65 + - localeCode: en + unitMode: 1 + targetFontScale: 0.65 + nonTargetFontScale: 0.58 + floatingFontScale: 0.58 + nonTargetCountScale: 0.65 + wordSpacing: 0.18 + visualEdgeConnectionDistance: 0.18 + minimumReadablePhraseScale: 0.65 + - localeCode: es + unitMode: 1 + targetFontScale: 0.65 + nonTargetFontScale: 0.58 + floatingFontScale: 0.58 + nonTargetCountScale: 0.65 + wordSpacing: 0.18 + visualEdgeConnectionDistance: 0.18 + minimumReadablePhraseScale: 0.65 + - localeCode: ru + unitMode: 1 + targetFontScale: 0.65 + nonTargetFontScale: 0.58 + floatingFontScale: 0.58 + nonTargetCountScale: 0.65 + wordSpacing: 0.18 + visualEdgeConnectionDistance: 0.18 + minimumReadablePhraseScale: 0.65 + - localeCode: pt-BR + unitMode: 1 + targetFontScale: 0.65 + nonTargetFontScale: 0.58 + floatingFontScale: 0.58 + nonTargetCountScale: 0.65 + wordSpacing: 0.18 + visualEdgeConnectionDistance: 0.18 + minimumReadablePhraseScale: 0.65 diff --git a/Assets/GameContent/Feature_Huoshan/Expression/ExpressionParticleLanguageProfile.asset.meta b/Assets/GameContent/Feature_Huoshan/Expression/ExpressionParticleLanguageProfile.asset.meta new file mode 100644 index 000000000..c759bc53f --- /dev/null +++ b/Assets/GameContent/Feature_Huoshan/Expression/ExpressionParticleLanguageProfile.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 57d812538c3e4bddafe59f8813562f46 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scenes/HuoShanFixScene.unity b/Assets/Scenes/HuoShanFixScene.unity index c500f550b..824dbb5f6 100644 --- a/Assets/Scenes/HuoShanFixScene.unity +++ b/Assets/Scenes/HuoShanFixScene.unity @@ -16128,6 +16128,7 @@ MonoBehaviour: logReleasePresentation: {fileID: 2138889543} screenPresentation: {fileID: 2138889544} contentCatalog: {fileID: 11400000, guid: b2d94952a18d4d178421691ab103f0fd, type: 2} + particleLanguageProfile: {fileID: 11400000, guid: 57d812538c3e4bddafe59f8813562f46, type: 2} screenOverlayRenderer: {fileID: 2063129674} volcanoOverlayRenderer: {fileID: 1944146357} spriteOverlayFadeInAlpha: 1 diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/CandidateParticle.cs b/Assets/Scripts/MiniGame/HuoShan/Language/CandidateParticle.cs index e5f3d6f7e..917558f7e 100644 --- a/Assets/Scripts/MiniGame/HuoShan/Language/CandidateParticle.cs +++ b/Assets/Scripts/MiniGame/HuoShan/Language/CandidateParticle.cs @@ -233,7 +233,7 @@ namespace AibisDream.MiniGame.Language /// 目标粒子:从本轮本地化默认池随机;非目标粒子:从笑话/干扰短句字符中随机。 /// 使用 IsTarget 判断,与红蓝颜色解绑。 /// - protected override string GetNextDisplayChar() + protected override string GetNextDisplayUnit() { if (TryGetOverridePoolChar(out string overrideChar)) return overrideChar; @@ -274,8 +274,7 @@ namespace AibisDream.MiniGame.Language if (changeTimer > 0f) return; - currentChar = GetNextDisplayChar(); - UpdateText(); + ForceSetUnitText(GetNextDisplayUnit()); changeTimer = Mathf.Max(0.02f, changeInterval); } @@ -431,6 +430,7 @@ namespace AibisDream.MiniGame.Language textMesh.fontSize = nonTargetFontSize; textMesh.fontStyle = nonTargetBold ? TMPro.FontStyles.Bold : TMPro.FontStyles.Normal; } + InvalidateVisualBounds(); } } } diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/ConnectionRenderer.cs b/Assets/Scripts/MiniGame/HuoShan/Language/ConnectionRenderer.cs index 6a39933bf..c673be267 100644 --- a/Assets/Scripts/MiniGame/HuoShan/Language/ConnectionRenderer.cs +++ b/Assets/Scripts/MiniGame/HuoShan/Language/ConnectionRenderer.cs @@ -133,15 +133,20 @@ namespace AibisDream.MiniGame.Language // 跳过红色粒子 if (p1.isRed && p2.isRed) continue; - float distance = Vector3.Distance(p1.transform.position, p2.transform.position); - if (distance < connectionDistance) - { - float alpha = Mathf.Lerp(0.7f, 0f, distance / connectionDistance) * fadeOutAlpha; - Color lineColor = nonTargetConnectionColor; - lineColor.a = alpha * nonTargetAlphaScale * nonTargetConnectionColor.a; + if (!p1.connections.Contains(p2)) + continue; - DrawClippedLine(p1.transform.position, p2.transform.position, lineColor); - } + ResolveConnectionVisual( + p1, + p2, + out Vector3 from, + out Vector3 to, + out float proximity); + float alpha = Mathf.Lerp(0f, 0.7f, proximity) * fadeOutAlpha; + Color lineColor = nonTargetConnectionColor; + lineColor.a = alpha * nonTargetAlphaScale * nonTargetConnectionColor.a; + + DrawClippedLine(from, to, lineColor); } } } @@ -162,23 +167,50 @@ namespace AibisDream.MiniGame.Language { for (int j = i + 1; j < redParticles.Count; j++) { - float distance = Vector3.Distance( - redParticles[i].transform.position, - redParticles[j].transform.position - ); + CandidateParticle first = redParticles[i]; + CandidateParticle second = redParticles[j]; + if (!first.connections.Contains(second)) + continue; - if (distance < connectionDistance) - { - float alpha = Mathf.Lerp(1f, 0.4f, distance / connectionDistance); - Color lineColor = targetConnectionColor; - lineColor.a = alpha * targetAlphaScale * targetConnectionColor.a; + ResolveConnectionVisual( + first, + second, + out Vector3 from, + out Vector3 to, + out float proximity); + float alpha = Mathf.Lerp(0.4f, 1f, proximity); + Color lineColor = targetConnectionColor; + lineColor.a = alpha * targetAlphaScale * targetConnectionColor.a; - DrawClippedLine(redParticles[i].transform.position, redParticles[j].transform.position, lineColor); - } + DrawClippedLine(from, to, lineColor); } } } + private void ResolveConnectionVisual( + CandidateParticle first, + CandidateParticle second, + out Vector3 from, + out Vector3 to, + out float proximity) + { + if (manager != null && + manager.TryGetConnectionVisual( + first, + second, + out from, + out to, + out proximity)) + { + return; + } + + from = first.transform.position; + to = second.transform.position; + float distance = Vector3.Distance(from, to); + proximity = 1f - Mathf.Clamp01(distance / Mathf.Max(0.0001f, connectionDistance)); + } + private void DrawPropagationEffects() { Draw.LineGeometry = LineGeometry.Flat2D; diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs index 4951855a6..99d2eba51 100644 --- a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs +++ b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs @@ -19,6 +19,7 @@ namespace AibisDream [SerializeField] private LogReleasePresentationController logReleasePresentation; [SerializeField] private ExpressionScreenPresentationController screenPresentation; [SerializeField] private ExpressionContentCatalog contentCatalog; + [SerializeField] private ExpressionParticleLanguageProfile particleLanguageProfile; [Header("Sprite 淡入淡出")] [SerializeField] private SpriteRenderer screenOverlayRenderer; @@ -48,6 +49,8 @@ namespace AibisDream public LogReleasePresentationController LogReleasePresentation => logReleasePresentation; public ExpressionContentCatalog ContentCatalog => contentCatalog; + public ExpressionParticleLanguageProfile ParticleLanguageProfile => + particleLanguageProfile; public Locale ActiveExpressionLocale { get; private set; } public Locale GetExpressionLocale(Locale fallback) @@ -151,39 +154,36 @@ namespace AibisDream /// /// 启动粒子系统(在打开视图后调用,通常通过 Yarn 对话控制) /// - /// 干扰短句列表(笑话等,非目标粒子从中随机取字符) - /// 目标句子 - /// 当前 Locale 的默认随机字符池 + /// 当前 Locale 已完成粒子单位解析的轮次快照 /// 完成时触发的对话节点名称(可选) - /// 非目标候选粒子总数;<0 时使用 Inspector 默认候选池大小 + /// 轮次配置中的基础非目标粒子数 public void StartSystem( - List anxietyPhrases, - string targetSentence, - IReadOnlyList defaultCharacterPool, + ExpressionRoundTextSnapshot snapshot, string completionNodeName = null, - int nonTargetParticleTotal = -1) + int baseNonTargetParticleCount = -1) { HideSpriteOverlaysForMinigame(); if (particleManager != null) { particleManager.SetPresentationController(logReleasePresentation); - if (anxietyPhrases == null || anxietyPhrases.Count == 0 || - string.IsNullOrWhiteSpace(targetSentence) || - defaultCharacterPool == null || - defaultCharacterPool.Count == 0) + if (snapshot == null || + snapshot.TargetUnits == null || + snapshot.TargetUnits.Count == 0 || + snapshot.DefaultPoolUnits == null || + snapshot.DefaultPoolUnits.Count == 0 || + snapshot.InterferencePoolUnits == null || + snapshot.InterferencePoolUnits.Count == 0) { Debug.LogError( - "[ExpressionManager] 启动参数缺少目标文字、干扰 token 或默认字符池;系统未启动。"); + "[ExpressionManager] 轮次粒子单位快照无效;系统未启动。"); return; } particleManager.InitializeSystem( - anxietyPhrases, - targetSentence, - defaultCharacterPool, + snapshot, completionNodeName, - nonTargetParticleTotal); + baseNonTargetParticleCount); } } @@ -216,25 +216,6 @@ namespace AibisDream } } - /// - /// 启动粒子系统(字符串数组版本,方便 Yarn 调用) - /// - public void StartSystem( - string[] anxietyPhrases, - string targetSentence, - IReadOnlyList defaultCharacterPool, - string completionNodeName = null, - int nonTargetParticleTotal = -1) - { - List phrasesList = anxietyPhrases != null ? new List(anxietyPhrases) : null; - StartSystem( - phrasesList, - targetSentence, - defaultCharacterPool, - completionNodeName, - nonTargetParticleTotal); - } - /// /// 若火山释放 log 界面已淡出,则先淡入再返回(start_expression 时调用) /// diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionParticleGeometry.cs b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionParticleGeometry.cs new file mode 100644 index 000000000..2854bc9fe --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionParticleGeometry.cs @@ -0,0 +1,106 @@ +using UnityEngine; + +namespace AibisDream.MiniGame.Language +{ + public static class ExpressionParticleGeometry + { + public static bool TryGetConnection( + TextParticle first, + TextParticle second, + float centerDistanceLimit, + float edgeDistanceLimit, + out Vector3 from, + out Vector3 to, + out float proximity) + { + from = first != null ? first.transform.position : Vector3.zero; + to = second != null ? second.transform.position : Vector3.zero; + proximity = 0f; + if (first == null || second == null) + return false; + + float centerDistance = Vector3.Distance( + first.transform.position, + second.transform.position); + bool centerConnected = + centerDistanceLimit > 0f && centerDistance < centerDistanceLimit; + + Bounds firstBounds = first.GetVisualWorldBounds(); + Bounds secondBounds = second.GetVisualWorldBounds(); + float edgeDistance = BoundsDistance(firstBounds, secondBounds); + bool edgeConnected = + edgeDistanceLimit > 0f && edgeDistance <= edgeDistanceLimit; + + if (!centerConnected && !edgeConnected) + return false; + + from = firstBounds.ClosestPoint(secondBounds.center); + to = secondBounds.ClosestPoint(firstBounds.center); + if ((to - from).sqrMagnitude < 0.000001f) + { + from = first.transform.position; + to = second.transform.position; + } + + float centerProximity = centerConnected + ? 1f - Mathf.Clamp01(centerDistance / centerDistanceLimit) + : 0f; + float edgeProximity = edgeConnected + ? 1f - Mathf.Clamp01(edgeDistance / Mathf.Max(0.0001f, edgeDistanceLimit)) + : 0f; + proximity = Mathf.Max(centerProximity, edgeProximity); + return true; + } + + public static float BoundsDistance(Bounds first, Bounds second) + { + float dx = Mathf.Max( + first.min.x - second.max.x, + second.min.x - first.max.x, + 0f); + float dy = Mathf.Max( + first.min.y - second.max.y, + second.min.y - first.max.y, + 0f); + return Mathf.Sqrt(dx * dx + dy * dy); + } + + public static bool TryGetSeparation( + Bounds first, + Bounds second, + float padding, + out Vector2 direction, + out float overlap) + { + float overlapX = + Mathf.Min(first.max.x, second.max.x) - + Mathf.Max(first.min.x, second.min.x) + + padding; + float overlapY = + Mathf.Min(first.max.y, second.max.y) - + Mathf.Max(first.min.y, second.min.y) + + padding; + + if (overlapX <= 0f || overlapY <= 0f) + { + direction = Vector2.zero; + overlap = 0f; + return false; + } + + Vector2 centerDelta = first.center - second.center; + if (overlapX <= overlapY) + { + direction = new Vector2(centerDelta.x >= 0f ? 1f : -1f, 0f); + overlap = overlapX; + } + else + { + direction = new Vector2(0f, centerDelta.y >= 0f ? 1f : -1f); + overlap = overlapY; + } + + return true; + } + } +} diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionParticleGeometry.cs.meta b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionParticleGeometry.cs.meta new file mode 100644 index 000000000..d7a38ca79 --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionParticleGeometry.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 34ce433966a94546a460c32821f6e741 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionParticleLanguageProfile.cs b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionParticleLanguageProfile.cs new file mode 100644 index 000000000..8d5ee0022 --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionParticleLanguageProfile.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Localization; + +namespace AibisDream.MiniGame.Language +{ + public enum ExpressionParticleUnitMode + { + Grapheme, + Word + } + + [Serializable] + public sealed class ExpressionParticleLocaleSettings + { + [SerializeField] private string localeCode = "zh-Hans"; + [SerializeField] private ExpressionParticleUnitMode unitMode = + ExpressionParticleUnitMode.Grapheme; + [SerializeField, Min(0.1f)] private float targetFontScale = 1f; + [SerializeField, Min(0.1f)] private float nonTargetFontScale = 1f; + [SerializeField, Min(0.1f)] private float floatingFontScale = 1f; + [SerializeField, Min(0f)] private float nonTargetCountScale = 1f; + [SerializeField, Min(0f)] private float wordSpacing = 0.18f; + [SerializeField, Min(0f)] private float visualEdgeConnectionDistance = 0f; + [SerializeField, Range(0.1f, 1f)] private float minimumReadablePhraseScale = 0.65f; + + public string LocaleCode => localeCode; + public ExpressionParticleUnitMode UnitMode => unitMode; + public float TargetFontScale => targetFontScale; + public float NonTargetFontScale => nonTargetFontScale; + public float FloatingFontScale => floatingFontScale; + public float NonTargetCountScale => nonTargetCountScale; + public float WordSpacing => wordSpacing; + public float VisualEdgeConnectionDistance => visualEdgeConnectionDistance; + public float MinimumReadablePhraseScale => minimumReadablePhraseScale; + + public int ScaleNonTargetCount(int baseCount) + { + if (baseCount < 0) + return baseCount; + return Mathf.Max(0, Mathf.RoundToInt(baseCount * nonTargetCountScale)); + } + + public static ExpressionParticleLocaleSettings CreateFallback() + { + return new ExpressionParticleLocaleSettings(); + } + } + + [CreateAssetMenu( + fileName = "ExpressionParticleLanguageProfile", + menuName = "AIBIS/火山/表达粒子语言配置")] + public sealed class ExpressionParticleLanguageProfile : ScriptableObject + { + [SerializeField] private List locales = new(); + + private readonly HashSet warnedFallbackLocales = + new(StringComparer.OrdinalIgnoreCase); + + public IReadOnlyList Locales => locales; + + public ExpressionParticleLocaleSettings Resolve(LocaleIdentifier locale) + { + string code = locale.Code ?? string.Empty; + ExpressionParticleLocaleSettings exact = FindExact(code); + if (exact != null) + return exact; + + int separator = code.IndexOf('-'); + string language = separator > 0 ? code.Substring(0, separator) : code; + ExpressionParticleLocaleSettings prefix = FindLanguage(language); + if (prefix != null) + return prefix; + + if (warnedFallbackLocales.Add(code)) + { + Debug.LogWarning( + $"[ExpressionParticleLanguageProfile] Locale '{code}' 未配置;" + + "回退到 Grapheme 粒子模式。"); + } + + return ExpressionParticleLocaleSettings.CreateFallback(); + } + + public List GetValidationErrors() + { + var errors = new List(); + var codes = new HashSet(StringComparer.OrdinalIgnoreCase); + if (locales == null || locales.Count == 0) + { + errors.Add("表达粒子语言配置没有 Locale 项。"); + return errors; + } + + foreach (ExpressionParticleLocaleSettings settings in locales) + { + if (settings == null) + { + errors.Add("表达粒子语言配置包含空项。"); + continue; + } + + if (string.IsNullOrWhiteSpace(settings.LocaleCode)) + { + errors.Add("表达粒子语言配置包含空 Locale Code。"); + continue; + } + + if (!codes.Add(settings.LocaleCode)) + errors.Add($"表达粒子语言配置存在重复 Locale Code:{settings.LocaleCode}"); + } + + return errors; + } + + private ExpressionParticleLocaleSettings FindExact(string code) + { + if (locales == null) + return null; + + foreach (ExpressionParticleLocaleSettings settings in locales) + { + if (settings != null && + string.Equals( + settings.LocaleCode, + code, + StringComparison.OrdinalIgnoreCase)) + { + return settings; + } + } + + return null; + } + + private ExpressionParticleLocaleSettings FindLanguage(string language) + { + if (string.IsNullOrEmpty(language) || locales == null) + return null; + + foreach (ExpressionParticleLocaleSettings settings in locales) + { + if (settings == null || string.IsNullOrWhiteSpace(settings.LocaleCode)) + continue; + + string configured = settings.LocaleCode; + int separator = configured.IndexOf('-'); + string configuredLanguage = + separator > 0 ? configured.Substring(0, separator) : configured; + if (string.Equals( + configuredLanguage, + language, + StringComparison.OrdinalIgnoreCase)) + { + return settings; + } + } + + return null; + } + } +} diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionParticleLanguageProfile.cs.meta b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionParticleLanguageProfile.cs.meta new file mode 100644 index 000000000..85a486969 --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionParticleLanguageProfile.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3a7f6c18f2184d09b551e103c67268a1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionRoundTextSnapshot.cs b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionRoundTextSnapshot.cs new file mode 100644 index 000000000..673c7c16b --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionRoundTextSnapshot.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using UnityEngine.Localization; + +namespace AibisDream.MiniGame.Language +{ + public sealed class ExpressionRoundTextSnapshot + { + private ExpressionRoundTextSnapshot( + LocaleIdentifier locale, + ExpressionParticleLocaleSettings profile, + string targetText, + List targetUnits, + List targetGapWeights, + List defaultPoolUnits, + List interferencePoolUnits) + { + Locale = locale; + Profile = profile; + TargetText = targetText; + TargetUnits = targetUnits.AsReadOnly(); + TargetGapWeights = targetGapWeights.AsReadOnly(); + DefaultPoolUnits = defaultPoolUnits.AsReadOnly(); + InterferencePoolUnits = interferencePoolUnits.AsReadOnly(); + } + + public LocaleIdentifier Locale { get; } + public ExpressionParticleLocaleSettings Profile { get; } + public string TargetText { get; } + public IReadOnlyList TargetUnits { get; } + public IReadOnlyList TargetGapWeights { get; } + public IReadOnlyList DefaultPoolUnits { get; } + public IReadOnlyList InterferencePoolUnits { get; } + + public static bool TryCreate( + LocaleIdentifier locale, + ExpressionParticleLocaleSettings profile, + string targetText, + IEnumerable interferencePhrases, + string defaultPool, + out ExpressionRoundTextSnapshot snapshot, + out string error) + { + snapshot = null; + profile ??= ExpressionParticleLocaleSettings.CreateFallback(); + + ExpressionTextTokenizer.UnitSequence target = + ExpressionTextTokenizer.BuildLayoutUnits(targetText, profile.UnitMode); + if (target.Count == 0) + { + error = "目标句没有有效的粒子单位。"; + return false; + } + + if (!ExpressionTextTokenizer.TryTokenizePool( + defaultPool, + profile.UnitMode, + out List defaultUnits)) + { + error = "默认粒子池为空或包含非法分隔符。"; + return false; + } + + List interferenceUnits = + ExpressionTextTokenizer.BuildUnitPool( + interferencePhrases, + profile.UnitMode); + if (interferenceUnits.Count == 0) + { + error = "干扰短句没有有效的粒子单位。"; + return false; + } + + snapshot = new ExpressionRoundTextSnapshot( + locale, + profile, + targetText, + new List(target.Units), + new List(target.GapWeights), + defaultUnits, + interferenceUnits); + error = null; + return true; + } + } +} diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionRoundTextSnapshot.cs.meta b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionRoundTextSnapshot.cs.meta new file mode 100644 index 000000000..b007c5725 --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionRoundTextSnapshot.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1972c8c6ecaa4512b9cc891a82a5d608 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionTextTokenizer.cs b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionTextTokenizer.cs index a0712b059..bcc843193 100644 --- a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionTextTokenizer.cs +++ b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionTextTokenizer.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Globalization; +using System.Text; namespace AibisDream.MiniGame.Language { @@ -9,6 +10,19 @@ namespace AibisDream.MiniGame.Language /// public static class ExpressionTextTokenizer { + public readonly struct UnitSequence + { + public UnitSequence(List units, List gapWeights) + { + Units = units; + GapWeights = gapWeights; + } + + public IReadOnlyList Units { get; } + public IReadOnlyList GapWeights { get; } + public int Count => Units?.Count ?? 0; + } + public readonly struct Layout { public Layout(List visibleElements, List offsets) @@ -40,16 +54,56 @@ namespace AibisDream.MiniGame.Language } public static List BuildCharacterPool(IEnumerable texts) + { + return BuildUnitPool(texts, ExpressionParticleUnitMode.Grapheme); + } + + public static List TokenizeText( + string text, + ExpressionParticleUnitMode mode) + { + return new List(BuildLayoutUnits(text, mode).Units); + } + + public static bool TryTokenizePool( + string pool, + ExpressionParticleUnitMode mode, + out List units) + { + units = new List(); + if (string.IsNullOrWhiteSpace(pool) || + pool.Contains("|") || + pool.Contains("|")) + { + return false; + } + + units = TokenizeText(pool, mode); + return units.Count > 0; + } + + public static List BuildUnitPool( + IEnumerable texts, + ExpressionParticleUnitMode mode) { var result = new List(); if (texts == null) return result; foreach (string text in texts) - result.AddRange(GetVisibleElements(text)); + result.AddRange(TokenizeText(text, mode)); return result; } + public static UnitSequence BuildLayoutUnits( + string text, + ExpressionParticleUnitMode mode) + { + return mode == ExpressionParticleUnitMode.Word + ? BuildWordUnits(text) + : BuildGraphemeUnits(text); + } + public static Layout BuildLayout( string text, float characterSpacing, @@ -115,5 +169,80 @@ namespace AibisDream.MiniGame.Language return -1; } + + public static int FindUnitSequence( + IReadOnlyList source, + IReadOnlyList fragment) + { + return FindVisibleSequence(source, fragment); + } + + private static UnitSequence BuildGraphemeUnits(string text) + { + var units = new List(); + var gaps = new List(); + if (string.IsNullOrEmpty(text)) + return new UnitSequence(units, gaps); + + float pendingGap = 0f; + TextElementEnumerator enumerator = + StringInfo.GetTextElementEnumerator(text); + while (enumerator.MoveNext()) + { + string element = enumerator.GetTextElement(); + if (string.IsNullOrWhiteSpace(element)) + { + if (units.Count > 0) + pendingGap += 1f; + continue; + } + + units.Add(element); + gaps.Add(units.Count == 1 ? 0f : pendingGap); + pendingGap = 0f; + } + + return new UnitSequence(units, gaps); + } + + private static UnitSequence BuildWordUnits(string text) + { + var units = new List(); + var gaps = new List(); + if (string.IsNullOrEmpty(text)) + return new UnitSequence(units, gaps); + + var word = new StringBuilder(); + bool sawSeparatorAfterUnit = false; + foreach (char value in text) + { + if (char.IsWhiteSpace(value)) + { + FlushWord(word, units, gaps, sawSeparatorAfterUnit); + if (units.Count > 0) + sawSeparatorAfterUnit = true; + continue; + } + + word.Append(value); + } + + FlushWord(word, units, gaps, sawSeparatorAfterUnit); + return new UnitSequence(units, gaps); + } + + private static void FlushWord( + StringBuilder word, + List units, + List gaps, + bool hasLeadingSeparator) + { + if (word.Length == 0) + return; + + units.Add(word.ToString()); + gaps.Add(units.Count == 1 ? 0f : hasLeadingSeparator ? 1f : 0f); + word.Clear(); + } } } diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/FloatingTextParticle.cs b/Assets/Scripts/MiniGame/HuoShan/Language/FloatingTextParticle.cs index 301a3eca6..f88efadf2 100644 --- a/Assets/Scripts/MiniGame/HuoShan/Language/FloatingTextParticle.cs +++ b/Assets/Scripts/MiniGame/HuoShan/Language/FloatingTextParticle.cs @@ -42,7 +42,7 @@ namespace AibisDream.MiniGame.Language /// /// 背景粒子从干扰短句(笑话等)字符中随机显示单字。 /// - protected override string GetNextDisplayChar() + protected override string GetNextDisplayUnit() { return GetRandomCharFromPhrases(); } @@ -56,6 +56,7 @@ namespace AibisDream.MiniGame.Language if (textMesh != null) { textMesh.fontSize = size; + InvalidateVisualBounds(); } } diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs index 475100b1b..e34b6fdd7 100644 --- a/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs +++ b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs @@ -64,10 +64,9 @@ namespace AibisDream.MiniGame.Language [SerializeField] private float textMargin = 1f; [Header("游戏配置")] - private List anxietyPhrases = new List(); - private string targetSentence = string.Empty; - private IReadOnlyList defaultCharacterPool = new List().AsReadOnly(); - private IReadOnlyList interferenceCharacterPool = new List().AsReadOnly(); + private ExpressionRoundTextSnapshot activeSnapshot; + private IReadOnlyList defaultUnitPool = new List().AsReadOnly(); + private IReadOnlyList interferenceUnitPool = new List().AsReadOnly(); [SerializeField] private string completionDialogNode = ""; // 完成时触发的对话节点 [Header("波形参数")] @@ -222,7 +221,7 @@ namespace AibisDream.MiniGame.Language /// 完成阶段屏幕钳制用 SmoothDamp 速度缓存(按粒子) private readonly Dictionary completionClampSmoothVelocity = new Dictionary(); private Vector3 focusCentroid; - private List finalTargetCharacters = new List(); + private List finalTargetUnits = new List(); private List finalTargetPositions = new List(); private bool interferenceLogicSuspended = false; private bool statusUIHidden = false; @@ -240,7 +239,7 @@ namespace AibisDream.MiniGame.Language { public CandidateParticle particle; public float startShakeIntensity; - public string finalChar; + public string finalUnitText; public bool stabilized; public Vector3 focusPosition; public Vector3 finalPosition; @@ -266,6 +265,11 @@ namespace AibisDream.MiniGame.Language private const string TruthGatherTweenId = "HuoshanTruthGather"; private const string FocusInterferenceTweenId = "HuoshanFocusInterference"; private int truthAttackSequenceIndex; + private float activePhraseScale = 1f; + + private ExpressionParticleUnitMode ActiveUnitMode => + activeSnapshot?.Profile?.UnitMode ?? ExpressionParticleUnitMode.Grapheme; + private float ResolvedFocusScale => focusZoomScale * activePhraseScale; // 边界 private Bounds movementBounds; @@ -288,11 +292,17 @@ namespace AibisDream.MiniGame.Language /// Inspector 中的候选粒子总数,用于未在 Yarn 中指定非目标数量时恢复默认 private int defaultCandidateCountFromInspector; private bool hasCapturedDefaultCandidateCount; + private float defaultTargetFontSizeFromInspector; + private float defaultNonTargetFontSizeFromInspector; + private float defaultFloatingFontSizeFromInspector; private void CaptureDefaultCandidateCountFromInspector() { if (hasCapturedDefaultCandidateCount) return; defaultCandidateCountFromInspector = candidateCount; + defaultTargetFontSizeFromInspector = targetParticleFontSize; + defaultNonTargetFontSizeFromInspector = nonTargetParticleFontSize; + defaultFloatingFontSizeFromInspector = floatingTextFontSize; hasCapturedDefaultCandidateCount = true; } @@ -346,65 +356,59 @@ namespace AibisDream.MiniGame.Language /// /// 初始化系统(可以由外部调用,用于配置游戏参数) /// - /// 焦虑短句列表 - /// 目标句子 - /// 当前 Locale 的默认随机字符池 + /// 当前 Locale 已完成粒子单位解析的轮次快照 /// 完成时触发的对话节点 - /// - /// 非目标候选粒子总数(蓝/干扰侧可交互粒子数量)。≥0 时候选池大小 = 目标句字数 + 该值;<0 时使用 Inspector 的 Candidate Count。 + /// + /// 轮次配置中的基础非目标粒子数。≥0 时先应用 Locale Profile 数量倍率; + /// <0 时使用 Inspector 的 Candidate Count。 /// public void InitializeSystem( - List phrases, - string sentence, - IReadOnlyList localizedDefaultPool, + ExpressionRoundTextSnapshot snapshot, string dialogNode = null, - int nonTargetParticleTotal = -1) + int baseNonTargetParticleCount = -1) { CaptureDefaultCandidateCountFromInspector(); - if (phrases == null || phrases.Count == 0 || string.IsNullOrWhiteSpace(sentence)) + if (snapshot == null || + snapshot.TargetUnits == null || + snapshot.TargetUnits.Count == 0 || + snapshot.DefaultPoolUnits == null || + snapshot.DefaultPoolUnits.Count == 0 || + snapshot.InterferencePoolUnits == null || + snapshot.InterferencePoolUnits.Count == 0) { Debug.LogError( - "[LanguageParticleManager] 目标文字或干扰 token 为空;初始化已中止。"); + "[LanguageParticleManager] 粒子单位快照无效;初始化已中止。"); return; } - List resolvedDefaultPool = - ExpressionTextTokenizer.BuildCharacterPool(localizedDefaultPool); - List resolvedInterferencePool = - ExpressionTextTokenizer.BuildCharacterPool(phrases); - if (resolvedDefaultPool.Count == 0 || resolvedInterferencePool.Count == 0) - { - Debug.LogError( - "[LanguageParticleManager] 默认字符池或干扰字符池没有有效的 Unicode 文本元素;初始化已中止。"); - return; - } + ExpressionParticleLocaleSettings profile = + snapshot.Profile ?? ExpressionParticleLocaleSettings.CreateFallback(); + int targetUnitCount = snapshot.TargetUnits.Count; + int scaledNonTargetCount = + profile.ScaleNonTargetCount(baseNonTargetParticleCount); - string useSentence = sentence; - int visibleTargetCount = ExpressionTextTokenizer.GetVisibleElements(useSentence).Count; - if (visibleTargetCount == 0) - { - Debug.LogError("[LanguageParticleManager] 目标句没有可显示的 Unicode 文本元素;初始化已中止。"); - return; - } - - if (nonTargetParticleTotal >= 0) + if (scaledNonTargetCount >= 0) { candidateCount = Mathf.Max( - visibleTargetCount + nonTargetParticleTotal, - visibleTargetCount); + targetUnitCount + scaledNonTargetCount, + targetUnitCount); } else { candidateCount = defaultCandidateCountFromInspector; } - // 在创建或复用粒子前先提交本轮不可变快照,防止首帧或上一轮字符泄漏。 - anxietyPhrases = new List(phrases); - targetSentence = useSentence; + activeSnapshot = snapshot; completionDialogNode = dialogNode ?? ""; - defaultCharacterPool = resolvedDefaultPool.AsReadOnly(); - interferenceCharacterPool = resolvedInterferencePool.AsReadOnly(); + defaultUnitPool = snapshot.DefaultPoolUnits; + interferenceUnitPool = snapshot.InterferencePoolUnits; + targetParticleFontSize = + defaultTargetFontSizeFromInspector * profile.TargetFontScale; + nonTargetParticleFontSize = + defaultNonTargetFontSizeFromInspector * profile.NonTargetFontScale; + floatingTextFontSize = + defaultFloatingFontSizeFromInspector * profile.FloatingFontScale; // 如果是第一次初始化,先执行基础初始化 if (!isInitialized) @@ -440,11 +444,10 @@ namespace AibisDream.MiniGame.Language else { EnsureCandidatePoolSize(candidateCount); - ApplyCharacterPoolsToParticles(); + ApplyUnitConfigurationToParticles(); } - // 根据目标句子更新目标粒子数量 - redParticleCount = Mathf.Min(visibleTargetCount, candidateCount); + redParticleCount = Mathf.Min(targetUnitCount, candidateCount); // 重新初始化游戏状态并播放入场动画 ResetGame(true); @@ -726,7 +729,7 @@ namespace AibisDream.MiniGame.Language Vector3 pos = GetRandomPositionInBounds(); GameObject obj = CreateParticleObject(pos, $"FloatingParticle_{i}"); FloatingTextParticle particle = obj.AddComponent(); - particle.SetCharacterPools(defaultCharacterPool, interferenceCharacterPool); + particle.SetUnitPools(defaultUnitPool, interferenceUnitPool); particle.SetFont(chineseFontAsset, chineseFontMaterial); particle.SetColor(floatingTextColor); particle.SetFontSize(floatingTextFontSize); @@ -771,7 +774,7 @@ namespace AibisDream.MiniGame.Language Vector3 pos = GetRandomPositionInBounds(); GameObject obj = CreateParticleObject(pos, $"CandidateParticle_{index}"); CandidateParticle particle = obj.AddComponent(); - particle.SetCharacterPools(defaultCharacterPool, interferenceCharacterPool); + particle.SetUnitPools(defaultUnitPool, interferenceUnitPool); particle.SetFont(chineseFontAsset, chineseFontMaterial); particle.SetColors(targetParticleColor, nonTargetParticleColor, nonTargetParticleColor, targetCalmColor); particle.SetNonTargetMarquee( @@ -788,20 +791,32 @@ namespace AibisDream.MiniGame.Language candidateParticles.Add(particle); } - private void ApplyCharacterPoolsToParticles() + private void ApplyUnitConfigurationToParticles() { foreach (CandidateParticle particle in candidateParticles) - particle?.SetCharacterPools(defaultCharacterPool, interferenceCharacterPool); + { + if (particle == null) continue; + particle.SetUnitPools(defaultUnitPool, interferenceUnitPool); + particle.SetFontStyles( + targetParticleFontSize, + nonTargetParticleFontSize, + targetParticleBold, + nonTargetParticleBold); + } foreach (FloatingTextParticle particle in floatingParticles) - particle?.SetCharacterPools(defaultCharacterPool, interferenceCharacterPool); + { + if (particle == null) continue; + particle.SetUnitPools(defaultUnitPool, interferenceUnitPool); + particle.SetFontSize(floatingTextFontSize); + } } - private void RefreshAllRandomCharacters() + private void RefreshAllRandomUnits() { foreach (CandidateParticle particle in candidateParticles) - particle?.InitializeRandomCharacter(); + particle?.InitializeRandomUnit(); foreach (FloatingTextParticle particle in floatingParticles) - particle?.InitializeRandomCharacter(); + particle?.InitializeRandomUnit(); } /// @@ -1183,12 +1198,12 @@ namespace AibisDream.MiniGame.Language continue; } - float distance = Vector3.Distance( - candidateParticles[i].transform.position, - candidateParticles[j].transform.position - ); - - if (distance < connectionDistance) + if (TryGetConnectionVisual( + candidateParticles[i], + candidateParticles[j], + out _, + out _, + out _)) { candidateParticles[i].connections.Add(candidateParticles[j]); candidateParticles[j].connections.Add(candidateParticles[i]); @@ -1204,12 +1219,12 @@ namespace AibisDream.MiniGame.Language { for (int j = i + 1; j < candidateParticles.Count; j++) { - float distance = Vector3.Distance( - candidateParticles[i].transform.position, - candidateParticles[j].transform.position - ); - - if (distance < connectionDistance) + if (TryGetConnectionVisual( + candidateParticles[i], + candidateParticles[j], + out _, + out _, + out _)) { string key = $"{Mathf.Min(i, j)}-{Mathf.Max(i, j)}"; previousConnections[key] = true; @@ -1226,12 +1241,12 @@ namespace AibisDream.MiniGame.Language { for (int j = i + 1; j < candidateParticles.Count; j++) { - float distance = Vector3.Distance( - candidateParticles[i].transform.position, - candidateParticles[j].transform.position - ); - - if (distance < connectionDistance) + if (TryGetConnectionVisual( + candidateParticles[i], + candidateParticles[j], + out _, + out _, + out _)) { string key = $"{Mathf.Min(i, j)}-{Mathf.Max(i, j)}"; currentConnections[key] = true; @@ -1276,8 +1291,7 @@ namespace AibisDream.MiniGame.Language { if (other.isRed) continue; - float dist = Vector3.Distance(red.transform.position, other.transform.position); - if (dist < connectionDistance) + if (red.connections.Contains(other)) return false; } } @@ -1285,26 +1299,50 @@ namespace AibisDream.MiniGame.Language return true; } + public bool TryGetConnectionVisual( + CandidateParticle first, + CandidateParticle second, + out Vector3 from, + out Vector3 to, + out float proximity) + { + float edgeDistance = + ActiveUnitMode == ExpressionParticleUnitMode.Word + ? activeSnapshot?.Profile?.VisualEdgeConnectionDistance ?? 0.18f + : 0f; + return ExpressionParticleGeometry.TryGetConnection( + first, + second, + connectionDistance, + edgeDistance, + out from, + out to, + out proximity); + } + private void OnTargetsCompleted() { completionPhase = CompletionPhase.Completed; completionTimer = 0f; focusTimer = 0f; fadeOutAlpha = 1f; + activePhraseScale = 1f; allowInput = false; DestroyFocusVisualDecorationsAndClear(); - finalTargetCharacters.Clear(); + finalTargetUnits.Clear(); finalTargetPositions.Clear(); focusTweensCompleted = false; interferenceLogicSuspended = false; SetStatusUIVisibility(true, 0f); orderedRedParticles = GetOrderedRedParticles(); - ExpressionTextTokenizer.Layout layout = ExpressionTextTokenizer.BuildLayout( - targetSentence, - 0.9f, - whitespaceSpacingMultiplier); - int displayLength = Mathf.Min(orderedRedParticles.Count, layout.Count); + IReadOnlyList targetUnits = + activeSnapshot?.TargetUnits ?? new List().AsReadOnly(); + IReadOnlyList targetGapWeights = + activeSnapshot?.TargetGapWeights ?? new List().AsReadOnly(); + List layoutPositions = + CalculatePhrasePositions(targetUnits, targetGapWeights); + int displayLength = Mathf.Min(orderedRedParticles.Count, targetUnits.Count); if (displayLength < orderedRedParticles.Count) { @@ -1313,18 +1351,17 @@ namespace AibisDream.MiniGame.Language for (int i = 0; i < displayLength; i++) { - finalTargetCharacters.Add(layout.VisibleElements[i]); + finalTargetUnits.Add(targetUnits[i]); } - Vector3 displayCenter = worldCanvas != null ? worldCanvas.transform.position : Vector3.zero; - if (finalTargetCharacters.Count > 0) + if (finalTargetUnits.Count > 0) { finalTargetPositions.Clear(); Bounds clip = GetCompletionClipBounds(); - for (int i = 0; i < finalTargetCharacters.Count; i++) + for (int i = 0; i < finalTargetUnits.Count; i++) { - Vector3 pos = displayCenter + new Vector3(layout.Offsets[i], 0f, 0f); - finalTargetPositions.Add(ClampPositionToBounds(pos, clip)); + finalTargetPositions.Add( + ClampPositionToBounds(layoutPositions[i], clip)); } } @@ -1545,7 +1582,8 @@ namespace AibisDream.MiniGame.Language Vector3 offset = particle.transform.position - focusCentroid; float indexCenter = (orderedRedParticles.Count - 1) * 0.5f; Vector3 spreadOffset = new Vector3((i - indexCenter) * spreadDistance, Mathf.Sin(i * 1.4f) * spreadDistance * 0.6f, 0f); - Vector3 focusPosition = focusTargetCenter + offset * focusZoomScale + spreadOffset; + Vector3 focusPosition = + focusTargetCenter + offset * ResolvedFocusScale + spreadOffset; Vector3 finalPosition = finalTargetPositions.Count > i ? finalTargetPositions[i] : focusTargetCenter; focusPosition = ClampPositionToBounds(focusPosition, focusClipBounds); finalPosition = ClampPositionToBounds(finalPosition, focusClipBounds); @@ -1559,7 +1597,10 @@ namespace AibisDream.MiniGame.Language if (completedTweens >= tweenTargetCount) focusMoveTweensCompleted = true; }); - particle.transform.DOScale(Vector3.one * focusZoomScale, focusMoveDuration).SetEase(Ease.InOutQuad); + particle.transform.DOScale( + Vector3.one * ResolvedFocusScale, + focusMoveDuration) + .SetEase(Ease.InOutQuad); DOTween.To( () => particle.resolvedColorProgress, value => particle.resolvedColorProgress = value, @@ -1569,8 +1610,11 @@ namespace AibisDream.MiniGame.Language .SetTarget(particle) .OnComplete(() => particle.resolvedColorProgress = 1f); - string finalChar = i < finalTargetCharacters.Count ? finalTargetCharacters[i] : particle.currentChar; - TargetFocusVisual visual = CreateFocusVisual(particle, finalChar); + string finalUnitText = i < finalTargetUnits.Count + ? finalTargetUnits[i] + : particle.CurrentUnitText; + TargetFocusVisual visual = + CreateFocusVisual(particle, finalUnitText); visual.focusPosition = focusPosition; visual.finalPosition = finalPosition; visual.orderIndex = i; @@ -1611,12 +1655,14 @@ namespace AibisDream.MiniGame.Language focusTransitionRoutine = null; } - private TargetFocusVisual CreateFocusVisual(CandidateParticle particle, string finalChar) + private TargetFocusVisual CreateFocusVisual( + CandidateParticle particle, + string finalUnitText) { return new TargetFocusVisual { particle = particle, - finalChar = finalChar, + finalUnitText = finalUnitText, startShakeIntensity = Mathf.Max(particle.anxietyShakeIntensity, 4f) }; } @@ -1712,7 +1758,7 @@ namespace AibisDream.MiniGame.Language if (!visual.gatherLocked && gather >= lockThreshold) { visual.gatherLocked = true; - visual.particle.ForceSetCharacter(visual.finalChar); + visual.particle.ForceSetUnitText(visual.finalUnitText); visual.particle.SetChangeInterval(2f); visual.particle.ResetChangeTimer(2f); visual.particle.anxietyShakeIntensity = 0.6f; @@ -1911,7 +1957,7 @@ namespace AibisDream.MiniGame.Language if (!visual.stabilized && probabilityProgress >= 1f) { - visual.particle.ForceSetCharacter(visual.finalChar); + visual.particle.ForceSetUnitText(visual.finalUnitText); visual.particle.SetChangeInterval(1.5f); visual.particle.ResetChangeTimer(visual.particle.changeSpeedMultiplier); visual.particle.isStatic = true; @@ -2054,14 +2100,16 @@ namespace AibisDream.MiniGame.Language } StopSlotMachineShuffle(); - string pool = SanitizeSlotMachinePool(charPool); - if (string.IsNullOrEmpty(pool)) + string particlePool = ActiveUnitMode == ExpressionParticleUnitMode.Word + ? charPool?.Replace("|", " ").Replace("|", " ") + : SanitizeSlotMachinePool(charPool); + List poolElements = + ExpressionTextTokenizer.TokenizeText(particlePool, ActiveUnitMode); + if (poolElements.Count == 0) { Debug.LogWarning("[LanguageParticleManager] 老虎机换字字符池为空;已安全跳过。"); return; } - IReadOnlyList poolElements = - ExpressionTextTokenizer.GetVisibleElements(pool).AsReadOnly(); float changeInterval = Mathf.Max(0.02f, interval); foreach (TargetFocusVisual visual in focusVisuals.Values) @@ -2069,7 +2117,7 @@ namespace AibisDream.MiniGame.Language if (visual?.particle == null) continue; - visual.particle.SetOverrideCharacterPool(poolElements); + visual.particle.SetOverrideUnitPool(poolElements); visual.particle.SetChangeInterval(changeInterval); visual.particle.ResetChangeTimer(changeInterval); visual.particle.isStatic = false; @@ -2103,7 +2151,7 @@ namespace AibisDream.MiniGame.Language } foreach (CandidateParticle particle in targetParticles) - particle?.SetOverrideCharacterPool(null); + particle?.SetOverrideUnitPool((IReadOnlyList)null); } private IEnumerator EndSlotMachineShuffleAfter(float duration) @@ -2111,7 +2159,7 @@ namespace AibisDream.MiniGame.Language yield return new WaitForSeconds(duration); slotMachineRoutine = null; foreach (CandidateParticle particle in targetParticles) - particle?.SetOverrideCharacterPool(null); + particle?.SetOverrideUnitPool((IReadOnlyList)null); } public void PrepareTruthReleaseFromLie() @@ -2211,7 +2259,7 @@ namespace AibisDream.MiniGame.Language List particles = GetPresentationPhraseParticles(); if (!ValidatePresentationPhrase(text, particles, "真话失稳")) yield break; - List characters = ExpressionTextTokenizer.GetVisibleElements(text); + List units = TokenizeActiveText(text); Color unstableColor = truthAttackColor; if (!string.IsNullOrWhiteSpace(colorHex)) @@ -2231,7 +2279,7 @@ namespace AibisDream.MiniGame.Language float resolvedShakeAmplitude = Mathf.Max(0f, shakeAmplitude); float resolvedShakeSpeed = Mathf.Max(0f, shakeSpeed); List destinations = CalculatePresentationPhrasePositions(text); - UpdateFinalPresentationPhrase(characters, destinations); + UpdateFinalPresentationPhrase(units, destinations); for (int i = 0; i < particles.Count; i++) { @@ -2258,13 +2306,13 @@ namespace AibisDream.MiniGame.Language particle.resolvedColorProgress = 1f; particle.calmProgress = 1f; - if (i >= characters.Count) + if (i >= units.Count) { particle.alpha = 0f; continue; } - particle.ForceSetCharacter(characters[i]); + particle.ForceSetUnitText(units[i]); particle.alpha = 1f; particle.SetFocusInterference( unstableColor, @@ -2282,7 +2330,7 @@ namespace AibisDream.MiniGame.Language yield return new WaitForSeconds(totalDuration); - for (int i = 0; i < characters.Count && i < particles.Count; i++) + for (int i = 0; i < units.Count && i < particles.Count; i++) { CandidateParticle particle = particles[i]; if (particle == null) @@ -2300,7 +2348,7 @@ namespace AibisDream.MiniGame.Language List particles = GetPresentationPhraseParticles(); if (!ValidatePresentationPhrase(text, particles, "真话攻击")) yield break; - List characters = ExpressionTextTokenizer.GetVisibleElements(text); + List units = TokenizeActiveText(text); PreparePresentationPhraseControl(); @@ -2310,7 +2358,7 @@ namespace AibisDream.MiniGame.Language float settleDuration = Mathf.Max(0.04f, totalDuration - travelDuration); int directionSign = truthAttackSequenceIndex++ % 2 == 0 ? -1 : 1; List destinations = CalculatePresentationPhrasePositions(text); - UpdateFinalPresentationPhrase(characters, destinations); + UpdateFinalPresentationPhrase(units, destinations); for (int i = 0; i < particles.Count; i++) { @@ -2338,7 +2386,7 @@ namespace AibisDream.MiniGame.Language particle.resolvedColorProgress = 1f; particle.calmProgress = 1f; - if (i >= characters.Count) + if (i >= units.Count) { DOTween.To( () => particle.alpha, @@ -2350,7 +2398,7 @@ namespace AibisDream.MiniGame.Language continue; } - particle.ForceSetCharacter(characters[i]); + particle.ForceSetUnitText(units[i]); particle.alpha = 1f; particle.focusInterferenceProgress = 1f; @@ -2360,9 +2408,9 @@ namespace AibisDream.MiniGame.Language particle.transform.position = destination + new Vector3(directionSign * travel, verticalKick, 0f); particle.transform.localScale = new Vector3( - focusZoomScale * Mathf.Lerp(1.12f, 1.42f, resolvedImpact), - focusZoomScale * Mathf.Lerp(0.86f, 0.66f, resolvedImpact), - focusZoomScale); + ResolvedFocusScale * Mathf.Lerp(1.12f, 1.42f, resolvedImpact), + ResolvedFocusScale * Mathf.Lerp(0.86f, 0.66f, resolvedImpact), + ResolvedFocusScale); float delay = Mathf.Min(totalDuration * 0.12f, i * 0.018f); float localTravelDuration = Mathf.Max(0.04f, travelDuration - delay); @@ -2374,7 +2422,7 @@ namespace AibisDream.MiniGame.Language .SetEase(Ease.OutExpo)); sequence.Join( particle.transform.DOScale( - Vector3.one * focusZoomScale, + Vector3.one * ResolvedFocusScale, localTravelDuration) .SetEase(Ease.OutCubic)); sequence.Append( @@ -2387,13 +2435,13 @@ namespace AibisDream.MiniGame.Language yield return new WaitForSeconds(totalDuration); - for (int i = 0; i < characters.Count && i < particles.Count; i++) + for (int i = 0; i < units.Count && i < particles.Count; i++) { CandidateParticle particle = particles[i]; if (particle == null) continue; particle.transform.position = destinations[i]; - particle.transform.localScale = Vector3.one * focusZoomScale; + particle.transform.localScale = Vector3.one * ResolvedFocusScale; particle.alpha = 1f; } } @@ -2406,7 +2454,7 @@ namespace AibisDream.MiniGame.Language List particles = GetPresentationPhraseParticles(); if (!ValidatePresentationPhrase(text, particles, "真话稳定")) yield break; - List characters = ExpressionTextTokenizer.GetVisibleElements(text); + List units = TokenizeActiveText(text); PreparePresentationPhraseControl(); @@ -2432,7 +2480,7 @@ namespace AibisDream.MiniGame.Language .SetEase(Ease.InCubic) .SetTarget(particle); particle.transform.DOScale( - Vector3.one * focusZoomScale * 0.72f, + Vector3.one * ResolvedFocusScale * 0.72f, breakDuration) .SetEase(Ease.InCubic) .SetTarget(particle); @@ -2448,7 +2496,7 @@ namespace AibisDream.MiniGame.Language yield return new WaitForSeconds(breakDuration); List destinations = CalculatePresentationPhrasePositions(text); - UpdateFinalPresentationPhrase(characters, destinations); + UpdateFinalPresentationPhrase(units, destinations); for (int i = 0; i < particles.Count; i++) { CandidateParticle particle = particles[i]; @@ -2473,14 +2521,14 @@ namespace AibisDream.MiniGame.Language particle.resolvedColorProgress = 1f; particle.calmProgress = 1f; - if (i >= characters.Count) + if (i >= units.Count) { particle.alpha = 0f; particle.ClearFocusInterference(); continue; } - particle.ForceSetCharacter(characters[i]); + particle.ForceSetUnitText(units[i]); particle.SetFocusInterference(truthAttackColor, 0f, 0f); particle.focusInterferenceProgress = 1f; particle.alpha = 0.15f; @@ -2488,7 +2536,7 @@ namespace AibisDream.MiniGame.Language .SetEase(Ease.OutCubic) .SetTarget(particle); particle.transform.DOScale( - Vector3.one * focusZoomScale, + Vector3.one * ResolvedFocusScale, reformDuration) .SetEase(Ease.OutBack) .SetTarget(particle); @@ -2516,11 +2564,11 @@ namespace AibisDream.MiniGame.Language if (particle == null) continue; particle.ClearFocusInterference(); - particle.alpha = i < characters.Count ? 1f : 0f; - if (i < characters.Count) + particle.alpha = i < units.Count ? 1f : 0f; + if (i < units.Count) { particle.transform.position = destinations[i]; - particle.transform.localScale = Vector3.one * focusZoomScale; + particle.transform.localScale = Vector3.one * ResolvedFocusScale; } } } @@ -2540,7 +2588,7 @@ namespace AibisDream.MiniGame.Language .ToList(); } - private static bool ValidatePresentationPhrase( + private bool ValidatePresentationPhrase( string text, List particles, string context) @@ -2557,7 +2605,7 @@ namespace AibisDream.MiniGame.Language return false; } - int requiredParticles = ExpressionTextTokenizer.GetVisibleElements(text).Count; + int requiredParticles = TokenizeActiveText(text).Count; if (requiredParticles == 0) { Debug.LogWarning($"[LanguageParticleManager] {context}没有可显示的文本元素;已安全跳过。"); @@ -2593,30 +2641,124 @@ namespace AibisDream.MiniGame.Language private List CalculatePresentationPhrasePositions(string text) { - float spacing = Mathf.Max(0.1f, truthAttackCharacterSpacing); - ExpressionTextTokenizer.Layout layout = ExpressionTextTokenizer.BuildLayout( - text, - spacing, - whitespaceSpacingMultiplier); - var positions = new List(layout.Count); - Vector3 center = worldCanvas != null ? worldCanvas.transform.position : transform.position; - Bounds clip = GetCompletionClipBounds(); - for (int i = 0; i < layout.Count; i++) + ExpressionTextTokenizer.UnitSequence sequence = + ExpressionTextTokenizer.BuildLayoutUnits(text, ActiveUnitMode); + return CalculatePhrasePositions(sequence.Units, sequence.GapWeights); + } + + private List TokenizeActiveText(string text) + { + return ExpressionTextTokenizer.TokenizeText(text, ActiveUnitMode); + } + + private List CalculatePhrasePositions( + IReadOnlyList units, + IReadOnlyList gapWeights) + { + int count = units?.Count ?? 0; + var offsets = new List(count); + var positions = new List(count); + if (count == 0) { - Vector3 position = center + new Vector3(layout.Offsets[i], 0f, 0f); + activePhraseScale = 1f; + return positions; + } + + bool wordMode = ActiveUnitMode == ExpressionParticleUnitMode.Word; + float graphemeSpacing = Mathf.Max(0.1f, truthAttackCharacterSpacing); + float wordSpacing = Mathf.Max( + 0f, + activeSnapshot?.Profile?.WordSpacing ?? 0.18f); + float cursor = 0f; + float maxUnitWidth = 0f; + + for (int i = 0; i < count; i++) + { + float width = MeasureUnitWorldWidth(units[i]); + maxUnitWidth = Mathf.Max(maxUnitWidth, width); + float gapWeight = + gapWeights != null && i < gapWeights.Count ? gapWeights[i] : 0f; + + if (wordMode) + { + if (i > 0) + cursor += wordSpacing * Mathf.Max(1f, gapWeight); + offsets.Add(cursor + width * 0.5f); + cursor += width; + } + else + { + if (i > 0 && gapWeight > 0f) + cursor += graphemeSpacing * whitespaceSpacingMultiplier * gapWeight; + offsets.Add(cursor); + cursor += graphemeSpacing; + } + } + + float baseCenter = (offsets[0] + offsets[offsets.Count - 1]) * 0.5f; + for (int i = 0; i < offsets.Count; i++) + offsets[i] -= baseCenter; + + float baseWidth = wordMode + ? cursor + : Mathf.Max( + maxUnitWidth * focusZoomScale, + offsets[offsets.Count - 1] - offsets[0] + + maxUnitWidth * focusZoomScale); + float widthAtFocus = wordMode ? baseWidth * focusZoomScale : baseWidth; + Bounds clip = GetCompletionClipBounds(); + float availableWidth = Mathf.Max(0.01f, clip.size.x); + activePhraseScale = widthAtFocus > availableWidth + ? availableWidth / widthAtFocus + : 1f; + + float minimumReadable = + activeSnapshot?.Profile?.MinimumReadablePhraseScale ?? 0.65f; + if (activePhraseScale < minimumReadable) + { + Debug.LogError( + $"[LanguageParticleManager] 表达文字需要缩放到 {activePhraseScale:0.00}," + + $"低于 Locale Profile 最小可读值 {minimumReadable:0.00};" + + "运行时仍会缩放以保证文字完整显示。"); + } + + Vector3 center = + worldCanvas != null ? worldCanvas.transform.position : transform.position; + float offsetScale = wordMode ? ResolvedFocusScale : activePhraseScale; + for (int i = 0; i < offsets.Count; i++) + { + Vector3 position = + center + new Vector3(offsets[i] * offsetScale, 0f, 0f); positions.Add(ClampPositionToBounds(position, clip)); } + return positions; } + private float MeasureUnitWorldWidth(string unit) + { + CandidateParticle reference = targetParticles.FirstOrDefault( + particle => particle != null && particle.textMesh != null); + if (reference?.textMesh == null) + return Mathf.Max(0.1f, truthAttackCharacterSpacing); + + float preferred = reference.textMesh.GetPreferredValues(unit).x; + float rootScale = Mathf.Max( + 0.0001f, + Mathf.Abs(reference.transform.localScale.x)); + float worldScale = + Mathf.Abs(reference.textMesh.transform.lossyScale.x) / rootScale; + return Mathf.Max(0.01f, preferred * worldScale); + } + private void UpdateFinalPresentationPhrase( - IReadOnlyList characters, + IReadOnlyList units, List positions) { - finalTargetCharacters.Clear(); + finalTargetUnits.Clear(); finalTargetPositions.Clear(); - for (int i = 0; i < characters.Count; i++) - finalTargetCharacters.Add(characters[i]); + for (int i = 0; i < units.Count; i++) + finalTargetUnits.Add(units[i]); finalTargetPositions.AddRange(positions); } @@ -2654,12 +2796,16 @@ namespace AibisDream.MiniGame.Language if (string.IsNullOrEmpty(fragment) || orderedRedParticles.Count == 0) yield break; - List targetElements = ExpressionTextTokenizer.GetVisibleElements(targetSentence); - List fragmentElements = ExpressionTextTokenizer.GetVisibleElements(fragment); - int startIndex = ExpressionTextTokenizer.FindVisibleSequence(targetElements, fragmentElements); + IReadOnlyList targetElements = + activeSnapshot?.TargetUnits ?? new List().AsReadOnly(); + List fragmentElements = TokenizeActiveText(fragment); + int startIndex = + ExpressionTextTokenizer.FindUnitSequence(targetElements, fragmentElements); if (startIndex < 0) { - Debug.LogWarning($"[LanguageParticleManager] 真话泄漏片段“{fragment}”不在目标句“{targetSentence}”中。"); + Debug.LogWarning( + $"[LanguageParticleManager] 真话泄漏片段“{fragment}”不在目标句" + + $"“{activeSnapshot?.TargetText}”中。"); yield break; } @@ -2669,7 +2815,7 @@ namespace AibisDream.MiniGame.Language { CandidateParticle particle = orderedRedParticles[i]; if (particle == null) continue; - particle.ForceSetCharacter(targetElements[i]); + particle.ForceSetUnitText(targetElements[i]); particle.isStatic = true; particle.alpha = 1f; leaked.Add(particle); @@ -2753,7 +2899,7 @@ namespace AibisDream.MiniGame.Language CandidateParticle particle = visual.particle; particle.transform.DOKill(); particle.transform.position = visual.finalPosition; - particle.ForceSetCharacter(visual.finalChar); + particle.ForceSetUnitText(visual.finalUnitText); particle.SetChangeInterval(1.5f); particle.ResetChangeTimer(1.5f); particle.anxietyShakeIntensity = 0f; @@ -2833,7 +2979,10 @@ namespace AibisDream.MiniGame.Language DestroyFocusVisualDecorationsAndClear(); foreach (var p in particlesToScale) { - p.transform.DOScale(Vector3.one * focusZoomScale, 0.3f).SetEase(Ease.OutQuad); + p.transform.DOScale( + Vector3.one * ResolvedFocusScale, + 0.3f) + .SetEase(Ease.OutQuad); p.isStatic = true; p.changeSpeedMultiplier = 1f; p.resolvedColorProgress = 1f; @@ -3069,15 +3218,16 @@ namespace AibisDream.MiniGame.Language foreach (var particle in candidateParticles) { Vector2 particlePos = particle.transform.position; - float distance = Vector2.Distance(center, particlePos); + float distance = particle.DistanceToVisualBounds(center); - if (distance < repulsionRadius && distance > 0.01f) + if (distance < repulsionRadius) { - // 计算方向和力度 - Vector2 direction = (particlePos - center).normalized; + Vector2 delta = particlePos - center; + Vector2 direction = delta.sqrMagnitude > 0.0001f + ? delta.normalized + : Vector2.up; float forceMagnitude = repulsionForce * (1f - distance / repulsionRadius); - // 应用力 particle.ApplyForce(direction * forceMagnitude); } } @@ -3112,7 +3262,7 @@ namespace AibisDream.MiniGame.Language allowInput = !playEntranceEffect; orderedRedParticles.Clear(); DestroyFocusVisualDecorationsAndClear(); - finalTargetCharacters.Clear(); + finalTargetUnits.Clear(); finalTargetPositions.Clear(); previousConnections.Clear(); initializationComplete = false; @@ -3201,7 +3351,7 @@ namespace AibisDream.MiniGame.Language RebuildScreenClipMaterials(); SelectRedParticles(); - RefreshAllRandomCharacters(); + RefreshAllRandomUnits(); UpdateStatusUI(); if (playEntranceEffect) { @@ -3403,32 +3553,56 @@ namespace AibisDream.MiniGame.Language if (particleB == null || (particleA.isStatic && particleB.isStatic)) continue; - Vector2 posA = particleA.transform.position; - Vector2 posB = particleB.transform.position; - Vector2 diff = posA - posB; - float distSqr = diff.sqrMagnitude; - float requiredSpacing = CalculateRequiredParticleSpacing(particleA, particleB); - if (distSqr >= requiredSpacing * requiredSpacing) - continue; - - float distance; Vector2 direction; - if (distSqr > 0.000001f) + float overlap; + bool differentTypes = + particleA.originalIsRed != particleB.originalIsRed; + + if (ActiveUnitMode == ExpressionParticleUnitMode.Word && + useGlyphBoundsForSeparation) { - distance = Mathf.Sqrt(distSqr); - direction = diff / distance; + float padding = Mathf.Max(0f, separationPadding); + if (differentTypes) + padding *= Mathf.Max(1f, differentTypeSpacingMultiplier); + if (!ExpressionParticleGeometry.TryGetSeparation( + particleA.GetVisualWorldBounds(), + particleB.GetVisualWorldBounds(), + padding, + out direction, + out overlap)) + { + continue; + } } else { - // 完全重合时 normalized 会得到零向量;使用稳定的配对方向打破死锁。 - distance = 0f; - float angle = (i * 37f + j * 61f) * 2.3999632f; - direction = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)); + Vector2 diff = + particleA.transform.position - particleB.transform.position; + float distSqr = diff.sqrMagnitude; + float requiredSpacing = + CalculateRequiredParticleSpacing(particleA, particleB); + if (distSqr >= requiredSpacing * requiredSpacing) + continue; + + float distance; + if (distSqr > 0.000001f) + { + distance = Mathf.Sqrt(distSqr); + direction = diff / distance; + } + else + { + distance = 0f; + float angle = (i * 37f + j * 61f) * 2.3999632f; + direction = + new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)); + } + + overlap = requiredSpacing - distance; } - float overlap = requiredSpacing - distance; - float normalizedOverlap = Mathf.Clamp01(overlap / Mathf.Max(requiredSpacing, 0.0001f)); - bool differentTypes = particleA.originalIsRed != particleB.originalIsRed; + float normalizedOverlap = Mathf.Clamp01( + overlap / Mathf.Max(minParticleSpacing, 0.0001f)); float forceMultiplier = differentTypes ? Mathf.Max(1f, differentTypeForceMultiplier) : 1f; diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/LanguageYarnCommand.cs b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageYarnCommand.cs index 77f54b34a..8b77fbab9 100644 --- a/Assets/Scripts/MiniGame/HuoShan/Language/LanguageYarnCommand.cs +++ b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageYarnCommand.cs @@ -90,18 +90,20 @@ namespace AibisDream.MiniGame.Language public static bool TryParseExpressionPool( string value, - out List characters) + out List units) { - characters = new List(); - if (string.IsNullOrWhiteSpace(value) || - value.Contains('|') || - value.Contains('|')) - { - return false; - } + return TryParseExpressionPool( + value, + ExpressionParticleUnitMode.Grapheme, + out units); + } - characters = ExpressionTextTokenizer.GetVisibleElements(value); - return characters.Count > 0; + public static bool TryParseExpressionPool( + string value, + ExpressionParticleUnitMode mode, + out List units) + { + return ExpressionTextTokenizer.TryTokenizePool(value, mode, out units); } private static bool TryGetPresentation(out LogReleasePresentationController presentation) @@ -171,10 +173,21 @@ namespace AibisDream.MiniGame.Language "[LanguageYarnCommand] start_expression 没有有效的干扰 token;命令已安全结束。"); yield break; } - if (!TryParseExpressionPool(localized[2], out List defaultCharacterPool)) + ExpressionParticleLocaleSettings profile = + ExpressionManager.ParticleLanguageProfile != null + ? ExpressionManager.ParticleLanguageProfile.Resolve(locale.Identifier) + : ExpressionParticleLocaleSettings.CreateFallback(); + if (!ExpressionRoundTextSnapshot.TryCreate( + locale.Identifier, + profile, + localized[1], + phrases, + localized[2], + out ExpressionRoundTextSnapshot snapshot, + out string snapshotError)) { UnityEngine.Debug.LogError( - "[LanguageYarnCommand] start_expression 的默认字符池无效;命令已安全结束。"); + $"[LanguageYarnCommand] start_expression 粒子单位解析失败:{snapshotError}"); yield break; } @@ -184,9 +197,7 @@ namespace AibisDream.MiniGame.Language yield return ExpressionManager.EnsureReleaseLogFadedIn(1f); ExpressionManager.SetActiveExpressionLocale(locale); ExpressionManager.StartSystem( - phrases, - localized[1], - defaultCharacterPool, + snapshot, completionNode, nonTargetParticleTotal); yield return ExpressionManager.WaitUntilExpressionFlowReady(); @@ -249,10 +260,21 @@ namespace AibisDream.MiniGame.Language $"[LanguageYarnCommand] 表达轮次 '{roundId}' 没有有效的干扰 token。"); yield break; } - if (!TryParseExpressionPool(localized[2], out List defaultCharacterPool)) + ExpressionParticleLocaleSettings profile = + ExpressionManager.ParticleLanguageProfile != null + ? ExpressionManager.ParticleLanguageProfile.Resolve(locale.Identifier) + : ExpressionParticleLocaleSettings.CreateFallback(); + if (!ExpressionRoundTextSnapshot.TryCreate( + locale.Identifier, + profile, + localized[1], + phrases, + localized[2], + out ExpressionRoundTextSnapshot snapshot, + out string snapshotError)) { UnityEngine.Debug.LogError( - $"[LanguageYarnCommand] 表达轮次 '{roundId}' 的默认字符池无效。"); + $"[LanguageYarnCommand] 表达轮次 '{roundId}' 粒子单位解析失败:{snapshotError}"); yield break; } @@ -261,9 +283,7 @@ namespace AibisDream.MiniGame.Language yield return ExpressionManager.EnsureReleaseLogFadedIn(1f); ExpressionManager.SetActiveExpressionLocale(locale); ExpressionManager.StartSystem( - phrases, - localized[1], - defaultCharacterPool, + snapshot, completionNode ?? string.Empty, round.NonTargetParticleCount); yield return ExpressionManager.WaitUntilExpressionFlowReady(); diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/TextParticle.cs b/Assets/Scripts/MiniGame/HuoShan/Language/TextParticle.cs index b7e20de86..ce7df5238 100644 --- a/Assets/Scripts/MiniGame/HuoShan/Language/TextParticle.cs +++ b/Assets/Scripts/MiniGame/HuoShan/Language/TextParticle.cs @@ -18,7 +18,21 @@ namespace AibisDream.MiniGame.Language public float alpha; public float baseAlpha = 1f; public float size = 4f; - public string currentChar; + [SerializeField] + [UnityEngine.Serialization.FormerlySerializedAs("currentChar")] + private string currentUnitText; + public string CurrentUnitText => currentUnitText; + + [System.Obsolete("Use CurrentUnitText instead.")] + public string currentChar + { + get => currentUnitText; + set + { + currentUnitText = value; + UpdateText(); + } + } [Header("状态")] public bool isStatic = false; @@ -29,9 +43,9 @@ namespace AibisDream.MiniGame.Language protected float changeInterval = 1f; private static readonly IReadOnlyList EmptyCharacterPool = new List().AsReadOnly(); - protected IReadOnlyList defaultCharacterPool = EmptyCharacterPool; - protected IReadOnlyList interferenceCharacterPool = EmptyCharacterPool; - protected IReadOnlyList overrideCharacterPool = EmptyCharacterPool; + protected IReadOnlyList defaultUnitPool = EmptyCharacterPool; + protected IReadOnlyList interferenceUnitPool = EmptyCharacterPool; + protected IReadOnlyList overrideUnitPool = EmptyCharacterPool; protected Bounds movementBounds; protected bool useCircularBounds = false; protected Vector3 boundsCenter; @@ -39,6 +53,8 @@ namespace AibisDream.MiniGame.Language protected TMP_FontAsset customFont; private Material customFontMaterial; private TMPRectClipper screenClipper; + private Vector2 preferredLocalSize; + private bool preferredSizeDirty = true; protected virtual void Awake() { @@ -82,7 +98,7 @@ namespace AibisDream.MiniGame.Language SyncLayerToChildren(); changeTimer = Random.Range(0.5f, 1.5f); - currentChar = string.Empty; + currentUnitText = string.Empty; UpdateText(); // TMP 重建 mesh / SubMesh 后再对齐一次 layer SyncLayerToChildren(); @@ -113,7 +129,7 @@ namespace AibisDream.MiniGame.Language changeTimer -= Time.deltaTime; if (changeTimer <= 0) { - currentChar = GetNextDisplayChar(); + currentUnitText = GetNextDisplayUnit(); UpdateText(); changeTimer = changeInterval; } @@ -169,13 +185,20 @@ namespace AibisDream.MiniGame.Language if (useCircularBounds) { - Vector2 offset = new Vector2(pos.x - boundsCenter.x, pos.y - boundsCenter.y); + Bounds visualBounds = GetVisualWorldBounds(); + float visualRadius = Mathf.Max( + visualBounds.extents.x, + visualBounds.extents.y); + float allowedRadius = Mathf.Max(0f, boundsRadius - visualRadius); + Vector2 visualCenter = visualBounds.center; + Vector2 offset = visualCenter - (Vector2)boundsCenter; float dist = offset.magnitude; - if (dist > boundsRadius && boundsRadius > 0.0001f) + if (dist > allowedRadius && boundsRadius > 0.0001f) { Vector2 norm = offset / dist; - pos = boundsCenter + (Vector3)(norm * boundsRadius); - pos.z = transform.position.z; + Vector2 correction = + (Vector2)boundsCenter + norm * allowedRadius - visualCenter; + pos += (Vector3)correction; // 反弹速度(沿径向反射) Vector2 vel = velocity; Vector2 reflect = vel - 2f * Vector2.Dot(vel, norm) * norm; @@ -185,17 +208,26 @@ namespace AibisDream.MiniGame.Language } else { - if (pos.x < movementBounds.min.x || pos.x > movementBounds.max.x) + Bounds visualBounds = GetVisualWorldBounds(); + if (visualBounds.min.x < movementBounds.min.x || + visualBounds.max.x > movementBounds.max.x) { velocity.x *= -1; - pos.x = Mathf.Clamp(pos.x, movementBounds.min.x, movementBounds.max.x); + if (visualBounds.min.x < movementBounds.min.x) + pos.x += movementBounds.min.x - visualBounds.min.x; + else + pos.x -= visualBounds.max.x - movementBounds.max.x; bounced = true; } - if (pos.y < movementBounds.min.y || pos.y > movementBounds.max.y) + if (visualBounds.min.y < movementBounds.min.y || + visualBounds.max.y > movementBounds.max.y) { velocity.y *= -1; - pos.y = Mathf.Clamp(pos.y, movementBounds.min.y, movementBounds.max.y); + if (visualBounds.min.y < movementBounds.min.y) + pos.y += movementBounds.min.y - visualBounds.min.y; + else + pos.y -= visualBounds.max.y - movementBounds.max.y; bounced = true; } } @@ -206,7 +238,7 @@ namespace AibisDream.MiniGame.Language protected string GetRandomChar() { - return GetRandomPoolElement(defaultCharacterPool); + return GetRandomPoolElement(defaultUnitPool); } /// @@ -215,16 +247,16 @@ namespace AibisDream.MiniGame.Language /// protected string GetRandomCharFromPhrases() { - if (interferenceCharacterPool == null || interferenceCharacterPool.Count == 0) + if (interferenceUnitPool == null || interferenceUnitPool.Count == 0) return GetRandomChar(); - return GetRandomPoolElement(interferenceCharacterPool); + return GetRandomPoolElement(interferenceUnitPool); } /// /// 获取下一次要显示的字符。子类可重写以区分目标粒子与非目标粒子(与红蓝颜色解绑)。 /// 默认从本轮本地化字符池随机返回一个 Unicode 文本元素。 /// - protected virtual string GetNextDisplayChar() + protected virtual string GetNextDisplayUnit() { if (TryGetOverridePoolChar(out string overrideChar)) return overrideChar; @@ -234,19 +266,25 @@ namespace AibisDream.MiniGame.Language /// /// 设置换字字符池覆盖(老虎机演出用);传空/null 清除,恢复默认字符池。 /// - public void SetOverrideCharPool(string pool) + public void SetOverrideUnitPool(string pool) { - SetOverrideCharacterPool( + SetOverrideUnitPool( ExpressionTextTokenizer.GetVisibleElements(pool)); } - public void SetOverrideCharacterPool(IReadOnlyList pool) + public void SetOverrideUnitPool(IReadOnlyList pool) { - overrideCharacterPool = pool != null && pool.Count > 0 + overrideUnitPool = pool != null && pool.Count > 0 ? pool : EmptyCharacterPool; } + [System.Obsolete("Use SetOverrideUnitPool instead.")] + public void SetOverrideCharacterPool(IReadOnlyList characterPool) + { + SetOverrideUnitPool(characterPool); + } + protected bool TryGetOverridePoolChar(out string character) { if (!HasOverrideCharacterPool) @@ -255,35 +293,49 @@ namespace AibisDream.MiniGame.Language return false; } - character = GetRandomPoolElement(overrideCharacterPool); + character = GetRandomPoolElement(overrideUnitPool); return true; } /// /// 设置本轮默认与干扰字符池。调用方负责传入不可变的本轮快照。 /// - public void SetCharacterPools( + public void SetUnitPools( IReadOnlyList defaultPool, IReadOnlyList interferencePool) { - defaultCharacterPool = defaultPool != null && defaultPool.Count > 0 + defaultUnitPool = defaultPool != null && defaultPool.Count > 0 ? defaultPool : EmptyCharacterPool; - interferenceCharacterPool = + interferenceUnitPool = interferencePool != null && interferencePool.Count > 0 ? interferencePool : EmptyCharacterPool; } - public void InitializeRandomCharacter() + [System.Obsolete("Use SetUnitPools instead.")] + public void SetCharacterPools( + IReadOnlyList defaultCharacters, + IReadOnlyList interferenceCharacters) { - currentChar = GetNextDisplayChar(); + SetUnitPools(defaultCharacters, interferenceCharacters); + } + + public void InitializeRandomUnit() + { + currentUnitText = GetNextDisplayUnit(); UpdateText(); changeTimer = Random.Range(0.5f, 1.5f); } + [System.Obsolete("Use InitializeRandomUnit instead.")] + public void InitializeRandomCharacter() + { + InitializeRandomUnit(); + } + protected bool HasOverrideCharacterPool => - overrideCharacterPool != null && overrideCharacterPool.Count > 0; + overrideUnitPool != null && overrideUnitPool.Count > 0; private static string GetRandomPoolElement(IReadOnlyList pool) { @@ -295,13 +347,62 @@ namespace AibisDream.MiniGame.Language protected void UpdateText() { if (textMesh != null) - textMesh.text = currentChar; + { + textMesh.text = currentUnitText; + preferredSizeDirty = true; + } } + public void ForceSetUnitText(string unitText) + { + currentUnitText = unitText; + UpdateText(); + } + + [System.Obsolete("Use ForceSetUnitText instead.")] public void ForceSetCharacter(string character) { - currentChar = character; - UpdateText(); + ForceSetUnitText(character); + } + + public Bounds GetVisualWorldBounds() + { + RefreshPreferredSize(); + Transform textTransform = textMesh != null ? textMesh.transform : transform; + Vector3 scale = textTransform.lossyScale; + Vector3 size = new Vector3( + Mathf.Max(0.001f, preferredLocalSize.x * Mathf.Abs(scale.x)), + Mathf.Max(0.001f, preferredLocalSize.y * Mathf.Abs(scale.y)), + 0.001f); + return new Bounds(textTransform.position, size); + } + + public float DistanceToVisualBounds(Vector2 worldPoint) + { + Bounds bounds = GetVisualWorldBounds(); + float dx = Mathf.Max(bounds.min.x - worldPoint.x, 0f, worldPoint.x - bounds.max.x); + float dy = Mathf.Max(bounds.min.y - worldPoint.y, 0f, worldPoint.y - bounds.max.y); + return Mathf.Sqrt(dx * dx + dy * dy); + } + + protected void InvalidateVisualBounds() + { + preferredSizeDirty = true; + } + + private void RefreshPreferredSize() + { + if (!preferredSizeDirty) + return; + + preferredSizeDirty = false; + if (textMesh == null || string.IsNullOrEmpty(currentUnitText)) + { + preferredLocalSize = Vector2.zero; + return; + } + + preferredLocalSize = textMesh.GetPreferredValues(currentUnitText); } public void SetStaticState(bool value) @@ -380,6 +481,8 @@ namespace AibisDream.MiniGame.Language if (customFontMaterial != null) textMesh.fontSharedMaterial = customFontMaterial; + + InvalidateVisualBounds(); } protected virtual void OnDestroy() diff --git a/Assets/Tests/Huoshan.meta b/Assets/Tests/Huoshan.meta new file mode 100644 index 000000000..43cc31015 --- /dev/null +++ b/Assets/Tests/Huoshan.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 84ac13641a36486d8f1a891bad6b1f28 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Huoshan/PlayMode.meta b/Assets/Tests/Huoshan/PlayMode.meta new file mode 100644 index 000000000..fd250017c --- /dev/null +++ b/Assets/Tests/Huoshan/PlayMode.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f6244beee7e24912bd0e6019ac802f92 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Huoshan/PlayMode/AibisDream.Huoshan.Tests.PlayMode.asmdef b/Assets/Tests/Huoshan/PlayMode/AibisDream.Huoshan.Tests.PlayMode.asmdef new file mode 100644 index 000000000..2268144d7 --- /dev/null +++ b/Assets/Tests/Huoshan/PlayMode/AibisDream.Huoshan.Tests.PlayMode.asmdef @@ -0,0 +1,19 @@ +{ + "name": "AibisDream.Huoshan.Tests.PlayMode", + "rootNamespace": "AibisDream.Huoshan.Tests.PlayMode", + "references": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": false, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "versionDefines": [], + "noEngineReferences": false, + "optionalUnityReferences": [ + "TestAssemblies" + ] +} diff --git a/Assets/Tests/Huoshan/PlayMode/AibisDream.Huoshan.Tests.PlayMode.asmdef.meta b/Assets/Tests/Huoshan/PlayMode/AibisDream.Huoshan.Tests.PlayMode.asmdef.meta new file mode 100644 index 000000000..47012d704 --- /dev/null +++ b/Assets/Tests/Huoshan/PlayMode/AibisDream.Huoshan.Tests.PlayMode.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e93af359c134479a88d141136397693e +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Huoshan/PlayMode/ExpressionParticlePlayModeTests.cs b/Assets/Tests/Huoshan/PlayMode/ExpressionParticlePlayModeTests.cs new file mode 100644 index 000000000..3bbb4bfa2 --- /dev/null +++ b/Assets/Tests/Huoshan/PlayMode/ExpressionParticlePlayModeTests.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections; +using System.Linq; +using System.Reflection; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace AibisDream.Huoshan.Tests.PlayMode +{ + public sealed class ExpressionParticlePlayModeTests + { + private const string RuntimeAssemblyName = "Assembly-CSharp"; + + [Test] + public void WordMode_TokenizesWholeWordsAtRuntime() + { + Type tokenizerType = RequireRuntimeType( + "AibisDream.MiniGame.Language.ExpressionTextTokenizer"); + Type modeType = RequireRuntimeType( + "AibisDream.MiniGame.Language.ExpressionParticleUnitMode"); + object wordMode = Enum.Parse(modeType, "Word"); + MethodInfo tokenize = tokenizerType.GetMethod( + "TokenizeText", + BindingFlags.Public | BindingFlags.Static); + + Assert.That(tokenize, Is.Not.Null); + IEnumerable result = (IEnumerable)tokenize.Invoke( + null, + new[] { "I am not self-doubt", wordMode }); + string[] units = result.Cast() + .Select(value => value.ToString()) + .ToArray(); + + Assert.That( + units, + Is.EqualTo(new[] { "I", "am", "not", "self-doubt" })); + } + + [UnityTest] + public IEnumerator WordParticle_BoundsIncludesBothPointerEdges() + { + GameObject gameObject = new GameObject("WordParticlePlayModeTest"); + try + { + Component particle = gameObject.AddComponent( + RequireRuntimeType( + "AibisDream.MiniGame.Language.CandidateParticle")); + Invoke(particle, "ForceSetUnitText", "self-doubt"); + yield return null; + + Bounds bounds = (Bounds)Invoke(particle, "GetVisualWorldBounds"); + Assert.That(bounds.size.x, Is.GreaterThan(0f)); + + float leftDistance = (float)Invoke( + particle, + "DistanceToVisualBounds", + new Vector2(bounds.min.x, bounds.center.y)); + float rightDistance = (float)Invoke( + particle, + "DistanceToVisualBounds", + new Vector2(bounds.max.x, bounds.center.y)); + Assert.That(leftDistance, Is.EqualTo(0f).Within(0.0001f)); + Assert.That(rightDistance, Is.EqualTo(0f).Within(0.0001f)); + } + finally + { + UnityEngine.Object.Destroy(gameObject); + } + + yield return null; + } + + [UnityTest] + public IEnumerator LongWords_ConnectThroughVisualEdgeDistance() + { + GameObject firstObject = new GameObject("FirstWordParticle"); + GameObject secondObject = new GameObject("SecondWordParticle"); + try + { + Type particleType = RequireRuntimeType( + "AibisDream.MiniGame.Language.CandidateParticle"); + Component first = firstObject.AddComponent(particleType); + Component second = secondObject.AddComponent(particleType); + Invoke(first, "ForceSetUnitText", "internationalization"); + Invoke(second, "ForceSetUnitText", "characteristically"); + yield return null; + + Bounds firstBounds = + (Bounds)Invoke(first, "GetVisualWorldBounds"); + Bounds secondBounds = + (Bounds)Invoke(second, "GetVisualWorldBounds"); + secondObject.transform.position = new Vector3( + firstBounds.extents.x + secondBounds.extents.x + 0.1f, + 0f, + 0f); + + Type geometryType = RequireRuntimeType( + "AibisDream.MiniGame.Language.ExpressionParticleGeometry"); + MethodInfo connection = geometryType.GetMethod( + "TryGetConnection", + BindingFlags.Public | BindingFlags.Static); + object[] arguments = + { + first, + second, + 0.01f, + 0.18f, + Vector3.zero, + Vector3.zero, + 0f + }; + + Assert.That(connection, Is.Not.Null); + bool connected = (bool)connection.Invoke(null, arguments); + Assert.That(connected, Is.True); + Assert.That((float)arguments[6], Is.GreaterThan(0f)); + Assert.That( + Vector3.Distance( + firstObject.transform.position, + secondObject.transform.position), + Is.GreaterThan(0.01f)); + } + finally + { + UnityEngine.Object.Destroy(firstObject); + UnityEngine.Object.Destroy(secondObject); + } + + yield return null; + } + + private static Type RequireRuntimeType(string fullName) + { + Type type = Type.GetType($"{fullName}, {RuntimeAssemblyName}"); + Assert.That(type, Is.Not.Null, fullName); + return type; + } + + private static object Invoke( + object target, + string methodName, + params object[] arguments) + { + MethodInfo method = target.GetType().GetMethod( + methodName, + BindingFlags.Public | BindingFlags.Instance); + Assert.That(method, Is.Not.Null, methodName); + return method.Invoke(target, arguments); + } + } +} diff --git a/Assets/Tests/Huoshan/PlayMode/ExpressionParticlePlayModeTests.cs.meta b/Assets/Tests/Huoshan/PlayMode/ExpressionParticlePlayModeTests.cs.meta new file mode 100644 index 000000000..9a2576074 --- /dev/null +++ b/Assets/Tests/Huoshan/PlayMode/ExpressionParticlePlayModeTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 26043594097949158ba1572f70c94f78 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Docs/HuoShan/HuoshanStage6LocalizationPlan.md b/Docs/HuoShan/HuoshanStage6LocalizationPlan.md index 1c3dbef77..7d5fb8f9e 100644 --- a/Docs/HuoShan/HuoshanStage6LocalizationPlan.md +++ b/Docs/HuoShan/HuoshanStage6LocalizationPlan.md @@ -60,7 +60,7 @@ Express 使用短前缀 `hs.exp.*`: 重复演出复用同一 Key,但 Yarn 中仍保留原有调用次数、等待、数值、顺序和 `#line:` 标签。 -## token 与 Unicode +## token 与粒子单位 干扰 token 只接受半角 `|`: @@ -69,15 +69,28 @@ Express 使用短前缀 `hs.exp.*`: - 全空时中止轮次; - 全角 `|` 是数据错误。 -正式目标句和释放演出文字按 `StringInfo` 的 Unicode 文本元素处理。汉字、 -假名、拉丁字母、组合字符、代理字符和标点均保持完整;空白不生成粒子,但 -最终排列保留词间距。目标粒子数量、最终排列、攻击词长度校验和 truth leak -匹配使用同一套文本元素结果。 +`ExpressionParticleLanguageProfile` 决定当前 Locale 的粒子单位: -随机字符池同样按 Unicode 文本元素处理。`hs.exp.pool` 是连续文本,不使用 -`|` 或全角 `|`,空白被忽略,重复元素保留为随机权重。目标粒子从该池取字, -非目标和背景浮动粒子继续从本轮 token 展开的干扰池取字;临时老虎机池结束 -后按粒子角色恢复。旧的 `chineseChars` 与 `chineseWords` 运行时常量均已删除。 +- `zh-Hans`、`ja-JP` 使用 `Grapheme`,通过 `StringInfo` 按 Unicode 文本元素 + 切分;空白不生成粒子,但最终排列保留间距; +- `en`、`es`、`ru`、`pt-BR` 使用 `Word`,按 Unicode 空白切分并折叠连续 + 空白;撇号、连字符和标点留在词内; +- Locale 先完整匹配,再按语言前缀匹配;未知 Locale 警告一次并回退 + `Grapheme`。 + +目标句、token、`hs.exp.pool` 在 Yarn 命令入口一次性解析为 +`ExpressionRoundTextSnapshot`。目标粒子数量、最终排列、攻击词长度校验、 +truth leak 和老虎机覆盖池都使用同一份快照或同一 Unit Mode,不会在 Manager +内再次按字母拆分。切换全局 Locale 不会改变进行中的轮次快照。 + +`hs.exp.pool` 仍禁止 `|` 和全角 `|`。Grapheme 模式使用连续文本,Word 模式 +使用空白分隔单词;两种模式都保留重复项作为随机权重。目标粒子从该池取单位, +非目标和背景浮动粒子从本轮 token 展开的干扰池取单位;临时老虎机池结束后按 +粒子角色恢复。 + +Word Profile 还会缩小三类字号、降低非目标粒子数量,并启用宽度感知排版、 +字形边缘连接、Bounds 分离、屏幕边界和鼠标命中。全屏 Actor 故障/老虎机演出 +仍保持原来的字符级视觉。 ## 内容校验 @@ -85,12 +98,15 @@ Express 使用短前缀 `hs.exp.*`: - Stage6 正式 Express 文字参数不得残留中文硬编码; - Yarn 和 Catalog 中引用的 `l10n.*` Key 必须存在于 `Params` Shared Data; +- Profile Locale Code 必须唯一,所有支持 Locale 必须可解析; - 中文值必须非空; - 未翻译 Locale 的空值只报告待翻译,不导致校验失败; -- `.tokens` 至少包含一个有效项,且不能使用全角 `|`; +- 每个非空 Locale 的目标文字和 `.tokens` 必须按该 Locale 模式得到有效单位, + `.tokens` 不能使用全角 `|`; - `hs.exp.pool` 必须存在于所有 Params Locale 表;中文非空,非空译文不得 - 包含 `|`、`|` 或只有空白; + 包含 `|`、`|` 或只有空白,并按 Locale 模式验证; - completion node、preset、memory key、颜色、Timeline 等结构参数不误报。 -运行时和 EditMode 测试同时覆盖原始字符串、空值不回退、缺 Key 占位、 -显式 Locale、轮次配置、token 解析和 Unicode 文本元素行为。 +EditMode 与 PlayMode 测试覆盖原始字符串、空值不回退、缺 Key 占位、显式 +Locale、轮次配置、Locale/Profile 解析、Word/Grapheme 切分、快照、粒子 +数量倍率,以及真实粒子组件的 Bounds 命中和字形边缘连接行为。 From ac0b20a3a8bb5084d9b46d347c14ef2aadc9a40d Mon Sep 17 00:00:00 2001 From: Ding Yuntian <1491671119@qq.com> Date: Wed, 29 Jul 2026 14:12:47 +0800 Subject: [PATCH 5/7] =?UTF-8?q?art:=20=E8=B5=84=E6=BA=90=E8=BF=81=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Huoshan/ExpressionLocalizationTests.cs | 17 +++++- .../{Huoshan => Feature_Huoshan}/Actor.meta | 0 .../Actor/彩带Clip.asset | 0 .../Actor/彩带Clip.asset.meta | 0 .../Actor/火山Graph.asset | 0 .../Actor/火山Graph.asset.meta | 0 .../Actor/火山像素版.aseprite | Bin .../Actor/火山像素版.aseprite.meta | 0 .../Actor/火山像素版.json | 0 .../Actor/火山像素版.json.meta | 0 .../Actor/火山像素版.png | 0 .../Actor/火山像素版.png.meta | 0 .../Actor/火山彩带.json | 0 .../Actor/火山彩带.json.meta | 0 .../Actor/火山彩带.playable | 0 .../Actor/火山彩带.playable.meta | 0 .../Actor/火山彩带.png | 0 .../Actor/火山彩带.png.meta | 0 .../Art.meta} | 2 +- .../Art}/维修火山释放完log火山脑部特写.psb | 0 .../维修火山释放完log火山脑部特写.psb.meta | 0 .../ExpressionParticleLanguageProfile.asset | 32 +++++----- .../{Huoshan => Feature_Huoshan}/sprite.meta | 0 .../sprite/告别夕阳.png | 0 .../sprite/告别夕阳.png.meta | 0 .../sprite/大办公室.png | 0 .../sprite/大办公室.png.meta | 0 .../sprite/火山脑部特写_红屏.png | 0 .../sprite/火山脑部特写_红屏.png.meta | 0 .../sprite/火山脑部特写_透明.png | 0 .../sprite/火山脑部特写_透明.png.meta | 0 .../sprite/私人办公室.png | 0 .../sprite/私人办公室.png.meta | 0 Assets/Language/Params/Params_en.asset | 56 +++++++++--------- 34 files changed, 59 insertions(+), 48 deletions(-) rename Assets/GameContent/{Huoshan => Feature_Huoshan}/Actor.meta (100%) rename Assets/GameContent/{Huoshan => Feature_Huoshan}/Actor/彩带Clip.asset (100%) rename Assets/GameContent/{Huoshan => Feature_Huoshan}/Actor/彩带Clip.asset.meta (100%) rename Assets/GameContent/{Huoshan => Feature_Huoshan}/Actor/火山Graph.asset (100%) rename Assets/GameContent/{Huoshan => Feature_Huoshan}/Actor/火山Graph.asset.meta (100%) rename Assets/GameContent/{Huoshan => Feature_Huoshan}/Actor/火山像素版.aseprite (100%) rename Assets/GameContent/{Huoshan => Feature_Huoshan}/Actor/火山像素版.aseprite.meta (100%) rename Assets/GameContent/{Huoshan => Feature_Huoshan}/Actor/火山像素版.json (100%) rename Assets/GameContent/{Huoshan => Feature_Huoshan}/Actor/火山像素版.json.meta (100%) rename Assets/GameContent/{Huoshan => Feature_Huoshan}/Actor/火山像素版.png (100%) rename Assets/GameContent/{Huoshan => Feature_Huoshan}/Actor/火山像素版.png.meta (100%) rename Assets/GameContent/{Huoshan => Feature_Huoshan}/Actor/火山彩带.json (100%) rename Assets/GameContent/{Huoshan => Feature_Huoshan}/Actor/火山彩带.json.meta (100%) rename Assets/GameContent/{Huoshan => Feature_Huoshan}/Actor/火山彩带.playable (100%) rename Assets/GameContent/{Huoshan => Feature_Huoshan}/Actor/火山彩带.playable.meta (100%) rename Assets/GameContent/{Huoshan => Feature_Huoshan}/Actor/火山彩带.png (100%) rename Assets/GameContent/{Huoshan => Feature_Huoshan}/Actor/火山彩带.png.meta (100%) rename Assets/GameContent/{Huoshan.meta => Feature_Huoshan/Art.meta} (77%) rename Assets/GameContent/{Huoshan => Feature_Huoshan/Art}/维修火山释放完log火山脑部特写.psb (100%) rename Assets/GameContent/{Huoshan => Feature_Huoshan/Art}/维修火山释放完log火山脑部特写.psb.meta (100%) rename Assets/GameContent/{Huoshan => Feature_Huoshan}/sprite.meta (100%) rename Assets/GameContent/{Huoshan => Feature_Huoshan}/sprite/告别夕阳.png (100%) rename Assets/GameContent/{Huoshan => Feature_Huoshan}/sprite/告别夕阳.png.meta (100%) rename Assets/GameContent/{Huoshan => Feature_Huoshan}/sprite/大办公室.png (100%) rename Assets/GameContent/{Huoshan => Feature_Huoshan}/sprite/大办公室.png.meta (100%) rename Assets/GameContent/{Huoshan => Feature_Huoshan}/sprite/火山脑部特写_红屏.png (100%) rename Assets/GameContent/{Huoshan => Feature_Huoshan}/sprite/火山脑部特写_红屏.png.meta (100%) rename Assets/GameContent/{Huoshan => Feature_Huoshan}/sprite/火山脑部特写_透明.png (100%) rename Assets/GameContent/{Huoshan => Feature_Huoshan}/sprite/火山脑部特写_透明.png.meta (100%) rename Assets/GameContent/{Huoshan => Feature_Huoshan}/sprite/私人办公室.png (100%) rename Assets/GameContent/{Huoshan => Feature_Huoshan}/sprite/私人办公室.png.meta (100%) diff --git a/Assets/Editor/Huoshan/ExpressionLocalizationTests.cs b/Assets/Editor/Huoshan/ExpressionLocalizationTests.cs index 2d6d0029a..6a5ba6ddd 100644 --- a/Assets/Editor/Huoshan/ExpressionLocalizationTests.cs +++ b/Assets/Editor/Huoshan/ExpressionLocalizationTests.cs @@ -87,7 +87,7 @@ namespace AibisDream.EditorTests.Huoshan LocalizationSettings.SelectedLocale = previous; Assert.That(chineseTask.Result, Is.EqualTo("我就是个笑话")); - Assert.That(englishTask.Result, Is.Empty); + Assert.That(englishTask.Result, Is.EqualTo("I am nothing but a joke")); } [UnityTest] @@ -415,7 +415,7 @@ namespace AibisDream.EditorTests.Huoshan } [Test] - public void Params_ContainsLocalizedExpressPoolForEveryLocale() + public void Params_ContainsEnglishWordPoolAndPendingPoolsForOtherLocales() { StringTableCollection collection = LocalizationEditorSettings.GetStringTableCollection(ConstRef.ParamsTable); @@ -441,7 +441,18 @@ namespace AibisDream.EditorTests.Huoshan chinese.GetEntry(key).LocalizedValue), Is.Not.Empty); - foreach (string localeCode in localeCodes.Skip(1)) + StringTable english = + collection.GetTable(new LocaleIdentifier("en")) as StringTable; + Assert.That( + LanguageYarnCommand.TryParseExpressionPool( + english.GetEntry(key).LocalizedValue, + ExpressionParticleUnitMode.Word, + out List englishPool), + Is.True); + Assert.That(englishPool, Does.Contain("truth")); + Assert.That(englishPool, Does.Contain("believe")); + + foreach (string localeCode in localeCodes.Skip(2)) { StringTable table = collection.GetTable(new LocaleIdentifier(localeCode)) as StringTable; diff --git a/Assets/GameContent/Huoshan/Actor.meta b/Assets/GameContent/Feature_Huoshan/Actor.meta similarity index 100% rename from Assets/GameContent/Huoshan/Actor.meta rename to Assets/GameContent/Feature_Huoshan/Actor.meta diff --git a/Assets/GameContent/Huoshan/Actor/彩带Clip.asset b/Assets/GameContent/Feature_Huoshan/Actor/彩带Clip.asset similarity index 100% rename from Assets/GameContent/Huoshan/Actor/彩带Clip.asset rename to Assets/GameContent/Feature_Huoshan/Actor/彩带Clip.asset diff --git a/Assets/GameContent/Huoshan/Actor/彩带Clip.asset.meta b/Assets/GameContent/Feature_Huoshan/Actor/彩带Clip.asset.meta similarity index 100% rename from Assets/GameContent/Huoshan/Actor/彩带Clip.asset.meta rename to Assets/GameContent/Feature_Huoshan/Actor/彩带Clip.asset.meta diff --git a/Assets/GameContent/Huoshan/Actor/火山Graph.asset b/Assets/GameContent/Feature_Huoshan/Actor/火山Graph.asset similarity index 100% rename from Assets/GameContent/Huoshan/Actor/火山Graph.asset rename to Assets/GameContent/Feature_Huoshan/Actor/火山Graph.asset diff --git a/Assets/GameContent/Huoshan/Actor/火山Graph.asset.meta b/Assets/GameContent/Feature_Huoshan/Actor/火山Graph.asset.meta similarity index 100% rename from Assets/GameContent/Huoshan/Actor/火山Graph.asset.meta rename to Assets/GameContent/Feature_Huoshan/Actor/火山Graph.asset.meta diff --git a/Assets/GameContent/Huoshan/Actor/火山像素版.aseprite b/Assets/GameContent/Feature_Huoshan/Actor/火山像素版.aseprite similarity index 100% rename from Assets/GameContent/Huoshan/Actor/火山像素版.aseprite rename to Assets/GameContent/Feature_Huoshan/Actor/火山像素版.aseprite diff --git a/Assets/GameContent/Huoshan/Actor/火山像素版.aseprite.meta b/Assets/GameContent/Feature_Huoshan/Actor/火山像素版.aseprite.meta similarity index 100% rename from Assets/GameContent/Huoshan/Actor/火山像素版.aseprite.meta rename to Assets/GameContent/Feature_Huoshan/Actor/火山像素版.aseprite.meta diff --git a/Assets/GameContent/Huoshan/Actor/火山像素版.json b/Assets/GameContent/Feature_Huoshan/Actor/火山像素版.json similarity index 100% rename from Assets/GameContent/Huoshan/Actor/火山像素版.json rename to Assets/GameContent/Feature_Huoshan/Actor/火山像素版.json diff --git a/Assets/GameContent/Huoshan/Actor/火山像素版.json.meta b/Assets/GameContent/Feature_Huoshan/Actor/火山像素版.json.meta similarity index 100% rename from Assets/GameContent/Huoshan/Actor/火山像素版.json.meta rename to Assets/GameContent/Feature_Huoshan/Actor/火山像素版.json.meta diff --git a/Assets/GameContent/Huoshan/Actor/火山像素版.png b/Assets/GameContent/Feature_Huoshan/Actor/火山像素版.png similarity index 100% rename from Assets/GameContent/Huoshan/Actor/火山像素版.png rename to Assets/GameContent/Feature_Huoshan/Actor/火山像素版.png diff --git a/Assets/GameContent/Huoshan/Actor/火山像素版.png.meta b/Assets/GameContent/Feature_Huoshan/Actor/火山像素版.png.meta similarity index 100% rename from Assets/GameContent/Huoshan/Actor/火山像素版.png.meta rename to Assets/GameContent/Feature_Huoshan/Actor/火山像素版.png.meta diff --git a/Assets/GameContent/Huoshan/Actor/火山彩带.json b/Assets/GameContent/Feature_Huoshan/Actor/火山彩带.json similarity index 100% rename from Assets/GameContent/Huoshan/Actor/火山彩带.json rename to Assets/GameContent/Feature_Huoshan/Actor/火山彩带.json diff --git a/Assets/GameContent/Huoshan/Actor/火山彩带.json.meta b/Assets/GameContent/Feature_Huoshan/Actor/火山彩带.json.meta similarity index 100% rename from Assets/GameContent/Huoshan/Actor/火山彩带.json.meta rename to Assets/GameContent/Feature_Huoshan/Actor/火山彩带.json.meta diff --git a/Assets/GameContent/Huoshan/Actor/火山彩带.playable b/Assets/GameContent/Feature_Huoshan/Actor/火山彩带.playable similarity index 100% rename from Assets/GameContent/Huoshan/Actor/火山彩带.playable rename to Assets/GameContent/Feature_Huoshan/Actor/火山彩带.playable diff --git a/Assets/GameContent/Huoshan/Actor/火山彩带.playable.meta b/Assets/GameContent/Feature_Huoshan/Actor/火山彩带.playable.meta similarity index 100% rename from Assets/GameContent/Huoshan/Actor/火山彩带.playable.meta rename to Assets/GameContent/Feature_Huoshan/Actor/火山彩带.playable.meta diff --git a/Assets/GameContent/Huoshan/Actor/火山彩带.png b/Assets/GameContent/Feature_Huoshan/Actor/火山彩带.png similarity index 100% rename from Assets/GameContent/Huoshan/Actor/火山彩带.png rename to Assets/GameContent/Feature_Huoshan/Actor/火山彩带.png diff --git a/Assets/GameContent/Huoshan/Actor/火山彩带.png.meta b/Assets/GameContent/Feature_Huoshan/Actor/火山彩带.png.meta similarity index 100% rename from Assets/GameContent/Huoshan/Actor/火山彩带.png.meta rename to Assets/GameContent/Feature_Huoshan/Actor/火山彩带.png.meta diff --git a/Assets/GameContent/Huoshan.meta b/Assets/GameContent/Feature_Huoshan/Art.meta similarity index 77% rename from Assets/GameContent/Huoshan.meta rename to Assets/GameContent/Feature_Huoshan/Art.meta index 4d645508a..7f33b7366 100644 --- a/Assets/GameContent/Huoshan.meta +++ b/Assets/GameContent/Feature_Huoshan/Art.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 91dd0288ab6cb7a4fb7bc684b9e3ed69 +guid: 5f2a0ba886c6cac40a1857418c5e16a4 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Assets/GameContent/Huoshan/维修火山释放完log火山脑部特写.psb b/Assets/GameContent/Feature_Huoshan/Art/维修火山释放完log火山脑部特写.psb similarity index 100% rename from Assets/GameContent/Huoshan/维修火山释放完log火山脑部特写.psb rename to Assets/GameContent/Feature_Huoshan/Art/维修火山释放完log火山脑部特写.psb diff --git a/Assets/GameContent/Huoshan/维修火山释放完log火山脑部特写.psb.meta b/Assets/GameContent/Feature_Huoshan/Art/维修火山释放完log火山脑部特写.psb.meta similarity index 100% rename from Assets/GameContent/Huoshan/维修火山释放完log火山脑部特写.psb.meta rename to Assets/GameContent/Feature_Huoshan/Art/维修火山释放完log火山脑部特写.psb.meta diff --git a/Assets/GameContent/Feature_Huoshan/Expression/ExpressionParticleLanguageProfile.asset b/Assets/GameContent/Feature_Huoshan/Expression/ExpressionParticleLanguageProfile.asset index 0c137ad5d..7a159a450 100644 --- a/Assets/GameContent/Feature_Huoshan/Expression/ExpressionParticleLanguageProfile.asset +++ b/Assets/GameContent/Feature_Huoshan/Expression/ExpressionParticleLanguageProfile.asset @@ -33,37 +33,37 @@ MonoBehaviour: minimumReadablePhraseScale: 0.65 - localeCode: en unitMode: 1 - targetFontScale: 0.65 - nonTargetFontScale: 0.58 - floatingFontScale: 0.58 - nonTargetCountScale: 0.65 + targetFontScale: 0.75 + nonTargetFontScale: 0.7 + floatingFontScale: 0.7 + nonTargetCountScale: 0.75 wordSpacing: 0.18 visualEdgeConnectionDistance: 0.18 minimumReadablePhraseScale: 0.65 - localeCode: es unitMode: 1 - targetFontScale: 0.65 - nonTargetFontScale: 0.58 - floatingFontScale: 0.58 - nonTargetCountScale: 0.65 + targetFontScale: 0.75 + nonTargetFontScale: 0.7 + floatingFontScale: 0.7 + nonTargetCountScale: 0.75 wordSpacing: 0.18 visualEdgeConnectionDistance: 0.18 minimumReadablePhraseScale: 0.65 - localeCode: ru unitMode: 1 - targetFontScale: 0.65 - nonTargetFontScale: 0.58 - floatingFontScale: 0.58 - nonTargetCountScale: 0.65 + targetFontScale: 0.75 + nonTargetFontScale: 0.7 + floatingFontScale: 0.7 + nonTargetCountScale: 0.75 wordSpacing: 0.18 visualEdgeConnectionDistance: 0.18 minimumReadablePhraseScale: 0.65 - localeCode: pt-BR unitMode: 1 - targetFontScale: 0.65 - nonTargetFontScale: 0.58 - floatingFontScale: 0.58 - nonTargetCountScale: 0.65 + targetFontScale: 0.75 + nonTargetFontScale: 0.7 + floatingFontScale: 0.7 + nonTargetCountScale: 0.75 wordSpacing: 0.18 visualEdgeConnectionDistance: 0.18 minimumReadablePhraseScale: 0.65 diff --git a/Assets/GameContent/Huoshan/sprite.meta b/Assets/GameContent/Feature_Huoshan/sprite.meta similarity index 100% rename from Assets/GameContent/Huoshan/sprite.meta rename to Assets/GameContent/Feature_Huoshan/sprite.meta diff --git a/Assets/GameContent/Huoshan/sprite/告别夕阳.png b/Assets/GameContent/Feature_Huoshan/sprite/告别夕阳.png similarity index 100% rename from Assets/GameContent/Huoshan/sprite/告别夕阳.png rename to Assets/GameContent/Feature_Huoshan/sprite/告别夕阳.png diff --git a/Assets/GameContent/Huoshan/sprite/告别夕阳.png.meta b/Assets/GameContent/Feature_Huoshan/sprite/告别夕阳.png.meta similarity index 100% rename from Assets/GameContent/Huoshan/sprite/告别夕阳.png.meta rename to Assets/GameContent/Feature_Huoshan/sprite/告别夕阳.png.meta diff --git a/Assets/GameContent/Huoshan/sprite/大办公室.png b/Assets/GameContent/Feature_Huoshan/sprite/大办公室.png similarity index 100% rename from Assets/GameContent/Huoshan/sprite/大办公室.png rename to Assets/GameContent/Feature_Huoshan/sprite/大办公室.png diff --git a/Assets/GameContent/Huoshan/sprite/大办公室.png.meta b/Assets/GameContent/Feature_Huoshan/sprite/大办公室.png.meta similarity index 100% rename from Assets/GameContent/Huoshan/sprite/大办公室.png.meta rename to Assets/GameContent/Feature_Huoshan/sprite/大办公室.png.meta diff --git a/Assets/GameContent/Huoshan/sprite/火山脑部特写_红屏.png b/Assets/GameContent/Feature_Huoshan/sprite/火山脑部特写_红屏.png similarity index 100% rename from Assets/GameContent/Huoshan/sprite/火山脑部特写_红屏.png rename to Assets/GameContent/Feature_Huoshan/sprite/火山脑部特写_红屏.png diff --git a/Assets/GameContent/Huoshan/sprite/火山脑部特写_红屏.png.meta b/Assets/GameContent/Feature_Huoshan/sprite/火山脑部特写_红屏.png.meta similarity index 100% rename from Assets/GameContent/Huoshan/sprite/火山脑部特写_红屏.png.meta rename to Assets/GameContent/Feature_Huoshan/sprite/火山脑部特写_红屏.png.meta diff --git a/Assets/GameContent/Huoshan/sprite/火山脑部特写_透明.png b/Assets/GameContent/Feature_Huoshan/sprite/火山脑部特写_透明.png similarity index 100% rename from Assets/GameContent/Huoshan/sprite/火山脑部特写_透明.png rename to Assets/GameContent/Feature_Huoshan/sprite/火山脑部特写_透明.png diff --git a/Assets/GameContent/Huoshan/sprite/火山脑部特写_透明.png.meta b/Assets/GameContent/Feature_Huoshan/sprite/火山脑部特写_透明.png.meta similarity index 100% rename from Assets/GameContent/Huoshan/sprite/火山脑部特写_透明.png.meta rename to Assets/GameContent/Feature_Huoshan/sprite/火山脑部特写_透明.png.meta diff --git a/Assets/GameContent/Huoshan/sprite/私人办公室.png b/Assets/GameContent/Feature_Huoshan/sprite/私人办公室.png similarity index 100% rename from Assets/GameContent/Huoshan/sprite/私人办公室.png rename to Assets/GameContent/Feature_Huoshan/sprite/私人办公室.png diff --git a/Assets/GameContent/Huoshan/sprite/私人办公室.png.meta b/Assets/GameContent/Feature_Huoshan/sprite/私人办公室.png.meta similarity index 100% rename from Assets/GameContent/Huoshan/sprite/私人办公室.png.meta rename to Assets/GameContent/Feature_Huoshan/sprite/私人办公室.png.meta diff --git a/Assets/Language/Params/Params_en.asset b/Assets/Language/Params/Params_en.asset index b087b7c69..7301e889e 100644 --- a/Assets/Language/Params/Params_en.asset +++ b/Assets/Language/Params/Params_en.asset @@ -47,115 +47,115 @@ MonoBehaviour: m_Metadata: m_Items: [] - m_Id: 200000000000001001 - m_Localized: + m_Localized: I am nothing but a joke m_Metadata: m_Items: [] - m_Id: 200000000000001002 - m_Localized: + m_Localized: Ha ha|Heh heh|So funny m_Metadata: m_Items: [] - m_Id: 200000000000001003 - m_Localized: + m_Localized: Please don't leave me m_Metadata: m_Items: [] - m_Id: 200000000000001004 - m_Localized: + m_Localized: Ha ha ha|Heh heh heh|That's hilarious|So funny|What a joke m_Metadata: m_Items: [] - m_Id: 200000000000001005 - m_Localized: + m_Localized: Why won't you believe in me m_Metadata: m_Items: [] - m_Id: 200000000000001006 - m_Localized: + m_Localized: Ha ha ha ha|I'm dying laughing|This is too funny|I can't hold it|I can't stop laughing|This is hilarious m_Metadata: m_Items: [] - m_Id: 200000000000001008 - m_Localized: + m_Localized: Why m_Metadata: m_Items: [] - m_Id: 200000000000001009 - m_Localized: + m_Localized: You don't believe me m_Metadata: m_Items: [] - m_Id: 200000000000001010 - m_Localized: + m_Localized: I know I come last m_Metadata: m_Items: [] - m_Id: 200000000000001011 - m_Localized: + m_Localized: The weather is terrible m_Metadata: m_Items: [] - m_Id: 200000000000001012 - m_Localized: + m_Localized: Nobody is buying m_Metadata: m_Items: [] - m_Id: 200000000000001013 - m_Localized: + m_Localized: Try to explain m_Metadata: m_Items: [] - m_Id: 200000000000001014 - m_Localized: + m_Localized: You refuse to believe me m_Metadata: m_Items: [] - m_Id: 200000000000001015 - m_Localized: + m_Localized: Just a little trust m_Metadata: m_Items: [] - m_Id: 200000000000001016 - m_Localized: + m_Localized: I've had enough m_Metadata: m_Items: [] - m_Id: 200000000000001017 - m_Localized: + m_Localized: Damn it m_Metadata: m_Items: [] - m_Id: 200000000000001018 - m_Localized: + m_Localized: I'm useless m_Metadata: m_Items: [] - m_Id: 200000000000001019 - m_Localized: + m_Localized: Prove myself m_Metadata: m_Items: [] - m_Id: 200000000000001020 - m_Localized: + m_Localized: I can't do anything m_Metadata: m_Items: [] - m_Id: 200000000000001021 - m_Localized: + m_Localized: Catch up with you m_Metadata: m_Items: [] - m_Id: 200000000000001022 - m_Localized: + m_Localized: Help Yingli m_Metadata: m_Items: [] - m_Id: 200000000000001023 - m_Localized: + m_Localized: I have no courage m_Metadata: m_Items: [] - m_Id: 200000000000001024 - m_Localized: + m_Localized: Say it out loud m_Metadata: m_Items: [] - m_Id: 200000000000001025 - m_Localized: + m_Localized: Trash m_Metadata: m_Items: [] - m_Id: 200000000000001026 - m_Localized: + m_Localized: Completely hopeless m_Metadata: m_Items: [] - m_Id: 200000000000001027 - m_Localized: + m_Localized: Why why why m_Metadata: m_Items: [] - m_Id: 200000000000001028 - m_Localized: + m_Localized: I want to win for once m_Metadata: m_Items: [] - m_Id: 200000000000001029 - m_Localized: + m_Localized: the a an and or but if then because I me my mine you your yours we our us they their them he his him she her it its this that these those who what where when why how is am are was were be been being do does did done can could will would shall should may might must have has had not no yes never always sometimes again still already just only even very really too so more most less least all any some none one two first last next now here there inside outside above below before after back away near far good bad right wrong true false real fake funny joke laugh smile cry fear trust doubt hope courage voice word words say tell ask answer explain believe leave stay come go follow catch help hold let make try fail win lose break fix remember forget think know feel want need like hate love angry tired lonely useless enough nothing everything someone nobody friend weather today tomorrow yesterday time world dream truth lie problem reason choice change begin end open close light dark red blue green warm cold quiet loud slow fast strong weak safe danger together apart broken whole m_Metadata: m_Items: [] references: From 61fb8820a39c2ffdcdb6959e6da95c56ed589887 Mon Sep 17 00:00:00 2001 From: Ding Yuntian <1491671119@qq.com> Date: Wed, 29 Jul 2026 15:57:44 +0800 Subject: [PATCH 6/7] =?UTF-8?q?feat:=20=E6=A2=A6=E5=A2=83dos=E7=95=8C?= =?UTF-8?q?=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Language/Params/Params Shared Data.asset | 32 +++++++++++++++++ Assets/Language/Params/Params_en.asset | 32 +++++++++++++++++ Assets/Language/Params/Params_ja-JP.asset | 32 +++++++++++++++++ Assets/Language/Params/Params_zh-Hans.asset | 32 +++++++++++++++++ Assets/Scenes/梦境.unity | 25 ------------- .../Scripts/UI/Components/DreamTerminalUI.cs | 35 ++++++++++++------- 6 files changed, 150 insertions(+), 38 deletions(-) diff --git a/Assets/Language/Params/Params Shared Data.asset b/Assets/Language/Params/Params Shared Data.asset index 1100a2da7..bfce594c1 100644 --- a/Assets/Language/Params/Params Shared Data.asset +++ b/Assets/Language/Params/Params Shared Data.asset @@ -155,6 +155,38 @@ MonoBehaviour: m_Key: hs.exp.pool m_Metadata: m_Items: [] + - m_Id: 200000000000001030 + m_Key: dream.terminal.boot.01 + m_Metadata: + m_Items: [] + - m_Id: 200000000000001031 + m_Key: dream.terminal.boot.02 + m_Metadata: + m_Items: [] + - m_Id: 200000000000001032 + m_Key: dream.terminal.boot.03 + m_Metadata: + m_Items: [] + - m_Id: 200000000000001033 + m_Key: dream.terminal.boot.04 + m_Metadata: + m_Items: [] + - m_Id: 200000000000001034 + m_Key: dream.terminal.boot.05 + m_Metadata: + m_Items: [] + - m_Id: 200000000000001035 + m_Key: dream.terminal.boot.06 + m_Metadata: + m_Items: [] + - m_Id: 200000000000001036 + m_Key: dream.terminal.boot.07 + m_Metadata: + m_Items: [] + - m_Id: 200000000000001037 + m_Key: dream.terminal.boot.08 + m_Metadata: + m_Items: [] m_Metadata: m_Items: [] m_KeyGenerator: diff --git a/Assets/Language/Params/Params_en.asset b/Assets/Language/Params/Params_en.asset index 7301e889e..44c1babea 100644 --- a/Assets/Language/Params/Params_en.asset +++ b/Assets/Language/Params/Params_en.asset @@ -158,6 +158,38 @@ MonoBehaviour: m_Localized: the a an and or but if then because I me my mine you your yours we our us they their them he his him she her it its this that these those who what where when why how is am are was were be been being do does did done can could will would shall should may might must have has had not no yes never always sometimes again still already just only even very really too so more most less least all any some none one two first last next now here there inside outside above below before after back away near far good bad right wrong true false real fake funny joke laugh smile cry fear trust doubt hope courage voice word words say tell ask answer explain believe leave stay come go follow catch help hold let make try fail win lose break fix remember forget think know feel want need like hate love angry tired lonely useless enough nothing everything someone nobody friend weather today tomorrow yesterday time world dream truth lie problem reason choice change begin end open close light dark red blue green warm cold quiet loud slow fast strong weak safe danger together apart broken whole m_Metadata: m_Items: [] + - m_Id: 200000000000001030 + m_Localized: root@sys:~$ dream.render --stop --session 0742 + m_Metadata: + m_Items: [] + - m_Id: 200000000000001031 + m_Localized: '[ OK ] Dream renderer stopped.' + m_Metadata: + m_Items: [] + - m_Id: 200000000000001032 + m_Localized: '[ RAW ] Untitled_Greybox_0 exposed.' + m_Metadata: + m_Items: [] + - m_Id: 200000000000001033 + m_Localized: root@sys:~$ trace --tail /log/dream_0742.rec + m_Metadata: + m_Items: [] + - m_Id: 200000000000001034 + m_Localized: '[0005] audio.play("sea_ambient", source=door.rear)' + m_Metadata: + m_Items: [] + - m_Id: 200000000000001035 + m_Localized: '[0006] memory.project("glass_sea")' + m_Metadata: + m_Items: [] + - m_Id: 200000000000001036 + m_Localized: '[ WARN ] memory source unresolved; overflow detected.' + m_Metadata: + m_Items: [] + - m_Id: 200000000000001037 + m_Localized: 'root@sys:~$ print_string("Does the sea exist?")' + m_Metadata: + m_Items: [] references: version: 2 RefIds: [] diff --git a/Assets/Language/Params/Params_ja-JP.asset b/Assets/Language/Params/Params_ja-JP.asset index f95e06640..af1b38966 100644 --- a/Assets/Language/Params/Params_ja-JP.asset +++ b/Assets/Language/Params/Params_ja-JP.asset @@ -158,6 +158,38 @@ MonoBehaviour: m_Localized: m_Metadata: m_Items: [] + - m_Id: 200000000000001030 + m_Localized: root@sys:~$ dream.render --stop --session 0742 + m_Metadata: + m_Items: [] + - m_Id: 200000000000001031 + m_Localized: '[ OK ] Dream renderer stopped.' + m_Metadata: + m_Items: [] + - m_Id: 200000000000001032 + m_Localized: '[ RAW ] Untitled_Greybox_0 exposed.' + m_Metadata: + m_Items: [] + - m_Id: 200000000000001033 + m_Localized: root@sys:~$ trace --tail /log/dream_0742.rec + m_Metadata: + m_Items: [] + - m_Id: 200000000000001034 + m_Localized: '[0005] audio.play("sea_ambient", source=door.rear)' + m_Metadata: + m_Items: [] + - m_Id: 200000000000001035 + m_Localized: '[0006] memory.project("glass_sea")' + m_Metadata: + m_Items: [] + - m_Id: 200000000000001036 + m_Localized: '[ WARN ] \u30E1\u30E2\u30EA\u30BD\u30FC\u30B9\u672A\u89E3\u6C7A\uFF1B\u30AA\u30FC\u30D0\u30FC\u30D5\u30ED\u30FC\u691C\u51FA\u3002' + m_Metadata: + m_Items: [] + - m_Id: 200000000000001037 + m_Localized: "root@sys:~$ print_string(\"\u6D77\u306F\u5B58\u5728\u3059\u308B\u306E\u304B?\")" + m_Metadata: + m_Items: [] references: version: 2 RefIds: [] diff --git a/Assets/Language/Params/Params_zh-Hans.asset b/Assets/Language/Params/Params_zh-Hans.asset index 7b31cbd11..d7bfc3012 100644 --- a/Assets/Language/Params/Params_zh-Hans.asset +++ b/Assets/Language/Params/Params_zh-Hans.asset @@ -158,6 +158,38 @@ MonoBehaviour: m_Localized: 的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞 m_Metadata: m_Items: [] + - m_Id: 200000000000001030 + m_Localized: root@sys:~$ dream.render --stop --session 0742 + m_Metadata: + m_Items: [] + - m_Id: 200000000000001031 + m_Localized: '[ OK ] Dream renderer stopped.' + m_Metadata: + m_Items: [] + - m_Id: 200000000000001032 + m_Localized: '[ RAW ] Untitled_Greybox_0 exposed.' + m_Metadata: + m_Items: [] + - m_Id: 200000000000001033 + m_Localized: root@sys:~$ trace --tail /log/dream_0742.rec + m_Metadata: + m_Items: [] + - m_Id: 200000000000001034 + m_Localized: '[0005] audio.play("sea_ambient", source=door.rear)' + m_Metadata: + m_Items: [] + - m_Id: 200000000000001035 + m_Localized: '[0006] memory.project("glass_sea")' + m_Metadata: + m_Items: [] + - m_Id: 200000000000001036 + m_Localized: '[ WARN ] memory source unresolved; overflow detected.' + m_Metadata: + m_Items: [] + - m_Id: 200000000000001037 + m_Localized: "root@sys:~$ print_string(\"\u6D77\u662F\u5B58\u5728\u7684\u5417?\")" + m_Metadata: + m_Items: [] references: version: 2 RefIds: [] diff --git a/Assets/Scenes/梦境.unity b/Assets/Scenes/梦境.unity index 56732bda5..7415c3207 100644 --- a/Assets/Scenes/梦境.unity +++ b/Assets/Scenes/梦境.unity @@ -1586,31 +1586,6 @@ MonoBehaviour: onCompleteNode: "\u82CF\u9192" typewriterCharDelay: 0.025 bootSpeedMultiplier: 20 - bootSequence: - - text: root@sys:~$ dream.render --stop --session 0742 - style: 3 - delayAfter: 0.15 - - text: '[ OK ] Dream renderer stopped.' - style: 0 - delayAfter: 0.25 - - text: '[ RAW ] Untitled_Greybox_0 exposed.' - style: 0 - delayAfter: 0.35 - - text: root@sys:~$ trace --tail /log/dream_0742.rec - style: 3 - delayAfter: 0.15 - - text: '[0005] audio.play("sea_ambient", source=door.rear)' - style: 3 - delayAfter: 0.1 - - text: '[0006] memory.project("glass_sea")' - style: 3 - delayAfter: 0.1 - - text: '[ WARN ] memory source unresolved; overflow detected.' - style: 1 - delayAfter: 0.4 - - text: "root@sys:~$ print_string(\"\u6D77\u662F\u5B58\u5728\u7684\u5417?\")" - style: 2 - delayAfter: 0.5 --- !u!224 &1195418929 RectTransform: m_ObjectHideFlags: 0 diff --git a/Assets/Scripts/UI/Components/DreamTerminalUI.cs b/Assets/Scripts/UI/Components/DreamTerminalUI.cs index a2a39ed77..dc5d1e046 100644 --- a/Assets/Scripts/UI/Components/DreamTerminalUI.cs +++ b/Assets/Scripts/UI/Components/DreamTerminalUI.cs @@ -1,9 +1,11 @@ using System; using System.Collections; using System.Collections.Generic; +using AibisDream.Framework; using Shapes; using TMPro; using UnityEngine; +using UnityEngine.Localization.Settings; using UnityEngine.Rendering; #if UNITY_EDITOR using UnityEditor; @@ -96,17 +98,20 @@ namespace AibisDream.UI [Range(0.25f, 4f)] [SerializeField] private float bootSpeedMultiplier = 1f; - [Header("启动日志")] - [SerializeField] private List bootSequence = new() + #endregion + + #region 启动日志(硬编码,文案见 Params 表 dream.terminal.boot.*) + + private static readonly BootLogEntry[] BootSequence = { - new BootLogEntry { text = "root@sys:~$ dream.render --stop --session 0742", style = LogStyle.Dim, delayAfter = 0.15f }, - new BootLogEntry { text = "[ OK ] Dream renderer stopped.", style = LogStyle.Normal, delayAfter = 0.25f }, - new BootLogEntry { text = "[ RAW ] Untitled_Greybox_0 exposed.", style = LogStyle.Normal, delayAfter = 0.35f }, - new BootLogEntry { text = "root@sys:~$ trace --tail /log/dream_0742.rec", style = LogStyle.Dim, delayAfter = 0.15f }, - new BootLogEntry { text = "[0005] audio.play(\"sea_ambient\", source=door.rear)", style = LogStyle.Dim, delayAfter = 0.1f }, - new BootLogEntry { text = "[0006] memory.project(\"glass_sea\")", style = LogStyle.Dim, delayAfter = 0.1f }, - new BootLogEntry { text = "[ WARN ] memory source unresolved; overflow detected.", style = LogStyle.Alert, delayAfter = 0.4f }, - new BootLogEntry { text = "root@sys:~$ print_string(\"海是存在的吗?\")", style = LogStyle.Highlight, delayAfter = 0.5f }, + new() { textKey = "l10n.dream.terminal.boot.01", style = LogStyle.Dim, delayAfter = 0.15f }, + new() { textKey = "l10n.dream.terminal.boot.02", style = LogStyle.Normal, delayAfter = 0.25f }, + new() { textKey = "l10n.dream.terminal.boot.03", style = LogStyle.Normal, delayAfter = 0.35f }, + new() { textKey = "l10n.dream.terminal.boot.04", style = LogStyle.Dim, delayAfter = 0.15f }, + new() { textKey = "l10n.dream.terminal.boot.05", style = LogStyle.Dim, delayAfter = 0.10f }, + new() { textKey = "l10n.dream.terminal.boot.06", style = LogStyle.Dim, delayAfter = 0.10f }, + new() { textKey = "l10n.dream.terminal.boot.07", style = LogStyle.Alert, delayAfter = 0.40f }, + new() { textKey = "l10n.dream.terminal.boot.08", style = LogStyle.Highlight, delayAfter = 0.50f }, }; #endregion @@ -752,13 +757,17 @@ namespace AibisDream.UI { _phase = TerminalPhase.BootSequence; float speed = Mathf.Max(0.25f, bootSpeedMultiplier); + yield return LocalizationSettings.InitializationOperation; yield return new WaitForSeconds(0.3f / speed); - foreach (var entry in bootSequence) + foreach (var entry in BootSequence) { + string line = null; + yield return LocalizationKit.LocalizeParam(entry.textKey, null, s => line = s); + Color c = StyleToColor(entry.style); float charDelay = typewriterCharDelay / speed; - _typewriterCoroutine = StartCoroutine(TypewriterLine(entry.text, c, charDelay)); + _typewriterCoroutine = StartCoroutine(TypewriterLine(line, c, charDelay)); yield return WaitForTypewriter(); if (entry.delayAfter > 0f) @@ -959,7 +968,7 @@ namespace AibisDream.UI [Serializable] public class BootLogEntry { - public string text; + public string textKey; public LogStyle style; public float delayAfter; } From 61a2e779df20986951ca734f4ee8fb95a465a5a5 Mon Sep 17 00:00:00 2001 From: Ding Yuntian <1491671119@qq.com> Date: Wed, 29 Jul 2026 20:35:25 +0800 Subject: [PATCH 7/7] =?UTF-8?q?feat:=20=E6=8C=87=E9=92=88=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/Scripts/UI/Cursor/CursorManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Assets/Scripts/UI/Cursor/CursorManager.cs b/Assets/Scripts/UI/Cursor/CursorManager.cs index efa69d366..b612fe9ee 100644 --- a/Assets/Scripts/UI/Cursor/CursorManager.cs +++ b/Assets/Scripts/UI/Cursor/CursorManager.cs @@ -45,7 +45,8 @@ namespace AibisDream _cursor.gameObject.SetActive(_cursorVisible); } - Cursor.visible = false; + // 隐藏自定义指针图时显示系统真实指针,便于录制等场景定位鼠标 + Cursor.visible = !_cursorVisible; } private void InitCursor()