using System.Collections.Generic; using System.Reflection; using GeometryTD.CustomComponent; using GeometryTD.DataTable; using GeometryTD.Definition; using GeometryTD.UI; using NUnit.Framework; using UnityEngine; namespace GeometryTD.Tests.EditMode { public sealed class CombatSettlementServiceTests { private const int MaxParticipantTowerCount = 4; private GameObject _inventoryObject; private PlayerInventoryComponent _originalPlayerInventory; [TearDown] public void TearDown() { SetStaticPlayerInventory(_originalPlayerInventory); if (_inventoryObject != null) { Object.DestroyImmediate(_inventoryObject); _inventoryObject = null; } } [Test] public void BuildSettlementContext_Captures_CombatStart_Participant_Tower_Ids_And_Fixed_Endurance_Loss() { PlayerInventoryComponent inventoryComponent = CreateBoundPlayerInventory(CreateInventory(100f, 100f, 100f)); CombatRunResourceStore resourceStore = new CombatRunResourceStore(); DRLevel level = CreateLevel(baseHp: 100, startCoin: 20, rewardGold: 30); resourceStore.InitializeForCombat(level); BackpackInventoryData changedInventory = inventoryComponent.GetInventorySnapshot(); changedInventory.ParticipantTowerInstanceIds.Clear(); changedInventory.ParticipantTowerInstanceIds.Add(90003); inventoryComponent.ReplaceInventorySnapshot(changedInventory); CombatSettlementContext settlementContext = new CombatSettlementService().BuildSettlementContext( didCombatWin: false, currentLevel: level, defeatedEnemyCount: 4, resourceStore: resourceStore); CollectionAssert.AreEqual( new long[] { 90001, 90002 }, settlementContext.Result.Endurance.TargetTowerInstanceIds); Assert.That(settlementContext.Result.Endurance.EnduranceLossPerComponent, Is.EqualTo(1f)); Assert.That(settlementContext.Flags.ShouldOpenRewardSelection, Is.False); } [Test] public void CommitSettlementInventory_Reduces_Only_CombatStart_Participant_Towers() { PlayerInventoryComponent inventoryComponent = CreateBoundPlayerInventory(CreateInventory(100f, 100f, 100f)); CombatRunResourceStore resourceStore = new CombatRunResourceStore(); DRLevel level = CreateLevel(baseHp: 100, startCoin: 20, rewardGold: 30); resourceStore.InitializeForCombat(level); BackpackInventoryData changedInventory = inventoryComponent.GetInventorySnapshot(); changedInventory.ParticipantTowerInstanceIds.Clear(); changedInventory.ParticipantTowerInstanceIds.Add(90003); inventoryComponent.ReplaceInventorySnapshot(changedInventory); CombatSettlementService settlementService = new CombatSettlementService(); CombatSettlementContext settlementContext = settlementService.BuildSettlementContext( didCombatWin: false, currentLevel: level, defeatedEnemyCount: 2, resourceStore: resourceStore); settlementService.CommitSettlementInventory(settlementContext); BackpackInventoryData committedInventory = inventoryComponent.GetInventorySnapshot(); Assert.That(GetTowerComponents(committedInventory, 90001).Muzzle.Endurance, Is.EqualTo(99f)); Assert.That(GetTowerComponents(committedInventory, 90002).Muzzle.Endurance, Is.EqualTo(99f)); Assert.That(GetTowerComponents(committedInventory, 90003).Muzzle.Endurance, Is.EqualTo(100f)); Assert.That(settlementContext.Result.Endurance.AffectedTowerCount, Is.EqualTo(2)); } [Test] public void CommitSettlementInventory_Reduces_Endurance_To_Zero_And_Next_Validation_Fails() { PlayerInventoryComponent inventoryComponent = CreateBoundPlayerInventory(CreateInventory(1f, 100f, 100f)); CombatRunResourceStore resourceStore = new CombatRunResourceStore(); DRLevel level = CreateLevel(baseHp: 100, startCoin: 20, rewardGold: 30); resourceStore.InitializeForCombat(level); CombatSettlementService settlementService = new CombatSettlementService(); CombatSettlementContext settlementContext = settlementService.BuildSettlementContext( didCombatWin: true, currentLevel: level, defeatedEnemyCount: 6, resourceStore: resourceStore); settlementService.CommitSettlementInventory(settlementContext); BackpackInventoryData committedInventory = inventoryComponent.GetInventorySnapshot(); CombatParticipantTowerValidationSummary summary = CombatParticipantTowerValidationService.ValidateParticipantTowers(committedInventory); Assert.That(GetTowerComponents(committedInventory, 90001).Muzzle.Endurance, Is.EqualTo(0f)); Assert.That(summary.HasAnyValidParticipantTower, Is.True); Assert.That(summary.InvalidResults.Count, Is.EqualTo(1)); Assert.That( summary.InvalidResults[0].FailureReason, Is.EqualTo(CombatParticipantTowerValidationFailureReason.BrokenMuzzleComponent)); } [Test] public void BuildSettlementContext_On_Full_BaseHp_Win_Adds_BonusGold_And_Opens_RewardSelection() { CreateBoundPlayerInventory(CreateInventory(100f, 100f, 100f)); CombatRunResourceStore resourceStore = new CombatRunResourceStore(); DRLevel level = CreateLevel(baseHp: 100, startCoin: 20, rewardGold: 30); resourceStore.InitializeForCombat(level); CombatSettlementContext settlementContext = new CombatSettlementService().BuildSettlementContext( didCombatWin: true, currentLevel: level, defeatedEnemyCount: 5, resourceStore: resourceStore); Assert.That(settlementContext.Result.DidCombatWin, Is.True); Assert.That(settlementContext.Result.FinalBaseHp, Is.EqualTo(100)); Assert.That(settlementContext.Result.MaxBaseHp, Is.EqualTo(100)); Assert.That(settlementContext.Result.GainedGold, Is.EqualTo(39)); Assert.That(settlementContext.Result.RewardInventory.Gold, Is.EqualTo(39)); Assert.That(settlementContext.Summary.GainedGold, Is.EqualTo(39)); Assert.That(settlementContext.Flags.ShouldOpenRewardSelection, Is.True); Assert.That(settlementContext.Flags.DidEnterRewardSelection, Is.False); } [Test] public void BuildSettlementContext_On_High_BaseHp_Win_Adds_SettlementGold_Without_RewardSelection() { CreateBoundPlayerInventory(CreateInventory(100f, 100f, 100f)); CombatRunResourceStore resourceStore = new CombatRunResourceStore(); DRLevel level = CreateLevel(baseHp: 100, startCoin: 20, rewardGold: 30); resourceStore.InitializeForCombat(level); SetCurrentBaseHp(resourceStore, 90); CombatSettlementContext settlementContext = new CombatSettlementService().BuildSettlementContext( didCombatWin: true, currentLevel: level, defeatedEnemyCount: 3, resourceStore: resourceStore); Assert.That(settlementContext.Result.FinalBaseHp, Is.EqualTo(90)); Assert.That(settlementContext.Result.GainedGold, Is.EqualTo(33)); Assert.That(settlementContext.Result.RewardInventory.Gold, Is.EqualTo(33)); Assert.That(settlementContext.Flags.ShouldOpenRewardSelection, Is.False); } [Test] public void BuildSettlementContext_On_Loss_Keeps_DropGold_Without_SettlementBonus_Or_RewardSelection() { CreateBoundPlayerInventory(CreateInventory(100f, 100f, 100f)); CombatRunResourceStore resourceStore = new CombatRunResourceStore(); DRLevel level = CreateLevel(baseHp: 100, startCoin: 20, rewardGold: 30); resourceStore.InitializeForCombat(level); resourceStore.AddEnemyDefeatedReward(gainedCoin: 0, gainedGold: 7); CombatSettlementContext settlementContext = new CombatSettlementService().BuildSettlementContext( didCombatWin: false, currentLevel: level, defeatedEnemyCount: 3, resourceStore: resourceStore); Assert.That(settlementContext.Result.DidCombatWin, Is.False); Assert.That(settlementContext.Result.GainedGold, Is.EqualTo(7)); Assert.That(settlementContext.Result.RewardInventory.Gold, Is.EqualTo(7)); Assert.That(settlementContext.Flags.ShouldOpenRewardSelection, Is.False); } [Test] public void TryPrepareRewardSelection_Returns_False_When_Required_Dependency_Is_Missing() { CreateBoundPlayerInventory(CreateInventory(100f, 100f, 100f)); CombatSettlementContext settlementContext = new CombatSettlementContext(); settlementContext.Flags.ShouldOpenRewardSelection = true; bool prepared = new CombatSettlementService().TryPrepareRewardSelection( settlementContext, null, displayPhaseIndex: 1, themeType: LevelThemeType.Plain, rewardSelectFormUseCase: new RewardSelectFormUseCase(), onRewardSelected: _ => { }, onGiveUp: null); Assert.That(prepared, Is.False); Assert.That(settlementContext.Flags.ShouldOpenRewardSelection, Is.True); Assert.That(settlementContext.Flags.DidEnterRewardSelection, Is.False); } [Test] public void ApplySelectedReward_Appends_Selected_Component_To_RewardInventory() { CombatSettlementContext settlementContext = new CombatSettlementContext(); settlementContext.Result.RewardInventory = new BackpackInventoryData(); MuzzleCompItemData reward = new MuzzleCompItemData { InstanceId = 70001, Name = "奖励枪口" }; new CombatSettlementService().ApplySelectedReward( settlementContext, new RewardSelectItemRawData { RewardId = reward.InstanceId, SlotType = TowerCompSlotType.Muzzle, SourceItem = reward }); Assert.That(settlementContext.Result.RewardInventory.MuzzleComponents.Count, Is.EqualTo(1)); Assert.That(settlementContext.Result.RewardInventory.MuzzleComponents[0], Is.SameAs(reward)); Assert.That(settlementContext.Summary.RewardInventory, Is.SameAs(settlementContext.Result.RewardInventory)); } [Test] public void CommitSettlementInventory_Merges_Selected_Reward_Component_Into_PlayerInventory() { PlayerInventoryComponent inventoryComponent = CreateBoundPlayerInventory(CreateInventory(100f, 100f, 100f)); CombatRunResourceStore resourceStore = new CombatRunResourceStore(); DRLevel level = CreateLevel(baseHp: 100, startCoin: 20, rewardGold: 30); resourceStore.InitializeForCombat(level); int muzzleCountBeforeCommit = inventoryComponent.GetInventorySnapshot().MuzzleComponents.Count; CombatSettlementService settlementService = new CombatSettlementService(); CombatSettlementContext settlementContext = settlementService.BuildSettlementContext( didCombatWin: true, currentLevel: level, defeatedEnemyCount: 4, resourceStore: resourceStore); MuzzleCompItemData reward = new MuzzleCompItemData { InstanceId = 70002, Name = "结算奖励枪口" }; settlementService.ApplySelectedReward( settlementContext, new RewardSelectItemRawData { RewardId = reward.InstanceId, SlotType = TowerCompSlotType.Muzzle, SourceItem = reward }); settlementService.CommitSettlementInventory(settlementContext); BackpackInventoryData committedInventory = inventoryComponent.GetInventorySnapshot(); Assert.That(committedInventory.MuzzleComponents.Count, Is.EqualTo(muzzleCountBeforeCommit + 1)); MuzzleCompItemData mergedReward = committedInventory.MuzzleComponents[^1]; Assert.That(mergedReward.Name, Is.EqualTo("结算奖励枪口")); Assert.That(mergedReward.SlotType, Is.EqualTo(TowerCompSlotType.Muzzle)); Assert.That(mergedReward.InstanceId, Is.Not.EqualTo(70002)); Assert.That(committedInventory.Gold, Is.EqualTo(39)); Assert.That(GetTowerComponents(committedInventory, 90001).Muzzle.Endurance, Is.EqualTo(99f)); } [Test] public void CommitSettlementInventory_Is_Idempotent_After_First_Commit() { PlayerInventoryComponent inventoryComponent = CreateBoundPlayerInventory(CreateInventory(100f, 100f, 100f)); CombatRunResourceStore resourceStore = new CombatRunResourceStore(); DRLevel level = CreateLevel(baseHp: 100, startCoin: 20, rewardGold: 30); resourceStore.InitializeForCombat(level); CombatSettlementService settlementService = new CombatSettlementService(); CombatSettlementContext settlementContext = settlementService.BuildSettlementContext( didCombatWin: true, currentLevel: level, defeatedEnemyCount: 2, resourceStore: resourceStore); settlementService.CommitSettlementInventory(settlementContext); BackpackInventoryData firstCommittedInventory = inventoryComponent.GetInventorySnapshot(); float firstEndurance = GetTowerComponents(firstCommittedInventory, 90001).Muzzle.Endurance; int firstAffectedTowerCount = settlementContext.Result.Endurance.AffectedTowerCount; int firstGold = firstCommittedInventory.Gold; settlementService.CommitSettlementInventory(settlementContext); BackpackInventoryData secondCommittedInventory = inventoryComponent.GetInventorySnapshot(); Assert.That(settlementContext.Flags.IsCommitted, Is.True); Assert.That(GetTowerComponents(secondCommittedInventory, 90001).Muzzle.Endurance, Is.EqualTo(firstEndurance)); Assert.That(settlementContext.Result.Endurance.AffectedTowerCount, Is.EqualTo(firstAffectedTowerCount)); Assert.That(secondCommittedInventory.Gold, Is.EqualTo(firstGold)); } private static void SetCurrentBaseHp(CombatRunResourceStore resourceStore, int currentBaseHp) { FieldInfo backingField = typeof(CombatRunResourceStore).GetField( "k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic); Assert.That(backingField, Is.Not.Null); backingField.SetValue(resourceStore, currentBaseHp); } private PlayerInventoryComponent CreateBoundPlayerInventory(BackpackInventoryData inventory) { _originalPlayerInventory = GameEntry.PlayerInventory; _inventoryObject = new GameObject("TestPlayerInventory"); PlayerInventoryComponent inventoryComponent = _inventoryObject.AddComponent(); SetStaticPlayerInventory(inventoryComponent); inventoryComponent.ReplaceInventorySnapshot(inventory); return inventoryComponent; } private static void SetStaticPlayerInventory(PlayerInventoryComponent inventoryComponent) { FieldInfo backingField = typeof(GameEntry).GetField( "k__BackingField", BindingFlags.Static | BindingFlags.NonPublic); Assert.That(backingField, Is.Not.Null); backingField.SetValue(null, inventoryComponent); } private static DRLevel CreateLevel(int baseHp, int startCoin, int rewardGold) { DRLevel level = new DRLevel(); bool parsed = level.ParseDataRow( $"\t1\t测试关卡\tPlain\t{baseHp}\t{startCoin}\tWaveCount\t10\t{rewardGold}", null); Assert.That(parsed, Is.True); return level; } private static BackpackInventoryData CreateInventory( float firstTowerEndurance, float secondTowerEndurance, float thirdTowerEndurance) { BackpackInventoryData inventory = new BackpackInventoryData(); AddTower(inventory, 90001, 10001, 20001, 30001, firstTowerEndurance, isParticipant: true); AddTower(inventory, 90002, 10002, 20002, 30002, secondTowerEndurance, isParticipant: true); AddTower(inventory, 90003, 10003, 20003, 30003, thirdTowerEndurance, isParticipant: false); return inventory; } private static void AddTower( BackpackInventoryData inventory, long towerId, long muzzleId, long bearingId, long baseId, float endurance, bool isParticipant) { inventory.MuzzleComponents.Add(new MuzzleCompItemData { InstanceId = muzzleId, Endurance = endurance }); inventory.BearingComponents.Add(new BearingCompItemData { InstanceId = bearingId, Endurance = endurance }); inventory.BaseComponents.Add(new BaseCompItemData { InstanceId = baseId, Endurance = endurance }); inventory.Towers.Add(new TowerItemData { InstanceId = towerId, MuzzleComponentInstanceId = muzzleId, BearingComponentInstanceId = bearingId, BaseComponentInstanceId = baseId, IsParticipatingInCombat = isParticipant, Stats = new TowerStatsData() }); if (isParticipant && inventory.ParticipantTowerInstanceIds.Count < MaxParticipantTowerCount) { inventory.ParticipantTowerInstanceIds.Add(towerId); } } private static (MuzzleCompItemData Muzzle, BearingCompItemData Bearing, BaseCompItemData Base) GetTowerComponents( BackpackInventoryData inventory, long towerInstanceId) { TowerItemData tower = inventory.Towers.Find(item => item.InstanceId == towerInstanceId); Assert.That(tower, Is.Not.Null); MuzzleCompItemData muzzle = inventory.MuzzleComponents.Find(item => item.InstanceId == tower.MuzzleComponentInstanceId); BearingCompItemData bearing = inventory.BearingComponents.Find(item => item.InstanceId == tower.BearingComponentInstanceId); BaseCompItemData baseComp = inventory.BaseComponents.Find(item => item.InstanceId == tower.BaseComponentInstanceId); Assert.That(muzzle, Is.Not.Null); Assert.That(bearing, Is.Not.Null); Assert.That(baseComp, Is.Not.Null); return (muzzle, bearing, baseComp); } } }