538 lines
23 KiB
C#
538 lines
23 KiB
C#
using System.Collections;
|
|
using System.IO;
|
|
using System.Reflection;
|
|
using BumpCombat.Combat;
|
|
using BumpCombat.Core;
|
|
using BumpCombat.Enemies;
|
|
using BumpCombat.Player;
|
|
using BumpCombat.Progression;
|
|
using BumpCombat.Spawning;
|
|
using NUnit.Framework;
|
|
using UnityEngine;
|
|
using UnityEngine.SceneManagement;
|
|
using UnityEngine.TestTools;
|
|
|
|
namespace BumpCombat.PlayModeTests
|
|
{
|
|
public sealed class RunMissionTrackerPlayModeTests
|
|
{
|
|
private const BindingFlags NonPublicInstance =
|
|
BindingFlags.Instance | BindingFlags.NonPublic;
|
|
|
|
private bool previousGrantCatalogForTests;
|
|
private bool previousProductionModeForTests;
|
|
private float previousTimeScale;
|
|
private string isolatedProfilePath;
|
|
|
|
[SetUp]
|
|
public void SetUp()
|
|
{
|
|
previousGrantCatalogForTests = ArtifactRewardController.GrantCatalogForTests;
|
|
previousProductionModeForTests = RunManager.ForceProductionModeForTests;
|
|
previousTimeScale = Time.timeScale;
|
|
ArtifactRewardController.GrantCatalogForTests = true;
|
|
RunManager.ForceProductionModeForTests = false;
|
|
Time.timeScale = 1f;
|
|
isolatedProfilePath = Path.Combine(
|
|
Path.GetTempPath(),
|
|
"BumpCombatMissionTests",
|
|
System.Guid.NewGuid().ToString("N") + ".json");
|
|
}
|
|
|
|
[TearDown]
|
|
public void TearDown()
|
|
{
|
|
ArtifactRewardController.GrantCatalogForTests = previousGrantCatalogForTests;
|
|
RunManager.ForceProductionModeForTests = previousProductionModeForTests;
|
|
Time.timeScale = previousTimeScale;
|
|
if (!string.IsNullOrEmpty(isolatedProfilePath))
|
|
{
|
|
if (File.Exists(isolatedProfilePath))
|
|
{
|
|
File.Delete(isolatedProfilePath);
|
|
}
|
|
|
|
string temporaryPath = isolatedProfilePath + ".tmp";
|
|
if (File.Exists(temporaryPath))
|
|
{
|
|
File.Delete(temporaryPath);
|
|
}
|
|
|
|
string directory = Path.GetDirectoryName(isolatedProfilePath);
|
|
if (Directory.Exists(directory)
|
|
&& Directory.GetFiles(directory).Length == 0)
|
|
{
|
|
Directory.Delete(directory);
|
|
}
|
|
}
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator ConfirmedCombatEvents_CountOnceBySpawnAndActivation()
|
|
{
|
|
yield return LoadCombatScene();
|
|
RunMissionTracker tracker = FindAny<RunMissionTracker>();
|
|
RunManager runManager = RunManager.Instance;
|
|
ExperienceSystem experience = FindAny<ExperienceSystem>();
|
|
PlayerHealth health = FindAny<PlayerHealth>();
|
|
SpawnDirector director = FindAny<SpawnDirector>();
|
|
Assert.That(tracker, Is.Not.Null);
|
|
Assert.That(runManager, Is.Not.Null);
|
|
Assert.That(experience, Is.Not.Null);
|
|
Assert.That(health, Is.Not.Null);
|
|
Assert.That(director, Is.Not.Null);
|
|
tracker.UseProfileStoreForTests(new UserProfileStore(isolatedProfilePath));
|
|
director.enabled = false;
|
|
foreach (EnemyController existing in Object.FindObjectsByType<EnemyController>(
|
|
FindObjectsSortMode.None))
|
|
{
|
|
existing.gameObject.SetActive(false);
|
|
}
|
|
|
|
ActiveArtifactController artifacts = health.GetComponent<ActiveArtifactController>();
|
|
Assert.That(artifacts, Is.Not.Null);
|
|
for (int i = 0; i < artifacts.OwnedArtifactCount
|
|
&& artifacts.CurrentArtifact?.Effect != ActiveArtifactEffect.Pulse; i++)
|
|
{
|
|
artifacts.SelectNext();
|
|
}
|
|
Assert.That(artifacts.CurrentArtifact?.Effect, Is.EqualTo(ActiveArtifactEffect.Pulse),
|
|
"The development catalog should expose the real Pulse activation for integration coverage.");
|
|
|
|
EnemyController artifactTarget = SpawnEnemy(
|
|
director,
|
|
health.transform.position + Vector3.right * 0.75f,
|
|
disableImmediately: false);
|
|
yield return null;
|
|
artifactTarget.enabled = false;
|
|
Physics2D.SyncTransforms();
|
|
Assert.That(artifacts.TryUseCurrent(charged: false), Is.True,
|
|
"The real artifact activation should execute with available gauge.");
|
|
float artifactDeadline = Time.realtimeSinceStartup + 1f;
|
|
while (tracker.GetProgress(3).Progress == 0
|
|
&& Time.realtimeSinceStartup < artifactDeadline)
|
|
{
|
|
yield return null;
|
|
}
|
|
Assert.That(tracker.GetProgress(3).Progress, Is.EqualTo(1),
|
|
"An actual successful Pulse contact must reach the tracker event hook.");
|
|
yield return new WaitForSeconds(0.2f);
|
|
|
|
EnemyController wrongColorShield = SpawnEnemy(
|
|
director,
|
|
health.transform.position + Vector3.right * 0.8f,
|
|
EnemyKind.ArmoredSkeleton,
|
|
disableImmediately: false);
|
|
yield return null;
|
|
wrongColorShield.enabled = false;
|
|
ConfigureShieldForArtifactTest(wrongColorShield, ArtifactColor.Blue);
|
|
Assert.That(wrongColorShield.ShieldColor, Is.EqualTo(ArtifactColor.Blue));
|
|
Assert.That(wrongColorShield.ShieldHitsRemaining, Is.EqualTo(2));
|
|
Assert.That(tracker.GetProgress(8).Progress, Is.Zero);
|
|
Physics2D.SyncTransforms();
|
|
Assert.That(artifacts.TryUseCurrent(charged: false), Is.True);
|
|
yield return new WaitForSeconds(0.2f);
|
|
Assert.That(tracker.GetProgress(3).Progress, Is.EqualTo(1),
|
|
"A wrong-color shield contact is not an effective artifact hit.");
|
|
Assert.That(tracker.GetProgress(8).Progress, Is.Zero,
|
|
"A wrong-color shield contact does not decrement its required hits.");
|
|
Assert.That(wrongColorShield.ShieldHitsRemaining, Is.EqualTo(2),
|
|
"Wrong-color contacts leave the shield's required-hit counter unchanged.");
|
|
|
|
EnemyController matchingColorShield = SpawnEnemy(
|
|
director,
|
|
health.transform.position + Vector3.right * 0.8f,
|
|
EnemyKind.ArmoredSkeleton,
|
|
disableImmediately: false);
|
|
yield return null;
|
|
matchingColorShield.enabled = false;
|
|
ConfigureShieldForArtifactTest(matchingColorShield, ArtifactColor.Red);
|
|
Physics2D.SyncTransforms();
|
|
Assert.That(artifacts.TryUseCurrent(charged: false), Is.True);
|
|
yield return new WaitForSeconds(0.2f);
|
|
Assert.That(tracker.GetProgress(3).Progress, Is.EqualTo(2),
|
|
"A matching shield pip is an effective artifact activation.");
|
|
Assert.That(tracker.GetProgress(8).Progress, Is.EqualTo(1),
|
|
"Only its actual matching-color decrement advances the shield mission.");
|
|
|
|
EnemyController first = SpawnEnemy(director, Vector2.right * 3f);
|
|
EnemyController second = SpawnEnemy(director, Vector2.left * 3f);
|
|
|
|
CombatEvents.RaiseValidHit(CreateBumpHit(experience.gameObject, first.gameObject,
|
|
HitSide.Back, 0f));
|
|
Assert.That(tracker.GetProgress(2).Progress, Is.Zero,
|
|
"A contact that dealt no damage must not count as a successful bump.");
|
|
|
|
CombatHitResult rearHit = CreateBumpHit(
|
|
experience.gameObject,
|
|
first.gameObject,
|
|
HitSide.Back,
|
|
1f);
|
|
CombatEvents.RaiseValidHit(rearHit);
|
|
CombatEvents.RaiseValidHit(rearHit);
|
|
CombatEvents.RaiseValidHit(CreateBumpHit(
|
|
experience.gameObject,
|
|
second.gameObject,
|
|
HitSide.Back,
|
|
1f));
|
|
Assert.That(tracker.GetProgress(2).Progress, Is.EqualTo(3));
|
|
Assert.That(tracker.GetProgress(4).Progress, Is.EqualTo(2),
|
|
"Rear progress is once per enemy lifetime, while bump progress records each real hit.");
|
|
|
|
EnemyController lethal = SpawnEnemy(
|
|
director,
|
|
health.transform.position + Vector3.left * 4f,
|
|
disableImmediately: false);
|
|
yield return null;
|
|
Assert.That(lethal.CurrentHealth, Is.GreaterThan(0f));
|
|
SetPrivateField<ExperienceOrb>(lethal, "experienceOrbPrefab", null);
|
|
Assert.That(lethal.TryTakeDamage(
|
|
lethal.CurrentHealth + 1f,
|
|
Vector2.right,
|
|
0.1f), Is.True,
|
|
"A real lethal EnemyController hit should publish its death event.");
|
|
Assert.That(tracker.GetProgress(0).Progress, Is.EqualTo(1));
|
|
CombatEvents.RaiseEnemyDied(lethal.gameObject);
|
|
Assert.That(tracker.GetProgress(0).Progress, Is.EqualTo(1),
|
|
"A duplicate callback for that same spawn lifetime must not count twice.");
|
|
|
|
experience.AddExperience(1);
|
|
Assert.That(tracker.GetProgress(1).Progress, Is.Zero,
|
|
"Free mission XP must not masquerade as an orb pickup.");
|
|
|
|
GameObject orbObject = new("Mission test experience orb");
|
|
orbObject.transform.position = experience.transform.position;
|
|
ExperienceOrb orb = orbObject.AddComponent<ExperienceOrb>();
|
|
Rigidbody2D orbBody = orbObject.GetComponent<Rigidbody2D>();
|
|
orbBody.gravityScale = 0f;
|
|
CircleCollider2D orbCollider = orbObject.GetComponent<CircleCollider2D>();
|
|
orbCollider.isTrigger = true;
|
|
orb.Initialize(1);
|
|
yield return null;
|
|
yield return new WaitForFixedUpdate();
|
|
Assert.That(tracker.GetProgress(1).Progress, Is.EqualTo(1),
|
|
"A nearby orb should report one actual pickup through its normal physics path.");
|
|
|
|
health.GrantInvulnerability(2f);
|
|
health.TryTakeDamage(0f, Vector2.zero, 0f);
|
|
health.TryTakeDamage(5f, Vector2.right, 0.1f);
|
|
Assert.That(tracker.GetProgress(7).Progress, Is.Zero,
|
|
"Zero damage and ordinary invulnerability do not count as a guard block.");
|
|
Assert.That(health.TryActivateGuard(), Is.True);
|
|
Assert.That(health.TryTakeDamage(5f, Vector2.right, 0.1f), Is.False);
|
|
Assert.That(health.TryTakeDamage(5f, Vector2.right, 0.1f), Is.False);
|
|
Assert.That(tracker.GetProgress(7).Progress, Is.EqualTo(1),
|
|
"Multiple blocked hits from one guard activation count once.");
|
|
|
|
CombatEvents.RaiseArtifactEffectiveHit(
|
|
first.gameObject,
|
|
2001,
|
|
ActiveArtifactEffect.Pulse,
|
|
isCharged: true,
|
|
matchedShieldRequirement: false);
|
|
CombatEvents.RaiseArtifactEffectiveHit(
|
|
first.gameObject,
|
|
2001,
|
|
ActiveArtifactEffect.Pulse,
|
|
isCharged: true,
|
|
matchedShieldRequirement: false);
|
|
Assert.That(tracker.GetProgress(3).Progress, Is.EqualTo(3));
|
|
Assert.That(tracker.GetProgress(9).Progress, Is.EqualTo(1),
|
|
"One charged activation counts once even if it affects a target repeatedly.");
|
|
Assert.That(tracker.GetProgress(8).Progress, Is.EqualTo(1),
|
|
"Only the earlier actual matching-color shield decrement has counted.");
|
|
|
|
EnemyController[] crowdTargets = new EnemyController[5];
|
|
for (int i = 0; i < crowdTargets.Length; i++)
|
|
{
|
|
crowdTargets[i] = i == 0
|
|
? first
|
|
: SpawnEnemy(director, new Vector2(i + 5f, 0f));
|
|
CombatEvents.RaiseArtifactEffectiveHit(
|
|
crowdTargets[i].gameObject,
|
|
3001,
|
|
ActiveArtifactEffect.Cyclone,
|
|
isCharged: false,
|
|
matchedShieldRequirement: i == 0);
|
|
}
|
|
|
|
Assert.That(tracker.GetProgress(5).Progress, Is.EqualTo(5));
|
|
Assert.That(tracker.GetProgress(5).Completed, Is.True);
|
|
Assert.That(tracker.GetProgress(8).Progress, Is.EqualTo(2),
|
|
"Only a counted matching-color shield decrement advances the shield mission.");
|
|
Assert.That(tracker.GetLifetimeMissionCompletions(), Is.Zero,
|
|
"A development/test run must not write permanent mission records.");
|
|
LogAssert.NoUnexpectedReceived();
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator RampageWindowIncludesExactThreeSecondBoundary_AndGameOverStopsEvents()
|
|
{
|
|
yield return LoadCombatScene();
|
|
RunMissionTracker tracker = FindAny<RunMissionTracker>();
|
|
RunManager runManager = RunManager.Instance;
|
|
ExperienceSystem experience = FindAny<ExperienceSystem>();
|
|
SpawnDirector director = FindAny<SpawnDirector>();
|
|
Assert.That(tracker, Is.Not.Null);
|
|
Assert.That(runManager, Is.Not.Null);
|
|
Assert.That(experience, Is.Not.Null);
|
|
Assert.That(director, Is.Not.Null);
|
|
tracker.UseProfileStoreForTests(new UserProfileStore(isolatedProfilePath));
|
|
director.enabled = false;
|
|
foreach (EnemyController existing in Object.FindObjectsByType<EnemyController>(
|
|
FindObjectsSortMode.None))
|
|
{
|
|
existing.gameObject.SetActive(false);
|
|
}
|
|
|
|
SetAutoProperty(runManager, "ElapsedTime", 0f);
|
|
EnemyController first = SpawnEnemy(director, new Vector2(2f, 0f));
|
|
CombatEvents.RaiseEnemyDied(first.gameObject);
|
|
CombatEvents.RaiseEnemyDied(first.gameObject);
|
|
Assert.That(tracker.GetProgress(10).Progress, Is.EqualTo(1),
|
|
"A duplicate death callback from one spawn lifetime counts once.");
|
|
|
|
EnemyController summoned = SpawnEnemy(director, new Vector2(3f, 0f));
|
|
summoned.ConfigureSummonedEnemy(
|
|
null,
|
|
RunEventEnemyTuning.Create(
|
|
EnemyKind.Skeleton,
|
|
1f,
|
|
1,
|
|
1f,
|
|
Color.white,
|
|
1f,
|
|
1f,
|
|
1f,
|
|
1f,
|
|
1f,
|
|
1f,
|
|
1f,
|
|
1f,
|
|
1f,
|
|
1,
|
|
1f,
|
|
1f,
|
|
1,
|
|
1f,
|
|
true),
|
|
phaseProtected: false);
|
|
CombatEvents.RaiseEnemyDied(summoned.gameObject);
|
|
Assert.That(tracker.GetProgress(10).Progress, Is.EqualTo(1),
|
|
"Summons do not count toward the non-summoned kill streak.");
|
|
Assert.That(tracker.GetProgress(0).Progress, Is.EqualTo(1),
|
|
"Summons also do not count toward the first-sweep mission.");
|
|
|
|
for (int i = 0; i < 8; i++)
|
|
{
|
|
EnemyController enemy = SpawnEnemy(director, new Vector2(i + 4f, 0f));
|
|
CombatEvents.RaiseEnemyDied(enemy.gameObject);
|
|
}
|
|
Assert.That(tracker.GetProgress(10).Progress, Is.EqualTo(9));
|
|
|
|
SetAutoProperty(runManager, "ElapsedTime", 4f);
|
|
EnemyController tenth = SpawnEnemy(director, Vector2.left * 4f);
|
|
CombatEvents.RaiseEnemyDied(tenth.gameObject);
|
|
Assert.That(tracker.GetProgress(10).Progress, Is.EqualTo(1),
|
|
"Kills older than three combat seconds leave the sliding window.");
|
|
|
|
for (int i = 0; i < 8; i++)
|
|
{
|
|
SetAutoProperty(runManager, "ElapsedTime", 4.1f + i * 0.1f);
|
|
EnemyController enemy = SpawnEnemy(director, new Vector2(i + 6f, 0f));
|
|
CombatEvents.RaiseEnemyDied(enemy.gameObject);
|
|
}
|
|
Assert.That(tracker.GetProgress(10).Progress, Is.EqualTo(9));
|
|
|
|
SetAutoProperty(runManager, "ElapsedTime", 7f);
|
|
EnemyController final = SpawnEnemy(director, Vector2.left * 6f);
|
|
CombatEvents.RaiseEnemyDied(final.gameObject);
|
|
Assert.That(tracker.GetProgress(10).Progress, Is.EqualTo(10));
|
|
Assert.That(tracker.GetProgress(10).Completed, Is.True,
|
|
"A kill exactly three combat seconds after the oldest valid kill completes the inclusive window.");
|
|
|
|
runManager.EndRun();
|
|
int bumpBeforeTerminalEvents = tracker.GetProgress(2).Progress;
|
|
int orbBeforeTerminalEvents = tracker.GetProgress(1).Progress;
|
|
CombatEvents.RaiseValidHit(CreateBumpHit(
|
|
experience.gameObject,
|
|
tenth.gameObject,
|
|
HitSide.Front,
|
|
1f));
|
|
CombatEvents.RaiseArtifactEffectiveHit(
|
|
tenth.gameObject,
|
|
4001,
|
|
ActiveArtifactEffect.Pulse,
|
|
isCharged: false,
|
|
matchedShieldRequirement: false);
|
|
experience.CollectExperienceOrb(1);
|
|
|
|
Assert.That(tracker.GetProgress(2).Progress, Is.EqualTo(bumpBeforeTerminalEvents));
|
|
Assert.That(tracker.GetProgress(1).Progress, Is.EqualTo(orbBeforeTerminalEvents));
|
|
Assert.That(tracker.GetProgress(3).Progress, Is.Zero);
|
|
Assert.That(tracker.GetProgress(10).Progress, Is.EqualTo(10),
|
|
"Completed rampage progress must not decay after its success or after game over.");
|
|
LogAssert.NoUnexpectedReceived();
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator ProductionRun_PersistsMissionCompletionAndHonorsUnlockGates()
|
|
{
|
|
ArtifactRewardController.GrantCatalogForTests = false;
|
|
yield return LoadCombatScene();
|
|
RunMissionTracker tracker = FindAny<RunMissionTracker>();
|
|
RunManager runManager = RunManager.Instance;
|
|
ExperienceSystem experience = FindAny<ExperienceSystem>();
|
|
Assert.That(tracker, Is.Not.Null);
|
|
Assert.That(runManager, Is.Not.Null);
|
|
Assert.That(experience, Is.Not.Null);
|
|
Assert.That(runManager.IsTitleScreen, Is.True);
|
|
tracker.UseProfileStoreForTests(new UserProfileStore(isolatedProfilePath));
|
|
|
|
Assert.That(runManager.BeginRun(RunMode.Production), Is.True);
|
|
Assert.That(tracker.GetProgress(7).Locked, Is.True,
|
|
"Guard missions stay locked until the run unlocks guard.");
|
|
Assert.That(tracker.GetProgress(9).Locked, Is.True,
|
|
"Charged missions stay locked until the run unlocks charge.");
|
|
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
experience.CollectExperienceOrb(0);
|
|
}
|
|
|
|
Assert.That(tracker.GetProgress(1).Completed, Is.True);
|
|
Assert.That(tracker.GetLifetimeMissionCompletions(), Is.EqualTo(1));
|
|
Assert.That(File.Exists(isolatedProfilePath), Is.True);
|
|
string savedProfile = File.ReadAllText(isolatedProfilePath);
|
|
Assert.That(savedProfile, Does.Contain("mission.growth_footing"));
|
|
|
|
runManager.UnlockGuard();
|
|
runManager.UnlockArtifactCharge();
|
|
Assert.That(tracker.GetProgress(7).Locked, Is.False);
|
|
Assert.That(tracker.GetProgress(9).Locked, Is.False);
|
|
Assert.That(tracker.ProfileStore.Profile.achievements, Is.Empty,
|
|
"No uncommissioned achievements are synthesized into the profile.");
|
|
Assert.That(tracker.ProfileStore.Profile.unlockedSkinIds, Is.Empty);
|
|
LogAssert.NoUnexpectedReceived();
|
|
}
|
|
|
|
private static IEnumerator LoadCombatScene()
|
|
{
|
|
AsyncOperation load = SceneManager.LoadSceneAsync("CombatPrototype");
|
|
while (!load.isDone)
|
|
{
|
|
yield return null;
|
|
}
|
|
|
|
yield return null;
|
|
yield return null;
|
|
}
|
|
|
|
private static T FindAny<T>() where T : Object
|
|
{
|
|
return Object.FindAnyObjectByType<T>();
|
|
}
|
|
|
|
private static EnemyController SpawnEnemy(
|
|
SpawnDirector director,
|
|
Vector2 position,
|
|
EnemyKind kind = EnemyKind.Skeleton,
|
|
bool disableImmediately = true)
|
|
{
|
|
EnemyController prefab = FindCatalogPrefab(director, kind);
|
|
Assert.That(prefab, Is.Not.Null);
|
|
EnemyController enemy = Object.Instantiate(
|
|
prefab,
|
|
position,
|
|
Quaternion.identity);
|
|
if (disableImmediately)
|
|
{
|
|
enemy.enabled = false;
|
|
}
|
|
return enemy;
|
|
}
|
|
|
|
private static EnemyController FindCatalogPrefab(
|
|
SpawnDirector director,
|
|
EnemyKind kind)
|
|
{
|
|
string[] fieldNames =
|
|
{
|
|
"enemyPrefabs",
|
|
"elitePrefabs",
|
|
"midBossPrefabs",
|
|
"finalBossPrefabs",
|
|
};
|
|
foreach (string fieldName in fieldNames)
|
|
{
|
|
FieldInfo field = typeof(SpawnDirector).GetField(
|
|
fieldName,
|
|
NonPublicInstance);
|
|
EnemyController[] prefabs = field?.GetValue(director) as EnemyController[];
|
|
if (prefabs == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (EnemyController prefab in prefabs)
|
|
{
|
|
if (prefab != null
|
|
&& prefab.Definition != null
|
|
&& prefab.Definition.Kind == kind)
|
|
{
|
|
return prefab;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static CombatHitResult CreateBumpHit(
|
|
GameObject attacker,
|
|
GameObject target,
|
|
HitSide side,
|
|
float damage)
|
|
{
|
|
return new CombatHitResult(
|
|
attacker,
|
|
target,
|
|
false,
|
|
side,
|
|
damage,
|
|
Vector2.right,
|
|
1f,
|
|
target.transform.position);
|
|
}
|
|
|
|
private static void ConfigureShieldForArtifactTest(
|
|
EnemyController enemy,
|
|
ArtifactColor shieldColor)
|
|
{
|
|
SetPrivateField(enemy, "groggyTierConfigured", true);
|
|
SetPrivateField(enemy, "groggyTier", RunTimedEvent.Elite);
|
|
SetPrivateField(enemy, "requiredGroggyDistinctArtifacts", 1);
|
|
SetPrivateField(enemy, "requiredGroggyContactsPerArtifact", 2);
|
|
SetPrivateField(enemy, "groggyContactCount", 0);
|
|
SetPrivateField(enemy, "isGroggy", false);
|
|
SetPrivateField(enemy, "shieldColor", shieldColor);
|
|
}
|
|
|
|
private static void SetPrivateField<T>(object target, string fieldName, T value)
|
|
{
|
|
target.GetType()
|
|
.GetField(fieldName, NonPublicInstance)
|
|
?.SetValue(target, value);
|
|
}
|
|
|
|
private static void SetAutoProperty<T>(object target, string propertyName, T value)
|
|
{
|
|
target.GetType()
|
|
.GetField(
|
|
$"<{propertyName}>k__BackingField",
|
|
NonPublicInstance)
|
|
?.SetValue(target, value);
|
|
}
|
|
}
|
|
}
|