feat: 火山表达维修粒子字符池拆分
This commit is contained in:
@@ -233,7 +233,7 @@ namespace AibisDream.MiniGame.Language
|
||||
/// 目标粒子:从本轮本地化默认池随机;非目标粒子:从笑话/干扰短句字符中随机。
|
||||
/// 使用 IsTarget 判断,与红蓝颜色解绑。
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// 启动粒子系统(在打开视图后调用,通常通过 Yarn 对话控制)
|
||||
/// </summary>
|
||||
/// <param name="anxietyPhrases">干扰短句列表(笑话等,非目标粒子从中随机取字符)</param>
|
||||
/// <param name="targetSentence">目标句子</param>
|
||||
/// <param name="defaultCharacterPool">当前 Locale 的默认随机字符池</param>
|
||||
/// <param name="snapshot">当前 Locale 已完成粒子单位解析的轮次快照</param>
|
||||
/// <param name="completionNodeName">完成时触发的对话节点名称(可选)</param>
|
||||
/// <param name="nonTargetParticleTotal">非目标候选粒子总数;<0 时使用 Inspector 默认候选池大小</param>
|
||||
/// <param name="baseNonTargetParticleCount">轮次配置中的基础非目标粒子数</param>
|
||||
public void StartSystem(
|
||||
List<string> anxietyPhrases,
|
||||
string targetSentence,
|
||||
IReadOnlyList<string> 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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动粒子系统(字符串数组版本,方便 Yarn 调用)
|
||||
/// </summary>
|
||||
public void StartSystem(
|
||||
string[] anxietyPhrases,
|
||||
string targetSentence,
|
||||
IReadOnlyList<string> defaultCharacterPool,
|
||||
string completionNodeName = null,
|
||||
int nonTargetParticleTotal = -1)
|
||||
{
|
||||
List<string> phrasesList = anxietyPhrases != null ? new List<string>(anxietyPhrases) : null;
|
||||
StartSystem(
|
||||
phrasesList,
|
||||
targetSentence,
|
||||
defaultCharacterPool,
|
||||
completionNodeName,
|
||||
nonTargetParticleTotal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 若火山释放 log 界面已淡出,则先淡入再返回(start_expression 时调用)
|
||||
/// </summary>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 34ce433966a94546a460c32821f6e741
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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<ExpressionParticleLocaleSettings> locales = new();
|
||||
|
||||
private readonly HashSet<string> warnedFallbackLocales =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public IReadOnlyList<ExpressionParticleLocaleSettings> 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<string> GetValidationErrors()
|
||||
{
|
||||
var errors = new List<string>();
|
||||
var codes = new HashSet<string>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3a7f6c18f2184d09b551e103c67268a1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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<string> targetUnits,
|
||||
List<float> targetGapWeights,
|
||||
List<string> defaultPoolUnits,
|
||||
List<string> 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<string> TargetUnits { get; }
|
||||
public IReadOnlyList<float> TargetGapWeights { get; }
|
||||
public IReadOnlyList<string> DefaultPoolUnits { get; }
|
||||
public IReadOnlyList<string> InterferencePoolUnits { get; }
|
||||
|
||||
public static bool TryCreate(
|
||||
LocaleIdentifier locale,
|
||||
ExpressionParticleLocaleSettings profile,
|
||||
string targetText,
|
||||
IEnumerable<string> 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<string> defaultUnits))
|
||||
{
|
||||
error = "默认粒子池为空或包含非法分隔符。";
|
||||
return false;
|
||||
}
|
||||
|
||||
List<string> interferenceUnits =
|
||||
ExpressionTextTokenizer.BuildUnitPool(
|
||||
interferencePhrases,
|
||||
profile.UnitMode);
|
||||
if (interferenceUnits.Count == 0)
|
||||
{
|
||||
error = "干扰短句没有有效的粒子单位。";
|
||||
return false;
|
||||
}
|
||||
|
||||
snapshot = new ExpressionRoundTextSnapshot(
|
||||
locale,
|
||||
profile,
|
||||
targetText,
|
||||
new List<string>(target.Units),
|
||||
new List<float>(target.GapWeights),
|
||||
defaultUnits,
|
||||
interferenceUnits);
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1972c8c6ecaa4512b9cc891a82a5d608
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
/// </summary>
|
||||
public static class ExpressionTextTokenizer
|
||||
{
|
||||
public readonly struct UnitSequence
|
||||
{
|
||||
public UnitSequence(List<string> units, List<float> gapWeights)
|
||||
{
|
||||
Units = units;
|
||||
GapWeights = gapWeights;
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> Units { get; }
|
||||
public IReadOnlyList<float> GapWeights { get; }
|
||||
public int Count => Units?.Count ?? 0;
|
||||
}
|
||||
|
||||
public readonly struct Layout
|
||||
{
|
||||
public Layout(List<string> visibleElements, List<float> offsets)
|
||||
@@ -40,16 +54,56 @@ namespace AibisDream.MiniGame.Language
|
||||
}
|
||||
|
||||
public static List<string> BuildCharacterPool(IEnumerable<string> texts)
|
||||
{
|
||||
return BuildUnitPool(texts, ExpressionParticleUnitMode.Grapheme);
|
||||
}
|
||||
|
||||
public static List<string> TokenizeText(
|
||||
string text,
|
||||
ExpressionParticleUnitMode mode)
|
||||
{
|
||||
return new List<string>(BuildLayoutUnits(text, mode).Units);
|
||||
}
|
||||
|
||||
public static bool TryTokenizePool(
|
||||
string pool,
|
||||
ExpressionParticleUnitMode mode,
|
||||
out List<string> units)
|
||||
{
|
||||
units = new List<string>();
|
||||
if (string.IsNullOrWhiteSpace(pool) ||
|
||||
pool.Contains("|") ||
|
||||
pool.Contains("|"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
units = TokenizeText(pool, mode);
|
||||
return units.Count > 0;
|
||||
}
|
||||
|
||||
public static List<string> BuildUnitPool(
|
||||
IEnumerable<string> texts,
|
||||
ExpressionParticleUnitMode mode)
|
||||
{
|
||||
var result = new List<string>();
|
||||
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<string> source,
|
||||
IReadOnlyList<string> fragment)
|
||||
{
|
||||
return FindVisibleSequence(source, fragment);
|
||||
}
|
||||
|
||||
private static UnitSequence BuildGraphemeUnits(string text)
|
||||
{
|
||||
var units = new List<string>();
|
||||
var gaps = new List<float>();
|
||||
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<string>();
|
||||
var gaps = new List<float>();
|
||||
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<string> units,
|
||||
List<float> gaps,
|
||||
bool hasLeadingSeparator)
|
||||
{
|
||||
if (word.Length == 0)
|
||||
return;
|
||||
|
||||
units.Add(word.ToString());
|
||||
gaps.Add(units.Count == 1 ? 0f : hasLeadingSeparator ? 1f : 0f);
|
||||
word.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ namespace AibisDream.MiniGame.Language
|
||||
/// <summary>
|
||||
/// 背景粒子从干扰短句(笑话等)字符中随机显示单字。
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,10 +64,9 @@ namespace AibisDream.MiniGame.Language
|
||||
[SerializeField] private float textMargin = 1f;
|
||||
|
||||
[Header("游戏配置")]
|
||||
private List<string> anxietyPhrases = new List<string>();
|
||||
private string targetSentence = string.Empty;
|
||||
private IReadOnlyList<string> defaultCharacterPool = new List<string>().AsReadOnly();
|
||||
private IReadOnlyList<string> interferenceCharacterPool = new List<string>().AsReadOnly();
|
||||
private ExpressionRoundTextSnapshot activeSnapshot;
|
||||
private IReadOnlyList<string> defaultUnitPool = new List<string>().AsReadOnly();
|
||||
private IReadOnlyList<string> interferenceUnitPool = new List<string>().AsReadOnly();
|
||||
[SerializeField] private string completionDialogNode = ""; // 完成时触发的对话节点
|
||||
|
||||
[Header("波形参数")]
|
||||
@@ -222,7 +221,7 @@ namespace AibisDream.MiniGame.Language
|
||||
/// <summary>完成阶段屏幕钳制用 SmoothDamp 速度缓存(按粒子)</summary>
|
||||
private readonly Dictionary<CandidateParticle, Vector3> completionClampSmoothVelocity = new Dictionary<CandidateParticle, Vector3>();
|
||||
private Vector3 focusCentroid;
|
||||
private List<string> finalTargetCharacters = new List<string>();
|
||||
private List<string> finalTargetUnits = new List<string>();
|
||||
private List<Vector3> finalTargetPositions = new List<Vector3>();
|
||||
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
|
||||
/// <summary>Inspector 中的候选粒子总数,用于未在 Yarn 中指定非目标数量时恢复默认</summary>
|
||||
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
|
||||
/// <summary>
|
||||
/// 初始化系统(可以由外部调用,用于配置游戏参数)
|
||||
/// </summary>
|
||||
/// <param name="phrases">焦虑短句列表</param>
|
||||
/// <param name="sentence">目标句子</param>
|
||||
/// <param name="localizedDefaultPool">当前 Locale 的默认随机字符池</param>
|
||||
/// <param name="snapshot">当前 Locale 已完成粒子单位解析的轮次快照</param>
|
||||
/// <param name="dialogNode">完成时触发的对话节点</param>
|
||||
/// <param name="nonTargetParticleTotal">
|
||||
/// 非目标候选粒子总数(蓝/干扰侧可交互粒子数量)。≥0 时候选池大小 = 目标句字数 + 该值;<0 时使用 Inspector 的 Candidate Count。
|
||||
/// <param name="baseNonTargetParticleCount">
|
||||
/// 轮次配置中的基础非目标粒子数。≥0 时先应用 Locale Profile 数量倍率;
|
||||
/// <0 时使用 Inspector 的 Candidate Count。
|
||||
/// </param>
|
||||
public void InitializeSystem(
|
||||
List<string> phrases,
|
||||
string sentence,
|
||||
IReadOnlyList<string> 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<string> resolvedDefaultPool =
|
||||
ExpressionTextTokenizer.BuildCharacterPool(localizedDefaultPool);
|
||||
List<string> 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<string>(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<FloatingTextParticle>();
|
||||
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<CandidateParticle>();
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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<string> targetUnits =
|
||||
activeSnapshot?.TargetUnits ?? new List<string>().AsReadOnly();
|
||||
IReadOnlyList<float> targetGapWeights =
|
||||
activeSnapshot?.TargetGapWeights ?? new List<float>().AsReadOnly();
|
||||
List<Vector3> 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<string> poolElements =
|
||||
ExpressionTextTokenizer.TokenizeText(particlePool, ActiveUnitMode);
|
||||
if (poolElements.Count == 0)
|
||||
{
|
||||
Debug.LogWarning("[LanguageParticleManager] 老虎机换字字符池为空;已安全跳过。");
|
||||
return;
|
||||
}
|
||||
IReadOnlyList<string> 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<string>)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<string>)null);
|
||||
}
|
||||
|
||||
public void PrepareTruthReleaseFromLie()
|
||||
@@ -2211,7 +2259,7 @@ namespace AibisDream.MiniGame.Language
|
||||
List<CandidateParticle> particles = GetPresentationPhraseParticles();
|
||||
if (!ValidatePresentationPhrase(text, particles, "真话失稳"))
|
||||
yield break;
|
||||
List<string> characters = ExpressionTextTokenizer.GetVisibleElements(text);
|
||||
List<string> 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<Vector3> 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<CandidateParticle> particles = GetPresentationPhraseParticles();
|
||||
if (!ValidatePresentationPhrase(text, particles, "真话攻击"))
|
||||
yield break;
|
||||
List<string> characters = ExpressionTextTokenizer.GetVisibleElements(text);
|
||||
List<string> 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<Vector3> 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<CandidateParticle> particles = GetPresentationPhraseParticles();
|
||||
if (!ValidatePresentationPhrase(text, particles, "真话稳定"))
|
||||
yield break;
|
||||
List<string> characters = ExpressionTextTokenizer.GetVisibleElements(text);
|
||||
List<string> 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<Vector3> 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<CandidateParticle> 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<Vector3> CalculatePresentationPhrasePositions(string text)
|
||||
{
|
||||
float spacing = Mathf.Max(0.1f, truthAttackCharacterSpacing);
|
||||
ExpressionTextTokenizer.Layout layout = ExpressionTextTokenizer.BuildLayout(
|
||||
text,
|
||||
spacing,
|
||||
whitespaceSpacingMultiplier);
|
||||
var positions = new List<Vector3>(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<string> TokenizeActiveText(string text)
|
||||
{
|
||||
return ExpressionTextTokenizer.TokenizeText(text, ActiveUnitMode);
|
||||
}
|
||||
|
||||
private List<Vector3> CalculatePhrasePositions(
|
||||
IReadOnlyList<string> units,
|
||||
IReadOnlyList<float> gapWeights)
|
||||
{
|
||||
int count = units?.Count ?? 0;
|
||||
var offsets = new List<float>(count);
|
||||
var positions = new List<Vector3>(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<string> characters,
|
||||
IReadOnlyList<string> units,
|
||||
List<Vector3> 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<string> targetElements = ExpressionTextTokenizer.GetVisibleElements(targetSentence);
|
||||
List<string> fragmentElements = ExpressionTextTokenizer.GetVisibleElements(fragment);
|
||||
int startIndex = ExpressionTextTokenizer.FindVisibleSequence(targetElements, fragmentElements);
|
||||
IReadOnlyList<string> targetElements =
|
||||
activeSnapshot?.TargetUnits ?? new List<string>().AsReadOnly();
|
||||
List<string> 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;
|
||||
|
||||
@@ -90,18 +90,20 @@ namespace AibisDream.MiniGame.Language
|
||||
|
||||
public static bool TryParseExpressionPool(
|
||||
string value,
|
||||
out List<string> characters)
|
||||
out List<string> units)
|
||||
{
|
||||
characters = new List<string>();
|
||||
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<string> 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<string> 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<string> 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();
|
||||
|
||||
@@ -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<string> EmptyCharacterPool =
|
||||
new List<string>().AsReadOnly();
|
||||
protected IReadOnlyList<string> defaultCharacterPool = EmptyCharacterPool;
|
||||
protected IReadOnlyList<string> interferenceCharacterPool = EmptyCharacterPool;
|
||||
protected IReadOnlyList<string> overrideCharacterPool = EmptyCharacterPool;
|
||||
protected IReadOnlyList<string> defaultUnitPool = EmptyCharacterPool;
|
||||
protected IReadOnlyList<string> interferenceUnitPool = EmptyCharacterPool;
|
||||
protected IReadOnlyList<string> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -215,16 +247,16 @@ namespace AibisDream.MiniGame.Language
|
||||
/// </summary>
|
||||
protected string GetRandomCharFromPhrases()
|
||||
{
|
||||
if (interferenceCharacterPool == null || interferenceCharacterPool.Count == 0)
|
||||
if (interferenceUnitPool == null || interferenceUnitPool.Count == 0)
|
||||
return GetRandomChar();
|
||||
return GetRandomPoolElement(interferenceCharacterPool);
|
||||
return GetRandomPoolElement(interferenceUnitPool);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取下一次要显示的字符。子类可重写以区分目标粒子与非目标粒子(与红蓝颜色解绑)。
|
||||
/// 默认从本轮本地化字符池随机返回一个 Unicode 文本元素。
|
||||
/// </summary>
|
||||
protected virtual string GetNextDisplayChar()
|
||||
protected virtual string GetNextDisplayUnit()
|
||||
{
|
||||
if (TryGetOverridePoolChar(out string overrideChar))
|
||||
return overrideChar;
|
||||
@@ -234,19 +266,25 @@ namespace AibisDream.MiniGame.Language
|
||||
/// <summary>
|
||||
/// 设置换字字符池覆盖(老虎机演出用);传空/null 清除,恢复默认字符池。
|
||||
/// </summary>
|
||||
public void SetOverrideCharPool(string pool)
|
||||
public void SetOverrideUnitPool(string pool)
|
||||
{
|
||||
SetOverrideCharacterPool(
|
||||
SetOverrideUnitPool(
|
||||
ExpressionTextTokenizer.GetVisibleElements(pool));
|
||||
}
|
||||
|
||||
public void SetOverrideCharacterPool(IReadOnlyList<string> pool)
|
||||
public void SetOverrideUnitPool(IReadOnlyList<string> pool)
|
||||
{
|
||||
overrideCharacterPool = pool != null && pool.Count > 0
|
||||
overrideUnitPool = pool != null && pool.Count > 0
|
||||
? pool
|
||||
: EmptyCharacterPool;
|
||||
}
|
||||
|
||||
[System.Obsolete("Use SetOverrideUnitPool instead.")]
|
||||
public void SetOverrideCharacterPool(IReadOnlyList<string> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置本轮默认与干扰字符池。调用方负责传入不可变的本轮快照。
|
||||
/// </summary>
|
||||
public void SetCharacterPools(
|
||||
public void SetUnitPools(
|
||||
IReadOnlyList<string> defaultPool,
|
||||
IReadOnlyList<string> 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<string> defaultCharacters,
|
||||
IReadOnlyList<string> 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<string> 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()
|
||||
|
||||
Reference in New Issue
Block a user