2451 lines
107 KiB
C#
2451 lines
107 KiB
C#
using System.Reflection;
|
|
using System.Collections.Generic;
|
|
using BumpCombat.Combat;
|
|
using BumpCombat.Constants;
|
|
using BumpCombat.Core;
|
|
using BumpCombat.Enemies;
|
|
using BumpCombat.Player;
|
|
using BumpCombat.Progression;
|
|
using BumpCombat.Spawning;
|
|
using NUnit.Framework;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
|
|
namespace BumpCombat.Tests
|
|
{
|
|
public sealed class ActiveArtifactControllerTests
|
|
{
|
|
private GameObject owner;
|
|
private GameObject runManagerObject;
|
|
private RunManager previousRunManagerInstance;
|
|
private ActiveArtifactController controller;
|
|
|
|
[SetUp]
|
|
public void SetUp()
|
|
{
|
|
RunManager existingRunManager = RunManager.Instance;
|
|
previousRunManagerInstance = existingRunManager == null
|
|
? null
|
|
: existingRunManager;
|
|
RunManager.ForceProductionModeForTests = false;
|
|
owner = new GameObject("Active Artifact Tests");
|
|
owner.AddComponent<PlayerStats>();
|
|
owner.AddComponent<Rigidbody2D>();
|
|
controller = owner.AddComponent<ActiveArtifactController>();
|
|
typeof(ActiveArtifactController)
|
|
.GetField(
|
|
"maxOwnedArtifacts",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)
|
|
?.SetValue(controller, 6);
|
|
typeof(ActiveArtifactController)
|
|
.GetField(
|
|
"startingGauge",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)
|
|
?.SetValue(controller, 100f);
|
|
typeof(ActiveArtifactController)
|
|
.GetMethod(
|
|
"Awake",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)
|
|
?.Invoke(controller, null);
|
|
}
|
|
|
|
[TearDown]
|
|
public void TearDown()
|
|
{
|
|
RunManager.ForceProductionModeForTests = false;
|
|
Object.DestroyImmediate(owner);
|
|
if (runManagerObject != null)
|
|
{
|
|
Object.DestroyImmediate(runManagerObject);
|
|
runManagerObject = null;
|
|
}
|
|
RestoreRunManagerInstance(previousRunManagerInstance);
|
|
previousRunManagerInstance = null;
|
|
}
|
|
|
|
[Test]
|
|
public void DebugGrantCatalogArtifacts_TestHarnessAcceptsSixAndRestoresThreeSlotCap()
|
|
{
|
|
ActiveArtifactDefinition first = CreatePulse("first", 25f, 60f);
|
|
ActiveArtifactDefinition duplicate = CreatePulse("first", 10f, 20f);
|
|
ActiveArtifactDefinition second = CreatePulse(
|
|
"second",
|
|
25f,
|
|
60f,
|
|
ActiveArtifactEffect.Dash);
|
|
ActiveArtifactDefinition third = CreatePulse(
|
|
"third",
|
|
25f,
|
|
60f,
|
|
ActiveArtifactEffect.ThunderCrash);
|
|
ActiveArtifactDefinition fourth = CreatePulse(
|
|
"fourth",
|
|
25f,
|
|
60f,
|
|
ActiveArtifactEffect.Cyclone);
|
|
ActiveArtifactDefinition fifth = CreatePulse(
|
|
"fifth",
|
|
25f,
|
|
60f,
|
|
ActiveArtifactEffect.Phoenix);
|
|
ActiveArtifactDefinition sixth = CreatePulse(
|
|
"sixth",
|
|
25f,
|
|
60f,
|
|
ActiveArtifactEffect.ChainLightning);
|
|
|
|
controller.Configure(
|
|
new[] { first, second, third, fourth, fifth, sixth });
|
|
controller.DebugGrantCatalogArtifacts();
|
|
|
|
Assert.That(controller.OwnedArtifactCount, Is.EqualTo(6));
|
|
Assert.That(controller.MaxOwnedArtifacts, Is.EqualTo(3));
|
|
Assert.That(controller.MaxArtifactsPerColor, Is.EqualTo(1));
|
|
Assert.That(controller.CurrentGauge, Is.EqualTo(controller.MaxGauge));
|
|
|
|
Assert.That(controller.TryAddArtifact(fourth), Is.False);
|
|
Assert.That(controller.TryAddArtifact(duplicate), Is.False);
|
|
|
|
DestroyDefinitions(
|
|
first,
|
|
duplicate,
|
|
second,
|
|
third,
|
|
fourth,
|
|
fifth,
|
|
sixth);
|
|
}
|
|
|
|
[Test]
|
|
public void EditorDebugBuild_EnablesArtifactStartupSelection()
|
|
{
|
|
GameObject runManagerObject = new("Debug Run Manager");
|
|
try
|
|
{
|
|
RunManager runManager = runManagerObject.AddComponent<RunManager>();
|
|
typeof(RunManager)
|
|
.GetField(
|
|
"useDebugEventTimes",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)
|
|
?.SetValue(runManager, false);
|
|
typeof(RunManager)
|
|
.GetMethod(
|
|
"Awake",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)
|
|
?.Invoke(runManager, null);
|
|
|
|
Assert.That(RunManager.Instance.UseDebugEventTimes, Is.False);
|
|
Assert.That(Debug.isDebugBuild, Is.True);
|
|
Assert.That(RunManager.Instance.UseDebugArtifactSelection, Is.True);
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(runManagerObject);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void SelectNext_CyclesAcrossAllTestArtifacts()
|
|
{
|
|
// This is the explicit six-item debug harness; normal ownership
|
|
// remains one artifact per color and three total.
|
|
typeof(ActiveArtifactController)
|
|
.GetField(
|
|
"maxArtifactsPerColor",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)
|
|
?.SetValue(controller, 6);
|
|
ActiveArtifactDefinition[] definitions =
|
|
{
|
|
CreatePulse("first", 25f, 60f),
|
|
CreatePulse("second", 25f, 60f),
|
|
CreatePulse("third", 25f, 60f),
|
|
CreatePulse("fourth", 25f, 60f),
|
|
CreatePulse("fifth", 25f, 60f),
|
|
CreatePulse("sixth", 25f, 60f),
|
|
};
|
|
foreach (ActiveArtifactDefinition definition in definitions)
|
|
{
|
|
controller.TryAddArtifact(definition);
|
|
}
|
|
|
|
Assert.That(controller.CurrentArtifact, Is.SameAs(definitions[0]));
|
|
for (int i = 1; i < definitions.Length; i++)
|
|
{
|
|
Assert.That(controller.SelectNext(), Is.True);
|
|
Assert.That(
|
|
controller.CurrentArtifact,
|
|
Is.SameAs(definitions[i]));
|
|
}
|
|
Assert.That(controller.SelectNext(), Is.True);
|
|
Assert.That(controller.CurrentArtifact, Is.SameAs(definitions[0]));
|
|
|
|
DestroyDefinitions(definitions);
|
|
}
|
|
|
|
[Test]
|
|
public void NormalAndChargedUse_SpendOneSharedGauge()
|
|
{
|
|
ActiveArtifactDefinition pulse = CreatePulse("pulse", 25f, 60f);
|
|
controller.TryAddArtifact(pulse);
|
|
|
|
Assert.That(controller.TryUseCurrent(false), Is.True);
|
|
Assert.That(controller.CurrentGauge, Is.EqualTo(75f));
|
|
Assert.That(controller.TryUseCurrent(true), Is.True);
|
|
Assert.That(controller.CurrentGauge, Is.EqualTo(15f));
|
|
Assert.That(controller.TryUseCurrent(false), Is.False);
|
|
Assert.That(controller.CurrentGauge, Is.EqualTo(15f));
|
|
|
|
DestroyDefinitions(pulse);
|
|
}
|
|
|
|
[Test]
|
|
public void ProductionChargeLocked_HoldOnlyUsesNormalArtifactOnceOnRelease()
|
|
{
|
|
RunManager runManager = CreateRunManager(RunMode.Production);
|
|
ActiveArtifactDefinition pulse = CreatePulse("locked-pulse", 25f, 60f);
|
|
try
|
|
{
|
|
Assert.That(runManager.IsArtifactChargeUnlocked, Is.False);
|
|
Assert.That(controller.TryAddArtifact(pulse), Is.True);
|
|
int chargeStartedCount = 0;
|
|
int useCount = 0;
|
|
bool lastUseWasCharged = true;
|
|
controller.OnArtifactChargeStarted += _ => chargeStartedCount++;
|
|
controller.OnArtifactUseSucceeded += (_, charged) =>
|
|
{
|
|
useCount++;
|
|
lastUseWasCharged = charged;
|
|
};
|
|
|
|
InvokeControllerMethod("HandleArtifactButtonPressed");
|
|
|
|
Assert.That(controller.IsCharging, Is.False);
|
|
Assert.That(controller.ChargeProgress, Is.Zero);
|
|
Assert.That(chargeStartedCount, Is.Zero);
|
|
Assert.That(controller.CurrentGauge, Is.EqualTo(100f));
|
|
Assert.That(controller.TryUseCurrent(true), Is.False);
|
|
Assert.That(controller.CurrentGauge, Is.EqualTo(100f));
|
|
|
|
InvokeControllerMethod("HandleArtifactButtonReleased");
|
|
InvokeControllerMethod("HandleArtifactButtonReleased");
|
|
|
|
Assert.That(controller.CurrentGauge, Is.EqualTo(75f));
|
|
Assert.That(useCount, Is.EqualTo(1));
|
|
Assert.That(lastUseWasCharged, Is.False);
|
|
Assert.That(controller.IsCharging, Is.False);
|
|
}
|
|
finally
|
|
{
|
|
DestroyDefinitions(pulse);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void ProductionChargeUnlock_RestoresChargedHoldUse()
|
|
{
|
|
RunManager runManager = CreateRunManager(RunMode.Production);
|
|
ActiveArtifactDefinition pulse = CreatePulse("unlocked-pulse", 25f, 60f);
|
|
try
|
|
{
|
|
Assert.That(controller.TryAddArtifact(pulse), Is.True);
|
|
Assert.That(runManager.IsArtifactChargeUnlocked, Is.False);
|
|
|
|
runManager.UnlockArtifactCharge();
|
|
|
|
Assert.That(runManager.IsArtifactChargeUnlocked, Is.True);
|
|
int chargedUseCount = 0;
|
|
controller.OnArtifactUseSucceeded += (_, charged) =>
|
|
{
|
|
if (charged)
|
|
{
|
|
chargedUseCount++;
|
|
}
|
|
};
|
|
InvokeControllerMethod("HandleArtifactButtonPressed");
|
|
|
|
Assert.That(controller.IsCharging, Is.True);
|
|
typeof(ActiveArtifactController)
|
|
.GetField(
|
|
"chargeStartedAt",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)
|
|
?.SetValue(
|
|
controller,
|
|
Time.time - pulse.ChargeDuration - 0.01f);
|
|
InvokeControllerMethod("HandleArtifactButtonReleased");
|
|
|
|
Assert.That(controller.IsCharging, Is.False);
|
|
Assert.That(controller.CurrentGauge, Is.EqualTo(40f));
|
|
Assert.That(chargedUseCount, Is.EqualTo(1));
|
|
}
|
|
finally
|
|
{
|
|
DestroyDefinitions(pulse);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void DevelopmentRun_KeepsArtifactChargingUnlocked()
|
|
{
|
|
RunManager runManager = CreateRunManager(RunMode.Development);
|
|
ActiveArtifactDefinition pulse = CreatePulse("development-pulse", 25f, 60f);
|
|
try
|
|
{
|
|
Assert.That(runManager.IsArtifactChargeUnlocked, Is.True);
|
|
Assert.That(controller.TryAddArtifact(pulse), Is.True);
|
|
|
|
InvokeControllerMethod("HandleArtifactButtonPressed");
|
|
|
|
Assert.That(controller.IsCharging, Is.True);
|
|
}
|
|
finally
|
|
{
|
|
DestroyDefinitions(pulse);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void ProductionChargeLocked_SwitchingArtifactCancelsPendingNormalUse()
|
|
{
|
|
CreateRunManager(RunMode.Production);
|
|
typeof(ActiveArtifactController)
|
|
.GetField(
|
|
"maxArtifactsPerColor",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)
|
|
?.SetValue(controller, 2);
|
|
ActiveArtifactDefinition first = CreatePulse("locked-first", 25f, 60f);
|
|
ActiveArtifactDefinition second = CreatePulse("locked-second", 25f, 60f);
|
|
try
|
|
{
|
|
Assert.That(controller.TryAddArtifact(first), Is.True);
|
|
Assert.That(controller.TryAddArtifact(second), Is.True);
|
|
int useCount = 0;
|
|
controller.OnArtifactUseSucceeded += (_, _) => useCount++;
|
|
|
|
InvokeControllerMethod("HandleArtifactButtonPressed");
|
|
Assert.That(controller.SelectNext(), Is.True);
|
|
InvokeControllerMethod("HandleArtifactButtonReleased");
|
|
|
|
Assert.That(controller.CurrentGauge, Is.EqualTo(100f));
|
|
Assert.That(useCount, Is.Zero);
|
|
}
|
|
finally
|
|
{
|
|
DestroyDefinitions(first, second);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void MovementAndNormalHit_AddConfiguredGaugeAmounts()
|
|
{
|
|
ActiveArtifactDefinition pulse = CreatePulse("pulse", 100f, 100f);
|
|
controller.TryAddArtifact(pulse);
|
|
Assert.That(controller.TryUseCurrent(false), Is.True);
|
|
Assert.That(controller.CurrentGauge, Is.Zero);
|
|
|
|
controller.AddMovementCharge(1f);
|
|
controller.AddNormalHitCharge();
|
|
|
|
Assert.That(controller.CurrentGauge, Is.EqualTo(6f));
|
|
DestroyDefinitions(pulse);
|
|
}
|
|
|
|
[Test]
|
|
public void PrototypeDefinitions_PhoenixPiercesAndThunderUsesFrontEffect()
|
|
{
|
|
ActiveArtifactDefinition phoenix =
|
|
AssetDatabase.LoadAssetAtPath<ActiveArtifactDefinition>(
|
|
"Assets/_Project/Constants/Artifacts/PhoenixArtifact.asset");
|
|
ActiveArtifactDefinition thunder =
|
|
AssetDatabase.LoadAssetAtPath<ActiveArtifactDefinition>(
|
|
"Assets/_Project/Constants/Artifacts/ThunderCrashArtifact.asset");
|
|
|
|
Assert.That(phoenix, Is.Not.Null);
|
|
Assert.That(phoenix.Effect, Is.EqualTo(ActiveArtifactEffect.Phoenix));
|
|
Assert.That(phoenix.DisplayName, Is.EqualTo("잿불의 마검"));
|
|
Assert.That(phoenix.PlaceholderSymbol, Is.EqualTo("SR"));
|
|
Assert.That(phoenix.NormalRange, Is.EqualTo(3f));
|
|
Assert.That(phoenix.NormalWidth, Is.EqualTo(0.4375f));
|
|
Assert.That(phoenix.NormalDamage, Is.EqualTo(35f));
|
|
Assert.That(phoenix.ChargedDamage, Is.EqualTo(70f));
|
|
Assert.That(phoenix.NormalKnockback, Is.EqualTo(0.2f));
|
|
Assert.That(phoenix.ChargedKnockback, Is.EqualTo(0.3f));
|
|
Assert.That(phoenix.ChargedRange, Is.EqualTo(6f));
|
|
Assert.That(phoenix.ChargedWidth, Is.EqualTo(0.875f));
|
|
Assert.That(phoenix.ChargedDuration, Is.EqualTo(0.3f));
|
|
Assert.That(phoenix.ChargedMaxHitsPerTarget, Is.EqualTo(1));
|
|
Assert.That(phoenix.NormalIgniteDuration, Is.EqualTo(3f));
|
|
Assert.That(phoenix.NormalIgniteInterval, Is.EqualTo(0.5f));
|
|
Assert.That(phoenix.NormalIgniteTickDamage, Is.EqualTo(3f));
|
|
Assert.That(phoenix.ChargedIgniteTickDamage, Is.EqualTo(5f));
|
|
Assert.That(thunder, Is.Not.Null);
|
|
Assert.That(
|
|
thunder.Effect,
|
|
Is.EqualTo(ActiveArtifactEffect.ThunderCrash));
|
|
Assert.That(thunder.NormalRange, Is.EqualTo(2f));
|
|
Assert.That(thunder.ChargedRange, Is.EqualTo(3f));
|
|
Assert.That(thunder.NormalDuration, Is.EqualTo(0.3f));
|
|
Assert.That(thunder.ChargedDuration, Is.EqualTo(0.55f));
|
|
Assert.That(thunder.NormalKnockback, Is.Zero);
|
|
Assert.That(thunder.ChargedKnockback, Is.Zero);
|
|
}
|
|
|
|
[Test]
|
|
public void PrototypeArtifactDefinitions_UseConfirmedEquipmentDisplayNames()
|
|
{
|
|
string[] paths =
|
|
{
|
|
"Assets/_Project/Constants/Artifacts/DashArtifact.asset",
|
|
"Assets/_Project/Constants/Artifacts/PulseArtifact.asset",
|
|
"Assets/_Project/Constants/Artifacts/PhoenixArtifact.asset",
|
|
"Assets/_Project/Constants/Artifacts/CycloneArtifact.asset",
|
|
"Assets/_Project/Constants/Artifacts/ThunderCrashArtifact.asset",
|
|
"Assets/_Project/Constants/Artifacts/ChainLightningArtifact.asset",
|
|
};
|
|
string[] expectedNames =
|
|
{
|
|
"척후의 장화",
|
|
"군주의 방패",
|
|
"잿불의 마검",
|
|
"바람깃 브로치",
|
|
"뇌격의 수갑",
|
|
"창뢰의 보주",
|
|
};
|
|
ActiveArtifactEffect[] expectedEffects =
|
|
{
|
|
ActiveArtifactEffect.Dash,
|
|
ActiveArtifactEffect.Pulse,
|
|
ActiveArtifactEffect.Phoenix,
|
|
ActiveArtifactEffect.Cyclone,
|
|
ActiveArtifactEffect.ThunderCrash,
|
|
ActiveArtifactEffect.ChainLightning,
|
|
};
|
|
|
|
for (int i = 0; i < paths.Length; i++)
|
|
{
|
|
ActiveArtifactDefinition definition =
|
|
AssetDatabase.LoadAssetAtPath<ActiveArtifactDefinition>(paths[i]);
|
|
Assert.That(definition, Is.Not.Null, paths[i]);
|
|
Assert.That(definition.Effect, Is.EqualTo(expectedEffects[i]));
|
|
Assert.That(definition.DisplayName, Is.EqualTo(expectedNames[i]));
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void PrototypeDefinition_PulseUsesRepelAndDefenseValues()
|
|
{
|
|
ActiveArtifactDefinition pulse =
|
|
AssetDatabase.LoadAssetAtPath<ActiveArtifactDefinition>(
|
|
"Assets/_Project/Constants/Artifacts/PulseArtifact.asset");
|
|
|
|
Assert.That(pulse, Is.Not.Null);
|
|
Assert.That(pulse.Effect, Is.EqualTo(ActiveArtifactEffect.Pulse));
|
|
Assert.That(pulse.NormalDamage, Is.Zero);
|
|
Assert.That(pulse.ChargedDamage, Is.Zero);
|
|
Assert.That(pulse.NormalKnockback, Is.EqualTo(1.5f));
|
|
Assert.That(pulse.ChargedKnockback, Is.EqualTo(2f));
|
|
Assert.That(pulse.NormalDuration, Is.EqualTo(0.2f));
|
|
Assert.That(pulse.ChargedDuration, Is.EqualTo(0.3f));
|
|
Assert.That(pulse.NormalDamageReduction, Is.EqualTo(0.2f));
|
|
Assert.That(pulse.ChargedDamageReduction, Is.EqualTo(0.35f));
|
|
Assert.That(pulse.NormalBuffDuration, Is.EqualTo(3f));
|
|
Assert.That(pulse.ChargedBuffDuration, Is.EqualTo(4f));
|
|
}
|
|
|
|
[Test]
|
|
public void PrototypeDefinition_CycloneUsesUpdatedNormalDuration()
|
|
{
|
|
ActiveArtifactDefinition cyclone =
|
|
AssetDatabase.LoadAssetAtPath<ActiveArtifactDefinition>(
|
|
"Assets/_Project/Constants/Artifacts/CycloneArtifact.asset");
|
|
|
|
Assert.That(cyclone, Is.Not.Null);
|
|
Assert.That(cyclone.Effect, Is.EqualTo(ActiveArtifactEffect.Cyclone));
|
|
Assert.That(cyclone.NormalDamage, Is.EqualTo(6f));
|
|
Assert.That(cyclone.ChargedDamage, Is.EqualTo(8f));
|
|
Assert.That(cyclone.NormalDuration, Is.EqualTo(0.36f));
|
|
Assert.That(cyclone.ChargedDuration, Is.EqualTo(0.65f));
|
|
Assert.That(cyclone.NormalMoveSpeedIncrease, Is.EqualTo(0.15f));
|
|
Assert.That(cyclone.ChargedMoveSpeedIncrease, Is.EqualTo(0.25f));
|
|
Assert.That(cyclone.NormalKnockback, Is.EqualTo(0.3f));
|
|
Assert.That(cyclone.ChargedKnockback, Is.EqualTo(0.5f));
|
|
}
|
|
|
|
[Test]
|
|
public void PrototypeDefinitions_UseThunderAndChainSecondaryEffects()
|
|
{
|
|
ActiveArtifactDefinition thunder =
|
|
AssetDatabase.LoadAssetAtPath<ActiveArtifactDefinition>(
|
|
"Assets/_Project/Constants/Artifacts/ThunderCrashArtifact.asset");
|
|
ActiveArtifactDefinition chain =
|
|
AssetDatabase.LoadAssetAtPath<ActiveArtifactDefinition>(
|
|
"Assets/_Project/Constants/Artifacts/ChainLightningArtifact.asset");
|
|
|
|
Assert.That(thunder.NormalDamage, Is.EqualTo(4f));
|
|
Assert.That(thunder.ChargedDamage, Is.EqualTo(6f));
|
|
Assert.That(thunder.NormalVulnerabilityIncrease, Is.EqualTo(0.25f));
|
|
Assert.That(thunder.ChargedVulnerabilityIncrease, Is.EqualTo(0.4f));
|
|
Assert.That(thunder.NormalVulnerabilityDuration, Is.EqualTo(3f));
|
|
Assert.That(thunder.ChargedVulnerabilityDuration, Is.EqualTo(4f));
|
|
Assert.That(chain.NormalDamage, Is.EqualTo(10f));
|
|
Assert.That(chain.ChargedDamage, Is.EqualTo(14f));
|
|
Assert.That(chain.DamageMultiplierPerChain, Is.EqualTo(1.2f));
|
|
Assert.That(chain.NormalShockChance, Is.EqualTo(0.2f));
|
|
Assert.That(chain.ChargedShockChance, Is.EqualTo(0.35f));
|
|
Assert.That(chain.ChargedShockIncrease, Is.EqualTo(0.3f));
|
|
}
|
|
|
|
[Test]
|
|
public void ScorchingRayVisual_UsesMeasuredBodyScaleAndNormalizedPivots()
|
|
{
|
|
string[] paths =
|
|
{
|
|
"Assets/_Project/Resources/Artifacts/ScorchingRay/"
|
|
+ "ScorchingRay-Ghostfire-Normal-v1.png",
|
|
"Assets/_Project/Resources/Artifacts/ScorchingRay/"
|
|
+ "ScorchingRay-Ghostfire-Charged-v1.png",
|
|
};
|
|
int[] widths = { 768, 1536 };
|
|
int[] heights = { 64, 128 };
|
|
for (int i = 0; i < paths.Length; i++)
|
|
{
|
|
Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(paths[i]);
|
|
TextureImporter importer = AssetImporter.GetAtPath(paths[i]) as TextureImporter;
|
|
Assert.That(texture, Is.Not.Null, paths[i]);
|
|
Assert.That(texture.width, Is.EqualTo(widths[i]));
|
|
Assert.That(texture.height, Is.EqualTo(heights[i]));
|
|
Assert.That(importer, Is.Not.Null, paths[i]);
|
|
Assert.That(importer.filterMode, Is.EqualTo(FilterMode.Point));
|
|
Assert.That(importer.mipmapEnabled, Is.False);
|
|
Assert.That(
|
|
importer.textureCompression,
|
|
Is.EqualTo(TextureImporterCompression.Uncompressed));
|
|
}
|
|
|
|
FieldInfo durations = typeof(ActiveArtifactController).GetField(
|
|
"ScorchingRayFrameDurations",
|
|
BindingFlags.Static | BindingFlags.NonPublic);
|
|
Assert.That(
|
|
(float[])durations.GetValue(null),
|
|
Is.EqualTo(new[] { 0.02f, 0.03f, 0.03f, 0.03f, 0.02f, 0.02f }));
|
|
|
|
MethodInfo load = typeof(ActiveArtifactController).GetMethod(
|
|
"TryLoadScorchingRayFrames",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
MethodInfo create = typeof(ActiveArtifactController).GetMethod(
|
|
"CreateScorchingRaySprite",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
FieldInfo normalField = typeof(ActiveArtifactController).GetField(
|
|
"scorchingRayNormalFrames",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
FieldInfo chargedField = typeof(ActiveArtifactController).GetField(
|
|
"scorchingRayChargedFrames",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
Assert.That(load.Invoke(controller, new object[] { false }), Is.True);
|
|
Assert.That(load.Invoke(controller, new object[] { true }), Is.True);
|
|
|
|
Sprite[] normal = (Sprite[])normalField.GetValue(controller);
|
|
Sprite[] charged = (Sprite[])chargedField.GetValue(controller);
|
|
Assert.That(normal[0].pivot, Is.EqualTo(new Vector2(8f, 32f)));
|
|
Assert.That(charged[0].pivot, Is.EqualTo(new Vector2(16f, 64f)));
|
|
|
|
GameObject normalVisual = new("Normal Scorching Ray Visual Test");
|
|
GameObject chargedVisual = new("Charged Scorching Ray Visual Test");
|
|
try
|
|
{
|
|
SpriteRenderer normalRenderer = (SpriteRenderer)create.Invoke(
|
|
controller,
|
|
new object[] {
|
|
normalVisual, false, Vector2.right, 3f, 0.4375f });
|
|
SpriteRenderer chargedRenderer = (SpriteRenderer)create.Invoke(
|
|
controller,
|
|
new object[] {
|
|
chargedVisual, true, Vector2.right, 6f, 0.875f });
|
|
Assert.That(normalRenderer.transform.localScale, Is.EqualTo(Vector3.one));
|
|
Assert.That(chargedRenderer.transform.localScale, Is.EqualTo(Vector3.one));
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(normalVisual);
|
|
Object.DestroyImmediate(chargedVisual);
|
|
typeof(ActiveArtifactController)
|
|
.GetMethod(
|
|
"DestroyScorchingRayFrames",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)
|
|
.Invoke(controller, null);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void ScheduledHitCount_UsesDurationAndIntervalBoundaries()
|
|
{
|
|
MethodInfo getCount = typeof(ActiveArtifactController).GetMethod(
|
|
"GetScheduledHitCount",
|
|
BindingFlags.Static | BindingFlags.NonPublic);
|
|
|
|
Assert.That(getCount.Invoke(null, new object[] { 0.36f, 0.14f }), Is.EqualTo(3));
|
|
Assert.That(getCount.Invoke(null, new object[] { 0.65f, 0.14f }), Is.EqualTo(5));
|
|
Assert.That(getCount.Invoke(null, new object[] { 0.30f, 0.13f }), Is.EqualTo(3));
|
|
Assert.That(getCount.Invoke(null, new object[] { 0.55f, 0.13f }), Is.EqualTo(5));
|
|
|
|
ActiveArtifactDefinition thunder = AssetDatabase.LoadAssetAtPath<ActiveArtifactDefinition>(
|
|
"Assets/_Project/Constants/Artifacts/ThunderCrashArtifact.asset");
|
|
Assert.That(thunder, Is.Not.Null);
|
|
Assert.That(
|
|
getCount.Invoke(null, new object[] { thunder.NormalDuration, thunder.HitInterval }),
|
|
Is.EqualTo(3));
|
|
Assert.That(
|
|
getCount.Invoke(null, new object[] { thunder.ChargedDuration, thunder.HitInterval }),
|
|
Is.EqualTo(5));
|
|
}
|
|
|
|
[Test]
|
|
public void EnemyIgniteTicksUseDamageTakenEffectsAtEachScheduledTime()
|
|
{
|
|
EnemyController enemy = CreateEnemyTarget(
|
|
"Timed Status Target",
|
|
Vector2.zero);
|
|
typeof(EnemyController)
|
|
.GetProperty("CurrentHealth")
|
|
?.SetValue(enemy, 100f);
|
|
|
|
Assert.That(enemy.ApplyShock(0.75f, 0.2f), Is.True);
|
|
Assert.That(enemy.ApplyIgnite(3f, 0.5f, 5.5f), Is.True);
|
|
MethodInfo updateTimedEffects = typeof(EnemyController).GetMethod(
|
|
"UpdateTimedEffects",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
updateTimedEffects.Invoke(enemy, new object[] { 1f });
|
|
|
|
Assert.That(
|
|
enemy.CurrentHealth,
|
|
Is.EqualTo(89f).Within(0.001f),
|
|
"The 0.5s ignite tick receives shock before its own damage floor; the 1.0s tick does not.");
|
|
}
|
|
|
|
[Test]
|
|
public void PulseRingRetouchedSheets_UseSevenFramesAndExpectedImport()
|
|
{
|
|
string[] paths = {
|
|
"Assets/_Project/Resources/Artifacts/PulseRing/PulseRing-normal-retouched-v1.png",
|
|
"Assets/_Project/Resources/Artifacts/PulseRing/PulseRing-charged-retouched-v1.png" };
|
|
int[] widths = { 1120, 1792 };
|
|
int[] heights = { 100, 104 };
|
|
for (int i = 0; i < paths.Length; i++)
|
|
{
|
|
Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(paths[i]);
|
|
TextureImporter importer = AssetImporter.GetAtPath(paths[i]) as TextureImporter;
|
|
Assert.That(texture, Is.Not.Null);
|
|
Assert.That(texture.width, Is.EqualTo(widths[i]));
|
|
Assert.That(texture.height, Is.EqualTo(heights[i]));
|
|
Assert.That(texture.filterMode, Is.EqualTo(FilterMode.Point));
|
|
Assert.That(importer, Is.Not.Null);
|
|
Assert.That(importer.textureCompression, Is.EqualTo(TextureImporterCompression.Uncompressed));
|
|
Assert.That(importer.mipmapEnabled, Is.False);
|
|
Assert.That(importer.alphaIsTransparency, Is.True);
|
|
Assert.That(texture.width / (i == 0 ? 160 : 256), Is.EqualTo(7));
|
|
}
|
|
|
|
MethodInfo load = typeof(ActiveArtifactController).GetMethod(
|
|
"TryLoadPulseRingFrames",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
MethodInfo destroy = typeof(ActiveArtifactController).GetMethod(
|
|
"DestroyPulseRingFrames",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
FieldInfo normalFrames = typeof(ActiveArtifactController).GetField(
|
|
"pulseRingNormalFrames",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
FieldInfo chargedFrames = typeof(ActiveArtifactController).GetField(
|
|
"pulseRingChargedFrames",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
Assert.That(load.Invoke(controller, new object[] { false }), Is.True);
|
|
Assert.That(load.Invoke(controller, new object[] { true }), Is.True);
|
|
Sprite[] normal = (Sprite[])normalFrames.GetValue(controller);
|
|
Sprite[] charged = (Sprite[])chargedFrames.GetValue(controller);
|
|
Assert.That(normal, Has.Length.EqualTo(7));
|
|
Assert.That(charged, Has.Length.EqualTo(7));
|
|
Assert.That(normal[0].rect, Is.EqualTo(new Rect(0f, 0f, 160f, 100f)));
|
|
Assert.That(charged[0].rect, Is.EqualTo(new Rect(0f, 0f, 256f, 104f)));
|
|
Assert.That(normal[0].pixelsPerUnit, Is.EqualTo(32f));
|
|
Assert.That(charged[0].pixelsPerUnit, Is.EqualTo(32f));
|
|
Assert.That(normal[0].pivot, Is.EqualTo(new Vector2(80f, 50.5f)));
|
|
Assert.That(charged[0].pivot, Is.EqualTo(new Vector2(128f, 52f)));
|
|
destroy.Invoke(controller, null);
|
|
Assert.That(normalFrames.GetValue(controller), Is.Null);
|
|
Assert.That(chargedFrames.GetValue(controller), Is.Null);
|
|
}
|
|
|
|
[Test]
|
|
public void Charging_DoesNotCreatePreviewOrPreliminaryRange()
|
|
{
|
|
ActiveArtifactDefinition pulse = CreatePulse("charge-test", 25f, 25f);
|
|
try
|
|
{
|
|
Assert.That(controller.TryAddArtifact(pulse), Is.True);
|
|
MethodInfo beginCharge = typeof(ActiveArtifactController).GetMethod(
|
|
"BeginCharge",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
MethodInfo cancelCharge = typeof(ActiveArtifactController).GetMethod(
|
|
"CancelCharge",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
|
|
beginCharge.Invoke(controller, null);
|
|
|
|
Assert.That(controller.IsCharging, Is.True);
|
|
Assert.That(
|
|
owner.GetComponentsInChildren<LineRenderer>(true),
|
|
Is.Empty);
|
|
Assert.That(
|
|
owner.transform.Find("Artifact Charge Preview"),
|
|
Is.Null);
|
|
|
|
cancelCharge.Invoke(controller, null);
|
|
Assert.That(controller.IsCharging, Is.False);
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(pulse);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void ChainLightningArc_UsesEightCroppedPointSampledFrames()
|
|
{
|
|
const string path =
|
|
"Assets/_Project/Resources/Artifacts/ChainLightning/"
|
|
+ "Ghostfire-Arc-edit-v1.png";
|
|
Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(path);
|
|
TextureImporter importer =
|
|
AssetImporter.GetAtPath(path) as TextureImporter;
|
|
|
|
Assert.That(texture, Is.Not.Null);
|
|
Assert.That(texture.width, Is.EqualTo(800));
|
|
Assert.That(texture.height, Is.EqualTo(100));
|
|
Assert.That(importer, Is.Not.Null);
|
|
Assert.That(importer.filterMode, Is.EqualTo(FilterMode.Point));
|
|
Assert.That(importer.textureCompression,
|
|
Is.EqualTo(TextureImporterCompression.Uncompressed));
|
|
Assert.That(importer.mipmapEnabled, Is.False);
|
|
|
|
MethodInfo load = typeof(ActiveArtifactController).GetMethod(
|
|
"TryLoadChainLightningArcFrames",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
MethodInfo destroy = typeof(ActiveArtifactController).GetMethod(
|
|
"DestroyChainLightningArcFrames",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
FieldInfo frames = typeof(ActiveArtifactController).GetField(
|
|
"chainLightningArcFrames",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
|
|
Assert.That(load.Invoke(controller, null), Is.True);
|
|
Sprite[] loadedFrames = (Sprite[])frames.GetValue(controller);
|
|
Assert.That(loadedFrames, Has.Length.EqualTo(8));
|
|
Assert.That(loadedFrames[0].rect, Is.EqualTo(new Rect(8f, 50f, 81f, 15f)));
|
|
Assert.That(loadedFrames[7].rect, Is.EqualTo(new Rect(708f, 50f, 81f, 15f)));
|
|
destroy.Invoke(controller, null);
|
|
Assert.That(frames.GetValue(controller), Is.Null);
|
|
}
|
|
|
|
[Test]
|
|
public void CycloneOrbitVisual_UsesFourSheetsAndMatchesOrbitBounds()
|
|
{
|
|
string root =
|
|
"Assets/_Project/Resources/Artifacts/ThreeColor-v1/"
|
|
+ "CycloneOrbit-v3/";
|
|
string[] assets =
|
|
{
|
|
"Cyclone-Normal-Back-v3.png",
|
|
"Cyclone-Normal-Front-v3.png",
|
|
"Cyclone-Charged-Back-v3.png",
|
|
"Cyclone-Charged-Front-v3.png",
|
|
};
|
|
foreach (string asset in assets)
|
|
{
|
|
string path = root + asset;
|
|
Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(path);
|
|
TextureImporter importer = AssetImporter.GetAtPath(path) as TextureImporter;
|
|
Assert.That(texture, Is.Not.Null, path);
|
|
Assert.That(texture.width, Is.EqualTo(800), path);
|
|
Assert.That(texture.height, Is.EqualTo(100), path);
|
|
Assert.That(importer, Is.Not.Null, path);
|
|
Assert.That(importer.filterMode, Is.EqualTo(FilterMode.Point), path);
|
|
Assert.That(importer.mipmapEnabled, Is.False, path);
|
|
Assert.That(importer.textureCompression,
|
|
Is.EqualTo(TextureImporterCompression.Uncompressed), path);
|
|
AssertCyclonePixelsInsideOrbit(
|
|
texture,
|
|
asset.Contains("Charged"),
|
|
path);
|
|
}
|
|
|
|
MethodInfo load = typeof(ActiveArtifactController).GetMethod(
|
|
"TryLoadCycloneOrbitFrames",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
MethodInfo destroy = typeof(ActiveArtifactController).GetMethod(
|
|
"DestroyCycloneOrbitFrames",
|
|
BindingFlags.Instance | BindingFlags.NonPublic,
|
|
null,
|
|
System.Type.EmptyTypes,
|
|
null);
|
|
Assert.That((bool)load.Invoke(controller, new object[] { false }), Is.True);
|
|
Assert.That((bool)load.Invoke(controller, new object[] { true }), Is.True);
|
|
AssertCycloneFrameSet("cycloneNormalBackFrames");
|
|
AssertCycloneFrameSet("cycloneNormalFrontFrames");
|
|
AssertCycloneFrameSet("cycloneChargedBackFrames");
|
|
AssertCycloneFrameSet("cycloneChargedFrontFrames");
|
|
|
|
owner.transform.localScale = Vector3.one * 1.25f;
|
|
SpriteRenderer playerRenderer = owner.AddComponent<SpriteRenderer>();
|
|
playerRenderer.sortingOrder = 120;
|
|
YSortRenderer ySort = owner.AddComponent<YSortRenderer>();
|
|
GameObject visual = new("Cyclone Orbit Layer Test");
|
|
MethodInfo create = typeof(ActiveArtifactController).GetMethod(
|
|
"CreateCycloneSpriteLayers",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
MethodInfo setFrame = typeof(ActiveArtifactController).GetMethod(
|
|
"SetCycloneSpriteFrame",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
object[] normalArgs = { visual, false, 1f, null, null };
|
|
create.Invoke(controller, normalArgs);
|
|
SpriteRenderer normalBack = (SpriteRenderer)normalArgs[3];
|
|
SpriteRenderer normalFront = (SpriteRenderer)normalArgs[4];
|
|
Assert.That(normalBack, Is.Not.Null);
|
|
Assert.That(normalFront, Is.Not.Null);
|
|
Assert.That(normalBack.sortingOrder, Is.EqualTo(999));
|
|
Assert.That(normalFront.sortingOrder, Is.EqualTo(1001));
|
|
AssertUniformScale(normalBack.transform.lossyScale, 1f);
|
|
AssertUniformScale(normalFront.transform.lossyScale, 1f);
|
|
|
|
owner.transform.position = new Vector3(0f, 0.25f, 0f);
|
|
typeof(ActiveArtifactController).GetMethod(
|
|
"UpdateCycloneSorting",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)
|
|
.Invoke(controller, new object[] { normalBack, normalFront });
|
|
Assert.That(ySort.CalculateSortingOrder(), Is.EqualTo(975));
|
|
Assert.That(normalBack.sortingOrder, Is.EqualTo(974));
|
|
Assert.That(normalFront.sortingOrder, Is.EqualTo(976));
|
|
|
|
GameObject chargedVisual = new("Charged Cyclone Orbit Layer Test");
|
|
object[] chargedArgs = { chargedVisual, true, 1f, null, null };
|
|
create.Invoke(controller, chargedArgs);
|
|
SpriteRenderer chargedBack = (SpriteRenderer)chargedArgs[3];
|
|
SpriteRenderer chargedFront = (SpriteRenderer)chargedArgs[4];
|
|
setFrame.Invoke(
|
|
controller,
|
|
new object[] { normalBack, normalFront, false, 0.1f });
|
|
setFrame.Invoke(
|
|
controller,
|
|
new object[] { chargedBack, chargedFront, true, 0.15f });
|
|
Assert.That(
|
|
normalBack.sprite,
|
|
Is.EqualTo(GetCycloneFrames("cycloneNormalBackFrames")[2]));
|
|
Assert.That(
|
|
normalFront.sprite,
|
|
Is.EqualTo(GetCycloneFrames("cycloneNormalFrontFrames")[2]));
|
|
Assert.That(
|
|
chargedBack.sprite,
|
|
Is.EqualTo(GetCycloneFrames("cycloneChargedBackFrames")[3]));
|
|
Assert.That(
|
|
chargedFront.sprite,
|
|
Is.EqualTo(GetCycloneFrames("cycloneChargedFrontFrames")[3]));
|
|
AssertUniformScale(chargedBack.transform.lossyScale, 1f);
|
|
AssertUniformScale(chargedFront.transform.lossyScale, 1f);
|
|
|
|
MethodInfo scale = typeof(ActiveArtifactController).GetMethod(
|
|
"GetCycloneVisualScale",
|
|
BindingFlags.Static | BindingFlags.NonPublic);
|
|
MethodInfo getOffset = typeof(ActiveArtifactController).GetMethod(
|
|
"GetCycloneHitCenterOffset",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
MethodInfo getRadii = typeof(ActiveArtifactController).GetMethod(
|
|
"GetCycloneHitRadii",
|
|
BindingFlags.Static | BindingFlags.NonPublic);
|
|
Assert.That((float)scale.Invoke(null, new object[] { 0.85f, false }), Is.EqualTo(1f));
|
|
Assert.That((float)scale.Invoke(null, new object[] { 1.25f, true }), Is.EqualTo(1f));
|
|
Assert.That((float)scale.Invoke(null, new object[] { 1.7f, false }), Is.EqualTo(2f));
|
|
|
|
Vector2 normalCenterOffset = (Vector2)getOffset.Invoke(
|
|
controller,
|
|
new object[] { 0.85f, false });
|
|
Vector2 chargedCenterOffset = (Vector2)getOffset.Invoke(
|
|
controller,
|
|
new object[] { 1.25f, true });
|
|
Vector2 expectedShadowOffset = new Vector2(0f, -7.5f / 32f * 1.25f);
|
|
AssertVector2Close(
|
|
normalCenterOffset,
|
|
expectedShadowOffset + Vector2.up * (5f / 32f));
|
|
AssertVector2Close(chargedCenterOffset, normalCenterOffset);
|
|
Vector2 normalRadii = (Vector2)getRadii.Invoke(
|
|
null,
|
|
new object[] { 0.85f, false });
|
|
Vector2 chargedRadii = (Vector2)getRadii.Invoke(
|
|
null,
|
|
new object[] { 1.25f, true });
|
|
AssertVector2Close(normalRadii, new Vector2(38.0625f, 15.03125f) / 32f);
|
|
AssertVector2Close(chargedRadii, new Vector2(46f, 18f) / 32f);
|
|
Assert.That(chargedRadii.x, Is.GreaterThan(normalRadii.x));
|
|
|
|
MethodInfo evaluate = typeof(ActiveArtifactController).GetMethod(
|
|
"EvaluateCycloneFrameIndex",
|
|
BindingFlags.Static | BindingFlags.NonPublic);
|
|
Assert.That(evaluate.Invoke(null, new object[] { 0f }), Is.EqualTo(0));
|
|
Assert.That(evaluate.Invoke(null, new object[] { 0.05f }), Is.EqualTo(1));
|
|
Assert.That(evaluate.Invoke(null, new object[] { 0.39999f }), Is.EqualTo(7));
|
|
Assert.That(evaluate.Invoke(null, new object[] { 0.4f }), Is.EqualTo(0));
|
|
|
|
Object.DestroyImmediate(visual);
|
|
Object.DestroyImmediate(chargedVisual);
|
|
destroy.Invoke(controller, null);
|
|
Assert.That(GetCycloneFrames("cycloneNormalBackFrames"), Is.Null);
|
|
Assert.That(GetCycloneFrames("cycloneNormalFrontFrames"), Is.Null);
|
|
Assert.That(GetCycloneFrames("cycloneChargedBackFrames"), Is.Null);
|
|
Assert.That(GetCycloneFrames("cycloneChargedFrontFrames"), Is.Null);
|
|
}
|
|
|
|
private void AssertCycloneFrameSet(string fieldName)
|
|
{
|
|
Sprite[] frames = GetCycloneFrames(fieldName);
|
|
Assert.That(frames, Has.Length.EqualTo(8), fieldName);
|
|
Assert.That(frames[0].pixelsPerUnit, Is.EqualTo(32f), fieldName);
|
|
Assert.That(Vector2.Distance(frames[0].pivot, new Vector2(50f, 30f)), Is.LessThan(0.001f), fieldName);
|
|
Assert.That(frames[7].rect, Is.EqualTo(new Rect(700f, 0f, 100f, 100f)), fieldName);
|
|
}
|
|
|
|
private Sprite[] GetCycloneFrames(string fieldName)
|
|
{
|
|
return (Sprite[])typeof(ActiveArtifactController).GetField(
|
|
fieldName,
|
|
BindingFlags.Instance | BindingFlags.NonPublic).GetValue(controller);
|
|
}
|
|
|
|
private static void AssertCyclonePixelsInsideOrbit(
|
|
Texture2D texture,
|
|
bool charged,
|
|
string context)
|
|
{
|
|
float radiusX = charged ? 46f : 38.0625f;
|
|
float radiusY = charged ? 18f : 15.03125f;
|
|
Color32[] pixels = texture.GetPixels32();
|
|
int visiblePixelCount = 0;
|
|
for (int y = 0; y < texture.height; y++)
|
|
{
|
|
float topY = texture.height - 1 - y;
|
|
for (int x = 0; x < texture.width; x++)
|
|
{
|
|
if (pixels[y * texture.width + x].a == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
visiblePixelCount++;
|
|
float localX = x % 100;
|
|
float nextX = localX + 1f;
|
|
float nextY = topY + 1f;
|
|
AssertCycloneCornerInside(localX, topY, radiusX, radiusY, context);
|
|
AssertCycloneCornerInside(nextX, topY, radiusX, radiusY, context);
|
|
AssertCycloneCornerInside(localX, nextY, radiusX, radiusY, context);
|
|
AssertCycloneCornerInside(nextX, nextY, radiusX, radiusY, context);
|
|
}
|
|
}
|
|
|
|
Assert.That(visiblePixelCount, Is.GreaterThan(0), context);
|
|
}
|
|
|
|
private static void AssertCycloneCornerInside(
|
|
float x,
|
|
float y,
|
|
float radiusX,
|
|
float radiusY,
|
|
string context)
|
|
{
|
|
float dx = x - 50f;
|
|
float dy = y - 65f;
|
|
float ratio = dx * dx / (radiusX * radiusX)
|
|
+ dy * dy / (radiusY * radiusY);
|
|
Assert.That(ratio, Is.LessThanOrEqualTo(1f), context);
|
|
}
|
|
|
|
[Test]
|
|
public void ThunderCrashVisuals_UseThreeColorDedicatedSheetAndExpandedDefinitionScale()
|
|
{
|
|
const string path = "Assets/_Project/Resources/Artifacts/ThreeColor-v1/ThunderCrash/ThunderCrash-Combined-v6.png";
|
|
var texture = AssetDatabase.LoadAssetAtPath<Texture2D>(path);
|
|
var importer = (TextureImporter)AssetImporter.GetAtPath(path);
|
|
Assert.That(texture, Is.Not.Null);
|
|
Assert.That(texture.width, Is.EqualTo(600));
|
|
Assert.That(texture.height, Is.EqualTo(100));
|
|
Assert.That(importer.filterMode, Is.EqualTo(FilterMode.Point));
|
|
Assert.That(importer.mipmapEnabled, Is.False);
|
|
Assert.That(importer.textureCompression, Is.EqualTo(TextureImporterCompression.Uncompressed));
|
|
const BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic;
|
|
var type = typeof(ActiveArtifactController);
|
|
Assert.That(type.GetMethod("TryLoadThunderCrashFrames", flags).Invoke(controller, null), Is.True);
|
|
var field = type.GetField("thunderCrashFrames", flags);
|
|
var frames = (Sprite[])field.GetValue(controller);
|
|
Assert.That(frames, Has.Length.EqualTo(6));
|
|
Assert.That(frames[0].texture, Is.SameAs(texture));
|
|
Assert.That(frames[0].pixelsPerUnit, Is.EqualTo(32f));
|
|
Assert.That(frames[0].pivot, Is.EqualTo(new Vector2(50.5f,42.5f)));
|
|
Assert.That(frames[5].rect, Is.EqualTo(new Rect(500f,0f,100f,100f)));
|
|
var definition = AssetDatabase.LoadAssetAtPath<ActiveArtifactDefinition>(
|
|
"Assets/_Project/Constants/Artifacts/ThunderCrashArtifact.asset");
|
|
Assert.That(definition.NormalRange, Is.EqualTo(2f));
|
|
Assert.That(definition.ChargedRange, Is.EqualTo(3f));
|
|
foreach (float range in new[] { definition.NormalRange, definition.ChargedRange })
|
|
{
|
|
var owner = new GameObject("Thunder Single Sheet Test");
|
|
var renderer = (SpriteRenderer)type.GetMethod("CreateThunderCrashSprite", flags)
|
|
.Invoke(controller, new object[] { owner, frames, range });
|
|
Assert.That(owner.GetComponentsInChildren<SpriteRenderer>(), Has.Length.EqualTo(1));
|
|
Assert.That(renderer.transform.localPosition, Is.EqualTo(Vector3.zero));
|
|
AssertUniformScale(renderer.transform.localScale, 1.05f * range);
|
|
Assert.That(renderer.sortingOrder, Is.EqualTo(211));
|
|
var setFrame = type.GetMethod("SetThunderCrashSpriteFrame", flags);
|
|
setFrame.Invoke(controller, new object[] { renderer, frames, .02f });
|
|
Assert.That(renderer.sprite, Is.SameAs(frames[1]));
|
|
setFrame.Invoke(controller, new object[] { renderer, frames, .13f });
|
|
Assert.That(renderer.sprite, Is.SameAs(frames[5]));
|
|
setFrame.Invoke(controller, new object[] { renderer, frames, .16f });
|
|
Assert.That(renderer.enabled, Is.False);
|
|
Object.DestroyImmediate(owner);
|
|
}
|
|
type.GetMethod("DestroyThunderCrashFrames", flags).Invoke(controller, null);
|
|
Assert.That(field.GetValue(controller), Is.Null);
|
|
}
|
|
[Test]
|
|
public void ThunderCrashGroundFootprint_ContainsEveryVisibleGroundPixelCorner()
|
|
{
|
|
const string path = "Assets/_Project/Resources/Artifacts/ThunderCrash/ThunderCrash-Combined-v6.png";
|
|
var texture = AssetDatabase.LoadAssetAtPath<Texture2D>(path);
|
|
var pixels = texture.GetPixels32();
|
|
const BindingFlags flags = BindingFlags.Static | BindingFlags.NonPublic;
|
|
var type = typeof(ActiveArtifactController);
|
|
foreach (float range in new[] { 1f, 1.5f })
|
|
{
|
|
var center = (Vector2)type.GetMethod("GetThunderCrashHitCenterOffset", flags)
|
|
.Invoke(null, new object[] { range, Vector2.right });
|
|
var radii = (Vector2)type.GetMethod("GetThunderCrashHitRadii", flags)
|
|
.Invoke(null, new object[] { range, Vector2.right });
|
|
int samples = 0;
|
|
for (int frame = 0; frame < 6; frame++)
|
|
for (int y = 38; y < 48; y++) // Source ground rows 52..61 from top.
|
|
for (int x = 0; x < 100; x++)
|
|
{
|
|
if (pixels[y * texture.width + frame * 100 + x].a == 0) continue;
|
|
for (int dy = 0; dy <= 1; dy++)
|
|
for (int dx = 0; dx <= 1; dx++)
|
|
{
|
|
Vector2 point = (new Vector2(x + dx, y + dy)
|
|
- new Vector2(50.5f, 42.5f)) / 32f * (1.05f * range);
|
|
Vector2 offset = point - center;
|
|
float distance = offset.x * offset.x / (radii.x * radii.x)
|
|
+ offset.y * offset.y / (radii.y * radii.y);
|
|
Assert.That(distance, Is.LessThanOrEqualTo(1.0001f),
|
|
$"Visible ground pixel outside hit area: frame {frame}, ({x},{y}), range {range}");
|
|
samples++;
|
|
}
|
|
}
|
|
Assert.That(samples, Is.GreaterThan(0));
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void ThunderCrashDirection_UsesFacingAndAuthoredGroundAnchor()
|
|
{
|
|
GameObject actor = new("Thunder Direction Test");
|
|
try
|
|
{
|
|
actor.AddComponent<PlayerStats>();
|
|
actor.AddComponent<Rigidbody2D>();
|
|
actor.AddComponent<SpriteRenderer>();
|
|
actor.AddComponent<Animator>();
|
|
PlayerController player = actor.AddComponent<PlayerController>();
|
|
ActiveArtifactController artifacts =
|
|
actor.AddComponent<ActiveArtifactController>();
|
|
|
|
BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic;
|
|
FieldInfo facing = typeof(PlayerController).GetField(
|
|
"<FacingDirection>k__BackingField", flags);
|
|
FieldInfo moving = typeof(PlayerController).GetField(
|
|
"<MoveDirection>k__BackingField", flags);
|
|
MethodInfo resolveDirection = typeof(ActiveArtifactController).GetMethod(
|
|
"GetThunderCrashDirection", flags);
|
|
MethodInfo resolveOrigin = typeof(ActiveArtifactController).GetMethod(
|
|
"GetThunderCrashOrigin", flags);
|
|
typeof(ActiveArtifactController).GetField(
|
|
"playerController", flags).SetValue(artifacts, player);
|
|
typeof(ActiveArtifactController).GetField(
|
|
"body", flags).SetValue(
|
|
artifacts,
|
|
actor.GetComponent<Rigidbody2D>());
|
|
Assert.That(facing, Is.Not.Null);
|
|
Assert.That(moving, Is.Not.Null);
|
|
Assert.That(resolveDirection, Is.Not.Null);
|
|
Assert.That(resolveOrigin, Is.Not.Null);
|
|
|
|
Vector2[] directions =
|
|
{
|
|
Vector2.right,
|
|
Vector2.left,
|
|
Vector2.up,
|
|
Vector2.down,
|
|
new Vector2(1f, 1f).normalized,
|
|
new Vector2(-1f, 1f).normalized,
|
|
new Vector2(1f, -1f).normalized,
|
|
new Vector2(-1f, -1f).normalized,
|
|
};
|
|
foreach (Vector2 direction in directions)
|
|
{
|
|
facing.SetValue(player, direction);
|
|
// A held movement input can be changed while the cast
|
|
// locks movement; the cast must retain the facing value.
|
|
moving.SetValue(player, -direction);
|
|
Vector2 resolved = (Vector2)resolveDirection.Invoke(
|
|
artifacts, null);
|
|
Assert.That(
|
|
Vector2.Distance(resolved, direction),
|
|
Is.LessThan(0.0001f),
|
|
$"Thunder Crash direction changed for {direction}");
|
|
}
|
|
|
|
facing.SetValue(player, new Vector2(-0.17f, 0.98f));
|
|
moving.SetValue(player, Vector2.right);
|
|
Assert.That(
|
|
Vector2.Distance(
|
|
(Vector2)resolveDirection.Invoke(artifacts, null),
|
|
Vector2.up),
|
|
Is.LessThan(0.0001f));
|
|
|
|
Rigidbody2D body = actor.GetComponent<Rigidbody2D>();
|
|
CircleCollider2D collider = actor.AddComponent<CircleCollider2D>();
|
|
actor.transform.localScale = Vector3.one * 1.25f;
|
|
collider.offset = new Vector2(0.125f, -0.28f);
|
|
body.position = new Vector2(2.5f, -1.75f);
|
|
Vector2 expectedOrigin = body.position
|
|
+ (Vector2)actor.transform.TransformVector(
|
|
new Vector2(0f, -0.1f));
|
|
Vector2 resolvedOrigin = (Vector2)resolveOrigin.Invoke(
|
|
artifacts, null);
|
|
Assert.That(
|
|
Vector2.Distance(resolvedOrigin, expectedOrigin),
|
|
Is.LessThan(0.0001f));
|
|
|
|
collider.offset = new Vector2(-0.75f, 0.9f);
|
|
Vector2 offsetIndependentOrigin = (Vector2)resolveOrigin.Invoke(
|
|
artifacts, null);
|
|
Assert.That(
|
|
Vector2.Distance(offsetIndependentOrigin, expectedOrigin),
|
|
Is.LessThan(0.0001f));
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(actor);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void CycloneAndThunderFootprints_UseAuthoredEllipseBoundaries()
|
|
{
|
|
MethodInfo getCycloneOffset = typeof(ActiveArtifactController).GetMethod(
|
|
"GetCycloneHitCenterOffset",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
MethodInfo getCycloneRadii = typeof(ActiveArtifactController).GetMethod(
|
|
"GetCycloneHitRadii",
|
|
BindingFlags.Static | BindingFlags.NonPublic);
|
|
MethodInfo getThunderOffset = typeof(ActiveArtifactController).GetMethod(
|
|
"GetThunderCrashHitCenterOffset",
|
|
BindingFlags.Static | BindingFlags.NonPublic);
|
|
MethodInfo getThunderRadii = typeof(ActiveArtifactController).GetMethod(
|
|
"GetThunderCrashHitRadii",
|
|
BindingFlags.Static | BindingFlags.NonPublic);
|
|
MethodInfo distance = typeof(ActiveArtifactController).GetMethod(
|
|
"GetEllipseDistance",
|
|
BindingFlags.Static | BindingFlags.NonPublic);
|
|
|
|
owner.transform.localScale = Vector3.one * 1.25f;
|
|
Vector2 cycloneOffset = (Vector2)getCycloneOffset.Invoke(
|
|
controller, new object[] { 0.85f, false });
|
|
Vector2 cycloneRadii = (Vector2)getCycloneRadii.Invoke(
|
|
null, new object[] { 0.85f, false });
|
|
Vector2 chargedCycloneOffset = (Vector2)getCycloneOffset.Invoke(
|
|
controller, new object[] { 1.25f, true });
|
|
Vector2 chargedCycloneRadii = (Vector2)getCycloneRadii.Invoke(
|
|
null, new object[] { 1.25f, true });
|
|
|
|
Vector2 expectedCenterOffset = new Vector2(
|
|
0f,
|
|
-7.5f / 32f * 1.25f + 5f / 32f);
|
|
AssertVector2Close(cycloneOffset, expectedCenterOffset);
|
|
AssertVector2Close(chargedCycloneOffset, expectedCenterOffset);
|
|
AssertVector2Close(
|
|
cycloneRadii,
|
|
new Vector2(38.0625f, 15.03125f) / 32f);
|
|
AssertVector2Close(
|
|
chargedCycloneRadii,
|
|
new Vector2(46f, 18f) / 32f);
|
|
Assert.That(chargedCycloneRadii.x, Is.GreaterThan(cycloneRadii.x));
|
|
AssertEllipseBoundary(distance, cycloneRadii);
|
|
AssertEllipseBoundary(distance, chargedCycloneRadii);
|
|
|
|
Vector2[] directions =
|
|
{
|
|
Vector2.right,
|
|
Vector2.left,
|
|
Vector2.up,
|
|
Vector2.down,
|
|
new Vector2(1f, 1f),
|
|
new Vector2(-1f, 1f),
|
|
new Vector2(1f, -1f),
|
|
new Vector2(-1f, -1f),
|
|
};
|
|
float[] ranges = { 1f, 1.5f };
|
|
foreach (float range in ranges)
|
|
{
|
|
foreach (Vector2 direction in directions)
|
|
{
|
|
Vector2 thunderOffset = (Vector2)getThunderOffset.Invoke(
|
|
null,
|
|
new object[] { range, direction });
|
|
Vector2 thunderRadii = (Vector2)getThunderRadii.Invoke(
|
|
null,
|
|
new object[] { range, direction });
|
|
Vector2 expectedOffset = new Vector2(-0.5f, 0.5f)
|
|
/ 32f
|
|
* (1.05f * range);
|
|
Vector2 expectedRadii = new Vector2(17f, 7.25f)
|
|
/ 32f
|
|
* (1.05f * range);
|
|
|
|
AssertVector2Close(thunderOffset, expectedOffset);
|
|
AssertVector2Close(thunderRadii, expectedRadii);
|
|
AssertEllipseBoundary(distance, thunderRadii);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void ThunderCrashBodyContact_UsesExpandedEllipseAndRejectsOutsideBody()
|
|
{
|
|
MethodInfo getOffset = typeof(ActiveArtifactController).GetMethod(
|
|
"GetThunderCrashHitCenterOffset",
|
|
BindingFlags.Static | BindingFlags.NonPublic,
|
|
null,
|
|
new[] { typeof(float), typeof(Vector2) },
|
|
null);
|
|
MethodInfo getRadii = typeof(ActiveArtifactController).GetMethod(
|
|
"GetThunderCrashHitRadii",
|
|
BindingFlags.Static | BindingFlags.NonPublic,
|
|
null,
|
|
new[] { typeof(float), typeof(Vector2) },
|
|
null);
|
|
MethodInfo overlap = typeof(ActiveArtifactController).GetMethod(
|
|
"DoesBodyColliderOverlapEllipse",
|
|
BindingFlags.Static | BindingFlags.NonPublic);
|
|
MethodInfo distance = typeof(ActiveArtifactController).GetMethod(
|
|
"GetEllipseDistance",
|
|
BindingFlags.Static | BindingFlags.NonPublic);
|
|
Assert.That(getOffset, Is.Not.Null);
|
|
Assert.That(getRadii, Is.Not.Null);
|
|
Assert.That(overlap, Is.Not.Null);
|
|
Assert.That(distance, Is.Not.Null);
|
|
|
|
const float range = 1f;
|
|
Vector2 direction = Vector2.right;
|
|
Vector2 effectCenter = direction * 0.45f;
|
|
Vector2 hitCenter = effectCenter
|
|
+ (Vector2)getOffset.Invoke(
|
|
null,
|
|
new object[] { range, direction });
|
|
Vector2 hitRadii = (Vector2)getRadii.Invoke(
|
|
null,
|
|
new object[] { range, direction });
|
|
EnemyController enemy = CreateEnemyTarget(
|
|
"Thunder Crash Body Contact",
|
|
hitCenter + Vector2.right * (hitRadii.x + 0.1f));
|
|
CircleCollider2D collider = enemy.GetComponent<CircleCollider2D>();
|
|
collider.radius = 0.2f;
|
|
Physics2D.SyncTransforms();
|
|
|
|
Assert.That(
|
|
(float)distance.Invoke(
|
|
null,
|
|
new object[]
|
|
{
|
|
enemy.GroundAnchorPosition - hitCenter,
|
|
hitRadii,
|
|
}),
|
|
Is.GreaterThan(1f),
|
|
"The ground anchor is outside the authored Thunder ellipse.");
|
|
Assert.That(
|
|
(bool)overlap.Invoke(
|
|
null,
|
|
new object[] { enemy, hitCenter, hitRadii }),
|
|
Is.True,
|
|
"A body touching the expanded Thunder ellipse must hit.");
|
|
|
|
enemy.transform.position += Vector3.right * 0.4f;
|
|
Physics2D.SyncTransforms();
|
|
Assert.That(
|
|
(bool)overlap.Invoke(
|
|
null,
|
|
new object[] { enemy, hitCenter, hitRadii }),
|
|
Is.False,
|
|
"A body fully outside the expanded Thunder ellipse must miss.");
|
|
}
|
|
|
|
[Test]
|
|
public void ThunderCrashHitQuery_IncludesFrontContactAcrossEightDirections()
|
|
{
|
|
MethodInfo getOffset = typeof(ActiveArtifactController).GetMethod(
|
|
"GetThunderCrashHitCenterOffset",
|
|
BindingFlags.Static | BindingFlags.NonPublic,
|
|
null,
|
|
new[] { typeof(float), typeof(Vector2) },
|
|
null);
|
|
MethodInfo getRadii = typeof(ActiveArtifactController).GetMethod(
|
|
"GetThunderCrashHitRadii",
|
|
BindingFlags.Static | BindingFlags.NonPublic,
|
|
null,
|
|
new[] { typeof(float), typeof(Vector2) },
|
|
null);
|
|
MethodInfo hit = typeof(ActiveArtifactController).GetMethod(
|
|
"HitEnemiesInEllipse",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
ActiveArtifactDefinition definition = CreateThunderCrashDefinition(
|
|
"thunder_front_contact");
|
|
Vector2[] directions =
|
|
{
|
|
Vector2.right,
|
|
Vector2.left,
|
|
Vector2.up,
|
|
Vector2.down,
|
|
new Vector2(1f, 1f),
|
|
new Vector2(-1f, 1f),
|
|
new Vector2(1f, -1f),
|
|
new Vector2(-1f, -1f),
|
|
};
|
|
float[] ranges = { 1f, 1.5f };
|
|
|
|
try
|
|
{
|
|
int caseIndex = 0;
|
|
foreach (float range in ranges)
|
|
{
|
|
foreach (Vector2 direction in directions)
|
|
{
|
|
Vector2 normalized = direction.normalized;
|
|
Vector2 effectCenter = normalized * 0.45f;
|
|
Vector2 hitCenter = effectCenter
|
|
+ (Vector2)getOffset.Invoke(
|
|
null,
|
|
new object[] { range, direction });
|
|
Vector2 radii = (Vector2)getRadii.Invoke(
|
|
null,
|
|
new object[] { range, direction });
|
|
EnemyController enemy = CreateEnemyTarget(
|
|
$"Thunder Front Contact {caseIndex}",
|
|
hitCenter - normalized * (0.35f * range));
|
|
typeof(EnemyController)
|
|
.GetField(
|
|
"<CurrentHealth>k__BackingField",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)
|
|
?.SetValue(enemy, 100f);
|
|
try
|
|
{
|
|
Physics2D.SyncTransforms();
|
|
int hitCount = (int)hit.Invoke(
|
|
controller,
|
|
new object[]
|
|
{
|
|
definition,
|
|
hitCenter,
|
|
radii,
|
|
1f,
|
|
0f,
|
|
5,
|
|
false,
|
|
false,
|
|
1000 + caseIndex,
|
|
true,
|
|
});
|
|
Assert.That(
|
|
hitCount,
|
|
Is.EqualTo(1),
|
|
$"Direction {direction}, range {range}");
|
|
Assert.That(enemy.CurrentHealth, Is.EqualTo(99f).Within(0.001f));
|
|
Assert.That(enemy.BumpVulnerabilityRemaining, Is.GreaterThan(0f));
|
|
Assert.That(enemy.BumpVulnerabilityIncrease, Is.EqualTo(0.25f));
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(enemy.gameObject);
|
|
}
|
|
|
|
caseIndex++;
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(definition);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void ThunderCrashHitQuery_DeduplicatesMultipleCollidersPerEnemy()
|
|
{
|
|
ActiveArtifactDefinition definition = CreateThunderCrashDefinition(
|
|
"thunder_query");
|
|
EnemyController enemy = CreateEnemyTarget(
|
|
"Thunder Crash Duplicate Collider",
|
|
Vector2.zero);
|
|
typeof(EnemyController)
|
|
.GetField(
|
|
"<CurrentHealth>k__BackingField",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)
|
|
?.SetValue(enemy, 100f);
|
|
GameObject duplicate = new("Duplicate Collider");
|
|
duplicate.transform.SetParent(enemy.transform, false);
|
|
CircleCollider2D duplicateCollider =
|
|
duplicate.AddComponent<CircleCollider2D>();
|
|
duplicateCollider.radius = 0.25f;
|
|
Physics2D.SyncTransforms();
|
|
|
|
MethodInfo hit = typeof(ActiveArtifactController).GetMethod(
|
|
"HitEnemiesInEllipse",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
try
|
|
{
|
|
int hitCount = (int)hit.Invoke(
|
|
controller,
|
|
new object[]
|
|
{
|
|
definition,
|
|
Vector2.zero,
|
|
new Vector2(1f, 1f),
|
|
1f,
|
|
0f,
|
|
5,
|
|
false,
|
|
false,
|
|
123,
|
|
true,
|
|
});
|
|
Assert.That(hitCount, Is.EqualTo(1));
|
|
Assert.That(enemy.CurrentHealth, Is.EqualTo(99f).Within(0.001f));
|
|
Assert.That(enemy.BumpVulnerabilityRemaining, Is.GreaterThan(0f));
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(duplicate);
|
|
Object.DestroyImmediate(definition);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void ThunderCrashHitQuery_RejectsOutsideBodyWithoutDamage()
|
|
{
|
|
ActiveArtifactDefinition definition = CreateThunderCrashDefinition(
|
|
"thunder_outside");
|
|
EnemyController enemy = CreateEnemyTarget(
|
|
"Thunder Crash Outside Body",
|
|
new Vector2(0.68f, 0f));
|
|
CircleCollider2D collider = enemy.GetComponent<CircleCollider2D>();
|
|
collider.radius = 0.1f;
|
|
typeof(EnemyController)
|
|
.GetField(
|
|
"<CurrentHealth>k__BackingField",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)
|
|
?.SetValue(enemy, 100f);
|
|
Physics2D.SyncTransforms();
|
|
|
|
MethodInfo hit = typeof(ActiveArtifactController).GetMethod(
|
|
"HitEnemiesInEllipse",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
try
|
|
{
|
|
int hitCount = (int)hit.Invoke(
|
|
controller,
|
|
new object[]
|
|
{
|
|
definition,
|
|
Vector2.zero,
|
|
new Vector2(17f, 7.25f) / 32f * 1.05f,
|
|
1f,
|
|
0f,
|
|
5,
|
|
false,
|
|
false,
|
|
124,
|
|
true,
|
|
});
|
|
Assert.That(hitCount, Is.EqualTo(0));
|
|
Assert.That(enemy.CurrentHealth, Is.EqualTo(100f));
|
|
Assert.That(enemy.BumpVulnerabilityRemaining, Is.EqualTo(0f));
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(definition);
|
|
Object.DestroyImmediate(enemy.gameObject);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void ThunderCrashHitQuery_ContactsVisibleLowerGroundOverlap()
|
|
{
|
|
ActiveArtifactDefinition definition = CreateThunderCrashDefinition(
|
|
"thunder_lower_ground_contact");
|
|
Vector2 effectCenter = Vector2.right * 0.45f;
|
|
MethodInfo getOffset = typeof(ActiveArtifactController).GetMethod(
|
|
"GetThunderCrashHitCenterOffset",
|
|
BindingFlags.Static | BindingFlags.NonPublic,
|
|
null,
|
|
new[] { typeof(float), typeof(Vector2) },
|
|
null);
|
|
MethodInfo getRadii = typeof(ActiveArtifactController).GetMethod(
|
|
"GetThunderCrashHitRadii",
|
|
BindingFlags.Static | BindingFlags.NonPublic,
|
|
null,
|
|
new[] { typeof(float), typeof(Vector2) },
|
|
null);
|
|
Vector2 direction = Vector2.right;
|
|
Vector2 hitCenter = effectCenter
|
|
+ (Vector2)getOffset.Invoke(
|
|
null,
|
|
new object[] { 1f, direction });
|
|
Vector2 hitRadii = (Vector2)getRadii.Invoke(
|
|
null,
|
|
new object[] { 1f, direction });
|
|
EnemyController enemy = CreateEnemyTarget(
|
|
"Thunder Crash Lower Ground Contact",
|
|
new Vector2(hitCenter.x, effectCenter.y - 0.06f));
|
|
CircleCollider2D collider = enemy.GetComponent<CircleCollider2D>();
|
|
collider.offset = new Vector2(0f, -0.28f);
|
|
collider.radius = 0.22f;
|
|
typeof(EnemyController)
|
|
.GetField(
|
|
"<CurrentHealth>k__BackingField",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)
|
|
?.SetValue(enemy, 100f);
|
|
float colliderTop = enemy.transform.position.y
|
|
+ collider.offset.y
|
|
+ collider.radius;
|
|
float previousEllipseBottom = effectCenter.y
|
|
+ (1.5f - 4f) / 32f * 1.05f;
|
|
Assert.That(
|
|
colliderTop,
|
|
Is.LessThan(previousEllipseBottom),
|
|
"This target reproduces the visible lower-ground gap from the previous ellipse.");
|
|
Physics2D.SyncTransforms();
|
|
|
|
MethodInfo hit = typeof(ActiveArtifactController).GetMethod(
|
|
"HitEnemiesInEllipse",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
try
|
|
{
|
|
int hitCount = (int)hit.Invoke(
|
|
controller,
|
|
new object[]
|
|
{
|
|
definition,
|
|
hitCenter,
|
|
hitRadii,
|
|
1f,
|
|
0f,
|
|
5,
|
|
false,
|
|
false,
|
|
126,
|
|
true,
|
|
});
|
|
Assert.That(hitCount, Is.EqualTo(1));
|
|
Assert.That(enemy.CurrentHealth, Is.EqualTo(99f).Within(0.001f));
|
|
Assert.That(enemy.BumpVulnerabilityRemaining, Is.GreaterThan(0f));
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(definition);
|
|
Object.DestroyImmediate(enemy.gameObject);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void ThunderCrashWrongColorContact_IsShieldedWithoutDamage()
|
|
{
|
|
EnemyDefinition enemyDefinition =
|
|
CreateProtectedEnemyDefinition();
|
|
GameObject enemyObject = null;
|
|
ActiveArtifactDefinition definition = CreateThunderCrashDefinition(
|
|
"thunder_protected");
|
|
try
|
|
{
|
|
EnemyController enemy = CreateProtectedEnemy(
|
|
"Thunder Crash Protected Target",
|
|
enemyDefinition,
|
|
out enemyObject);
|
|
typeof(EnemyController)
|
|
.GetField(
|
|
"<CurrentHealth>k__BackingField",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)
|
|
?.SetValue(enemy, 100f);
|
|
Assert.That(enemy.IsDamageInvulnerable, Is.True);
|
|
Physics2D.SyncTransforms();
|
|
|
|
MethodInfo hit = typeof(ActiveArtifactController).GetMethod(
|
|
"HitEnemiesInEllipse",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
int hitCount = (int)hit.Invoke(
|
|
controller,
|
|
new object[]
|
|
{
|
|
definition,
|
|
Vector2.zero,
|
|
new Vector2(17f, 7.25f) / 32f * 1.05f,
|
|
1f,
|
|
0f,
|
|
5,
|
|
false,
|
|
false,
|
|
125,
|
|
true,
|
|
});
|
|
|
|
Assert.That(hitCount, Is.EqualTo(0));
|
|
Assert.That(enemy.CurrentHealth, Is.EqualTo(100f));
|
|
Assert.That(definition.ArtifactColor, Is.EqualTo(ArtifactColor.Blue));
|
|
Assert.That(enemy.ShieldColor, Is.EqualTo(ArtifactColor.Green));
|
|
Assert.That(enemy.IsGroggy, Is.False);
|
|
Assert.That(enemy.IsDamageInvulnerable, Is.True);
|
|
Assert.That(enemy.GroggyQualifyingContactCount, Is.Zero);
|
|
Assert.That(enemy.ShieldHitsRemaining, Is.EqualTo(1));
|
|
Assert.That(enemy.BumpVulnerabilityRemaining, Is.EqualTo(0f));
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(definition);
|
|
Object.DestroyImmediate(enemyObject);
|
|
Object.DestroyImmediate(enemyDefinition);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void CycloneBodyOverlap_PreservesTouchingAndVisibleGapBehavior()
|
|
{
|
|
EnemyController enemy = CreateEnemyTarget(
|
|
"Cyclone Geometry Target",
|
|
new Vector2(1.21f, 0f));
|
|
CircleCollider2D collider = enemy.GetComponent<CircleCollider2D>();
|
|
collider.radius = 0.2f;
|
|
Physics2D.SyncTransforms();
|
|
MethodInfo overlap = typeof(ActiveArtifactController).GetMethod(
|
|
"DoesBodyColliderOverlapEllipse",
|
|
BindingFlags.Static | BindingFlags.NonPublic);
|
|
|
|
Assert.That(
|
|
(bool)overlap.Invoke(
|
|
null,
|
|
new object[]
|
|
{
|
|
enemy,
|
|
Vector2.zero,
|
|
new Vector2(1f, 0.5f),
|
|
}),
|
|
Is.False,
|
|
"A root body with a visible gap must remain outside Cyclone.");
|
|
|
|
enemy.transform.position = new Vector2(1.2f, 0f);
|
|
Physics2D.SyncTransforms();
|
|
Assert.That(
|
|
(bool)overlap.Invoke(
|
|
null,
|
|
new object[]
|
|
{
|
|
enemy,
|
|
Vector2.zero,
|
|
new Vector2(1f, 0.5f),
|
|
}),
|
|
Is.True,
|
|
"A root body touching the authored ellipse boundary must hit.");
|
|
}
|
|
|
|
[Test]
|
|
public void CycloneFollowsAndThunderCrashRemainsFixedAtVisualRoot()
|
|
{
|
|
MethodInfo createVisual = typeof(ActiveArtifactController).GetMethod(
|
|
"CreateVisual",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
MethodInfo destroyVisual = typeof(ActiveArtifactController).GetMethod(
|
|
"DestroyVisual",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
Rigidbody2D body = owner.GetComponent<Rigidbody2D>();
|
|
body.position = new Vector2(2f, 3f);
|
|
|
|
GameObject cyclone = (GameObject)createVisual.Invoke(
|
|
controller, new object[] { "Cyclone Test", true });
|
|
GameObject thunder = (GameObject)createVisual.Invoke(
|
|
controller, new object[] { "Thunder Test", false });
|
|
try
|
|
{
|
|
Assert.That(cyclone.transform.parent, Is.EqualTo(owner.transform));
|
|
Assert.That(thunder.transform.parent, Is.Null);
|
|
Assert.That(thunder.transform.position,
|
|
Is.EqualTo(new Vector3(2f, 3f, 0f)));
|
|
|
|
body.position = new Vector2(4f, 3f);
|
|
Assert.That(cyclone.transform.parent, Is.EqualTo(owner.transform));
|
|
Assert.That(thunder.transform.position,
|
|
Is.EqualTo(new Vector3(2f, 3f, 0f)));
|
|
}
|
|
finally
|
|
{
|
|
destroyVisual.Invoke(controller, new object[] { cyclone });
|
|
destroyVisual.Invoke(controller, new object[] { thunder });
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void ChainLightningArc_MapsEndpointScaleAndExpiresAfterAuthoredDuration()
|
|
{
|
|
MethodInfo load = typeof(ActiveArtifactController).GetMethod(
|
|
"TryLoadChainLightningArcFrames",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
MethodInfo getFrame = typeof(ActiveArtifactController).GetMethod(
|
|
"GetChainLightningArcFrame",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
MethodInfo setProgress = typeof(ActiveArtifactController).GetMethod(
|
|
"SetChainLightningArcProgress",
|
|
BindingFlags.Static | BindingFlags.NonPublic);
|
|
Assert.That(load.Invoke(controller, null), Is.True);
|
|
Assert.That(getFrame.Invoke(controller, new object[] { 0.299f }), Is.Not.Null);
|
|
Assert.That(getFrame.Invoke(controller, new object[] { 0.3001f }), Is.Null);
|
|
|
|
GameObject segment = new("Arc Scale Test");
|
|
SpriteRenderer renderer = segment.AddComponent<SpriteRenderer>();
|
|
setProgress.Invoke(null, new object[] {
|
|
renderer,
|
|
new Vector2(1f, 2f),
|
|
new Vector2(3f, 2f),
|
|
Vector2.zero,
|
|
1f,
|
|
1.35f,
|
|
});
|
|
Assert.That(renderer.transform.localPosition, Is.EqualTo(new Vector3(1f, 2f, 0f)));
|
|
Assert.That(renderer.transform.localScale.x, Is.EqualTo(2f / (81f / 32f)).Within(0.0001f));
|
|
Assert.That(renderer.transform.localScale.y, Is.EqualTo(1.35f).Within(0.0001f));
|
|
Object.DestroyImmediate(segment);
|
|
}
|
|
|
|
[Test]
|
|
public void PulseRingPalette_VisibleBoundsMatchNormalAndChargedRanges()
|
|
{
|
|
GameObject visual = new GameObject("Pulse Palette Test");
|
|
try
|
|
{
|
|
SpriteRenderer renderer = visual.AddComponent<SpriteRenderer>();
|
|
MethodInfo configure = typeof(ActiveArtifactController).GetMethod(
|
|
"ConfigurePulseRingPaletteVisual",
|
|
BindingFlags.Instance | BindingFlags.NonPublic,
|
|
null,
|
|
new[] { typeof(SpriteRenderer), typeof(float), typeof(bool) },
|
|
null);
|
|
|
|
configure.Invoke(controller, new object[] { renderer, 1.25f, false });
|
|
Assert.That(renderer.transform.localScale.x, Is.EqualTo(1f));
|
|
Assert.That(renderer.transform.localScale.y, Is.EqualTo(1.0f * 32.5f / 33f).Within(0.0001f));
|
|
configure.Invoke(controller, new object[] { renderer, 2f, true });
|
|
Assert.That(renderer.transform.localScale.x, Is.EqualTo(1f));
|
|
Assert.That(renderer.transform.localScale.y, Is.EqualTo(1f).Within(0.0001f));
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(visual);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void PulseRingEllipse_UsesHorizontalRangeAndSpriteAspect()
|
|
{
|
|
MethodInfo distance = typeof(ActiveArtifactController).GetMethod(
|
|
"GetPulseEllipseDistance",
|
|
BindingFlags.Static | BindingFlags.NonPublic);
|
|
Assert.That(distance, Is.Not.Null);
|
|
|
|
float normal = (float)distance.Invoke(
|
|
null, new object[] { new Vector2(1.25f, 0f), 1.25f, 0.5078125f });
|
|
float normalVertical = (float)distance.Invoke(
|
|
null, new object[] { new Vector2(0f, 0.5078125f), 1.25f, 0.5078125f });
|
|
float normalOutside = (float)distance.Invoke(
|
|
null, new object[] { new Vector2(0f, 0.508f), 1.25f, 0.5078125f });
|
|
Assert.That(normal, Is.EqualTo(1f).Within(0.0001f));
|
|
Assert.That(normalVertical, Is.EqualTo(1f).Within(0.0001f));
|
|
Assert.That(normalOutside, Is.GreaterThan(1f));
|
|
|
|
float chargedDiagonal = (float)distance.Invoke(
|
|
null, new object[] { new Vector2(2f / Mathf.Sqrt(2f),
|
|
0.8125f / Mathf.Sqrt(2f)), 2f, 0.8125f });
|
|
Assert.That(chargedDiagonal, Is.EqualTo(1f).Within(0.0001f));
|
|
}
|
|
|
|
[Test]
|
|
public void PrototypeDefinitions_NormalAndChargedGaugeCostsMatch()
|
|
{
|
|
string[] paths =
|
|
{
|
|
"Assets/_Project/Constants/Artifacts/DashArtifact.asset",
|
|
"Assets/_Project/Constants/Artifacts/PulseArtifact.asset",
|
|
"Assets/_Project/Constants/Artifacts/PhoenixArtifact.asset",
|
|
"Assets/_Project/Constants/Artifacts/CycloneArtifact.asset",
|
|
"Assets/_Project/Constants/Artifacts/ThunderCrashArtifact.asset",
|
|
"Assets/_Project/Constants/Artifacts/ChainLightningArtifact.asset",
|
|
};
|
|
float[] expectedCosts = { 35f, 25f, 25f, 30f, 35f, 30f };
|
|
|
|
for (int i = 0; i < paths.Length; i++)
|
|
{
|
|
string path = paths[i];
|
|
ActiveArtifactDefinition definition =
|
|
AssetDatabase.LoadAssetAtPath<ActiveArtifactDefinition>(path);
|
|
|
|
Assert.That(definition, Is.Not.Null, path);
|
|
Assert.That(
|
|
definition.ChargedGaugeCost,
|
|
Is.EqualTo(definition.NormalGaugeCost),
|
|
definition.DisplayName);
|
|
Assert.That(
|
|
definition.NormalGaugeCost,
|
|
Is.EqualTo(expectedCosts[i]),
|
|
definition.DisplayName);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void ChainLightning_InitialTargetIsNearestRegardlessOfDirection()
|
|
{
|
|
EnemyController nearerBehind = CreateEnemyTarget(
|
|
"Nearer Behind",
|
|
new Vector2(-1f, 0f));
|
|
CreateEnemyTarget("Farther Ahead", new Vector2(2f, 0f));
|
|
Physics2D.SyncTransforms();
|
|
MethodInfo findInitialTarget = typeof(ActiveArtifactController)
|
|
.GetMethod(
|
|
"FindInitialChainTarget",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
|
|
EnemyController selected = (EnemyController)findInitialTarget.Invoke(
|
|
controller,
|
|
new object[] { Vector2.zero, 5f });
|
|
|
|
Assert.That(selected, Is.SameAs(nearerBehind));
|
|
}
|
|
|
|
[Test]
|
|
public void ChainLightningTargetSearch_UsesBodyOriginWhenTransformDiffers()
|
|
{
|
|
Rigidbody2D body = owner.GetComponent<Rigidbody2D>();
|
|
EnemyController bodySideTarget = null;
|
|
EnemyController transformSideTarget = null;
|
|
try
|
|
{
|
|
bodySideTarget = CreateEnemyTarget(
|
|
"Body Origin Target",
|
|
new Vector2(2.5f, 0f));
|
|
transformSideTarget = CreateEnemyTarget(
|
|
"Transform Origin Target",
|
|
new Vector2(-1.5f, 0f));
|
|
bodySideTarget.transform.SetParent(null);
|
|
transformSideTarget.transform.SetParent(null);
|
|
Physics2D.SyncTransforms();
|
|
|
|
owner.transform.position = new Vector2(-2f, 0f);
|
|
body.position = new Vector2(2f, 0f);
|
|
Assert.That(
|
|
(Vector2)owner.transform.position,
|
|
Is.Not.EqualTo(body.position));
|
|
|
|
MethodInfo getOrigin = typeof(ActiveArtifactController)
|
|
.GetMethod(
|
|
"GetCurrentArtifactOrigin",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
MethodInfo findInitialTarget = typeof(ActiveArtifactController)
|
|
.GetMethod(
|
|
"FindInitialChainTarget",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
Vector2 origin = (Vector2)getOrigin.Invoke(controller, null);
|
|
EnemyController selected =
|
|
(EnemyController)findInitialTarget.Invoke(
|
|
controller,
|
|
new object[] { origin, 1f });
|
|
|
|
Assert.That(origin, Is.EqualTo(body.position));
|
|
Assert.That(selected, Is.SameAs(bodySideTarget));
|
|
}
|
|
finally
|
|
{
|
|
if (bodySideTarget != null)
|
|
{
|
|
Object.DestroyImmediate(bodySideTarget.gameObject);
|
|
}
|
|
if (transformSideTarget != null)
|
|
{
|
|
Object.DestroyImmediate(transformSideTarget.gameObject);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void ProjectileProgress_UsesNormalizedExponentialAcceleration()
|
|
{
|
|
float start = ActiveArtifactController.EvaluateProjectileProgress(0f);
|
|
float firstQuarter =
|
|
ActiveArtifactController.EvaluateProjectileProgress(0.25f);
|
|
float midpoint =
|
|
ActiveArtifactController.EvaluateProjectileProgress(0.5f);
|
|
float thirdQuarter =
|
|
ActiveArtifactController.EvaluateProjectileProgress(0.75f);
|
|
float end = ActiveArtifactController.EvaluateProjectileProgress(1f);
|
|
|
|
Assert.That(start, Is.EqualTo(0f).Within(0.0001f));
|
|
Assert.That(end, Is.EqualTo(1f).Within(0.0001f));
|
|
Assert.That(midpoint, Is.EqualTo(0.2689f).Within(0.001f));
|
|
Assert.That(firstQuarter - start, Is.LessThan(midpoint - firstQuarter));
|
|
Assert.That(
|
|
midpoint - firstQuarter,
|
|
Is.LessThan(thirdQuarter - midpoint));
|
|
Assert.That(
|
|
thirdQuarter - midpoint,
|
|
Is.LessThan(end - thirdQuarter));
|
|
}
|
|
|
|
private RunManager CreateRunManager(RunMode mode)
|
|
{
|
|
bool previousGrantCatalogForTests =
|
|
ArtifactRewardController.GrantCatalogForTests;
|
|
ArtifactRewardController.GrantCatalogForTests = false;
|
|
try
|
|
{
|
|
runManagerObject = new GameObject("Artifact Charge Run Manager");
|
|
RunManager runManager = runManagerObject.AddComponent<RunManager>();
|
|
if (RunManager.Instance != runManager)
|
|
{
|
|
typeof(RunManager)
|
|
.GetMethod(
|
|
"Awake",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)
|
|
?.Invoke(runManager, null);
|
|
}
|
|
|
|
Assert.That(runManager.BeginRun(mode), Is.True);
|
|
return runManager;
|
|
}
|
|
finally
|
|
{
|
|
ArtifactRewardController.GrantCatalogForTests =
|
|
previousGrantCatalogForTests;
|
|
}
|
|
}
|
|
|
|
private void InvokeControllerMethod(string methodName)
|
|
{
|
|
typeof(ActiveArtifactController)
|
|
.GetMethod(
|
|
methodName,
|
|
BindingFlags.Instance | BindingFlags.NonPublic)
|
|
?.Invoke(controller, null);
|
|
}
|
|
|
|
private static void RestoreRunManagerInstance(RunManager runManager)
|
|
{
|
|
if (runManager == null)
|
|
{
|
|
runManager = null;
|
|
}
|
|
|
|
typeof(RunManager)
|
|
.GetProperty(
|
|
nameof(RunManager.Instance),
|
|
BindingFlags.Static | BindingFlags.Public)
|
|
?.GetSetMethod(true)
|
|
?.Invoke(null, new object[] { runManager });
|
|
}
|
|
|
|
private static ActiveArtifactDefinition CreatePulse(
|
|
string id,
|
|
float normalCost,
|
|
float chargedCost,
|
|
ActiveArtifactEffect effect = ActiveArtifactEffect.Pulse)
|
|
{
|
|
ActiveArtifactDefinition definition =
|
|
ScriptableObject.CreateInstance<ActiveArtifactDefinition>();
|
|
definition.Configure(
|
|
id,
|
|
id,
|
|
"O",
|
|
effect,
|
|
DamageTag.Collision,
|
|
Color.white,
|
|
Color.magenta,
|
|
normalCost,
|
|
chargedCost,
|
|
0.8f,
|
|
15f,
|
|
30f,
|
|
1.25f,
|
|
2f,
|
|
1.5f,
|
|
3f);
|
|
return definition;
|
|
}
|
|
|
|
private static ActiveArtifactDefinition CreateThunderCrashDefinition(
|
|
string id)
|
|
{
|
|
ActiveArtifactDefinition definition =
|
|
ScriptableObject.CreateInstance<ActiveArtifactDefinition>();
|
|
definition.Configure(
|
|
id,
|
|
id,
|
|
"TH",
|
|
ActiveArtifactEffect.ThunderCrash,
|
|
DamageTag.Collision,
|
|
Color.white,
|
|
Color.yellow,
|
|
0f,
|
|
0f,
|
|
0.9f,
|
|
4f,
|
|
6f,
|
|
1f,
|
|
1.5f,
|
|
0f,
|
|
0f,
|
|
normalEffectWidth: 1f,
|
|
chargedEffectWidth: 1.5f,
|
|
normalEffectDuration: 0.3f,
|
|
chargedEffectDuration: 0.55f,
|
|
multiHitInterval: 0.13f,
|
|
normalTargetLimit: 12,
|
|
chargedTargetLimit: 20,
|
|
normalInvulnerability: 0.16f,
|
|
chargedInvulnerability: 0.24f,
|
|
normalBumpVulnerabilityIncrease: 0.25f,
|
|
chargedBumpVulnerabilityIncrease: 0.4f,
|
|
normalBumpVulnerabilityDuration: 3f,
|
|
chargedBumpVulnerabilityDuration: 4f);
|
|
return definition;
|
|
}
|
|
|
|
private EnemyController CreateEnemyTarget(
|
|
string name,
|
|
Vector2 position)
|
|
{
|
|
GameObject enemy = new(name);
|
|
enemy.transform.SetParent(owner.transform);
|
|
enemy.transform.position = position;
|
|
enemy.AddComponent<CircleCollider2D>();
|
|
EnemyController controller = enemy.AddComponent<EnemyController>();
|
|
typeof(EnemyController)
|
|
.GetMethod("Awake", BindingFlags.Instance | BindingFlags.NonPublic)
|
|
?.Invoke(controller, null);
|
|
return controller;
|
|
}
|
|
|
|
private static void AssertEllipseBoundary(
|
|
MethodInfo distance,
|
|
Vector2 radii)
|
|
{
|
|
float inside = (float)distance.Invoke(
|
|
null, new object[] { new Vector2(radii.x * 0.5f, 0f), radii });
|
|
float edge = (float)distance.Invoke(
|
|
null, new object[] { new Vector2(radii.x, 0f), radii });
|
|
float outside = (float)distance.Invoke(
|
|
null, new object[] { new Vector2(radii.x * 1.001f, 0f), radii });
|
|
Assert.That(inside, Is.LessThan(1f));
|
|
Assert.That(edge, Is.EqualTo(1f).Within(0.0001f));
|
|
Assert.That(outside, Is.GreaterThan(1f));
|
|
}
|
|
|
|
private static void AssertVector2Close(Vector2 actual, Vector2 expected)
|
|
{
|
|
Assert.That(actual.x, Is.EqualTo(expected.x).Within(0.0001f));
|
|
Assert.That(actual.y, Is.EqualTo(expected.y).Within(0.0001f));
|
|
}
|
|
|
|
private static void AssertUniformScale(Vector3 actual, float expected)
|
|
{
|
|
Assert.That(actual.x, Is.EqualTo(expected).Within(0.0001f));
|
|
Assert.That(actual.y, Is.EqualTo(expected).Within(0.0001f));
|
|
Assert.That(actual.z, Is.EqualTo(expected).Within(0.0001f));
|
|
}
|
|
|
|
private static void DestroyDefinitions(
|
|
params ActiveArtifactDefinition[] definitions)
|
|
{
|
|
foreach (ActiveArtifactDefinition definition in definitions)
|
|
{
|
|
Object.DestroyImmediate(definition);
|
|
}
|
|
}
|
|
|
|
private static EnemyDefinition CreateProtectedEnemyDefinition()
|
|
{
|
|
EnemyDefinition definition =
|
|
ScriptableObject.CreateInstance<EnemyDefinition>();
|
|
definition.Configure(
|
|
EnemyKind.ArmoredSkeleton,
|
|
EnemyAttackShape.Box,
|
|
10f,
|
|
1f,
|
|
1f,
|
|
0.5f,
|
|
0.2f,
|
|
0.5f,
|
|
1f,
|
|
0f,
|
|
1f,
|
|
1f,
|
|
0f,
|
|
0);
|
|
definition.ConfigureRole(EnemyRole.Normal);
|
|
return definition;
|
|
}
|
|
|
|
private static EnemyController CreateProtectedEnemy(
|
|
string name,
|
|
EnemyDefinition definition,
|
|
out GameObject enemyObject)
|
|
{
|
|
enemyObject = new GameObject(name);
|
|
enemyObject.AddComponent<Rigidbody2D>();
|
|
enemyObject.AddComponent<CircleCollider2D>();
|
|
enemyObject.AddComponent<SpriteRenderer>();
|
|
EnemyController enemy = enemyObject.AddComponent<EnemyController>();
|
|
enemy.Configure(definition, null);
|
|
EnemyAttack enemyAttack = enemyObject.GetComponent<EnemyAttack>();
|
|
BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic;
|
|
enemyAttack.GetType().GetMethod("Awake", flags)?.Invoke(
|
|
enemyAttack,
|
|
null);
|
|
enemy.GetType().GetMethod("Awake", flags)?.Invoke(enemy, null);
|
|
return enemy;
|
|
}
|
|
}
|
|
|
|
public sealed class ProgressionAndRunFlowTests
|
|
{
|
|
private static readonly string[] ModifierPaths =
|
|
{
|
|
"Assets/_Project/Constants/LevelUp/CollisionDamage.asset",
|
|
"Assets/_Project/Constants/LevelUp/MoveSpeed.asset",
|
|
"Assets/_Project/Constants/LevelUp/MaxHealth.asset",
|
|
"Assets/_Project/Constants/LevelUp/ArtifactGaugeGain.asset",
|
|
};
|
|
|
|
[Test]
|
|
public void ModifierCatalog_IsDataDrivenAndUsesUniqueSourceIds()
|
|
{
|
|
HashSet<string> ids = new();
|
|
foreach (string path in ModifierPaths)
|
|
{
|
|
ModifierDefinition definition =
|
|
AssetDatabase.LoadAssetAtPath<ModifierDefinition>(path);
|
|
Assert.That(definition, Is.Not.Null, path);
|
|
Assert.That(ids.Add(definition.ModifierId), Is.True);
|
|
Assert.That(definition.DisplayName, Is.Not.Empty);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void ModifierDefinition_StopsAtConfiguredMaximumStacks()
|
|
{
|
|
GameObject owner = new("Modifier Test");
|
|
ModifierDefinition definition =
|
|
ScriptableObject.CreateInstance<ModifierDefinition>();
|
|
try
|
|
{
|
|
PlayerStats stats = owner.AddComponent<PlayerStats>();
|
|
definition.Configure(
|
|
"test.move-speed",
|
|
"Move speed",
|
|
CharacterStat.MoveSpeed,
|
|
ModifierOperation.Increased,
|
|
0.1f,
|
|
2);
|
|
|
|
Assert.That(definition.TryApply(stats, null), Is.True);
|
|
Assert.That(definition.TryApply(stats, null), Is.True);
|
|
Assert.That(definition.TryApply(stats, null), Is.False);
|
|
Assert.That(
|
|
stats.GetStackCount("test.move-speed", CharacterStat.MoveSpeed),
|
|
Is.EqualTo(2));
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(definition);
|
|
Object.DestroyImmediate(owner);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void FormalPlayerPrefab_UsesSixItemCatalogAndThreeColorOwnershipCap()
|
|
{
|
|
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(
|
|
"Assets/_Project/Prefabs/Player/Player.prefab");
|
|
ActiveArtifactController artifacts =
|
|
prefab.GetComponent<ActiveArtifactController>();
|
|
|
|
Assert.That(artifacts, Is.Not.Null);
|
|
Assert.That(artifacts.CatalogCount, Is.EqualTo(6));
|
|
Assert.That(artifacts.MaxOwnedArtifacts, Is.EqualTo(3));
|
|
Assert.That(artifacts.MaxArtifactsPerColor, Is.EqualTo(1));
|
|
Assert.That(artifacts.OwnedArtifactCount, Is.Zero);
|
|
|
|
Assert.That(GameplayConstants.Current.Artifacts.StartingGauge, Is.Zero);
|
|
Assert.That(
|
|
GameplayConstants.Current.Artifacts.NormalBumpGaugeGain,
|
|
Is.EqualTo(4f));
|
|
Assert.That(
|
|
GameplayConstants.Current.Artifacts.MovementGaugeGainPerSecond,
|
|
Is.EqualTo(2f));
|
|
}
|
|
|
|
[Test]
|
|
public void RunTimingAndEnemyTargets_UseFormalProductionValues()
|
|
{
|
|
Assert.That(
|
|
RunManager.GetEventTime(RunTimedEvent.Elite, false),
|
|
Is.EqualTo(180f));
|
|
Assert.That(
|
|
RunManager.GetEventTime(RunTimedEvent.MidBoss, false),
|
|
Is.EqualTo(600f));
|
|
Assert.That(
|
|
RunManager.GetEventTime(RunTimedEvent.FinalBoss, false),
|
|
Is.EqualTo(1200f));
|
|
Assert.That(
|
|
RunManager.GetEventTime(RunTimedEvent.Elite, true),
|
|
Is.EqualTo(36f));
|
|
Assert.That(
|
|
RunManager.GetEventTime(RunTimedEvent.MidBoss, true),
|
|
Is.EqualTo(120f));
|
|
Assert.That(
|
|
RunManager.GetEventTime(RunTimedEvent.FinalBoss, true),
|
|
Is.EqualTo(240f));
|
|
Assert.That(
|
|
RunManager.GetProgressionTime(60f, true),
|
|
Is.EqualTo(300f));
|
|
Assert.That(
|
|
RunManager.GetProgressionTime(60f, false),
|
|
Is.EqualTo(60f));
|
|
Assert.That(SpawnDirector.TargetAliveCountAt(0f), Is.EqualTo(6));
|
|
Assert.That(SpawnDirector.TargetAliveCountAt(60f), Is.EqualTo(10));
|
|
Assert.That(SpawnDirector.TargetAliveCountAt(180f), Is.EqualTo(16));
|
|
Assert.That(SpawnDirector.TargetAliveCountAt(300f), Is.EqualTo(24));
|
|
Assert.That(SpawnDirector.TargetAliveCountAt(600f), Is.EqualTo(32));
|
|
|
|
Assert.That(SpawnDirector.TargetCrowdAliveCountAt(0f), Is.EqualTo(3));
|
|
Assert.That(SpawnDirector.TargetCrowdAliveCountAt(60f), Is.EqualTo(6));
|
|
Assert.That(SpawnDirector.TargetCrowdAliveCountAt(180f), Is.EqualTo(12));
|
|
Assert.That(SpawnDirector.TargetCrowdAliveCountAt(300f), Is.EqualTo(19));
|
|
Assert.That(SpawnDirector.TargetCrowdAliveCountAt(600f), Is.EqualTo(27));
|
|
|
|
Assert.That(SpawnDirector.TargetNormalAliveCountAt(0f), Is.EqualTo(3));
|
|
Assert.That(SpawnDirector.TargetNormalAliveCountAt(60f), Is.EqualTo(4));
|
|
Assert.That(SpawnDirector.TargetNormalAliveCountAt(180f), Is.EqualTo(4));
|
|
Assert.That(SpawnDirector.TargetNormalAliveCountAt(300f), Is.EqualTo(5));
|
|
Assert.That(SpawnDirector.TargetNormalAliveCountAt(600f), Is.EqualTo(5));
|
|
}
|
|
|
|
[Test]
|
|
public void RunEventEnemies_UseDistinctCombatSequencesAndArtifactRoles()
|
|
{
|
|
GameObject owner = new("Run Event Tuning Test");
|
|
try
|
|
{
|
|
SpawnDirector director = owner.AddComponent<SpawnDirector>();
|
|
RunEventEnemyTuning elite =
|
|
director.GetEventEnemyTuning(RunTimedEvent.Elite);
|
|
RunEventEnemyTuning midBoss =
|
|
director.GetEventEnemyTuning(RunTimedEvent.MidBoss);
|
|
RunEventEnemyTuning finalBoss =
|
|
director.GetEventEnemyTuning(RunTimedEvent.FinalBoss);
|
|
|
|
Assert.That(elite.PrefabKind, Is.EqualTo(EnemyKind.ArmoredSkeleton));
|
|
Assert.That(elite.AttacksPerSequence, Is.EqualTo(2));
|
|
Assert.That(elite.CanBeLaunched, Is.True);
|
|
EnemyKind[] expectedKinds =
|
|
{
|
|
EnemyKind.ArmoredSkeleton,
|
|
EnemyKind.Werewolf,
|
|
EnemyKind.Werebear,
|
|
EnemyKind.ArmoredSkeleton,
|
|
EnemyKind.Werewolf,
|
|
EnemyKind.Werebear,
|
|
};
|
|
int[] expectedSequences = { 2, 2, 3, 2, 2, 3 };
|
|
for (int ordinal = 0; ordinal < expectedKinds.Length; ordinal++)
|
|
{
|
|
RunEventEnemyTuning scheduledElite =
|
|
director.GetEventEnemyTuning(RunTimedEvent.Elite, ordinal);
|
|
Assert.That(scheduledElite.PrefabKind, Is.EqualTo(expectedKinds[ordinal]));
|
|
Assert.That(scheduledElite.AttacksPerSequence, Is.EqualTo(expectedSequences[ordinal]));
|
|
}
|
|
|
|
Assert.That(
|
|
midBoss.PrefabKind == EnemyKind.GreatswordSkeleton
|
|
|| midBoss.PrefabKind == EnemyKind.NecroGolem,
|
|
Is.True,
|
|
"The mid-boss tuning must select one of the two requested kinds.");
|
|
Assert.That(midBoss.AttacksPerSequence, Is.EqualTo(3));
|
|
Assert.That(midBoss.CanBeLaunched, Is.False);
|
|
|
|
Assert.That(
|
|
finalBoss.PrefabKind,
|
|
Is.EqualTo(EnemyKind.Necromancer));
|
|
Assert.That(finalBoss.AttacksPerSequence, Is.GreaterThanOrEqualTo(1));
|
|
Assert.That(finalBoss.CanBeLaunched, Is.False);
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(owner);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void PrototypeEnemies_UseRequestedCrowdAndNormalCatalog()
|
|
{
|
|
string[] crowdNames = { "Bat", "Slime" };
|
|
foreach (string crowdName in crowdNames)
|
|
{
|
|
EnemyDefinition definition =
|
|
AssetDatabase.LoadAssetAtPath<EnemyDefinition>(
|
|
$"Assets/_Project/Constants/Enemies/{crowdName}.asset");
|
|
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(
|
|
$"Assets/_Project/Prefabs/Enemies/{crowdName}.prefab");
|
|
|
|
Assert.That(definition, Is.Not.Null, crowdName);
|
|
Assert.That(prefab, Is.Not.Null, crowdName);
|
|
Assert.That(definition.Role, Is.EqualTo(EnemyRole.Crowd), crowdName);
|
|
Assert.That(definition.MaxHealth, Is.GreaterThan(0f), crowdName);
|
|
Assert.That(prefab.transform.localScale.x, Is.EqualTo(1f), crowdName);
|
|
Assert.That(prefab.transform.localScale.y, Is.EqualTo(1f), crowdName);
|
|
Assert.That(prefab.GetComponent<Collider2D>().isTrigger, Is.True, crowdName);
|
|
}
|
|
|
|
string[] normalNames = { "Skeleton", "Necrofire", "SkeletonArcher" };
|
|
foreach (string normalName in normalNames)
|
|
{
|
|
EnemyDefinition definition =
|
|
AssetDatabase.LoadAssetAtPath<EnemyDefinition>(
|
|
$"Assets/_Project/Constants/Enemies/{normalName}.asset");
|
|
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(
|
|
$"Assets/_Project/Prefabs/Enemies/{normalName}.prefab");
|
|
|
|
Assert.That(definition, Is.Not.Null, normalName);
|
|
Assert.That(definition.Role, Is.EqualTo(EnemyRole.Normal), normalName);
|
|
Assert.That(prefab, Is.Not.Null, normalName);
|
|
Assert.That(prefab.transform.localScale.x, Is.EqualTo(1.25f).Within(0.0001f), normalName);
|
|
Assert.That(prefab.transform.localScale.y, Is.EqualTo(1.25f).Within(0.0001f), normalName);
|
|
Assert.That(prefab.GetComponent<Collider2D>().isTrigger, Is.False, normalName);
|
|
}
|
|
|
|
Assert.That(
|
|
AssetDatabase.LoadAssetAtPath<EnemyDefinition>(
|
|
"Assets/_Project/Constants/Enemies/Lancer.asset"),
|
|
Is.Not.Null,
|
|
"The legacy Lancer definition remains available for regressions.");
|
|
}
|
|
|
|
[Test]
|
|
public void CrowdAttackTokens_AreIndependentAndRateLimited()
|
|
{
|
|
GameObject managerObject = new("Attack Token Test");
|
|
GameObject firstCrowdObject = null;
|
|
GameObject secondCrowdObject = null;
|
|
GameObject normalObject = null;
|
|
EnemyDefinition crowdDefinition = null;
|
|
EnemyDefinition normalDefinition = null;
|
|
try
|
|
{
|
|
AttackTokenManager manager =
|
|
managerObject.AddComponent<AttackTokenManager>();
|
|
crowdDefinition = CreateEnemyDefinition(EnemyRole.Crowd);
|
|
normalDefinition = CreateEnemyDefinition(EnemyRole.Normal);
|
|
EnemyController firstCrowd = CreateEnemy(
|
|
"First Crowd",
|
|
crowdDefinition,
|
|
out firstCrowdObject);
|
|
EnemyController secondCrowd = CreateEnemy(
|
|
"Second Crowd",
|
|
crowdDefinition,
|
|
out secondCrowdObject);
|
|
EnemyController normal = CreateEnemy(
|
|
"Normal",
|
|
normalDefinition,
|
|
out normalObject);
|
|
|
|
Assert.That(manager.TryAcquire(firstCrowd), Is.True);
|
|
Assert.That(manager.TryAcquire(normal), Is.True);
|
|
Assert.That(manager.TryAcquire(secondCrowd), Is.False);
|
|
Assert.That(manager.ActiveCrowdAttackers, Is.EqualTo(1));
|
|
|
|
manager.Release(firstCrowd);
|
|
Assert.That(manager.TryAcquire(secondCrowd), Is.False);
|
|
Assert.That(manager.ActiveCrowdAttackers, Is.Zero);
|
|
Assert.That(
|
|
AttackTokenManager.GetCrowdAttackLimit(179f, 1, 2, 180f),
|
|
Is.EqualTo(1));
|
|
Assert.That(
|
|
AttackTokenManager.GetCrowdAttackLimit(180f, 1, 2, 180f),
|
|
Is.EqualTo(2));
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(firstCrowdObject);
|
|
Object.DestroyImmediate(secondCrowdObject);
|
|
Object.DestroyImmediate(normalObject);
|
|
Object.DestroyImmediate(crowdDefinition);
|
|
Object.DestroyImmediate(normalDefinition);
|
|
Object.DestroyImmediate(managerObject);
|
|
}
|
|
}
|
|
|
|
private static EnemyDefinition CreateEnemyDefinition(EnemyRole role)
|
|
{
|
|
EnemyDefinition definition =
|
|
ScriptableObject.CreateInstance<EnemyDefinition>();
|
|
definition.Configure(
|
|
EnemyKind.Skeleton,
|
|
EnemyAttackShape.Box,
|
|
10f,
|
|
1f,
|
|
1f,
|
|
0.5f,
|
|
0.2f,
|
|
0.5f,
|
|
1f,
|
|
0f,
|
|
1f,
|
|
1f,
|
|
0f,
|
|
0);
|
|
definition.ConfigureRole(role);
|
|
return definition;
|
|
}
|
|
|
|
private static EnemyController CreateEnemy(
|
|
string name,
|
|
EnemyDefinition definition,
|
|
out GameObject enemyObject)
|
|
{
|
|
enemyObject = new GameObject(name);
|
|
enemyObject.SetActive(false);
|
|
enemyObject.AddComponent<SpriteRenderer>();
|
|
enemyObject.AddComponent<Rigidbody2D>();
|
|
enemyObject.AddComponent<CircleCollider2D>();
|
|
EnemyController enemy = enemyObject.AddComponent<EnemyController>();
|
|
enemy.Configure(definition, null);
|
|
enemyObject.SetActive(true);
|
|
return enemy;
|
|
}
|
|
}
|
|
}
|
|
|
|
|