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; } } }