feat(huoshan): 实现Log释放演出控制器与场景挂接

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-23 18:54:29 +08:00
co-authored by Cursor
parent f55cd31f6f
commit e3a69ea0f5
11 changed files with 3720 additions and 83 deletions
+39 -2
View File
@@ -9251,6 +9251,7 @@ MonoBehaviour:
_currentStage: 0 _currentStage: 0
_intensity: 0 _intensity: 0
_transitionDuration: 0.5 _transitionDuration: 0.5
_compositeMode: 0
_stage1Clip: {fileID: 8300000, guid: 9f4ca2e877b18ff46902c106f56ed672, type: 3} _stage1Clip: {fileID: 8300000, guid: 9f4ca2e877b18ff46902c106f56ed672, type: 3}
_stage2Clip: {fileID: 8300000, guid: 5b5068defdc3579479bf0efa306a07ca, type: 3} _stage2Clip: {fileID: 8300000, guid: 5b5068defdc3579479bf0efa306a07ca, type: 3}
_stage3Clip: {fileID: 8300000, guid: a965c5c0ea798db4787e279d5bef59fa, type: 3} _stage3Clip: {fileID: 8300000, guid: a965c5c0ea798db4787e279d5bef59fa, type: 3}
@@ -13253,7 +13254,7 @@ GameObject:
- component: {fileID: 1711705855} - component: {fileID: 1711705855}
- component: {fileID: 1711705856} - component: {fileID: 1711705856}
m_Layer: 5 m_Layer: 5
m_Name: bound m_Name: ExpressionScreenMaskRect
m_TagString: Untagged m_TagString: Untagged
m_Icon: {fileID: 0} m_Icon: {fileID: 0}
m_NavMeshLayer: 0 m_NavMeshLayer: 0
@@ -15006,6 +15007,7 @@ GameObject:
m_Component: m_Component:
- component: {fileID: 2138889539} - component: {fileID: 2138889539}
- component: {fileID: 2138889540} - component: {fileID: 2138889540}
- component: {fileID: 2138889543}
- component: {fileID: 2138889542} - component: {fileID: 2138889542}
- component: {fileID: 2138889541} - component: {fileID: 2138889541}
m_Layer: 0 m_Layer: 0
@@ -15048,6 +15050,7 @@ MonoBehaviour:
m_EditorClassIdentifier: m_EditorClassIdentifier:
particleManager: {fileID: 3669563163941125876} particleManager: {fileID: 3669563163941125876}
expressionView: {fileID: 5162511683866016917} expressionView: {fileID: 5162511683866016917}
logReleasePresentation: {fileID: 2138889543}
defaultAnxietyPhrases: defaultAnxietyPhrases:
- "\u6211\u597D\u6015" - "\u6211\u597D\u6015"
- "\u600E\u4E48\u529E" - "\u600E\u4E48\u529E"
@@ -15060,6 +15063,36 @@ MonoBehaviour:
ambientNoiseDensity: 0 ambientNoiseDensity: 0
ambientNoiseScale: 360 ambientNoiseScale: 360
ambientNoiseBrightness: 0.5 ambientNoiseBrightness: 0.5
--- !u!114 &2138889543
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2138889538}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 2d202e7d79d64fb5a9d7d42e974cbacd, type: 3}
m_Name:
m_EditorClassIdentifier:
particleManager: {fileID: 3669563163941125876}
faceRenderer: {fileID: 2468058580162010626}
glitchController: {fileID: 741515695}
expressionScreenMaskRect: {fileID: 1711705855}
screenFont: {fileID: 11400000, guid: f242fb3dde1933640859f1c54591c9c8, type: 2}
officeMemory: {fileID: 21300000, guid: 1f0146f4ad75ca54da8909a24be3ce03, type: 3}
sunsetMemory: {fileID: 21300000, guid: 4d9b651ad3ad15941a4a49bb381c5a2d, type: 3}
privateOfficeMemory: {fileID: 21300000, guid: 56e0c1ba37badb94d8fa982258ade9e7, type: 3}
memoryMaterial: {fileID: 2100000, guid: f99b647eb8e7425fb1f0e32590dfc77d, type: 2}
faceFadeDuration: 1
memoryFadeDuration: 0.55
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: 0.84, b: 0.64, a: 1}
screenGlowSortingOrder: 8
screenTextSortingOrder: 18
--- !u!114 &2138889541 --- !u!114 &2138889541
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -16677,7 +16710,7 @@ SpriteRenderer:
m_SortingLayerID: 0 m_SortingLayerID: 0
m_SortingLayer: 0 m_SortingLayer: 0
m_SortingOrder: 2 m_SortingOrder: 2
m_Sprite: {fileID: 21300000, guid: 3af0a72c847ead14baf8582c9358a985, type: 3} m_Sprite: {fileID: 21300000, guid: 6d0210d3312a4bd4a5042d6aa3d59f5b, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 0} m_Color: {r: 1, g: 1, b: 1, a: 0}
m_FlipX: 0 m_FlipX: 0
m_FlipY: 0 m_FlipY: 0
@@ -17295,6 +17328,7 @@ MonoBehaviour:
clampCompletionPhasePositions: 1 clampCompletionPhasePositions: 1
completionScreenClipRect: {fileID: 1711705855} completionScreenClipRect: {fileID: 1711705855}
completionClipEdgePadding: 0.2 completionClipEdgePadding: 0.2
minimumScreenMargin: 0.12
completionPositionSmoothTime: 0.2 completionPositionSmoothTime: 0.2
languageDeepPanel1: {fileID: 1752165850} languageDeepPanel1: {fileID: 1752165850}
languageDeepPanel2: {fileID: 2141248582} languageDeepPanel2: {fileID: 2141248582}
@@ -17304,6 +17338,9 @@ MonoBehaviour:
expressionConfirmTimelineName: "\u706B\u5C71\u8868\u8FBE\u6A21\u5757/\u706B\u5C71\u8868\u8FBE\u786E\u8BA4" expressionConfirmTimelineName: "\u706B\u5C71\u8868\u8FBE\u6A21\u5757/\u706B\u5C71\u8868\u8FBE\u786E\u8BA4"
focusZoomScale: 1.6 focusZoomScale: 1.6
focusMoveDuration: 1.2 focusMoveDuration: 1.2
focusFadeDuration: 0.6
logOperationFadeDuration: 0.6
glowSortingOrder: 8
focusFadeRatio: 0.8 focusFadeRatio: 0.8
restartExpressionFadeDuration: 0.55 restartExpressionFadeDuration: 0.55
probabilityDuration: 3.5 probabilityDuration: 3.5
@@ -215,6 +215,8 @@ namespace AibisDream.MiniGame.Language
/// </summary> /// </summary>
protected override string GetNextDisplayChar() protected override string GetNextDisplayChar()
{ {
if (TryGetOverridePoolChar(out string overrideChar))
return overrideChar;
return IsTarget ? GetRandomChar() : GetRandomCharFromPhrases(); return IsTarget ? GetRandomChar() : GetRandomCharFromPhrases();
} }
@@ -37,6 +37,10 @@ namespace AibisDream.MiniGame.Language
private float fadeOutAlpha; private float fadeOutAlpha;
private float nonTargetAlphaScale = 1f; private float nonTargetAlphaScale = 1f;
private float targetAlphaScale = 1f; private float targetAlphaScale = 1f;
private Bounds screenBounds;
private bool hasScreenBounds;
private bool screenClippingEnabled;
private bool warnedMissingBounds;
public float NonTargetAlphaScale public float NonTargetAlphaScale
{ {
@@ -62,11 +66,30 @@ namespace AibisDream.MiniGame.Language
fadeOutAlpha = fadeAlpha; fadeOutAlpha = fadeAlpha;
} }
public void SetScreenBounds(Bounds bounds, bool valid = true)
{
screenBounds = bounds;
hasScreenBounds = valid && bounds.size.x > 0.001f && bounds.size.y > 0.001f;
if (hasScreenBounds)
warnedMissingBounds = false;
}
public void SetScreenClippingEnabled(bool enabled)
{
screenClippingEnabled = enabled;
}
public override void DrawShapes(Camera cam) public override void DrawShapes(Camera cam)
{ {
if (candidateParticles == null || candidateParticles.Count == 0) if (candidateParticles == null || candidateParticles.Count == 0)
return; return;
if (screenClippingEnabled && !hasScreenBounds && !warnedMissingBounds)
{
warnedMissingBounds = true;
Debug.LogWarning("[ConnectionRenderer] 未提供有效的 ExpressionScreenMaskRect,连线将沿用未裁切行为。");
}
using (Draw.Command(cam)) using (Draw.Command(cam))
{ {
Draw.BlendMode = ShapesBlendMode.Transparent; Draw.BlendMode = ShapesBlendMode.Transparent;
@@ -117,7 +140,7 @@ namespace AibisDream.MiniGame.Language
Color lineColor = ApplyGlow(nonTargetConnectionColor, nonTargetGlowIntensity); Color lineColor = ApplyGlow(nonTargetConnectionColor, nonTargetGlowIntensity);
lineColor.a = alpha * nonTargetAlphaScale; lineColor.a = alpha * nonTargetAlphaScale;
Draw.Line(p1.transform.position, p2.transform.position, lineColor); DrawClippedLine(p1.transform.position, p2.transform.position, lineColor);
} }
} }
} }
@@ -150,7 +173,7 @@ namespace AibisDream.MiniGame.Language
Color lineColor = ApplyGlow(targetConnectionColor, targetGlowIntensity); Color lineColor = ApplyGlow(targetConnectionColor, targetGlowIntensity);
lineColor.a = alpha * targetAlphaScale; lineColor.a = alpha * targetAlphaScale;
Draw.Line(redParticles[i].transform.position, redParticles[j].transform.position, lineColor); DrawClippedLine(redParticles[i].transform.position, redParticles[j].transform.position, lineColor);
} }
} }
} }
@@ -172,7 +195,7 @@ namespace AibisDream.MiniGame.Language
// 绘制连接线(应用发光) // 绘制连接线(应用发光)
Color lineColor = ApplyGlow(propagationColor, propagationGlowIntensity); Color lineColor = ApplyGlow(propagationColor, propagationGlowIntensity);
lineColor.a = 0.3f; lineColor.a = 0.3f;
Draw.Line(info.source.transform.position, info.particle.transform.position, lineColor); DrawClippedLine(info.source.transform.position, info.particle.transform.position, lineColor);
// 绘制脉冲光点 // 绘制脉冲光点
if (info.pulseProgress < 1f) if (info.pulseProgress < 1f)
@@ -188,11 +211,15 @@ namespace AibisDream.MiniGame.Language
// 脉冲光点使用更强的 HDR 发光 // 脉冲光点使用更强的 HDR 发光
Color pulseColor = ApplyGlow(new Color(1f, 0.78f, 0.39f, pulseAlpha), propagationGlowIntensity * 1.5f); Color pulseColor = ApplyGlow(new Color(1f, 0.78f, 0.39f, pulseAlpha), propagationGlowIntensity * 1.5f);
Draw.Ring(pulsePos, pulseSize, pulseSize * 0.5f, pulseColor); float visualRadius = pulseSize * 1.5f;
if (TryConstrainPoint(pulsePos, visualRadius, out Vector3 containedPulse))
// 光晕 {
pulseColor.a = pulseAlpha * 0.5f; Draw.Ring(containedPulse, pulseSize, pulseSize * 0.5f, pulseColor);
Draw.Disc(pulsePos, pulseSize * 1.5f, pulseColor);
// 光晕
pulseColor.a = pulseAlpha * 0.5f;
Draw.Disc(containedPulse, visualRadius, pulseColor);
}
} }
// 绘制箭头 // 绘制箭头
@@ -208,7 +235,9 @@ namespace AibisDream.MiniGame.Language
float angle = Mathf.Atan2(direction.y, direction.x); float angle = Mathf.Atan2(direction.y, direction.x);
Color arrowColor = ApplyGlow(new Color(1f, 0.78f, 0.39f, 0.8f), propagationGlowIntensity); Color arrowColor = ApplyGlow(new Color(1f, 0.78f, 0.39f, 0.8f), propagationGlowIntensity);
DrawArrow(arrowPos, angle, 0.1f, arrowColor); const float arrowSize = 0.1f;
if (TryConstrainPoint(arrowPos, arrowSize, out Vector3 containedArrow))
DrawArrow(containedArrow, angle, arrowSize, arrowColor);
} }
} }
} }
@@ -231,6 +260,94 @@ namespace AibisDream.MiniGame.Language
Draw.Triangle(tip, left, right, color); Draw.Triangle(tip, left, right, color);
} }
private void DrawClippedLine(Vector3 from, Vector3 to, Color color)
{
if (screenClippingEnabled && hasScreenBounds)
{
if (!TryClipLineToBounds(from, to, screenBounds, out from, out to))
return;
}
Draw.Line(from, to, color);
}
private bool TryConstrainPoint(Vector3 point, float radius, out Vector3 constrained)
{
if (!screenClippingEnabled || !hasScreenBounds)
{
constrained = point;
return true;
}
float minX = screenBounds.min.x + radius;
float maxX = screenBounds.max.x - radius;
float minY = screenBounds.min.y + radius;
float maxY = screenBounds.max.y - radius;
if (minX > maxX || minY > maxY)
{
constrained = point;
return false;
}
constrained = new Vector3(
Mathf.Clamp(point.x, minX, maxX),
Mathf.Clamp(point.y, minY, maxY),
point.z);
return true;
}
/// <summary>
/// LiangBarsky line clipping against a world-space axis-aligned rectangle.
/// Kept public for component tests and editor diagnostics.
/// </summary>
public static bool TryClipLineToBounds(
Vector3 from,
Vector3 to,
Bounds bounds,
out Vector3 clippedFrom,
out Vector3 clippedTo)
{
clippedFrom = from;
clippedTo = to;
float dx = to.x - from.x;
float dy = to.y - from.y;
float t0 = 0f;
float t1 = 1f;
if (!ClipTest(-dx, from.x - bounds.min.x, ref t0, ref t1) ||
!ClipTest(dx, bounds.max.x - from.x, ref t0, ref t1) ||
!ClipTest(-dy, from.y - bounds.min.y, ref t0, ref t1) ||
!ClipTest(dy, bounds.max.y - from.y, ref t0, ref t1))
{
return false;
}
clippedFrom = Vector3.LerpUnclamped(from, to, t0);
clippedTo = Vector3.LerpUnclamped(from, to, t1);
return true;
}
private static bool ClipTest(float p, float q, ref float t0, ref float t1)
{
if (Mathf.Approximately(p, 0f))
return q >= 0f;
float r = q / p;
if (p < 0f)
{
if (r > t1) return false;
if (r > t0) t0 = r;
}
else
{
if (r < t0) return false;
if (r < t1) t1 = r;
}
return true;
}
/// <summary> /// <summary>
/// 设置连接线样式 /// 设置连接线样式
/// </summary> /// </summary>
@@ -279,4 +396,3 @@ namespace AibisDream.MiniGame.Language
} }
} }
} }
@@ -12,9 +12,10 @@ namespace AibisDream
/// </summary> /// </summary>
public class ExpressionManager : MonoBehaviour public class ExpressionManager : MonoBehaviour
{ {
[Header("系统组件")] [Header("系统组件")]
[SerializeField] private LanguageParticleManager particleManager; [SerializeField] private LanguageParticleManager particleManager;
[SerializeField] private GameObject expressionView; [SerializeField] private GameObject expressionView;
[SerializeField] private LogReleasePresentationController logReleasePresentation;
[Header("默认配置")] [Header("默认配置")]
[SerializeField] private List<string> defaultAnxietyPhrases = new List<string> [SerializeField] private List<string> defaultAnxietyPhrases = new List<string>
@@ -32,6 +33,8 @@ namespace AibisDream
[Tooltip("噪波故障效果控制器,可选;置于 Expression View 子物体上")] [Tooltip("噪波故障效果控制器,可选;置于 Expression View 子物体上")]
[SerializeField] private SpriteNoiseGlitchController glitchController; [SerializeField] private SpriteNoiseGlitchController glitchController;
public LogReleasePresentationController LogReleasePresentation => logReleasePresentation;
void Start() void Start()
{ {
// 注册到系统字典 // 注册到系统字典
@@ -48,16 +51,26 @@ namespace AibisDream
expressionView.SetActive(false); expressionView.SetActive(false);
} }
// 查找粒子管理器 // 查找粒子管理器
if (particleManager == null) if (particleManager == null)
{ {
particleManager = GetComponentInChildren<LanguageParticleManager>(true); particleManager = GetComponentInChildren<LanguageParticleManager>(true);
} }
if (glitchController == null) if (glitchController == null)
{ {
glitchController = GetComponentInChildren<SpriteNoiseGlitchController>(true); glitchController = GetComponentInChildren<SpriteNoiseGlitchController>(true);
} }
if (logReleasePresentation == null)
{
logReleasePresentation = GetComponent<LogReleasePresentationController>();
}
if (particleManager != null)
{
particleManager.SetPresentationController(logReleasePresentation);
}
} }
/// <summary> /// <summary>
@@ -89,6 +102,7 @@ namespace AibisDream
{ {
if (particleManager != null) if (particleManager != null)
{ {
particleManager.SetPresentationController(logReleasePresentation);
// 使用传入的配置,如果没有则使用默认配置 // 使用传入的配置,如果没有则使用默认配置
var phrases = anxietyPhrases ?? defaultAnxietyPhrases; var phrases = anxietyPhrases ?? defaultAnxietyPhrases;
var sentence = targetSentence ?? defaultTargetSentence; var sentence = targetSentence ?? defaultTargetSentence;
@@ -126,6 +140,12 @@ namespace AibisDream
yield return StartCoroutine(particleManager.FadeOutBeforeNewExpressionRound(duration)); yield return StartCoroutine(particleManager.FadeOutBeforeNewExpressionRound(duration));
} }
public IEnumerator PreparePresentationForNextRound()
{
if (logReleasePresentation == null) yield break;
yield return logReleasePresentation.PrepareForNextRound();
}
/// <summary> /// <summary>
/// 等待表达流程就绪(开场表现 + 火山表达panel 播完) /// 等待表达流程就绪(开场表现 + 火山表达panel 播完)
/// </summary> /// </summary>
@@ -161,6 +181,11 @@ namespace AibisDream
/// </summary> /// </summary>
public void CloseView() public void CloseView()
{ {
if (logReleasePresentation != null)
{
logReleasePresentation.ForceCleanupImmediate(true);
}
if (expressionView != null) if (expressionView != null)
{ {
expressionView.SetActive(false); expressionView.SetActive(false);
@@ -177,6 +202,11 @@ namespace AibisDream
/// </summary> /// </summary>
public void ResetSystem(bool playEntranceEffect = true) public void ResetSystem(bool playEntranceEffect = true)
{ {
if (logReleasePresentation != null)
{
logReleasePresentation.ForceCleanupImmediate(true);
}
if (particleManager != null) if (particleManager != null)
{ {
particleManager.ResetGame(playEntranceEffect); particleManager.ResetGame(playEntranceEffect);
@@ -235,5 +265,66 @@ namespace AibisDream
yield return glitchController.StartCoroutine(glitchController.TransitionToAndWait(to, duration)); yield return glitchController.StartCoroutine(glitchController.TransitionToAndWait(to, duration));
} }
public IEnumerator BeginExpressionMemory(
string memoryKey,
string preset,
float fadeDuration = -1f,
float pushSpeed = -1f,
float horizontalSpeed = -1f,
bool preserveGlitch = false)
{
if (logReleasePresentation == null)
{
Debug.LogError("[ExpressionManager] LogReleasePresentationController 未配置;记忆命令安全结束。");
yield break;
}
yield return logReleasePresentation.BeginMemory(
memoryKey,
preset,
fadeDuration,
pushSpeed,
horizontalSpeed,
preserveGlitch);
}
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;
yield return logReleasePresentation.EndMemory();
}
public void ImpactExpressionTruth()
{
logReleasePresentation?.TruthImpact();
}
private void OnDisable()
{
if (logReleasePresentation != null)
logReleasePresentation.ForceCleanupImmediate(true);
}
}
} }
}
@@ -120,6 +120,8 @@ namespace AibisDream.MiniGame.Language
[SerializeField] private RectTransform completionScreenClipRect; [SerializeField] private RectTransform completionScreenClipRect;
[Tooltip("在裁剪区基础上再向内缩进的世界单位距离,避免放大后的字半个挂在边外")] [Tooltip("在裁剪区基础上再向内缩进的世界单位距离,避免放大后的字半个挂在边外")]
[SerializeField] private float completionClipEdgePadding = 0.2f; [SerializeField] private float completionClipEdgePadding = 0.2f;
[Tooltip("屏幕内容的最小安全边距;最终边距还会根据字形、光晕、圆环和晃动幅度动态增大。")]
[SerializeField, Min(0f)] private float minimumScreenMargin = 0.12f;
[Tooltip("将目标粒子拉回屏幕裁剪区时的平滑时间(越大越柔和,略增加拖尾感)")] [Tooltip("将目标粒子拉回屏幕裁剪区时的平滑时间(越大越柔和,略增加拖尾感)")]
[SerializeField, Range(0.02f, 0.55f)] private float completionPositionSmoothTime = 0.2f; [SerializeField, Range(0.02f, 0.55f)] private float completionPositionSmoothTime = 0.2f;
@@ -136,6 +138,8 @@ namespace AibisDream.MiniGame.Language
[SerializeField] private float focusZoomScale = 1.6f; [SerializeField] private float focusZoomScale = 1.6f;
[SerializeField] private float focusMoveDuration = 1.2f; [SerializeField] private float focusMoveDuration = 1.2f;
[SerializeField] private float focusFadeDuration = 1f; [SerializeField] private float focusFadeDuration = 1f;
[SerializeField] private float logOperationFadeDuration = 0.6f;
[SerializeField] private int glowSortingOrder = 8;
[Tooltip("再次 start_expression 时,若上一轮粒子/UI 仍在,先淡出再重开;协程 duration≤0 时用此值(秒)")] [Tooltip("再次 start_expression 时,若上一轮粒子/UI 仍在,先淡出再重开;协程 duration≤0 时用此值(秒)")]
[SerializeField] private float restartExpressionFadeDuration = 0.55f; [SerializeField] private float restartExpressionFadeDuration = 0.55f;
[SerializeField] private float probabilityDuration = 3.5f; [SerializeField] private float probabilityDuration = 3.5f;
@@ -212,6 +216,8 @@ namespace AibisDream.MiniGame.Language
private Coroutine expressionPanelRoutine; private Coroutine expressionPanelRoutine;
private Coroutine debugTestRoutine; private Coroutine debugTestRoutine;
private Coroutine confirmTimelineRoutine; private Coroutine confirmTimelineRoutine;
private Coroutine focusTransitionRoutine;
private Coroutine slotMachineRoutine;
private readonly Dictionary<CandidateParticle, Vector3> entranceTargets = new Dictionary<CandidateParticle, Vector3>(); private readonly Dictionary<CandidateParticle, Vector3> entranceTargets = new Dictionary<CandidateParticle, Vector3>();
private class TargetFocusVisual private class TargetFocusVisual
@@ -219,18 +225,22 @@ namespace AibisDream.MiniGame.Language
public CandidateParticle particle; public CandidateParticle particle;
public GameObject ringObject; public GameObject ringObject;
public Disc ringDisc; public Disc ringDisc;
public TMP_Text probabilityLabel;
public float probability = 90f;
public float targetProbability = 100f;
public float startShakeIntensity; public float startShakeIntensity;
public string finalChar; public string finalChar;
public bool stabilized; public bool stabilized;
public Vector3 focusPosition; public Vector3 focusPosition;
public Vector3 finalPosition; public Vector3 finalPosition;
public Vector3 lieEdgePosition;
public int orderIndex; public int orderIndex;
} }
private bool focusTweensCompleted = false; private bool focusTweensCompleted = false;
private bool focusMoveTweensCompleted;
private float resolveTotalDurationOverride = -1f;
private float lieEdgeCompression;
private float lieEdgeJitter;
private LogReleasePresentationController presentationController;
private const string LieCompressionTweenId = "HuoshanLieEdgeCompression";
// 边界 // 边界
private Bounds movementBounds; private Bounds movementBounds;
@@ -350,6 +360,9 @@ namespace AibisDream.MiniGame.Language
if (!isInitialized) if (!isInitialized)
yield break; yield break;
StopFocusTransitionRoutine();
KillCompletionUiTweens();
resolveTotalDurationOverride = -1f;
float d = duration > 0f ? duration : restartExpressionFadeDuration; float d = duration > 0f ? duration : restartExpressionFadeDuration;
// 火山释放 log 与文字同时叠在屏幕上时,先把 log 藏起来,等文字淡出结束后再由 FadeInReleaseLogIfNeeded 出现 // 火山释放 log 与文字同时叠在屏幕上时,先把 log 藏起来,等文字淡出结束后再由 FadeInReleaseLogIfNeeded 出现
@@ -440,6 +453,7 @@ namespace AibisDream.MiniGame.Language
/// </summary> /// </summary>
public void SetInitialStatesBeforeOpen() public void SetInitialStatesBeforeOpen()
{ {
RestoreCompletionUiAlpha();
if (languageDeepPanel1 != null) if (languageDeepPanel1 != null)
languageDeepPanel1.SetActive(false); languageDeepPanel1.SetActive(false);
if (languageDeepPanel2 != null) if (languageDeepPanel2 != null)
@@ -471,6 +485,10 @@ namespace AibisDream.MiniGame.Language
// 如果未初始化,直接返回 // 如果未初始化,直接返回
if (!isInitialized) if (!isInitialized)
return; return;
StopFocusTransitionRoutine();
KillCompletionUiTweens();
RestoreTargetPresentationDefaults();
if (entranceRoutine != null) if (entranceRoutine != null)
{ {
@@ -502,6 +520,7 @@ namespace AibisDream.MiniGame.Language
{ {
if (particle != null) if (particle != null)
{ {
DOTween.Kill(particle);
particle.transform.DOKill(); particle.transform.DOKill();
} }
} }
@@ -509,6 +528,7 @@ namespace AibisDream.MiniGame.Language
{ {
if (particle != null) if (particle != null)
{ {
DOTween.Kill(particle);
particle.transform.DOKill(); particle.transform.DOKill();
} }
} }
@@ -518,6 +538,7 @@ namespace AibisDream.MiniGame.Language
completionPhase = CompletionPhase.None; completionPhase = CompletionPhase.None;
allowInput = false; allowInput = false;
ClearCompletionClampSmoothVelocity(); ClearCompletionClampSmoothVelocity();
ReleaseScreenClipMaterials();
} }
private void InitializeCanvas() private void InitializeCanvas()
@@ -589,6 +610,7 @@ namespace AibisDream.MiniGame.Language
particle.SetColor(floatingTextColor); particle.SetColor(floatingTextColor);
particle.SetFontSize(floatingTextFontSize); particle.SetFontSize(floatingTextFontSize);
SetParticleBounds(particle); SetParticleBounds(particle);
ConfigureParticleScreenMask(particle);
// 设置发光效果 // 设置发光效果
if (enableBloomGlow) if (enableBloomGlow)
@@ -635,6 +657,7 @@ namespace AibisDream.MiniGame.Language
particle.SetFontStyles(targetParticleFontSize, nonTargetParticleFontSize, targetParticleBold, nonTargetParticleBold); particle.SetFontStyles(targetParticleFontSize, nonTargetParticleFontSize, targetParticleBold, nonTargetParticleBold);
particle.SetInterferenceParams(nonTargetShakeIntensity, nonTargetShakeSpeed, nonTargetChangeInterval, nonTargetFloatAmplitude); particle.SetInterferenceParams(nonTargetShakeIntensity, nonTargetShakeSpeed, nonTargetChangeInterval, nonTargetFloatAmplitude);
SetParticleBounds(particle); SetParticleBounds(particle);
ConfigureParticleScreenMask(particle);
if (enableBloomGlow) if (enableBloomGlow)
{ {
@@ -690,6 +713,16 @@ namespace AibisDream.MiniGame.Language
particle.SetCircularBounds(boundsCenter, boundsRadius); particle.SetCircularBounds(boundsCenter, boundsRadius);
} }
private void ConfigureParticleScreenMask(TextParticle particle)
{
if (particle == null)
return;
particle.SetScreenClipRect(completionScreenClipRect);
int sortingLayerId = worldCanvas != null ? worldCanvas.sortingLayerID : 0;
particle.SetGlowMasking(sortingLayerId, glowSortingOrder);
}
private void InitializeRenderers() private void InitializeRenderers()
{ {
// 连接线渲染器 // 连接线渲染器
@@ -899,6 +932,21 @@ namespace AibisDream.MiniGame.Language
/// 完成阶段使用的裁剪范围:优先用 Inspector 指定的屏幕 Rect,否则与粒子运动边界一致。 /// 完成阶段使用的裁剪范围:优先用 Inspector 指定的屏幕 Rect,否则与粒子运动边界一致。
/// </summary> /// </summary>
private Bounds GetCompletionClipBounds() private Bounds GetCompletionClipBounds()
{
Bounds b = GetScreenWorldBounds();
Vector2 safetyPadding = CalculateScreenSafetyPadding();
if (safetyPadding.sqrMagnitude > 0f)
{
Bounds shrunk = b;
shrunk.Expand(new Vector3(-safetyPadding.x * 2f, -safetyPadding.y * 2f, 0f));
if (shrunk.size.x >= 0.01f && shrunk.size.y >= 0.01f)
b = shrunk;
}
return b;
}
public Bounds GetScreenWorldBounds()
{ {
Bounds b; Bounds b;
if (completionScreenClipRect != null) if (completionScreenClipRect != null)
@@ -924,17 +972,40 @@ namespace AibisDream.MiniGame.Language
b = movementBounds; b = movementBounds;
} }
if (completionClipEdgePadding > 0f) return b;
}
private Vector2 CalculateScreenSafetyPadding()
{
float padding = Mathf.Max(minimumScreenMargin, completionClipEdgePadding);
float glyphExtent = 0f;
float glowExtent = 0f;
foreach (CandidateParticle particle in targetParticles)
{ {
Bounds shrunk = b; if (particle == null)
shrunk.Expand(-completionClipEdgePadding); continue;
if (shrunk.size.x >= 0.01f && shrunk.size.y >= 0.01f)
if (particle.textMesh != null && particle.textMesh.renderer != null)
{ {
b = shrunk; Bounds textBounds = particle.textMesh.renderer.bounds;
glyphExtent = Mathf.Max(glyphExtent, textBounds.extents.x, textBounds.extents.y);
}
if (particle.glowSprite != null)
{
Bounds glowBounds = particle.glowSprite.bounds;
glowExtent = Mathf.Max(glowExtent, glowBounds.extents.x, glowBounds.extents.y);
} }
} }
return b; float zoomedGlyph = glyphExtent * Mathf.Max(1f, focusZoomScale);
float focusRingRadius = Mathf.Clamp(targetParticleFontSize * 0.05f, 0.35f, 0.75f) +
Mathf.Max(focusRingThickness, 0f);
float shakeExtent = Mathf.Max(shuffleDistance, nonTargetShakeIntensity, completionPositionSmoothTime * 0.1f);
float horizontal = Mathf.Max(padding, zoomedGlyph, glowExtent, focusRingRadius, shakeExtent);
return new Vector2(horizontal, horizontal);
} }
private static Vector3 ClampPositionToBounds(Vector3 worldPos, Bounds b) private static Vector3 ClampPositionToBounds(Vector3 worldPos, Bounds b)
@@ -951,6 +1022,11 @@ namespace AibisDream.MiniGame.Language
// 更新连接线渲染器 // 更新连接线渲染器
if (connectionRenderer != null) if (connectionRenderer != null)
{ {
Bounds screenBounds = GetScreenWorldBounds();
connectionRenderer.SetScreenBounds(
screenBounds,
completionScreenClipRect != null || (screenBounds.size.x > 0.001f && screenBounds.size.y > 0.001f));
connectionRenderer.SetScreenClippingEnabled(ShouldClipConnections(completionPhase));
connectionRenderer.SetData( connectionRenderer.SetData(
candidateParticles, candidateParticles,
s_emptyPropagations, s_emptyPropagations,
@@ -1225,6 +1301,7 @@ namespace AibisDream.MiniGame.Language
private void BeginFocusSequence() private void BeginFocusSequence()
{ {
StopFocusTransitionRoutine();
if (focusSequenceButton != null) if (focusSequenceButton != null)
{ {
focusSequenceButton.interactable = false; focusSequenceButton.interactable = false;
@@ -1235,17 +1312,21 @@ namespace AibisDream.MiniGame.Language
completionPhase = CompletionPhase.Focusing; completionPhase = CompletionPhase.Focusing;
focusTimer = 0f; focusTimer = 0f;
focusTweensCompleted = false; focusTweensCompleted = false;
focusMoveTweensCompleted = false;
lieEdgeCompression = 0f;
lieEdgeJitter = 0f;
interferenceLogicSuspended = true; interferenceLogicSuspended = true;
SetStatusUIVisibility(false, statusFadeOutDuration); SetStatusUIVisibility(false, statusFadeOutDuration);
// 两个 panel、confirm button、火山释放 log 界面在 focus 触发时淡出 // 两个 panel、confirm button、火山释放 log 界面在 focus 触发时淡出
FadeOutUIElement(languageDeepPanel1, focusFadeDuration); float operationFadeDuration = Mathf.Max(0.01f, logOperationFadeDuration);
FadeOutUIElement(languageDeepPanel2, focusFadeDuration); FadeOutUIElement(languageDeepPanel1, operationFadeDuration);
FadeOutUIElement(languageDeepPanel2, operationFadeDuration);
if (focusSequenceButton != null) if (focusSequenceButton != null)
{ {
FadeOutUIElement(focusSequenceButton.gameObject, focusFadeDuration); FadeOutUIElement(focusSequenceButton.gameObject, operationFadeDuration);
} }
FadeOutSpriteRenderer(releaseLogSpriteRenderer, focusFadeDuration); FadeOutSpriteRenderer(releaseLogSpriteRenderer, operationFadeDuration);
if (connectionRenderer != null) if (connectionRenderer != null)
{ {
@@ -1257,7 +1338,7 @@ namespace AibisDream.MiniGame.Language
{ {
particle.isCalmed = true; particle.isCalmed = true;
particle.velocity = Vector2.zero; particle.velocity = Vector2.zero;
FadeOutTextParticle(particle, focusFadeDuration); FadeOutTextParticle(particle, operationFadeDuration);
} }
foreach (var particle in candidateParticles) foreach (var particle in candidateParticles)
@@ -1266,7 +1347,7 @@ namespace AibisDream.MiniGame.Language
{ {
particle.isCalmed = true; particle.isCalmed = true;
particle.velocity = Vector2.zero; particle.velocity = Vector2.zero;
FadeOutTextParticle(particle, focusFadeDuration); FadeOutTextParticle(particle, operationFadeDuration);
} }
else else
{ {
@@ -1306,9 +1387,7 @@ namespace AibisDream.MiniGame.Language
{ {
completedTweens++; completedTweens++;
if (completedTweens >= tweenTargetCount) if (completedTweens >= tweenTargetCount)
{ focusMoveTweensCompleted = true;
EnterFocusHolding();
}
}); });
particle.transform.DOScale(Vector3.one * focusZoomScale, focusMoveDuration).SetEase(Ease.InOutQuad); particle.transform.DOScale(Vector3.one * focusZoomScale, focusMoveDuration).SetEase(Ease.InOutQuad);
@@ -1321,9 +1400,37 @@ namespace AibisDream.MiniGame.Language
} }
if (tweenTargetCount == 0) if (tweenTargetCount == 0)
focusMoveTweensCompleted = true;
focusTransitionRoutine = StartCoroutine(WaitForFocusPresentationEntry());
}
private IEnumerator WaitForFocusPresentationEntry()
{
while (releaseLogSpriteRenderer != null &&
releaseLogSpriteRenderer.gameObject.activeSelf &&
releaseLogSpriteRenderer.enabled &&
releaseLogSpriteRenderer.color.a > 0.001f)
{ {
EnterFocusHolding(); yield return null;
} }
while (!focusMoveTweensCompleted && completionPhase == CompletionPhase.Focusing)
yield return null;
if (completionPhase == CompletionPhase.Focusing && !focusTweensCompleted)
EnterFocusHolding();
focusTransitionRoutine = null;
}
private void StopFocusTransitionRoutine()
{
if (focusTransitionRoutine == null)
return;
StopCoroutine(focusTransitionRoutine);
focusTransitionRoutine = null;
} }
private TargetFocusVisual CreateFocusVisual(CandidateParticle particle, string finalChar) private TargetFocusVisual CreateFocusVisual(CandidateParticle particle, string finalChar)
@@ -1355,24 +1462,6 @@ namespace AibisDream.MiniGame.Language
disc.Color = ringColor; disc.Color = ringColor;
visual.ringDisc = disc; visual.ringDisc = disc;
GameObject labelObj = new GameObject("ProbabilityLabel");
labelObj.layer = renderLayer;
labelObj.transform.SetParent(particle.transform, false);
RectTransform rectTransform = labelObj.AddComponent<RectTransform>();
rectTransform.localPosition = new Vector3(0f, estimatedRadius + 0.55f, 0f);
rectTransform.localScale = Vector3.one;
rectTransform.sizeDelta = new Vector2(2f, 0.6f);
var label = labelObj.AddComponent<TextMeshPro>();
if (chineseFontAsset != null)
{
label.font = chineseFontAsset;
}
label.fontSize = 2f;
label.alignment = TextAlignmentOptions.Center;
label.text = $"{Mathf.RoundToInt(visual.probability)}%";
visual.probabilityLabel = label;
return visual; return visual;
} }
@@ -1390,14 +1479,21 @@ namespace AibisDream.MiniGame.Language
private void UpdateFocusHolding() private void UpdateFocusHolding()
{ {
focusTimer += Time.deltaTime; focusTimer += Time.deltaTime;
float compression = Mathf.SmoothStep(0f, 1f, lieEdgeCompression);
foreach (var visual in focusVisuals.Values) foreach (var visual in focusVisuals.Values)
{ {
if (visual == null || visual.particle == null) if (visual == null || visual.particle == null)
continue; continue;
Vector3 shuffleOffset = CalculateShuffleOffset(visual, 0f, focusTimer); Vector3 compressedPosition = Vector3.Lerp(
visual.particle.transform.position = visual.focusPosition + shuffleOffset; visual.focusPosition,
visual.lieEdgePosition,
compression);
Vector3 shuffleOffset = CalculateShuffleOffset(visual, compression, focusTimer) *
Mathf.Lerp(1f, 0.18f, compression);
Vector3 edgeJitter = CalculateLieEdgeJitter(visual, compression);
visual.particle.transform.position = compressedPosition + shuffleOffset + edgeJitter;
} }
} }
@@ -1407,28 +1503,31 @@ namespace AibisDream.MiniGame.Language
return; return;
focusTimer += Time.deltaTime; focusTimer += Time.deltaTime;
float baseInterval = Mathf.Max(0.2f, probabilityDuration); CalculateResolveTiming(
focusVisuals.Count,
resolveTotalDurationOverride,
Mathf.Max(0.2f, probabilityDuration),
Mathf.Max(0f, predictionSequenceGap),
out float baseInterval,
out float activeStagger);
float arrangementDuration = Mathf.Max(0.2f, probabilityDuration * 0.6f); float arrangementDuration = Mathf.Max(0.2f, probabilityDuration * 0.6f);
if (resolveTotalDurationOverride > 0f)
arrangementDuration = Mathf.Max(0.05f, baseInterval * 0.65f);
foreach (var visual in focusVisuals.Values) foreach (var visual in focusVisuals.Values)
{ {
if (visual == null || visual.particle == null) if (visual == null || visual.particle == null)
continue; continue;
float localTime = focusTimer - visual.orderIndex * predictionSequenceGap; float localTime = focusTimer - visual.orderIndex * activeStagger;
float probabilityProgress = Mathf.Clamp01(localTime / baseInterval); float probabilityProgress = Mathf.Clamp01(localTime / baseInterval);
float arrangementProgress = Mathf.Clamp01(localTime / arrangementDuration); float arrangementProgress = Mathf.Clamp01(localTime / arrangementDuration);
float probability = Mathf.Lerp(visual.probability, visual.targetProbability, probabilityProgress);
if (visual.probabilityLabel != null)
{
visual.probabilityLabel.text = $"{Mathf.RoundToInt(probability)}%";
}
float interval = Mathf.Lerp(0.05f, 0.8f, probabilityProgress); float interval = Mathf.Lerp(0.05f, 0.8f, probabilityProgress);
visual.particle.SetChangeInterval(interval); visual.particle.SetChangeInterval(interval);
visual.particle.changeSpeedMultiplier = Mathf.Lerp(6f, 1f, probabilityProgress); visual.particle.changeSpeedMultiplier = Mathf.Lerp(6f, 1f, probabilityProgress);
visual.particle.anxietyShakeIntensity = Mathf.Lerp(visual.startShakeIntensity, 0f, probabilityProgress); visual.particle.anxietyShakeIntensity = Mathf.Lerp(visual.startShakeIntensity, 0f, probabilityProgress);
visual.particle.calmProgress = probabilityProgress;
Vector3 dynamicTarget = GetDynamicArrangementTarget(visual, arrangementProgress, localTime); Vector3 dynamicTarget = GetDynamicArrangementTarget(visual, arrangementProgress, localTime);
Vector3 targetPosition = Vector3.Lerp(visual.focusPosition, dynamicTarget, Mathf.SmoothStep(0f, 1f, arrangementProgress)); Vector3 targetPosition = Vector3.Lerp(visual.focusPosition, dynamicTarget, Mathf.SmoothStep(0f, 1f, arrangementProgress));
@@ -1442,10 +1541,6 @@ namespace AibisDream.MiniGame.Language
visual.particle.ResetChangeTimer(visual.particle.changeSpeedMultiplier); visual.particle.ResetChangeTimer(visual.particle.changeSpeedMultiplier);
visual.particle.isStatic = true; visual.particle.isStatic = true;
visual.stabilized = true; visual.stabilized = true;
if (visual.probabilityLabel != null)
{
visual.probabilityLabel.text = "100%";
}
} }
} }
@@ -1457,18 +1552,45 @@ namespace AibisDream.MiniGame.Language
} }
/// <summary> /// <summary>
/// 从 FocusHolding 进入稳定化阶段(概率递增、文字锁定、排列到最终位置) /// 从 FocusHolding 进入稳定化阶段(文字锁定、排列到最终位置)
/// </summary> /// </summary>
public void ResolveFocusSequence() public void ResolveFocusSequence(float totalDuration = -1f)
{ {
if (completionPhase != CompletionPhase.FocusHolding) if (completionPhase != CompletionPhase.FocusHolding)
return; return;
resolveTotalDurationOverride = totalDuration > 0f ? Mathf.Max(0.05f, totalDuration) : -1f;
completionPhase = CompletionPhase.Focusing; completionPhase = CompletionPhase.Focusing;
focusTweensCompleted = true; focusTweensCompleted = true;
focusTimer = 0f; focusTimer = 0f;
} }
public static void CalculateResolveTiming(
int characterCount,
float totalDuration,
float defaultCharacterDuration,
float defaultStagger,
out float characterDuration,
out float stagger)
{
int count = Mathf.Max(1, characterCount);
if (totalDuration <= 0f)
{
characterDuration = Mathf.Max(0.05f, defaultCharacterDuration);
stagger = Mathf.Max(0f, defaultStagger);
return;
}
float total = Mathf.Max(0.05f, totalDuration);
float staggerBudget = count > 1 ? total * 0.35f : 0f;
stagger = count > 1 ? staggerBudget / (count - 1) : 0f;
characterDuration = Mathf.Max(0.05f, total - staggerBudget);
float computedEnd = characterDuration + stagger * (count - 1);
if (computedEnd > total)
characterDuration = Mathf.Max(0.01f, characterDuration - (computedEnd - total));
}
/// <summary> /// <summary>
/// 等待聚焦流程全部完成(稳定化 → Finished) /// 等待聚焦流程全部完成(稳定化 → Finished)
/// </summary> /// </summary>
@@ -1480,6 +1602,366 @@ namespace AibisDream.MiniGame.Language
} }
} }
public static bool ShouldClipConnections(CompletionPhase phase)
{
return phase == CompletionPhase.Focusing ||
phase == CompletionPhase.FocusHolding ||
phase == CompletionPhase.Finished;
}
public void SetPresentationController(LogReleasePresentationController controller)
{
presentationController = controller;
}
public void BeginLieEdgeCompression(float duration, float jitterAmount)
{
if (completionPhase != CompletionPhase.FocusHolding)
{
Debug.LogWarning(
$"[LanguageParticleManager] 谎话边缘挤压在 {completionPhase} 状态被调用;已安全跳过。");
return;
}
Bounds edgeBounds = GetCompletionClipBounds();
int visualCount = Mathf.Max(1, focusVisuals.Count);
int index = 0;
foreach (TargetFocusVisual visual in focusVisuals.Values.OrderBy(value => value.orderIndex))
{
if (visual?.particle == null)
continue;
visual.lieEdgePosition = CalculateScreenEdgePosition(
edgeBounds,
index,
visualCount,
0.04f);
visual.particle.isStatic = false;
visual.particle.anxietyShakeIntensity = Mathf.Max(visual.particle.anxietyShakeIntensity, 5.5f);
visual.particle.SetChangeInterval(0.045f);
index++;
}
DOTween.Kill(LieCompressionTweenId);
DOTween.To(
() => lieEdgeCompression,
value => lieEdgeCompression = value,
1f,
Mathf.Max(0.05f, duration))
.SetEase(Ease.InCubic)
.SetId(LieCompressionTweenId)
.SetTarget(this);
DOTween.To(
() => lieEdgeJitter,
value => lieEdgeJitter = value,
Mathf.Max(0f, jitterAmount),
Mathf.Max(0.04f, duration * 0.7f))
.SetEase(Ease.OutQuad)
.SetId(LieCompressionTweenId)
.SetTarget(this);
}
/// <summary>
/// 老虎机换字演出:聚焦字在 duration 内只从 charPool 里疯狂换字,到点后自动恢复默认字符池并继续抖动。
/// </summary>
public void BeginSlotMachineShuffle(string charPool, float duration, float interval = 0.05f)
{
if (completionPhase != CompletionPhase.FocusHolding)
{
Debug.LogWarning(
$"[LanguageParticleManager] 老虎机换字在 {completionPhase} 状态被调用;已安全跳过。");
return;
}
StopSlotMachineShuffle();
string pool = SanitizeSlotMachinePool(charPool);
if (string.IsNullOrEmpty(pool))
{
Debug.LogWarning("[LanguageParticleManager] 老虎机换字字符池为空;已安全跳过。");
return;
}
float changeInterval = Mathf.Max(0.02f, interval);
foreach (TargetFocusVisual visual in focusVisuals.Values)
{
if (visual?.particle == null)
continue;
visual.particle.SetOverrideCharPool(pool);
visual.particle.SetChangeInterval(changeInterval);
visual.particle.ResetChangeTimer(changeInterval);
visual.particle.isStatic = false;
visual.particle.anxietyShakeIntensity = Mathf.Max(visual.particle.anxietyShakeIntensity, 5f);
}
slotMachineRoutine = StartCoroutine(EndSlotMachineShuffleAfter(Mathf.Max(0.05f, duration)));
}
/// <summary>
/// 清洗老虎机字符池:去掉空格与 | 分隔符,空结果返回 null。
/// </summary>
public static string SanitizeSlotMachinePool(string charPool)
{
if (string.IsNullOrEmpty(charPool))
return null;
string cleaned = charPool.Replace(" ", string.Empty).Replace("|", string.Empty);
return cleaned.Length == 0 ? null : cleaned;
}
public void StopSlotMachineShuffle()
{
if (slotMachineRoutine != null)
{
StopCoroutine(slotMachineRoutine);
slotMachineRoutine = null;
}
foreach (CandidateParticle particle in targetParticles)
particle?.SetOverrideCharPool(null);
}
private IEnumerator EndSlotMachineShuffleAfter(float duration)
{
yield return new WaitForSeconds(duration);
slotMachineRoutine = null;
foreach (CandidateParticle particle in targetParticles)
particle?.SetOverrideCharPool(null);
}
public void PrepareTruthReleaseFromLie()
{
DOTween.Kill(LieCompressionTweenId);
foreach (TargetFocusVisual visual in focusVisuals.Values)
{
if (visual?.particle == null)
continue;
// The resolve sequence now starts from the squeezed perimeter position,
// making every real character visibly rush back through the broken lie.
visual.focusPosition = visual.particle.transform.position;
visual.lieEdgePosition = visual.focusPosition;
visual.startShakeIntensity = Mathf.Max(visual.startShakeIntensity, 6.5f);
visual.particle.anxietyShakeIntensity = visual.startShakeIntensity;
}
lieEdgeCompression = 0f;
lieEdgeJitter = 0f;
}
public Bounds GetFinalTruthWorldBounds()
{
if (finalTargetPositions.Count == 0)
return GetTargetWorldBounds();
Bounds result = new Bounds(finalTargetPositions[0], Vector3.zero);
for (int i = 1; i < finalTargetPositions.Count; i++)
result.Encapsulate(finalTargetPositions[i]);
float glyphPadding = Mathf.Max(0.3f, targetParticleFontSize * 0.09f * focusZoomScale);
result.Expand(new Vector3(glyphPadding * 2f, glyphPadding * 2f, 0f));
return result;
}
public static Vector3 CalculateScreenEdgePosition(
Bounds bounds,
int index,
int count,
float inset)
{
int safeCount = Mathf.Max(1, count);
float angle = -Mathf.PI * 0.5f +
Mathf.PI * 2f * ((index + 0.5f) / safeCount);
Vector2 direction = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle));
float availableX = Mathf.Max(0f, bounds.extents.x - Mathf.Max(0f, inset));
float availableY = Mathf.Max(0f, bounds.extents.y - Mathf.Max(0f, inset));
float distanceX = Mathf.Abs(direction.x) > 0.0001f
? availableX / Mathf.Abs(direction.x)
: float.PositiveInfinity;
float distanceY = Mathf.Abs(direction.y) > 0.0001f
? availableY / Mathf.Abs(direction.y)
: float.PositiveInfinity;
float distance = Mathf.Min(distanceX, distanceY);
return bounds.center + (Vector3)(direction * distance);
}
public void SetTargetParticlesAlpha(float targetAlpha, float duration = 0f)
{
float alphaValue = Mathf.Clamp01(targetAlpha);
foreach (CandidateParticle particle in targetParticles)
{
if (particle == null) continue;
DOTween.Kill(particle);
if (duration <= 0f)
{
particle.alpha = alphaValue;
}
else
{
DOTween.To(() => particle.alpha, value => particle.alpha = value, alphaValue, duration)
.SetEase(Ease.OutQuad)
.SetTarget(particle);
}
}
}
public void SetTruthWarmVisuals(Color warmColor)
{
foreach (CandidateParticle particle in targetParticles)
{
if (particle == null) continue;
particle.SetColors(targetParticleColor, nonTargetParticleColor, nonTargetParticleColor, warmColor);
}
}
public IEnumerator FlashTruthFragment(string fragment, float duration = 0.22f)
{
if (string.IsNullOrEmpty(fragment) || orderedRedParticles.Count == 0)
yield break;
int startIndex = targetSentence.IndexOf(fragment, System.StringComparison.Ordinal);
if (startIndex < 0)
{
Debug.LogWarning($"[LanguageParticleManager] 真话泄漏片段“{fragment}”不在目标句“{targetSentence}”中。");
yield break;
}
var leaked = new List<CandidateParticle>();
int endExclusive = Mathf.Min(startIndex + fragment.Length, orderedRedParticles.Count);
for (int i = startIndex; i < endExclusive; i++)
{
CandidateParticle particle = orderedRedParticles[i];
if (particle == null) continue;
particle.ForceSetCharacter(targetSentence[i].ToString());
particle.isStatic = true;
particle.alpha = 1f;
leaked.Add(particle);
}
yield return new WaitForSeconds(Mathf.Max(0.02f, duration));
if (completionPhase == CompletionPhase.FocusHolding)
{
foreach (CandidateParticle particle in leaked)
{
if (particle == null) continue;
particle.alpha = 0.2f;
particle.isStatic = false;
particle.ResetChangeTimer(0.02f);
}
}
}
public Bounds GetTargetWorldBounds()
{
bool hasBounds = false;
Bounds result = new Bounds(
worldCanvas != null ? worldCanvas.transform.position : transform.position,
Vector3.zero);
foreach (CandidateParticle particle in targetParticles)
{
if (particle == null || particle.textMesh == null || particle.textMesh.renderer == null)
continue;
Bounds textBounds = particle.textMesh.renderer.bounds;
if (!hasBounds)
{
result = textBounds;
hasBounds = true;
}
else
{
result.Encapsulate(textBounds);
}
}
return result;
}
public IEnumerator ResolveFocusAndWait(float totalDuration)
{
if (completionPhase != CompletionPhase.FocusHolding)
{
Debug.LogWarning(
$"[LanguageParticleManager] ResolveFocusAndWait 在 {completionPhase} 状态被调用;已安全跳过。");
yield break;
}
ResolveFocusSequence(totalDuration);
yield return WaitUntilFocusFinished();
}
public void SnapTruthImmediate()
{
if (completionPhase != CompletionPhase.FocusHolding)
{
Debug.LogWarning(
$"[LanguageParticleManager] 真话瞬间稳定在 {completionPhase} 状态被调用;已安全跳过。");
return;
}
DOTween.Kill(LieCompressionTweenId);
StopSlotMachineShuffle();
completionPhase = CompletionPhase.Focusing;
focusTweensCompleted = true;
focusTimer = 0f;
foreach (TargetFocusVisual visual in focusVisuals.Values)
{
if (visual?.particle == null)
continue;
CandidateParticle particle = visual.particle;
particle.transform.DOKill();
particle.transform.position = visual.finalPosition;
particle.ForceSetCharacter(visual.finalChar);
particle.SetChangeInterval(1.5f);
particle.ResetChangeTimer(1.5f);
particle.anxietyShakeIntensity = 0f;
particle.changeSpeedMultiplier = 1f;
particle.calmProgress = 1f;
particle.isStatic = true;
visual.stabilized = true;
}
resolveTotalDurationOverride = -1f;
lieEdgeCompression = 0f;
lieEdgeJitter = 0f;
CompleteFocusSequence();
}
public void RestoreTargetPresentationDefaults(float alphaValue = 1f)
{
DOTween.Kill(LieCompressionTweenId);
StopSlotMachineShuffle();
foreach (CandidateParticle particle in targetParticles)
{
if (particle == null) continue;
DOTween.Kill(particle);
particle.alpha = Mathf.Clamp01(alphaValue);
particle.calmProgress = 0f;
particle.SetColors(targetParticleColor, nonTargetParticleColor, nonTargetParticleColor, targetCalmColor);
}
resolveTotalDurationOverride = -1f;
lieEdgeCompression = 0f;
lieEdgeJitter = 0f;
}
public void ReleaseScreenClipMaterials()
{
foreach (CandidateParticle particle in candidateParticles)
particle?.ClearScreenClip();
foreach (FloatingTextParticle particle in floatingParticles)
particle?.ClearScreenClip();
}
private void RebuildScreenClipMaterials()
{
foreach (CandidateParticle particle in candidateParticles)
ConfigureParticleScreenMask(particle);
foreach (FloatingTextParticle particle in floatingParticles)
ConfigureParticleScreenMask(particle);
}
private void DestroyFocusVisualDecorationsAndClear() private void DestroyFocusVisualDecorationsAndClear()
{ {
foreach (var visual in focusVisuals.Values) foreach (var visual in focusVisuals.Values)
@@ -1488,8 +1970,6 @@ namespace AibisDream.MiniGame.Language
continue; continue;
if (visual.ringObject != null) if (visual.ringObject != null)
Destroy(visual.ringObject); Destroy(visual.ringObject);
if (visual.probabilityLabel != null)
Destroy(visual.probabilityLabel.gameObject);
} }
focusVisuals.Clear(); focusVisuals.Clear();
} }
@@ -1515,6 +1995,8 @@ namespace AibisDream.MiniGame.Language
connectionRenderer.TargetAlphaScale = 0f; connectionRenderer.TargetAlphaScale = 0f;
} }
lieEdgeCompression = 0f;
lieEdgeJitter = 0f;
completionPhase = CompletionPhase.Finished; completionPhase = CompletionPhase.Finished;
} }
@@ -1558,16 +2040,30 @@ namespace AibisDream.MiniGame.Language
return new Vector3(x, y, 0f); return new Vector3(x, y, 0f);
} }
private Vector3 CalculateLieEdgeJitter(TargetFocusVisual visual, float compression)
{
if (visual == null || compression <= 0f || lieEdgeJitter <= 0f)
return Vector3.zero;
float seed = visual.orderIndex * 1.731f;
float x = Mathf.Sin(focusTimer * 29f + seed) +
Mathf.Sin(focusTimer * 53f + seed * 0.61f) * 0.35f;
float y = Mathf.Cos(focusTimer * 37f + seed * 1.17f) +
Mathf.Sin(focusTimer * 61f + seed) * 0.28f;
return new Vector3(x, y, 0f) * lieEdgeJitter * compression;
}
private void FadeOutTextParticle(TextParticle particle, float duration) private void FadeOutTextParticle(TextParticle particle, float duration)
{ {
if (particle == null) return; if (particle == null) return;
DOTween.To(() => particle.alpha, value => particle.alpha = value, 0f, duration) DOTween.To(() => particle.alpha, value => particle.alpha = value, 0f, duration)
.SetEase(Ease.OutQuad); .SetEase(Ease.OutQuad)
.SetTarget(particle);
if (particle.glowSprite != null) if (particle.glowSprite != null)
{ {
particle.glowSprite.DOFade(0f, duration); particle.glowSprite.DOFade(0f, duration).SetTarget(particle);
} }
} }
@@ -1581,6 +2077,7 @@ namespace AibisDream.MiniGame.Language
var cg = obj.GetComponent<CanvasGroup>(); var cg = obj.GetComponent<CanvasGroup>();
if (cg == null) if (cg == null)
cg = obj.AddComponent<CanvasGroup>(); cg = obj.AddComponent<CanvasGroup>();
cg.DOKill();
cg.alpha = 1f; cg.alpha = 1f;
cg.DOFade(0f, duration) cg.DOFade(0f, duration)
.SetEase(Ease.OutQuad) .SetEase(Ease.OutQuad)
@@ -1651,6 +2148,40 @@ namespace AibisDream.MiniGame.Language
sr.color = c; sr.color = c;
} }
private void KillCompletionUiTweens()
{
KillCanvasGroupTween(languageDeepPanel1);
KillCanvasGroupTween(languageDeepPanel2);
if (focusSequenceButton != null)
KillCanvasGroupTween(focusSequenceButton.gameObject);
if (releaseLogSpriteRenderer != null)
DOTween.Kill(releaseLogSpriteRenderer);
}
private static void KillCanvasGroupTween(GameObject obj)
{
if (obj == null) return;
CanvasGroup group = obj.GetComponent<CanvasGroup>();
if (group != null)
group.DOKill();
}
private void RestoreCompletionUiAlpha()
{
RestoreCanvasGroupAlpha(languageDeepPanel1);
RestoreCanvasGroupAlpha(languageDeepPanel2);
if (focusSequenceButton != null)
RestoreCanvasGroupAlpha(focusSequenceButton.gameObject);
}
private static void RestoreCanvasGroupAlpha(GameObject obj)
{
if (obj == null) return;
CanvasGroup group = obj.GetComponent<CanvasGroup>();
if (group != null)
group.alpha = 1f;
}
private Vector3 ComputeCentroid(List<CandidateParticle> particles) private Vector3 ComputeCentroid(List<CandidateParticle> particles)
{ {
if (particles == null || particles.Count == 0) if (particles == null || particles.Count == 0)
@@ -1722,6 +2253,10 @@ namespace AibisDream.MiniGame.Language
public void ResetGame(bool playEntranceEffect = true) public void ResetGame(bool playEntranceEffect = true)
{ {
StopFocusTransitionRoutine();
KillCompletionUiTweens();
RestoreTargetPresentationDefaults();
RestoreCompletionUiAlpha();
isCompleted = false; isCompleted = false;
completionPhase = CompletionPhase.None; completionPhase = CompletionPhase.None;
completionTimer = 0f; completionTimer = 0f;
@@ -1792,8 +2327,10 @@ namespace AibisDream.MiniGame.Language
p.velocity = new Vector2(Random.Range(-0.5f, 0.5f), Random.Range(-0.5f, 0.5f)); p.velocity = new Vector2(Random.Range(-0.5f, 0.5f), Random.Range(-0.5f, 0.5f));
p.transform.position = GetRandomPositionInBounds(); p.transform.position = GetRandomPositionInBounds();
p.isCalmed = false; p.isCalmed = false;
p.calmProgress = 0f;
p.alpha = p.baseAlpha; p.alpha = p.baseAlpha;
p.transform.localScale = Vector3.one; p.transform.localScale = Vector3.one;
DOTween.Kill(p);
p.transform.DOKill(); p.transform.DOKill();
p.SetInterferenceParams(nonTargetShakeIntensity, nonTargetShakeSpeed, nonTargetChangeInterval, nonTargetFloatAmplitude); p.SetInterferenceParams(nonTargetShakeIntensity, nonTargetShakeSpeed, nonTargetChangeInterval, nonTargetFloatAmplitude);
@@ -1809,9 +2346,13 @@ namespace AibisDream.MiniGame.Language
{ {
floating.isCalmed = false; floating.isCalmed = false;
floating.alpha = floating.baseAlpha; floating.alpha = floating.baseAlpha;
DOTween.Kill(floating);
floating.transform.DOKill(); floating.transform.DOKill();
} }
ReleaseScreenClipMaterials();
RebuildScreenClipMaterials();
SelectRedParticles(); SelectRedParticles();
UpdateStatusUI(); UpdateStatusUI();
if (playEntranceEffect) if (playEntranceEffect)
@@ -13,6 +13,19 @@ namespace AibisDream.MiniGame.Language
{ {
private static AibisDream.ExpressionManager ExpressionManager => FixSystemCenter.SystemDic.Get<AibisDream.ExpressionManager>(); private static AibisDream.ExpressionManager ExpressionManager => FixSystemCenter.SystemDic.Get<AibisDream.ExpressionManager>();
private static bool TryGetPresentation(out LogReleasePresentationController presentation)
{
presentation = ExpressionManager != null
? ExpressionManager.LogReleasePresentation
: null;
if (presentation != null)
return true;
UnityEngine.Debug.LogError(
"[LanguageYarnCommand] LogReleasePresentationController 未找到;演出命令安全结束。");
return false;
}
/// <summary> /// <summary>
/// 启动表达粒子系统(在打开视图后调用),等待火山表达panel播完再返回。 /// 启动表达粒子系统(在打开视图后调用),等待火山表达panel播完再返回。
/// 流程:若有上一轮则先淡出字与连线 → 再淡入火山释放 log → 再启动新一轮并等表达 panel 就绪。 /// 流程:若有上一轮则先淡出字与连线 → 再淡入火山释放 log → 再启动新一轮并等表达 panel 就绪。
@@ -57,7 +70,8 @@ namespace AibisDream.MiniGame.Language
} }
} }
// 先淡出上一轮文字/连线等,再淡入火山释放 log,最后开新一轮粒子(避免 log 与旧字叠在一起) // 先清理上一轮演出,再淡出旧文字/连线并恢复 Log 操作界面。
yield return ExpressionManager.PreparePresentationForNextRound();
yield return ExpressionManager.FadeOutParticlesBeforeNewRound(0f); yield return ExpressionManager.FadeOutParticlesBeforeNewRound(0f);
yield return ExpressionManager.EnsureReleaseLogFadedIn(1f); yield return ExpressionManager.EnsureReleaseLogFadedIn(1f);
ExpressionManager.StartSystem(phrases, targetSentence, completionNode, nonTargetParticleTotal); ExpressionManager.StartSystem(phrases, targetSentence, completionNode, nonTargetParticleTotal);
@@ -165,6 +179,400 @@ namespace AibisDream.MiniGame.Language
yield return ExpressionManager.StartCoroutine(ExpressionManager.TransitionGlitchAndWait(to, duration)); yield return ExpressionManager.StartCoroutine(ExpressionManager.TransitionGlitchAndWait(to, duration));
} }
[YarnCommand("expression_memory_begin")]
public static IEnumerator ExpressionMemoryBegin(
string memoryKey,
string preset,
float fadeDuration = -1f,
float pushSpeed = -1f,
float horizontalSpeed = -1f,
bool preserveGlitch = false)
{
if (ExpressionManager == null)
{
UnityEngine.Debug.LogError("ExpressionManager 未找到!");
yield break;
}
yield return ExpressionManager.StartCoroutine(
ExpressionManager.BeginExpressionMemory(
memoryKey,
preset,
fadeDuration,
pushSpeed,
horizontalSpeed,
preserveGlitch));
}
/// <summary>
/// 显式淡入火山脸,并等待淡入完成。
/// <<expression_face_fade_in 0.45>>
/// </summary>
[YarnCommand("expression_face_fade_in")]
public static IEnumerator ExpressionFaceFadeIn(float duration = 0.45f)
{
if (!TryGetPresentation(out LogReleasePresentationController presentation))
yield break;
yield return ExpressionManager.StartCoroutine(
presentation.BeginFaceEntry(duration));
}
[YarnCommand("expression_lie_begin")]
public static IEnumerator ExpressionLieBegin(string keyword, string preset)
{
if (ExpressionManager == null)
{
UnityEngine.Debug.LogError("ExpressionManager 未找到!");
yield break;
}
yield return ExpressionManager.StartCoroutine(
ExpressionManager.BeginExpressionLie(keyword, 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")]
public static void ExpressionLiePrepare(float freezeDuration, float noiseTarget)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.PrepareLieVisuals(freezeDuration, noiseTarget);
}
[YarnCommand("expression_truth_squeeze")]
public static void ExpressionTruthSqueeze(float duration, float jitter, float alpha)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.SqueezeTruthCharacters(duration, jitter, alpha);
}
[YarnCommand("expression_lie_show")]
public static void ExpressionLieShow(string keyword, float duration, float characterSpacing)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.ShowLieKeyword(keyword, duration, characterSpacing);
}
[YarnCommand("expression_scanline")]
public static void ExpressionScanline(float duration, bool stalled)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.PlayScreenScanline(duration, stalled);
}
[YarnCommand("expression_system_message")]
public static void ExpressionSystemMessage(string value, string style, float fadeDuration)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.ShowSystemMessage(value, style, fadeDuration);
}
[YarnCommand("expression_system_message_hide")]
public static void ExpressionSystemMessageHide(float fadeDuration)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.HideSystemMessage(fadeDuration);
}
[YarnCommand("expression_lie_crack_begin")]
public static void ExpressionLieCrackBegin(float lieAlpha)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.BeginLieCracking(lieAlpha);
}
[YarnCommand("expression_screen_dim")]
public static void ExpressionScreenDim(float alpha, float riseDuration, float fallDuration)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.PlayScreenDim(alpha, riseDuration, fallDuration);
}
[YarnCommand("expression_memory_jolt")]
public static void ExpressionMemoryJolt(float offset, float duration, float tear)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.PlayMemoryJolt(offset, duration, tear);
}
[YarnCommand("expression_truth_leak")]
public static IEnumerator ExpressionTruthLeak(string fragment, float duration)
{
if (!TryGetPresentation(out LogReleasePresentationController presentation))
yield break;
yield return ExpressionManager.StartCoroutine(
presentation.LeakTruthFragment(fragment, duration));
}
[YarnCommand("expression_lie_shuffle")]
public static void ExpressionLieShuffle(
string characterPool,
float duration,
float interval)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.StartLieCharacterShuffle(characterPool, duration, interval);
}
/// <summary>
/// 演员大字闪现(阻塞):抖动的粒子字瞬间隐藏,屏幕中央出现白色大字(红青色散双影+急速缩放),
/// 停顿 holdDuration 秒后恢复粒子字抖动。
/// <<expression_actor_flash "没问题" 0.6 10.5 1.15 1.60 0.03 0.22 0.5 0.2>>
/// </summary>
[YarnCommand("expression_actor_flash")]
public static IEnumerator ExpressionActorFlash(
string text,
float holdDuration = 0.6f,
float fontSize = 10.5f,
float settledScale = -1f,
float punchScale = -1f,
float chromaDistance = -1f,
float chromaAlpha = -1f,
float impact = -1f,
float glitchPeak = -1f)
{
if (!TryGetPresentation(out LogReleasePresentationController presentation))
yield break;
yield return ExpressionManager.StartCoroutine(
presentation.FlashActorLie(
text,
holdDuration,
fontSize,
settledScale,
punchScale,
chromaDistance,
chromaAlpha,
impact,
glitchPeak));
}
/// <summary>
/// 老虎机换字(非阻塞,节拍由 Yarn wait 控制):聚焦字在 duration 内只从 charPool 里疯狂换字。
/// <<expression_actor_slot "没问题冇" 5 0.05 14>>
/// </summary>
[YarnCommand("expression_actor_slot")]
public static void ExpressionActorSlot(
string charPool,
float duration,
float interval = 0.05f,
float fontSize = 14f)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.StartActorSlotMachine(charPool, duration, interval, fontSize);
}
/// <summary>
/// 将 Glitch、记忆冲击与画面推近缓慢堆到峰值,停住后保持峰值,等待后续硬切。
/// <<expression_overload_peak 0.75 0.22 1.10 1 1>>
/// </summary>
[YarnCommand("expression_overload_peak")]
public static IEnumerator ExpressionOverloadPeak(
float riseDuration = 0.75f,
float holdDuration = 0.22f,
float zoom = 1.10f,
float glitchPeak = 1f,
float impact = 1f)
{
if (!TryGetPresentation(out LogReleasePresentationController presentation))
yield break;
yield return ExpressionManager.StartCoroutine(
presentation.BuildActorOverloadPeak(
riseDuration,
holdDuration,
zoom,
glitchPeak,
impact));
}
/// <summary>
/// 单字/短词满屏重击,并等待该拍结束。
/// <<expression_actor_word_hit "没" 0.16 18 1.55>>
/// </summary>
[YarnCommand("expression_actor_word_hit")]
public static IEnumerator ExpressionActorWordHit(
string text,
float holdDuration = 0.16f,
float fontSize = 18f,
float punchScale = 1.55f)
{
if (!TryGetPresentation(out LogReleasePresentationController presentation))
yield break;
yield return ExpressionManager.StartCoroutine(
presentation.PlayActorWordHit(text, holdDuration, fontSize, punchScale));
}
/// <summary>
/// 单列词组严格裁在表情屏内,像字幕一样无缝向下滚动。
/// <<expression_actor_scroll "没问题|冇问题|帽问题" 1.8 0.85 0.45 5.2>>
/// </summary>
[YarnCommand("expression_actor_scroll")]
public static IEnumerator ExpressionActorScroll(
string phraseList,
float scrollDuration = 1.8f,
float loopDuration = 0.85f,
float holdDuration = 0.45f,
float fontSize = 5.2f)
{
if (!TryGetPresentation(out LogReleasePresentationController presentation))
yield break;
yield return ExpressionManager.StartCoroutine(
presentation.ScrollActorLies(
phraseList,
scrollDuration,
loopDuration,
holdDuration,
fontSize));
}
[YarnCommand("expression_memory_cut")]
public static void ExpressionMemoryCut()
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.CutMemoryAndGlitchImmediate();
}
[YarnCommand("expression_truth_snap")]
public static void ExpressionTruthSnap(float peripheral, float calmRadius)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.SnapTruthImmediate(peripheral, calmRadius);
}
[YarnCommand("expression_glitch_pulse")]
public static void ExpressionGlitchPulse(float peak, float riseDuration, float fallDuration)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.PlayGlitchPulse(peak, riseDuration, fallDuration);
}
[YarnCommand("expression_truth_blackout")]
public static IEnumerator ExpressionTruthBlackout(float duration, float alpha)
{
if (!TryGetPresentation(out LogReleasePresentationController presentation))
yield break;
yield return ExpressionManager.StartCoroutine(
presentation.HoldTruthBlackout(duration, alpha));
}
[YarnCommand("expression_lie_split")]
public static void ExpressionLieSplit(
float duration,
float distanceRatio,
float stretchX,
float squashY)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
{
presentation.SplitLieVisual(
duration,
distanceRatio,
stretchX,
squashY);
}
}
[YarnCommand("expression_truth_flash")]
public static void ExpressionTruthFlash(
float alpha,
float riseDuration,
float holdDuration,
float fallDuration)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
{
presentation.PlayTruthFlash(
alpha,
riseDuration,
holdDuration,
fallDuration);
}
}
[YarnCommand("expression_memory_brightness_pulse")]
public static void ExpressionMemoryBrightnessPulse(
float amount,
float riseDuration,
float fallDuration)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.PlayMemoryBrightnessPulse(amount, riseDuration, fallDuration);
}
[YarnCommand("expression_memory_tear")]
public static void ExpressionMemoryTear(float peak, float duration)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.PlayMemoryTear(peak, duration);
}
[YarnCommand("expression_face_dip")]
public static void ExpressionFaceDip(float duration, float ratio)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.PlayFaceDip(duration, ratio);
}
[YarnCommand("expression_truth_release")]
public static void ExpressionTruthRelease(float alphaDuration)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.ReleaseTruthCharacters(alphaDuration);
}
[YarnCommand("expression_truth_resolve")]
public static IEnumerator ExpressionTruthResolve(
float duration,
float peripheral,
float calmRadius)
{
if (!TryGetPresentation(out LogReleasePresentationController presentation))
yield break;
yield return ExpressionManager.StartCoroutine(
presentation.ResolveTruthCharacters(duration, peripheral, calmRadius));
}
[YarnCommand("expression_memory_end")]
public static IEnumerator ExpressionMemoryEnd()
{
if (ExpressionManager == null)
{
UnityEngine.Debug.LogError("ExpressionManager 未找到!");
yield break;
}
yield return ExpressionManager.StartCoroutine(ExpressionManager.EndExpressionMemory());
}
[YarnCommand("expression_truth_impact")]
public static void ExpressionTruthImpact()
{
if (ExpressionManager == null)
{
UnityEngine.Debug.LogError("ExpressionManager 未找到!");
return;
}
ExpressionManager.ImpactExpressionTruth();
}
// /// <summary> // /// <summary>
// /// Glitch 噪波过渡(指定起止值),等待完成后再继续下一步。 // /// Glitch 噪波过渡(指定起止值),等待完成后再继续下一步。
// /// </summary> // /// </summary>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2d202e7d79d64fb5a9d7d42e974cbacd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,116 @@
using TMPro;
using UnityEngine;
namespace AibisDream.MiniGame.Language
{
/// <summary>
/// Gives one TMP object its own hard rectangular clip material. The screen rect is
/// converted from world corners into the TMP object's local coordinate system every
/// frame, so clipping remains correct while the text moves, scales or rotates.
/// </summary>
[DisallowMultipleComponent]
public sealed class TMPRectClipper : MonoBehaviour
{
private static readonly int ClipRectId = Shader.PropertyToID("_ClipRect");
private TMP_Text target;
private RectTransform screenRect;
private Material sourceMaterial;
private Material runtimeMaterial;
public void Initialize(TMP_Text text, RectTransform rect)
{
target = text;
screenRect = rect;
RebuildMaterial();
UpdateClipRect();
}
public void SetScreenRect(RectTransform rect)
{
screenRect = rect;
UpdateClipRect();
}
private void LateUpdate()
{
UpdateClipRect();
}
public void UpdateClipRect()
{
if (target == null || screenRect == null)
return;
if (runtimeMaterial == null)
RebuildMaterial();
if (runtimeMaterial == null)
return;
runtimeMaterial.SetVector(ClipRectId, CalculateLocalClipRect(target.transform, screenRect));
}
public static Vector4 CalculateLocalClipRect(Transform textTransform, RectTransform rect)
{
if (textTransform == null || rect == null)
return new Vector4(-32767f, -32767f, 32767f, 32767f);
Vector3[] corners = new Vector3[4];
rect.GetWorldCorners(corners);
float minX = float.PositiveInfinity;
float minY = float.PositiveInfinity;
float maxX = float.NegativeInfinity;
float maxY = float.NegativeInfinity;
for (int i = 0; i < corners.Length; i++)
{
Vector3 local = textTransform.InverseTransformPoint(corners[i]);
minX = Mathf.Min(minX, local.x);
minY = Mathf.Min(minY, local.y);
maxX = Mathf.Max(maxX, local.x);
maxY = Mathf.Max(maxY, local.y);
}
return new Vector4(minX, minY, maxX, maxY);
}
public void ReleaseMaterial()
{
if (target != null && sourceMaterial != null)
target.fontSharedMaterial = sourceMaterial;
if (runtimeMaterial != null)
{
if (Application.isPlaying)
Destroy(runtimeMaterial);
else
DestroyImmediate(runtimeMaterial);
}
runtimeMaterial = null;
sourceMaterial = null;
}
private void RebuildMaterial()
{
ReleaseMaterial();
if (target == null || target.fontSharedMaterial == null)
return;
sourceMaterial = target.fontSharedMaterial;
runtimeMaterial = new Material(sourceMaterial)
{
name = $"{sourceMaterial.name} (Screen Clip)",
hideFlags = HideFlags.DontSave
};
runtimeMaterial.EnableKeyword("MASK_HARD");
target.fontSharedMaterial = runtimeMaterial;
}
private void OnDestroy()
{
ReleaseMaterial();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0a71be4f42384665b776b10a4a3fe735
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -37,11 +37,14 @@ namespace AibisDream.MiniGame.Language
protected float changeTimer; protected float changeTimer;
protected float changeInterval = 1f; protected float changeInterval = 1f;
protected string overrideCharPool;
protected Bounds movementBounds; protected Bounds movementBounds;
protected bool useCircularBounds = false; protected bool useCircularBounds = false;
protected Vector3 boundsCenter; protected Vector3 boundsCenter;
protected float boundsRadius; protected float boundsRadius;
protected TMP_FontAsset customFont; protected TMP_FontAsset customFont;
private TMPRectClipper screenClipper;
private bool ownsGlowSprite;
// 字符集 // 字符集
protected static string chineseChars = "的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞"; protected static string chineseChars = "的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞";
@@ -100,6 +103,7 @@ namespace AibisDream.MiniGame.Language
glowSprite = glowObj.AddComponent<SpriteRenderer>(); glowSprite = glowObj.AddComponent<SpriteRenderer>();
glowSprite.sprite = CreateCircleSprite(); glowSprite.sprite = CreateCircleSprite();
glowSprite.color = new Color(1, 1, 1, 0.2f); glowSprite.color = new Color(1, 1, 1, 0.2f);
ownsGlowSprite = true;
} }
changeTimer = Random.Range(0.5f, 1.5f); changeTimer = Random.Range(0.5f, 1.5f);
@@ -140,6 +144,36 @@ namespace AibisDream.MiniGame.Language
useCircularBounds = false; useCircularBounds = false;
} }
/// <summary>
/// Registers the shared television-screen rect. Each particle owns a separate TMP
/// material instance, so moving one particle never changes another particle's clip.
/// </summary>
public void SetScreenClipRect(RectTransform rect)
{
if (textMesh == null)
return;
if (screenClipper == null)
screenClipper = textMesh.GetComponent<TMPRectClipper>() ?? textMesh.gameObject.AddComponent<TMPRectClipper>();
screenClipper.Initialize(textMesh, rect);
}
public void ClearScreenClip()
{
if (screenClipper != null)
screenClipper.ReleaseMaterial();
}
public void SetGlowMasking(int sortingLayerId, int sortingOrder)
{
if (glowSprite == null)
return;
glowSprite.sortingLayerID = sortingLayerId;
glowSprite.sortingOrder = sortingOrder;
glowSprite.maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
}
/// <summary> /// <summary>
/// 设置圆形运动边界(与 SetMovementBounds 二选一,圆形模式下会覆盖矩形边界) /// 设置圆形运动边界(与 SetMovementBounds 二选一,圆形模式下会覆盖矩形边界)
/// </summary> /// </summary>
@@ -222,8 +256,30 @@ namespace AibisDream.MiniGame.Language
/// </summary> /// </summary>
protected virtual string GetNextDisplayChar() protected virtual string GetNextDisplayChar()
{ {
if (TryGetOverridePoolChar(out string overrideChar))
return overrideChar;
return GetRandomChar(); return GetRandomChar();
} }
/// <summary>
/// 设置换字字符池覆盖(老虎机演出用);传空/null 清除,恢复默认字符池。
/// </summary>
public void SetOverrideCharPool(string pool)
{
overrideCharPool = string.IsNullOrEmpty(pool) ? null : pool;
}
protected bool TryGetOverridePoolChar(out string character)
{
if (string.IsNullOrEmpty(overrideCharPool))
{
character = null;
return false;
}
character = overrideCharPool[Random.Range(0, overrideCharPool.Length)].ToString();
return true;
}
/// <summary> /// <summary>
/// 设置全局干扰短句配置(笑话等,由 LanguageParticleManager 调用) /// 设置全局干扰短句配置(笑话等,由 LanguageParticleManager 调用)
@@ -411,6 +467,26 @@ namespace AibisDream.MiniGame.Language
return Sprite.Create(texture, new Rect(0, 0, resolution, resolution), new Vector2(0.5f, 0.5f)); return Sprite.Create(texture, new Rect(0, 0, resolution, resolution), new Vector2(0.5f, 0.5f));
} }
protected virtual void OnDestroy()
{
ClearScreenClip();
if (!ownsGlowSprite || glowSprite == null || glowSprite.sprite == null)
return;
Texture2D texture = glowSprite.sprite.texture;
if (Application.isPlaying)
{
Destroy(glowSprite.sprite);
if (texture != null) Destroy(texture);
}
else
{
DestroyImmediate(glowSprite.sprite);
if (texture != null) DestroyImmediate(texture);
}
}
} }
} }