Files
tiny_tackle_heroes/Assets/_Project/Tests/PlayMode/Defense25DRewardChoicePlayModeTests.cs
T
2026-09-28 11:07:17 +09:00

843 lines
37 KiB
C#

using System.Linq;
using BumpCombat.Constants;
using BumpCombat.Core;
using BumpCombat.Defense;
using BumpCombat.Defense.Comparison;
using BumpCombat.Player;
using BumpCombat.Progression;
using NUnit.Framework;
using UnityEngine;
namespace BumpCombat.PlayModeTests
{
public sealed class Defense25DRewardChoicePlayModeTests
{
[Test]
public void LevelUp_OffersMixedDistinctChoicesAndIgnoresDuplicateToken()
{
GameObject playerObject = new("Reward Choice Player");
ModifierDefinition playerModifier = CreatePlayerModifier("move-speed");
Defense25DRewardConstants constants = CreateConstants(includeEpic: true);
try
{
PlayerStats player = playerObject.AddComponent<PlayerStats>();
Defense25DTowerModifiers towerModifiers =
new(constants);
Defense25DRewardChoices choices = new(
constants,
new[] { playerModifier },
player,
towerModifiers,
_ => true);
Assert.That(choices.QueueReward(
Defense25DRewardSource.LevelUp, "level:2"), Is.True);
Assert.That(choices.QueueReward(
Defense25DRewardSource.LevelUp, "level:2"), Is.False);
Assert.That(choices.CurrentChoices, Has.Count.EqualTo(3));
Assert.That(choices.CurrentChoices.Any(choice =>
choice.TargetKind == Defense25DRewardTargetKind.PlayerModifier),
Is.True);
Assert.That(choices.CurrentChoices.Any(choice =>
choice.TargetKind == Defense25DRewardTargetKind.TowerModifier),
Is.True);
Assert.That(choices.CurrentChoices.Select(choice =>
(choice.TargetKind, choice.DefinitionId, choice.Rarity))
.Distinct().ToList(),
Has.Count.EqualTo(choices.CurrentChoices.Count));
}
finally
{
Object.DestroyImmediate(playerModifier);
Object.DestroyImmediate(constants);
Object.DestroyImmediate(playerObject);
}
}
[Test]
public void PlayerChoice_ShowsKoreanEffectAndRareValueThatMatchesAppliedModifier()
{
GameObject playerObject = new("Reward Display Player");
ModifierDefinition playerModifier = CreatePlayerModifier("move-speed");
Defense25DRewardConstants constants = CreateConstants(includeEpic: true);
constants.ChoiceCount = 1;
constants.LevelUpRarityWeights = new Defense25DRewardRarityWeights
{
Common = 0,
Rare = 100,
Epic = 0,
};
constants.TowerModifierCatalog = System.Array.Empty<Defense25DTowerModifierDefinition>();
try
{
PlayerStats player = playerObject.AddComponent<PlayerStats>();
Defense25DRewardChoices choices = new(
constants,
new[] { playerModifier },
player,
new Defense25DTowerModifiers(constants),
_ => false);
Assert.That(choices.QueueReward(
Defense25DRewardSource.LevelUp, "level:display"), Is.True);
Defense25DRewardChoice choice = choices.CurrentChoices.Single();
Assert.That(choice.Rarity, Is.EqualTo(Defense25DRewardRarity.Rare));
Assert.That(choice.DisplayData.Title, Is.EqualTo("이동 속도 강화"));
Assert.That(choice.DisplayData.Description,
Is.EqualTo("이동 속도 +30%\n선택하면 즉시 적용됩니다."));
Assert.That(choices.TryChooseReward(0, "level:display"), Is.True);
Assert.That(player.Evaluate(CharacterStat.MoveSpeed, 100f),
Is.EqualTo(130f).Within(0.001f));
}
finally
{
Object.DestroyImmediate(playerModifier);
Object.DestroyImmediate(constants);
Object.DestroyImmediate(playerObject);
}
}
[Test]
public void RefreshChoices_PreservesSnapshotAndReplacesOnlyAnIneligibleEntry()
{
GameObject playerObject = new("Refresh Snapshot Player");
ModifierDefinition[] definitions = Enumerable.Range(0, 4)
.Select(index => CreatePlayerModifier($"snapshot-{index}"))
.ToArray();
Defense25DRewardConstants constants = CreateConstants(includeEpic: true);
constants.TowerModifierCatalog = System.Array.Empty<Defense25DTowerModifierDefinition>();
constants.LevelUpRarityWeights = new Defense25DRewardRarityWeights
{
Common = 100,
Rare = 0,
Epic = 0,
};
try
{
PlayerStats player = playerObject.AddComponent<PlayerStats>();
Defense25DRewardChoices choices = new(
constants,
definitions,
player,
new Defense25DTowerModifiers(constants),
_ => false);
choices.QueueReward(Defense25DRewardSource.LevelUp, "level:snapshot");
Defense25DRewardChoice[] snapshot = choices.CurrentChoices.ToArray();
Assert.That(snapshot, Has.Length.EqualTo(3));
constants.LevelUpRarityWeights = new Defense25DRewardRarityWeights
{
Common = 0,
Rare = 0,
Epic = 100,
};
choices.RefreshChoices();
for (int index = 0; index < snapshot.Length; index++)
{
Assert.That(choices.CurrentChoices[index], Is.SameAs(snapshot[index]),
"A UI rebind retains each eligible token candidate and its order.");
}
ModifierDefinition invalidated = definitions.Single(definition =>
definition.ModifierId == snapshot[0].DefinitionId);
Assert.That(invalidated.TryApplyToModel(player, null), Is.True);
Assert.That(invalidated.TryApplyToModel(player, null), Is.True);
choices.RefreshChoices();
Assert.That(choices.CurrentChoices, Has.Count.EqualTo(3));
Assert.That(choices.CurrentChoices[0], Is.Not.SameAs(snapshot[0]));
Assert.That(choices.CurrentChoices[0].DefinitionId,
Is.Not.EqualTo(snapshot[0].DefinitionId));
Assert.That(choices.CurrentChoices[1], Is.SameAs(snapshot[1]));
Assert.That(choices.CurrentChoices[2], Is.SameAs(snapshot[2]));
}
finally
{
foreach (ModifierDefinition definition in definitions)
Object.DestroyImmediate(definition);
Object.DestroyImmediate(constants);
Object.DestroyImmediate(playerObject);
}
}
[Test]
public void EmptyRarityFallbacksOnlyMoveDownward()
{
Defense25DRewardConstants constants = CreateConstants(includeEpic: true);
constants.TowerModifierCatalog = new[]
{
TowerModifier("only-rare", DefenseTowerKind.Direct,
Defense25DTowerModifierEffect.AttackDamageIncreased,
Defense25DRewardRarity.Rare, 0.2f, 5),
};
try
{
constants.LevelUpRarityWeights = new Defense25DRewardRarityWeights
{
Common = 100,
Rare = 0,
Epic = 0,
};
Defense25DRewardChoices commonWithoutCommon = NewTowerOnlyChoices(constants);
commonWithoutCommon.QueueReward(
Defense25DRewardSource.LevelUp, "level:common-no-upgrade");
Assert.That(commonWithoutCommon.CurrentChoices, Is.Empty,
"A Common request does not upgrade to a populated Rare tier.");
constants.TowerModifierCatalog = new[]
{
TowerModifier("only-common", DefenseTowerKind.Direct,
Defense25DTowerModifierEffect.AttackDamageIncreased,
Defense25DRewardRarity.Common, 0.1f, 5),
};
constants.LevelUpRarityWeights = new Defense25DRewardRarityWeights
{
Common = 0,
Rare = 100,
Epic = 0,
};
Defense25DRewardChoices rareToCommon = NewTowerOnlyChoices(constants);
rareToCommon.QueueReward(
Defense25DRewardSource.LevelUp, "level:rare-to-common");
Assert.That(rareToCommon.CurrentChoices.Single().Rarity,
Is.EqualTo(Defense25DRewardRarity.Common));
constants.TowerModifierCatalog = new[]
{
TowerModifier("only-rare", DefenseTowerKind.Direct,
Defense25DTowerModifierEffect.AttackDamageIncreased,
Defense25DRewardRarity.Rare, 0.2f, 5),
};
constants.LevelUpRarityWeights = new Defense25DRewardRarityWeights
{
Common = 0,
Rare = 0,
Epic = 100,
};
Defense25DRewardChoices epicToRare = NewTowerOnlyChoices(constants);
epicToRare.QueueReward(
Defense25DRewardSource.LevelUp, "level:epic-to-rare");
Assert.That(epicToRare.CurrentChoices.Single().Rarity,
Is.EqualTo(Defense25DRewardRarity.Rare));
}
finally
{
Object.DestroyImmediate(constants);
}
}
[Test]
public void PendingLevelUpPreemptsUnopenedRewardsButNotAnOpenedReward()
{
GameObject playerObject = new("Reward Priority Player");
ModifierDefinition playerModifier = CreatePlayerModifier("priority");
Defense25DRewardConstants constants = CreateConstants(includeEpic: true);
constants.TowerModifierCatalog = System.Array.Empty<Defense25DTowerModifierDefinition>();
constants.LevelUpRarityWeights = new Defense25DRewardRarityWeights
{
Common = 100,
Rare = 0,
Epic = 0,
};
constants.GlobalEliteRarityWeights = constants.LevelUpRarityWeights;
try
{
PlayerStats player = playerObject.AddComponent<PlayerStats>();
Defense25DRewardChoices choices = new(
constants,
new[] { playerModifier },
player,
new Defense25DTowerModifiers(constants),
_ => false);
choices.QueueReward(Defense25DRewardSource.GlobalElite, "elite:pending");
Defense25DRewardChoice unopenedEliteChoice = choices.CurrentChoices[0];
choices.QueueReward(Defense25DRewardSource.LevelUp, "level:priority");
Assert.That(choices.CurrentRewardToken, Is.EqualTo("level:priority"));
Assert.That(choices.CurrentRewardIsOpened, Is.False);
Assert.That(choices.GetPendingRewardCount(
Defense25DRewardSource.GlobalElite), Is.EqualTo(1));
Assert.That(choices.TryChooseReward(0, "level:priority"), Is.True);
Assert.That(choices.CurrentRewardToken, Is.EqualTo("elite:pending"));
Assert.That(choices.CurrentChoices[0], Is.SameAs(unopenedEliteChoice),
"The preempted unopened reward retains its token snapshot.");
choices.Reset();
choices.QueueReward(Defense25DRewardSource.GlobalElite, "elite:opened");
Assert.That(choices.MarkCurrentRewardOpened(), Is.True);
choices.QueueReward(Defense25DRewardSource.LevelUp, "level:while-open");
Assert.That(choices.CurrentRewardToken, Is.EqualTo("elite:opened"));
Assert.That(choices.CurrentRewardIsOpened, Is.True);
Assert.That(choices.TryChooseReward(0, "elite:opened"), Is.True);
Assert.That(choices.CurrentRewardToken, Is.EqualTo("level:while-open"));
Assert.That(choices.CurrentRewardIsOpened, Is.False);
}
finally
{
Object.DestroyImmediate(playerModifier);
Object.DestroyImmediate(constants);
Object.DestroyImmediate(playerObject);
}
}
[Test]
public void MixedTargetGuaranteeIsResolvedBeforeEachTargetsRarityRoll()
{
GameObject playerObject = new("Target First Player");
ModifierDefinition playerModifier = CreatePlayerModifier("target-first");
Defense25DRewardConstants constants = CreateConstants(includeEpic: true);
constants.TowerModifierCatalog = new[]
{
TowerModifier("tower.epic.only", DefenseTowerKind.Direct,
Defense25DTowerModifierEffect.AttackDamageIncreased,
Defense25DRewardRarity.Epic, 0.35f, 1),
};
constants.LevelUpRarityWeights = new Defense25DRewardRarityWeights
{
Common = 80,
Rare = 0,
Epic = 20,
};
try
{
PlayerStats player = playerObject.AddComponent<PlayerStats>();
Defense25DRewardChoices choices = new(
constants,
new[] { playerModifier },
player,
new Defense25DTowerModifiers(constants),
_ => true);
for (int index = 0; index < 20; index++)
{
choices.Reset();
choices.QueueReward(
Defense25DRewardSource.LevelUp,
$"level:target-first-{index}");
Assert.That(choices.CurrentChoices.Select(choice => choice.TargetKind)
.Distinct().ToList(), Is.EquivalentTo(new[]
{
Defense25DRewardTargetKind.PlayerModifier,
Defense25DRewardTargetKind.TowerModifier,
}), "Both eligible target groups are selected before rarity is rolled.");
}
}
finally
{
Object.DestroyImmediate(playerModifier);
Object.DestroyImmediate(constants);
Object.DestroyImmediate(playerObject);
}
}
[Test]
public void MissionRelicRewardUsesTokenCandidatesAndCanResolveOnlyOnce()
{
Defense25DRewardConstants constants = CreateConstants(includeEpic: true);
constants.TowerModifierCatalog = System.Array.Empty<Defense25DTowerModifierDefinition>();
Defense25DRewardChoice[] candidates =
{
Defense25DRewardChoice.CreatePersonalMissionRelic(
"relic.first", "Relic", new Defense25DRewardDisplayData("First", "First relic")),
Defense25DRewardChoice.CreatePersonalMissionRelic(
"relic.second", "Relic", new Defense25DRewardDisplayData("Second", "Second relic")),
};
int resolved = 0;
try
{
Defense25DRewardChoices choices = new(
constants,
System.Array.Empty<ModifierDefinition>(),
null,
new Defense25DTowerModifiers(constants),
_ => false,
getMissionRelicCandidates: _ => candidates,
chooseMissionRelic: choice =>
{
resolved++;
return choice.DefinitionId == "relic.second";
});
Assert.That(choices.QueueReward(
Defense25DRewardSource.PersonalMissionRelic,
"mission:15:relic"), Is.True);
Assert.That(choices.QueueReward(
Defense25DRewardSource.PersonalMissionRelic,
"mission:15:relic"), Is.False);
Assert.That(choices.CurrentChoices, Has.Count.EqualTo(2));
Assert.That(choices.TryChooseReward(1, "mission:15:relic"), Is.True);
Assert.That(resolved, Is.EqualTo(1));
Assert.That(choices.TryChooseReward(1, "mission:15:relic"), Is.False);
Assert.That(choices.QueueReward(
Defense25DRewardSource.PersonalMissionRelic,
"mission:15:relic"), Is.False);
}
finally
{
Object.DestroyImmediate(constants);
}
}
[Test]
public void SameSeedAndTokenProduceSameCandidateSnapshot()
{
GameObject firstPlayerObject = new("Seeded Reward Player A");
GameObject secondPlayerObject = new("Seeded Reward Player B");
ModifierDefinition playerModifier = CreatePlayerModifier("seeded");
Defense25DRewardConstants constants = CreateConstants(includeEpic: true);
try
{
PlayerStats firstPlayer = firstPlayerObject.AddComponent<PlayerStats>();
PlayerStats secondPlayer = secondPlayerObject.AddComponent<PlayerStats>();
Defense25DRewardChoices first = new(
constants,
new[] { playerModifier },
firstPlayer,
new Defense25DTowerModifiers(constants),
_ => true);
Defense25DRewardChoices replay = new(
constants,
new[] { playerModifier },
secondPlayer,
new Defense25DTowerModifiers(constants),
_ => true);
first.QueueReward(Defense25DRewardSource.LevelUp, "level:seeded-replay");
replay.QueueReward(Defense25DRewardSource.LevelUp, "level:seeded-replay");
Assert.That(first.CurrentChoices.Select(choice =>
(choice.TargetKind, choice.DefinitionId, choice.Rarity)).ToArray(),
Is.EqualTo(replay.CurrentChoices.Select(choice =>
(choice.TargetKind, choice.DefinitionId, choice.Rarity)).ToArray()));
}
finally
{
Object.DestroyImmediate(playerModifier);
Object.DestroyImmediate(constants);
Object.DestroyImmediate(firstPlayerObject);
Object.DestroyImmediate(secondPlayerObject);
}
}
[Test]
public void GlobalMidBoss_OffersEpicAndFallsBackToCurrencyWhenNoEpicIsEligible()
{
GameObject playerObject = new("MidBoss Reward Player");
ModifierDefinition playerModifier = CreatePlayerModifier("collision-damage");
Defense25DRewardConstants constants = CreateConstants(includeEpic: true);
int grantedCurrency = 0;
int grantedAmount = 0;
try
{
PlayerStats player = playerObject.AddComponent<PlayerStats>();
Defense25DRewardChoices choices = new(
constants,
new[] { playerModifier },
player,
new Defense25DTowerModifiers(constants),
_ => true);
choices.QueueReward(
Defense25DRewardSource.GlobalMidBoss,
"midboss:240");
Assert.That(choices.CurrentChoices.Any(choice =>
choice.Rarity == Defense25DRewardRarity.Epic), Is.True);
constants.TowerModifierCatalog = constants.TowerModifierCatalog
.Where(definition => definition.Rarity != Defense25DRewardRarity.Epic)
.ToArray();
player.AddModifier(new StatModifier(
playerModifier.ModifierId,
playerModifier.Stat,
playerModifier.Operation,
playerModifier.Value,
playerModifier.MaximumStacks));
player.AddModifier(new StatModifier(
playerModifier.ModifierId,
playerModifier.Stat,
playerModifier.Operation,
playerModifier.Value,
playerModifier.MaximumStacks));
Defense25DRewardChoices noEpicChoices = new(
constants,
new[] { playerModifier },
player,
new Defense25DTowerModifiers(constants),
_ => true,
grantCurrency: amount =>
{
grantedCurrency++;
grantedAmount += amount;
});
noEpicChoices.QueueReward(
Defense25DRewardSource.GlobalMidBoss,
"midboss:no-epic");
Assert.That(noEpicChoices.CurrentChoices.Any(choice =>
choice.TargetKind == Defense25DRewardTargetKind.Currency),
Is.True);
int currencyChoiceIndex = noEpicChoices.CurrentChoices
.Select((choice, index) => (choice, index))
.Single(pair => pair.choice.TargetKind
== Defense25DRewardTargetKind.Currency).index;
Assert.That(noEpicChoices.TryChooseReward(
currencyChoiceIndex, "midboss:no-epic"), Is.True);
Assert.That(grantedCurrency, Is.EqualTo(1));
Assert.That(grantedAmount,
Is.EqualTo(constants.EmptyEpicFallbackCurrency));
Assert.That(noEpicChoices.TryChooseReward(
currencyChoiceIndex, "midboss:no-epic"), Is.False);
Assert.That(grantedCurrency, Is.EqualTo(1));
}
finally
{
Object.DestroyImmediate(playerModifier);
Object.DestroyImmediate(constants);
Object.DestroyImmediate(playerObject);
}
}
[TestCase(Defense25DRewardSource.LevelUp)]
[TestCase(Defense25DRewardSource.GlobalElite)]
public void EmptyModifierPoolOffersResolvableCurrencyFallback(
Defense25DRewardSource source)
{
Defense25DRewardConstants constants = CreateConstants(includeEpic: true);
constants.TowerModifierCatalog = System.Array.Empty<Defense25DTowerModifierDefinition>();
int grantCount = 0;
int grantedAmount = 0;
try
{
Defense25DRewardChoices choices = new(
constants,
System.Array.Empty<ModifierDefinition>(),
null,
new Defense25DTowerModifiers(constants),
_ => false,
grantCurrency: amount =>
{
grantCount++;
grantedAmount = amount;
});
Assert.That(choices.QueueReward(source, "empty:" + source), Is.True);
Assert.That(choices.CurrentChoices, Has.Count.EqualTo(1));
Assert.That(choices.CurrentChoices[0].TargetKind,
Is.EqualTo(Defense25DRewardTargetKind.Currency));
Assert.That(choices.CurrentChoices[0].DisplayData.Title,
Is.EqualTo("보상 대체"));
Assert.That(choices.CurrentChoices[0].DisplayData.Description,
Is.EqualTo("적격한 보상이 없어 재화 10개를 받습니다.\n선택하면 즉시 적용됩니다."));
Assert.That(choices.TryChooseReward(0, "empty:" + source), Is.True);
Assert.That(grantCount, Is.EqualTo(1));
Assert.That(grantedAmount,
Is.EqualTo(constants.EmptyEpicFallbackCurrency));
Assert.That(choices.HasPendingReward, Is.False);
}
finally
{
Object.DestroyImmediate(constants);
}
}
[Test]
public void EmptyRewardWithoutCurrencyCallbackAutomaticallyResolvesToken()
{
Defense25DRewardConstants constants = CreateConstants(includeEpic: true);
constants.TowerModifierCatalog = System.Array.Empty<Defense25DTowerModifierDefinition>();
try
{
Defense25DRewardChoices choices = new(
constants,
System.Array.Empty<ModifierDefinition>(),
null,
new Defense25DTowerModifiers(constants),
_ => false);
int autoResolvedCount = 0;
string autoResolvedToken = null;
choices.AutomaticallyResolved += (source, token) =>
{
Assert.That(source, Is.EqualTo(Defense25DRewardSource.LevelUp));
autoResolvedCount++;
autoResolvedToken = token;
};
Assert.That(choices.QueueReward(
Defense25DRewardSource.LevelUp, "empty:no-currency"), Is.True);
Assert.That(choices.HasPendingReward, Is.False);
Assert.That(choices.PendingRewardCount, Is.Zero);
Assert.That(autoResolvedCount, Is.EqualTo(1));
Assert.That(autoResolvedToken, Is.EqualTo("empty:no-currency"));
}
finally
{
Object.DestroyImmediate(constants);
}
}
[Test]
public void FullyInvalidatedOpenSnapshotFallsBackToCurrency()
{
GameObject playerObject = new("Invalidated Reward Player");
ModifierDefinition playerModifier = CreatePlayerModifier("invalidated");
Defense25DRewardConstants constants = CreateConstants(includeEpic: true);
constants.TowerModifierCatalog = System.Array.Empty<Defense25DTowerModifierDefinition>();
constants.LevelUpRarityWeights = new Defense25DRewardRarityWeights
{
Common = 100,
Rare = 0,
Epic = 0,
};
int grantedCurrency = 0;
try
{
PlayerStats player = playerObject.AddComponent<PlayerStats>();
Defense25DRewardChoices choices = new(
constants,
new[] { playerModifier },
player,
new Defense25DTowerModifiers(constants),
_ => false,
grantCurrency: amount => grantedCurrency += amount);
choices.QueueReward(Defense25DRewardSource.LevelUp, "stale:all-invalid");
Assert.That(choices.CurrentChoices, Has.Count.EqualTo(1));
Assert.That(choices.MarkCurrentRewardOpened(), Is.True);
for (int index = 0; index < playerModifier.MaximumStacks; index++)
{
Assert.That(playerModifier.TryApplyToModel(player, null), Is.True);
}
choices.RefreshChoices();
Assert.That(choices.CurrentRewardToken, Is.EqualTo("stale:all-invalid"));
Assert.That(choices.CurrentRewardIsOpened, Is.True);
Assert.That(choices.CurrentChoices, Has.Count.EqualTo(1));
Assert.That(choices.CurrentChoices[0].TargetKind,
Is.EqualTo(Defense25DRewardTargetKind.Currency));
Assert.That(choices.TryChooseReward(0, "stale:all-invalid"), Is.True);
Assert.That(grantedCurrency,
Is.EqualTo(constants.EmptyEpicFallbackCurrency));
Assert.That(choices.HasPendingReward, Is.False);
}
finally
{
Object.DestroyImmediate(playerModifier);
Object.DestroyImmediate(constants);
Object.DestroyImmediate(playerObject);
}
}
[Test]
public void ChoosingPlayerModifier_ChangesOnlyPlayerModelAndConsumesRewardOnce()
{
GameObject playerObject = new("Apply Reward Player");
ModifierDefinition playerModifier = CreatePlayerModifier("collision-damage");
Defense25DRewardConstants constants = CreateConstants(includeEpic: true);
try
{
PlayerStats player = playerObject.AddComponent<PlayerStats>();
Defense25DTowerModifiers towerModifiers = new(constants);
Defense25DRewardChoices choices = new(
constants,
new[] { playerModifier },
player,
towerModifiers,
_ => true);
choices.QueueReward(
Defense25DRewardSource.LevelUp,
"level:apply-once");
int playerChoiceIndex = choices.CurrentChoices.ToList()
.FindIndex(choice => choice.TargetKind
== Defense25DRewardTargetKind.PlayerModifier);
Assert.That(playerChoiceIndex, Is.GreaterThanOrEqualTo(0));
Assert.That(choices.TryChooseReward(playerChoiceIndex), Is.True);
Assert.That(player.Evaluate(CharacterStat.CollisionDamage, 10f),
Is.GreaterThan(10f));
Assert.That(towerModifiers.AppliedChoiceCount, Is.Zero);
Assert.That(choices.HasPendingReward, Is.False);
Assert.That(choices.TryChooseReward(playerChoiceIndex), Is.False);
}
finally
{
Object.DestroyImmediate(playerModifier);
Object.DestroyImmediate(constants);
Object.DestroyImmediate(playerObject);
}
}
[Test]
public void PlayerRarityVariantsShareStacksAndHealOnlyAfterSuccessfulApply()
{
GameObject playerObject = new("Rarity Variant Player");
ModifierDefinition playerModifier = CreatePlayerModifier("move-speed");
playerModifier.Configure(
"test.move-speed", "Move speed", CharacterStat.MoveSpeed,
ModifierOperation.Increased, 0.2f, 2, 6f);
Defense25DRewardConstants constants = CreateConstants(includeEpic: true);
int healCalls = 0;
float totalHeal = 0f;
try
{
constants.LevelUpRarityWeights = new Defense25DRewardRarityWeights
{
Common = 0,
Rare = 100,
Epic = 0,
};
PlayerStats player = playerObject.AddComponent<PlayerStats>();
Defense25DRewardChoices choices = new(
constants,
new[] { playerModifier },
player,
new Defense25DTowerModifiers(constants),
_ => false,
amount =>
{
healCalls++;
totalHeal += amount;
});
Assert.That(choices.QueueReward(
Defense25DRewardSource.LevelUp, "level:rare"), Is.True);
Assert.That(choices.CurrentChoices, Has.Count.EqualTo(1));
Assert.That(choices.CurrentChoices[0].Rarity,
Is.EqualTo(Defense25DRewardRarity.Rare));
Assert.That(choices.TryChooseReward(0), Is.True);
Assert.That(player.Evaluate(CharacterStat.MoveSpeed, 10f),
Is.EqualTo(13f).Within(0.001f));
Assert.That(healCalls, Is.EqualTo(1));
Assert.That(totalHeal, Is.EqualTo(9f).Within(0.001f));
constants.LevelUpRarityWeights = new Defense25DRewardRarityWeights
{
Common = 0,
Rare = 0,
Epic = 100,
};
Assert.That(playerModifier.CanApplyToModel(player), Is.True,
"A rare stack leaves one shared stack available to an Epic variant.");
Assert.That(choices.QueueReward(
Defense25DRewardSource.LevelUp, "level:epic"), Is.True);
Assert.That(choices.CurrentChoices[0].Rarity,
Is.EqualTo(Defense25DRewardRarity.Epic));
Assert.That(choices.TryChooseReward(0), Is.True);
Assert.That(player.Evaluate(CharacterStat.MoveSpeed, 10f),
Is.EqualTo(17f).Within(0.001f));
Assert.That(healCalls, Is.EqualTo(2));
Assert.That(totalHeal, Is.EqualTo(21f).Within(0.001f));
Assert.That(playerModifier.TryApplyToModel(
player,
amount =>
{
healCalls++;
totalHeal += amount;
},
1.5f), Is.False,
"The shared cap rejects a later rarity variant.");
Assert.That(healCalls, Is.EqualTo(2));
Assert.That(totalHeal, Is.EqualTo(21f).Within(0.001f));
}
finally
{
Object.DestroyImmediate(playerModifier);
Object.DestroyImmediate(constants);
Object.DestroyImmediate(playerObject);
}
}
private static ModifierDefinition CreatePlayerModifier(string id)
{
ModifierDefinition definition =
ScriptableObject.CreateInstance<ModifierDefinition>();
definition.Configure(
$"test.{id}",
id,
id == "move-speed"
? CharacterStat.MoveSpeed
: CharacterStat.CollisionDamage,
ModifierOperation.Increased,
0.2f,
2);
return definition;
}
private static Defense25DRewardChoices NewTowerOnlyChoices(
Defense25DRewardConstants constants)
{
return new Defense25DRewardChoices(
constants,
System.Array.Empty<ModifierDefinition>(),
null,
new Defense25DTowerModifiers(constants),
_ => true);
}
private static Defense25DRewardConstants CreateConstants(bool includeEpic)
{
Defense25DRewardConstants constants =
ScriptableObject.CreateInstance<Defense25DRewardConstants>();
constants.RunSeed = 137;
constants.ChoiceCount = 3;
constants.LevelUpRarityWeights = new Defense25DRewardRarityWeights
{
Common = 75,
Rare = 20,
Epic = 5,
};
constants.GlobalEliteRarityWeights = new Defense25DRewardRarityWeights
{
Common = 60,
Rare = 30,
Epic = 10,
};
constants.EmptyEpicFallbackCurrency = 10;
constants.TowerModifierCatalog = new[]
{
TowerModifier("fire.attack.common", DefenseTowerKind.Direct,
Defense25DTowerModifierEffect.AttackDamageIncreased,
Defense25DRewardRarity.Common, 0.1f, 5),
TowerModifier("fire.attack.rare", DefenseTowerKind.Direct,
Defense25DTowerModifierEffect.AttackDamageIncreased,
Defense25DRewardRarity.Rare, 0.2f, 5),
TowerModifier("fire.attack.epic", DefenseTowerKind.Direct,
Defense25DTowerModifierEffect.AttackDamageIncreased,
Defense25DRewardRarity.Epic, 0.35f, 5),
TowerModifier("lightning.chain.rare", DefenseTowerKind.Lightning,
Defense25DTowerModifierEffect.ChainCount,
Defense25DRewardRarity.Rare, 1f, 3),
TowerModifier("cold.projectile.epic", DefenseTowerKind.Cold,
Defense25DTowerModifierEffect.AdditionalProjectile,
Defense25DRewardRarity.Epic, 1f, 2),
}.Where(definition => includeEpic
|| definition.Rarity != Defense25DRewardRarity.Epic).ToArray();
return constants;
}
private static Defense25DTowerModifierDefinition TowerModifier(
string id,
DefenseTowerKind family,
Defense25DTowerModifierEffect effect,
Defense25DRewardRarity rarity,
float value,
int maximumStacks)
{
return new Defense25DTowerModifierDefinition
{
DefinitionId = id,
DisplayName = id,
Description = id,
Family = family,
Effect = effect,
Rarity = rarity,
Value = value,
MaximumStacks = maximumStacks,
};
}
}
}