843 lines
37 KiB
C#
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,
|
|
};
|
|
}
|
|
}
|
|
}
|