diff --git a/Assets/Editor/Huoshan.meta b/Assets/Editor/Huoshan.meta new file mode 100644 index 000000000..e1c8bc930 --- /dev/null +++ b/Assets/Editor/Huoshan.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8b9ae118461543bda553c3a38e369dde +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/Huoshan/LogReleasePresentationTests.cs b/Assets/Editor/Huoshan/LogReleasePresentationTests.cs new file mode 100644 index 000000000..8c316f3d7 --- /dev/null +++ b/Assets/Editor/Huoshan/LogReleasePresentationTests.cs @@ -0,0 +1,226 @@ +using System; +using System.Collections; +using System.Reflection; +using AibisDream.MiniGame.Language; +using NUnit.Framework; +using UnityEngine; + +namespace AibisDream.EditorTests.Huoshan +{ + public sealed class LogReleasePresentationTests + { + [Test] + public void LineClip_RejectsOutsideSegment() + { + Bounds bounds = new Bounds(Vector3.zero, new Vector3(2f, 2f, 0f)); + bool visible = ConnectionRenderer.TryClipLineToBounds( + new Vector3(-3f, 2f), + new Vector3(3f, 2f), + bounds, + out _, + out _); + + Assert.That(visible, Is.False); + } + + [TestCase(-0.5f, 0f, 0.5f, 0f, -0.5f, 0f, 0.5f, 0f)] + [TestCase(-2f, 0f, 2f, 0f, -1f, 0f, 1f, 0f)] + [TestCase(0f, -2f, 0f, 0.5f, 0f, -1f, 0f, 0.5f)] + [TestCase(-2f, -2f, 2f, 2f, -1f, -1f, 1f, 1f)] + public void LineClip_ReturnsExpectedInteriorSegment( + float ax, + float ay, + float bx, + float by, + float expectedAx, + float expectedAy, + float expectedBx, + float expectedBy) + { + Bounds bounds = new Bounds(Vector3.zero, new Vector3(2f, 2f, 0f)); + bool visible = ConnectionRenderer.TryClipLineToBounds( + new Vector3(ax, ay), + new Vector3(bx, by), + bounds, + out Vector3 clippedA, + out Vector3 clippedB); + + Assert.That(visible, Is.True); + Assert.That(clippedA.x, Is.EqualTo(expectedAx).Within(0.0001f)); + Assert.That(clippedA.y, Is.EqualTo(expectedAy).Within(0.0001f)); + Assert.That(clippedB.x, Is.EqualTo(expectedBx).Within(0.0001f)); + Assert.That(clippedB.y, Is.EqualTo(expectedBy).Within(0.0001f)); + } + + [TestCase(CompletionPhase.None, false)] + [TestCase(CompletionPhase.Completed, false)] + [TestCase(CompletionPhase.Focusing, true)] + [TestCase(CompletionPhase.FocusHolding, true)] + [TestCase(CompletionPhase.Finished, true)] + public void ConnectionClipping_OnlyStartsAfterOutput( + CompletionPhase phase, + bool expected) + { + Assert.That(LanguageParticleManager.ShouldClipConnections(phase), Is.EqualTo(expected)); + } + + [Test] + public void LieEdgePosition_LandsOnScreenPerimeterAndStaysInside() + { + Bounds bounds = new Bounds(Vector3.zero, new Vector3(6f, 4f, 0f)); + const float inset = 0.1f; + for (int i = 0; i < 8; i++) + { + Vector3 position = LanguageParticleManager.CalculateScreenEdgePosition( + bounds, + i, + 8, + inset); + + Assert.That(position.x, Is.InRange(bounds.min.x, bounds.max.x)); + Assert.That(position.y, Is.InRange(bounds.min.y, bounds.max.y)); + bool touchesVerticalEdge = + Mathf.Abs(Mathf.Abs(position.x) - (bounds.extents.x - inset)) < 0.0001f; + bool touchesHorizontalEdge = + Mathf.Abs(Mathf.Abs(position.y) - (bounds.extents.y - inset)) < 0.0001f; + Assert.That(touchesVerticalEdge || touchesHorizontalEdge, Is.True); + } + } + + [TestCase(6, 0.70f)] + [TestCase(6, 0.90f)] + [TestCase(6, 1.10f)] + public void ResolveTiming_LastCharacterCompletesWithinPreset(int characterCount, float totalDuration) + { + LanguageParticleManager.CalculateResolveTiming( + characterCount, + totalDuration, + 3.5f, + 0.4f, + out float characterDuration, + out float stagger); + + float lastCompletion = characterDuration + stagger * (characterCount - 1); + Assert.That(lastCompletion, Is.LessThanOrEqualTo(totalDuration + 0.0001f)); + Assert.That(lastCompletion, Is.EqualTo(totalDuration).Within(0.0001f)); + } + + [TestCase("LightPreset", 0.10f, 0.35f, 0.25f, 0.25f, 0.70f, 0.15f)] + [TestCase("MediumPreset", 0.20f, 0.55f, 0.50f, 0.35f, 0.90f, 0.30f)] + [TestCase("HeavyPreset", 0.35f, 0.80f, 0.85f, 0.50f, 1.10f, 0.50f)] + public void PresentationPreset_UsesApprovedValues( + string presetFieldName, + float baseDistortion, + float liePause, + float breakPeak, + float breakDuration, + float truthDuration, + float truthPeripheral) + { + const BindingFlags flags = BindingFlags.Static | BindingFlags.NonPublic; + FieldInfo presetField = typeof(LogReleasePresentationController).GetField(presetFieldName, flags); + Assert.That(presetField, Is.Not.Null); + object preset = presetField.GetValue(null); + Type presetType = preset.GetType(); + + AssertPresetField(presetType, preset, "BaseDistortion", baseDistortion); + AssertPresetField(presetType, preset, "LiePause", liePause); + AssertPresetField(presetType, preset, "BreakPeak", breakPeak); + AssertPresetField(presetType, preset, "BreakDuration", breakDuration); + AssertPresetField(presetType, preset, "TruthDuration", truthDuration); + AssertPresetField(presetType, preset, "TruthPeripheral", truthPeripheral); + } + + [Test] + public void TmpClipRect_ConvertsWorldCornersIntoTextLocalSpace() + { + GameObject rectObject = new GameObject("ScreenRect", typeof(RectTransform)); + GameObject textObject = new GameObject("TextTransform"); + try + { + RectTransform rect = rectObject.GetComponent(); + rect.sizeDelta = new Vector2(4f, 2f); + rect.position = new Vector3(2f, -1f, 0f); + rect.rotation = Quaternion.Euler(0f, 0f, 17f); + rect.localScale = new Vector3(1.2f, 0.8f, 1f); + + textObject.transform.position = new Vector3(-0.5f, 0.75f, 0f); + textObject.transform.rotation = Quaternion.Euler(0f, 0f, -11f); + textObject.transform.localScale = new Vector3(1.4f, 0.65f, 1f); + + Vector4 actual = TMPRectClipper.CalculateLocalClipRect(textObject.transform, rect); + Vector3[] corners = new Vector3[4]; + rect.GetWorldCorners(corners); + Vector3 first = textObject.transform.InverseTransformPoint(corners[0]); + float minX = first.x; + float minY = first.y; + float maxX = first.x; + float maxY = first.y; + for (int i = 1; i < corners.Length; i++) + { + Vector3 local = textObject.transform.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); + } + + Assert.That(actual.x, Is.EqualTo(minX).Within(0.0001f)); + Assert.That(actual.y, Is.EqualTo(minY).Within(0.0001f)); + Assert.That(actual.z, Is.EqualTo(maxX).Within(0.0001f)); + Assert.That(actual.w, Is.EqualTo(maxY).Within(0.0001f)); + } + finally + { + UnityEngine.Object.DestroyImmediate(rectObject); + UnityEngine.Object.DestroyImmediate(textObject); + } + } + + [TestCase(null, null)] + [TestCase("", null)] + [TestCase(" | ", null)] + [TestCase("没问题|冇", "没问题冇")] + [TestCase("没 问 题", "没问题")] + public void SlotMachinePool_StripsSeparatorsAndWhitespace(string input, string expected) + { + Assert.That(LanguageParticleManager.SanitizeSlotMachinePool(input), Is.EqualTo(expected)); + } + + [Test] + public void ActorPhraseList_PreservesWordsAndDropsEmptyEntries() + { + string[] phrases = LogReleasePresentationController.ParseActorPhrases( + " 没问题 | | 冇问题|帽问题 "); + + Assert.That(phrases, Is.EqualTo(new[] { "没问题", "冇问题", "帽问题" })); + } + + [Test] + public void ActorFlash_OutsideMemoryState_CompletesImmediatelyWithoutStateChange() + { + GameObject host = new GameObject("PresentationHost"); + try + { + var controller = host.AddComponent(); + IEnumerator routine = controller.FlashActorLie("没问题", 0.6f); + + Assert.That(routine.MoveNext(), Is.False); + Assert.That( + controller.State, + Is.EqualTo(LogReleasePresentationController.PresentationState.Idle)); + } + finally + { + UnityEngine.Object.DestroyImmediate(host); + } + } + + private static void AssertPresetField(Type presetType, object preset, string name, float expected) + { + FieldInfo field = presetType.GetField(name, BindingFlags.Instance | BindingFlags.Public); + Assert.That(field, Is.Not.Null); + Assert.That((float)field.GetValue(preset), Is.EqualTo(expected).Within(0.0001f)); + } + } +} diff --git a/Assets/Editor/Huoshan/LogReleasePresentationTests.cs.meta b/Assets/Editor/Huoshan/LogReleasePresentationTests.cs.meta new file mode 100644 index 000000000..fffd6d2d1 --- /dev/null +++ b/Assets/Editor/Huoshan/LogReleasePresentationTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5f75b14634314395a3a67721065f86bd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: