S6 回归与文档收尾

This commit is contained in:
SepComet
2026-03-11 21:19:53 +08:00
parent ac1c91c1b4
commit 09ebe6e3f3
20 changed files with 1498 additions and 136 deletions
@@ -3,6 +3,7 @@ using System.Reflection;
using GeometryTD.CustomComponent;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using GeometryTD.UI;
using NUnit.Framework;
using UnityEngine;
@@ -110,6 +111,198 @@ namespace GeometryTD.Tests.EditMode
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(
"<CurrentBaseHp>k__BackingField",
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(backingField, Is.Not.Null);
backingField.SetValue(resourceStore, currentBaseHp);
}
private PlayerInventoryComponent CreateBoundPlayerInventory(BackpackInventoryData inventory)
{
_originalPlayerInventory = GameEntry.PlayerInventory;
@@ -175,6 +175,20 @@ namespace GeometryTD.Tests.EditMode
TagGenerationRuleRegistry.ResetToDefaults();
}
[Test]
public void TagGenerationRuleRegistry_Defaults_Align_With_Current_TagTable_Baseline()
{
TagGenerationRuleRegistry.ResetToDefaults();
Assert.That(TagGenerationRuleRegistry.TryGetRule(TagType.Ice, out TagGenerationRule iceRule), Is.True);
Assert.That(iceRule.MinRarity, Is.EqualTo(RarityType.White));
Assert.That(iceRule.Weight, Is.EqualTo(1));
Assert.That(TagGenerationRuleRegistry.TryGetRule(TagType.AbsoluteZero, out TagGenerationRule absoluteZeroRule), Is.True);
Assert.That(absoluteZeroRule.MinRarity, Is.EqualTo(RarityType.Purple));
Assert.That(absoluteZeroRule.Weight, Is.EqualTo(1));
}
[Test]
public void ResolveRarityTagBudget_Uses_TableDriven_Range_Rules()
{
@@ -238,6 +252,28 @@ namespace GeometryTD.Tests.EditMode
RarityTagBudgetRuleRegistry.ResetToDefaults();
}
[Test]
public void RarityTagBudgetRuleRegistry_Defaults_Align_With_Current_BudgetTable_Baseline()
{
RarityTagBudgetRuleRegistry.ResetToDefaults();
Assert.That(RarityTagBudgetRuleRegistry.TryGetRule(RarityType.Green, out RarityTagBudgetRule greenRule), Is.True);
Assert.That(greenRule.MinCount, Is.EqualTo(0));
Assert.That(greenRule.MaxCount, Is.EqualTo(2));
Assert.That(RarityTagBudgetRuleRegistry.TryGetRule(RarityType.Blue, out RarityTagBudgetRule blueRule), Is.True);
Assert.That(blueRule.MinCount, Is.EqualTo(1));
Assert.That(blueRule.MaxCount, Is.EqualTo(3));
Assert.That(RarityTagBudgetRuleRegistry.TryGetRule(RarityType.Purple, out RarityTagBudgetRule purpleRule), Is.True);
Assert.That(purpleRule.MinCount, Is.EqualTo(1));
Assert.That(purpleRule.MaxCount, Is.EqualTo(3));
Assert.That(RarityTagBudgetRuleRegistry.TryGetRule(RarityType.Red, out RarityTagBudgetRule redRule), Is.True);
Assert.That(redRule.MinCount, Is.EqualTo(2));
Assert.That(redRule.MaxCount, Is.EqualTo(4));
}
[Test]
public void CreateSampleInventory_Generates_SeedTags_Within_Launch_Set_And_Matches_Tower_Stats()
{
@@ -0,0 +1,419 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using GameFramework.DataTable;
using GeometryTD.CustomComponent;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using NUnit.Framework;
using UnityEngine;
namespace GeometryTD.Tests.EditMode
{
public sealed class PlayerInventoryTowerAssemblyServiceTests
{
private GameObject _inventoryObject;
private PlayerInventoryComponent _originalPlayerInventory;
[TearDown]
public void TearDown()
{
SetStaticPlayerInventory(_originalPlayerInventory);
if (_inventoryObject != null)
{
UnityEngine.Object.DestroyImmediate(_inventoryObject);
_inventoryObject = null;
}
}
[Test]
public void TryAssembleTower_Builds_Expected_Rarity_Stats_And_Tags()
{
PlayerInventoryComponent inventoryComponent = CreateBoundPlayerInventory(new BackpackInventoryData
{
MuzzleComponents =
{
new MuzzleCompItemData
{
InstanceId = 10001,
ConfigId = 1,
Name = "测试枪口",
Rarity = RarityType.Green,
AttackDamage = new[] { 10, 20, 30, 40, 50 },
DamageRandomRate = 0.15f,
AttackMethodType = AttackMethodType.NormalBullet,
Tags = new[] { TagType.Fire }
}
},
BearingComponents =
{
new BearingCompItemData
{
InstanceId = 20001,
ConfigId = 1,
Name = "测试轴承",
Rarity = RarityType.Blue,
RotateSpeed = new[] { 1f, 2f, 3f, 4f, 5f },
AttackRange = new[] { 10f, 20f, 30f, 40f, 50f },
Tags = new[] { TagType.Ice }
}
},
BaseComponents =
{
new BaseCompItemData
{
InstanceId = 30001,
ConfigId = 1,
Name = "测试底座",
Rarity = RarityType.Purple,
AttackSpeed = new[] { 2f, 4f, 6f, 8f, 10f },
AttackPropertyType = AttackPropertyType.Fire,
Tags = new[] { TagType.Fire }
}
}
});
InjectAssemblyTables(
inventoryComponent,
new FakeDataTable<DRMuzzleComp>(CreateMuzzleRow()),
new FakeDataTable<DRBearingComp>(CreateBearingRow()),
new FakeDataTable<DRBaseComp>(CreateBaseRow()));
bool assembled = inventoryComponent.TryAssembleTower(10001, 20001, 30001, out TowerItemData tower);
BackpackInventoryData committedInventory = inventoryComponent.GetInventorySnapshot();
Assert.That(assembled, Is.True);
Assert.That(tower, Is.Not.Null);
Assert.That(tower.Rarity, Is.EqualTo(RarityType.Blue));
Assert.That(tower.Stats.AttackDamage, Is.EqualTo(new[] { 20, 23, 26, 29, 32 }));
Assert.That(tower.Stats.RotateSpeed, Is.EqualTo(new[] { 3f, 3.5f, 4f, 4.5f, 5f }));
Assert.That(tower.Stats.AttackRange, Is.EqualTo(new[] { 30f, 31f, 32f, 33f, 34f }));
Assert.That(tower.Stats.AttackSpeed, Is.EqualTo(new[] { 8f, 7.75f, 7.5f, 7.25f, 7f }));
Assert.That(tower.Stats.DamageRandomRate, Is.EqualTo(0.15f));
Assert.That(tower.Stats.AttackMethodType, Is.EqualTo(AttackMethodType.NormalBullet));
Assert.That(tower.Stats.AttackPropertyType, Is.EqualTo(AttackPropertyType.Fire));
Assert.That(tower.Stats.Tags, Is.EqualTo(new[] { TagType.Fire, TagType.Ice }));
Assert.That(tower.Stats.TagRuntimes, Has.Length.EqualTo(2));
Assert.That(tower.Stats.TagRuntimes[0].TagType, Is.EqualTo(TagType.Fire));
Assert.That(tower.Stats.TagRuntimes[0].TotalStack, Is.EqualTo(2));
Assert.That(tower.Stats.TagRuntimes[1].TagType, Is.EqualTo(TagType.Ice));
Assert.That(tower.Stats.TagRuntimes[1].TotalStack, Is.EqualTo(1));
Assert.That(committedInventory.Towers, Has.Count.EqualTo(1));
Assert.That(committedInventory.MuzzleComponents[0].IsAssembledIntoTower, Is.True);
Assert.That(committedInventory.BearingComponents[0].IsAssembledIntoTower, Is.True);
Assert.That(committedInventory.BaseComponents[0].IsAssembledIntoTower, Is.True);
}
[Test]
public void TryAssembleTower_Returns_False_When_Required_Config_Row_Is_Missing()
{
PlayerInventoryComponent inventoryComponent = CreateBoundPlayerInventory(new BackpackInventoryData
{
MuzzleComponents =
{
new MuzzleCompItemData
{
InstanceId = 10001,
ConfigId = 1,
Rarity = RarityType.White,
AttackDamage = new[] { 10, 20, 30, 40, 50 },
AttackMethodType = AttackMethodType.NormalBullet
}
},
BearingComponents =
{
new BearingCompItemData
{
InstanceId = 20001,
ConfigId = 1,
Rarity = RarityType.White,
RotateSpeed = new[] { 1f, 2f, 3f, 4f, 5f },
AttackRange = new[] { 1f, 2f, 3f, 4f, 5f }
}
},
BaseComponents =
{
new BaseCompItemData
{
InstanceId = 30001,
ConfigId = 999,
Rarity = RarityType.White,
AttackSpeed = new[] { 1f, 2f, 3f, 4f, 5f },
AttackPropertyType = AttackPropertyType.Physics
}
}
});
InjectAssemblyTables(
inventoryComponent,
new FakeDataTable<DRMuzzleComp>(CreateMuzzleRow()),
new FakeDataTable<DRBearingComp>(CreateBearingRow()),
new FakeDataTable<DRBaseComp>(CreateBaseRow()));
bool assembled = inventoryComponent.TryAssembleTower(10001, 20001, 30001, out TowerItemData tower);
BackpackInventoryData committedInventory = inventoryComponent.GetInventorySnapshot();
Assert.That(assembled, Is.False);
Assert.That(tower, Is.Null);
Assert.That(committedInventory.Towers, Has.Count.EqualTo(0));
Assert.That(committedInventory.MuzzleComponents[0].IsAssembledIntoTower, Is.False);
Assert.That(committedInventory.BearingComponents[0].IsAssembledIntoTower, Is.False);
Assert.That(committedInventory.BaseComponents[0].IsAssembledIntoTower, Is.False);
}
private PlayerInventoryComponent CreateBoundPlayerInventory(BackpackInventoryData inventory)
{
_originalPlayerInventory = GameEntry.PlayerInventory;
_inventoryObject = new GameObject("TestPlayerInventory");
PlayerInventoryComponent inventoryComponent = _inventoryObject.AddComponent<PlayerInventoryComponent>();
SetStaticPlayerInventory(inventoryComponent);
inventoryComponent.ReplaceInventorySnapshot(inventory);
return inventoryComponent;
}
private static void SetStaticPlayerInventory(PlayerInventoryComponent inventoryComponent)
{
FieldInfo backingField = typeof(GameEntry).GetField(
"<PlayerInventory>k__BackingField",
BindingFlags.Static | BindingFlags.NonPublic);
Assert.That(backingField, Is.Not.Null);
backingField.SetValue(null, inventoryComponent);
}
private static void InjectAssemblyTables(
PlayerInventoryComponent inventoryComponent,
IDataTable<DRMuzzleComp> muzzleTable,
IDataTable<DRBearingComp> bearingTable,
IDataTable<DRBaseComp> baseTable)
{
FieldInfo serviceField = typeof(PlayerInventoryComponent).GetField(
"_towerAssemblyService",
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(serviceField, Is.Not.Null);
object assemblyService = serviceField.GetValue(inventoryComponent);
Assert.That(assemblyService, Is.Not.Null);
SetPrivateField(assemblyService, "_drMuzzleComp", muzzleTable);
SetPrivateField(assemblyService, "_drBearingComp", bearingTable);
SetPrivateField(assemblyService, "_drBaseComp", baseTable);
}
private static void SetPrivateField(object instance, string fieldName, object value)
{
FieldInfo field = instance.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(field, Is.Not.Null);
field.SetValue(instance, value);
}
private static DRMuzzleComp CreateMuzzleRow()
{
DRMuzzleComp row = new DRMuzzleComp();
Assert.That(row.ParseDataRow("\t1\t\t测试枪口\t[10,20,30,40,50]\t3\t0.15\tNormalBullet\t\t[Fire]", null), Is.True);
return row;
}
private static DRBearingComp CreateBearingRow()
{
DRBearingComp row = new DRBearingComp();
Assert.That(row.ParseDataRow("\t1\t\t测试轴承\t[1,2,3,4,5]\t0.5\t[10,20,30,40,50]\t1\t\t[Ice]", null), Is.True);
return row;
}
private static DRBaseComp CreateBaseRow()
{
DRBaseComp row = new DRBaseComp();
Assert.That(row.ParseDataRow("\t1\t\t测试底座\t[2,4,6,8,10]\t-0.25\tFire\t\t[Fire]", null), Is.True);
return row;
}
private sealed class FakeDataTable<TRow> : IDataTable<TRow> where TRow : class, IDataRow
{
private readonly Dictionary<int, TRow> _rowsById = new();
public FakeDataTable(params TRow[] rows)
{
if (rows != null)
{
for (int i = 0; i < rows.Length; i++)
{
TRow row = rows[i];
if (row != null)
{
_rowsById[row.Id] = row;
}
}
}
}
public string Name => typeof(TRow).Name;
public string FullName => typeof(TRow).FullName;
public Type Type => typeof(TRow);
public int Count => _rowsById.Count;
public TRow this[int id] => GetDataRow(id);
public TRow MinIdDataRow => _rowsById.Count <= 0 ? null : GetDataRow(GetOrderedIds()[0]);
public TRow MaxIdDataRow => _rowsById.Count <= 0 ? null : GetDataRow(GetOrderedIds()[^1]);
public bool HasDataRow(int id)
{
return _rowsById.ContainsKey(id);
}
public bool HasDataRow(Predicate<TRow> condition)
{
return GetDataRow(condition) != null;
}
public TRow GetDataRow(int id)
{
return _rowsById.TryGetValue(id, out TRow row) ? row : null;
}
public TRow GetDataRow(Predicate<TRow> condition)
{
if (condition == null)
{
return null;
}
foreach (TRow row in _rowsById.Values)
{
if (row != null && condition(row))
{
return row;
}
}
return null;
}
public TRow[] GetDataRows(Predicate<TRow> condition)
{
List<TRow> results = new();
GetDataRows(condition, results);
return results.ToArray();
}
public void GetDataRows(Predicate<TRow> condition, List<TRow> results)
{
results?.Clear();
if (condition == null || results == null)
{
return;
}
foreach (TRow row in _rowsById.Values)
{
if (row != null && condition(row))
{
results.Add(row);
}
}
}
public TRow[] GetDataRows(Comparison<TRow> comparison)
{
List<TRow> results = new();
GetDataRows(comparison, results);
return results.ToArray();
}
public void GetDataRows(Comparison<TRow> comparison, List<TRow> results)
{
results?.Clear();
if (results == null)
{
return;
}
results.AddRange(_rowsById.Values);
if (comparison != null)
{
results.Sort(comparison);
}
}
public TRow[] GetDataRows(Predicate<TRow> condition, Comparison<TRow> comparison)
{
List<TRow> results = new();
GetDataRows(condition, comparison, results);
return results.ToArray();
}
public void GetDataRows(Predicate<TRow> condition, Comparison<TRow> comparison, List<TRow> results)
{
GetDataRows(condition, results);
if (results != null && comparison != null)
{
results.Sort(comparison);
}
}
public TRow[] GetAllDataRows()
{
List<TRow> results = new();
GetAllDataRows(results);
return results.ToArray();
}
public void GetAllDataRows(List<TRow> results)
{
results?.Clear();
if (results == null)
{
return;
}
foreach (int id in GetOrderedIds())
{
results.Add(_rowsById[id]);
}
}
public bool AddDataRow(string dataRowString, object userData)
{
throw new NotSupportedException();
}
public bool AddDataRow(byte[] dataRowBytes, int startIndex, int length, object userData)
{
throw new NotSupportedException();
}
public bool RemoveDataRow(int id)
{
return _rowsById.Remove(id);
}
public void RemoveAllDataRows()
{
_rowsById.Clear();
}
public IEnumerator<TRow> GetEnumerator()
{
foreach (int id in GetOrderedIds())
{
yield return _rowsById[id];
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
private int[] GetOrderedIds()
{
int[] ids = new int[_rowsById.Count];
_rowsById.Keys.CopyTo(ids, 0);
Array.Sort(ids);
return ids;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: aa889597670a1524ebf4e952871c5414
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,531 @@
using System.Collections.Generic;
using System.Reflection;
using CustomComponent;
using GeometryTD.CustomComponent;
using GeometryTD.CustomEvent;
using GeometryTD.Definition;
using GeometryTD.Procedure;
using GeometryTD.UI;
using NUnit.Framework;
using UnityEngine;
namespace GeometryTD.Tests.EditMode
{
public sealed class ProcedureMainFlowTests
{
private GameObject _uiRouterObject;
private GameObject _inventoryObject;
private GameObject _nodeMapFormObject;
private UIRouterComponent _originalUIRouter;
private PlayerInventoryComponent _originalPlayerInventory;
[TearDown]
public void TearDown()
{
SetStaticUIRouter(_originalUIRouter);
SetStaticPlayerInventory(_originalPlayerInventory);
if (_uiRouterObject != null)
{
Object.DestroyImmediate(_uiRouterObject);
_uiRouterObject = null;
}
if (_inventoryObject != null)
{
Object.DestroyImmediate(_inventoryObject);
_inventoryObject = null;
}
if (_nodeMapFormObject != null)
{
Object.DestroyImmediate(_nodeMapFormObject);
_nodeMapFormObject = null;
}
}
[Test]
public void OnNodeEnter_Accepted_Event_Closes_Hub_And_Enters_NodeActive()
{
RecorderUIRouter recorderRouter = CreateBoundUIRouter();
ProcedureMain procedure = CreateProcedureMain(CreateTwoNodeRun(), ProcedureMainFlowPhase.Hub);
InvokePrivate(
procedure,
"OnNodeEnter",
null,
NodeEnterEventArgs.Create("testrun", 101, RunNodeType.Combat, 0));
Assert.That(GetFlowPhase(procedure), Is.EqualTo(ProcedureMainFlowPhase.NodeActive));
Assert.That(recorderRouter.NodeMapController.CloseCount, Is.EqualTo(1));
Assert.That(recorderRouter.MainController.CloseCount, Is.EqualTo(1));
Assert.That(recorderRouter.DialogController.OpenCount, Is.EqualTo(0));
}
[Test]
public void OnNodeEnter_Ignores_Mismatched_Node_And_Leaves_Hub_Visible()
{
RecorderUIRouter recorderRouter = CreateBoundUIRouter();
ProcedureMain procedure = CreateProcedureMain(CreateTwoNodeRun(), ProcedureMainFlowPhase.Hub);
InvokePrivate(
procedure,
"OnNodeEnter",
null,
NodeEnterEventArgs.Create("testrun", 999, RunNodeType.Combat, 0));
Assert.That(GetFlowPhase(procedure), Is.EqualTo(ProcedureMainFlowPhase.Hub));
Assert.That(recorderRouter.NodeMapController.CloseCount, Is.EqualTo(0));
Assert.That(recorderRouter.MainController.CloseCount, Is.EqualTo(0));
}
[Test]
public void OnNodeComplete_Normal_Completion_Returns_To_Hub_And_Advances_Run()
{
RecorderUIRouter recorderRouter = CreateBoundUIRouter();
RunState runState = CreateTwoNodeRun();
ProcedureMain procedure = CreateProcedureMain(runState, ProcedureMainFlowPhase.NodeActive);
InvokePrivate(
procedure,
"OnNodeComplete",
null,
NodeCompleteEventArgs.Create(
"testrun",
101,
RunNodeType.Combat,
0,
RunNodeCompletionStatus.Completed,
true,
new BackpackInventoryData { Gold = 77 }));
Assert.That(GetFlowPhase(procedure), Is.EqualTo(ProcedureMainFlowPhase.Hub));
Assert.That(runState.CurrentNodeIndex, Is.EqualTo(1));
Assert.That(runState.Nodes[0].Status, Is.EqualTo(RunNodeStatus.Completed));
Assert.That(runState.CurrentNode.Status, Is.EqualTo(RunNodeStatus.Available));
Assert.That(runState.RunInventorySnapshot.Gold, Is.EqualTo(77));
Assert.That(recorderRouter.NodeMapController.OpenCount, Is.EqualTo(1));
Assert.That(recorderRouter.MainController.OpenCount, Is.EqualTo(1));
Assert.That(recorderRouter.DialogController.OpenCount, Is.EqualTo(0));
NodeMapFormUseCase nodeMapUseCase = GetNodeMapUseCase(procedure);
NodeMapFormRawData rawData = nodeMapUseCase.CreateInitialModel();
Assert.That(rawData.ProgressText, Is.EqualTo("1 / 2"));
Assert.That(rawData.CurrentNodeText, Is.EqualTo("当前节点: 事件"));
}
[Test]
public void OnNodeComplete_Exception_Returns_To_Hub_And_Marks_Current_Node_Exception()
{
RecorderUIRouter recorderRouter = CreateBoundUIRouter();
RunState runState = CreateTwoNodeRun();
ProcedureMain procedure = CreateProcedureMain(runState, ProcedureMainFlowPhase.NodeActive);
InvokePrivate(
procedure,
"OnNodeComplete",
null,
NodeCompleteEventArgs.Create(
"testrun",
101,
RunNodeType.Combat,
0,
RunNodeCompletionStatus.Exception,
false,
new BackpackInventoryData { Gold = 12 }));
Assert.That(GetFlowPhase(procedure), Is.EqualTo(ProcedureMainFlowPhase.Hub));
Assert.That(runState.CurrentNodeIndex, Is.EqualTo(0));
Assert.That(runState.CurrentNode.Status, Is.EqualTo(RunNodeStatus.Exception));
Assert.That(runState.RunInventorySnapshot.Gold, Is.EqualTo(12));
Assert.That(recorderRouter.NodeMapController.OpenCount, Is.EqualTo(1));
Assert.That(recorderRouter.MainController.OpenCount, Is.EqualTo(1));
Assert.That(recorderRouter.DialogController.OpenCount, Is.EqualTo(0));
}
[Test]
public void OnNodeComplete_With_Broken_Participant_Tower_Opens_Removal_Dialog_After_Returning_To_Hub()
{
RecorderUIRouter recorderRouter = CreateBoundUIRouter();
PlayerInventoryComponent inventoryComponent = CreateBoundPlayerInventory(CreateParticipantInventory(100f));
RunState runState = CreateTwoNodeRun();
ProcedureMain procedure = CreateProcedureMain(runState, ProcedureMainFlowPhase.NodeActive);
BackpackInventoryData brokenSnapshot = inventoryComponent.GetInventorySnapshot();
brokenSnapshot.MuzzleComponents[0].Endurance = 0f;
InvokePrivate(
procedure,
"OnNodeComplete",
null,
NodeCompleteEventArgs.Create(
"testrun",
101,
RunNodeType.Combat,
0,
RunNodeCompletionStatus.Completed,
true,
brokenSnapshot));
BackpackInventoryData replacedInventory = inventoryComponent.GetInventorySnapshot();
DialogFormRawData dialogRawData = recorderRouter.DialogController.LastOpenedUserData as DialogFormRawData;
Assert.That(GetFlowPhase(procedure), Is.EqualTo(ProcedureMainFlowPhase.Hub));
Assert.That(replacedInventory.ParticipantTowerInstanceIds.Count, Is.EqualTo(0));
Assert.That(recorderRouter.NodeMapController.OpenCount, Is.EqualTo(1));
Assert.That(recorderRouter.MainController.OpenCount, Is.EqualTo(1));
Assert.That(recorderRouter.DialogController.OpenCount, Is.EqualTo(1));
Assert.That(dialogRawData, Is.Not.Null);
Assert.That(dialogRawData.Title, Is.EqualTo("出战塔已损坏"));
}
[Test]
public void OnNodeComplete_Boss_Run_Completed_Shows_Run_Complete_Dialog_And_Does_Not_Override_It_With_Cleanup_Dialog()
{
RecorderUIRouter recorderRouter = CreateBoundUIRouter();
PlayerInventoryComponent inventoryComponent = CreateBoundPlayerInventory(CreateParticipantInventory(100f));
RunState runState = RunStateFactory.Create(
LevelThemeType.Plain,
inventoryComponent.GetInventorySnapshot(),
new[]
{
new RunNodeSeed { NodeId = 901, NodeType = RunNodeType.BossCombat, LinkedLevelId = 4 }
},
"testrun");
ProcedureMain procedure = CreateProcedureMain(runState, ProcedureMainFlowPhase.NodeActive);
BackpackInventoryData brokenSnapshot = inventoryComponent.GetInventorySnapshot();
brokenSnapshot.MuzzleComponents[0].Endurance = 0f;
InvokePrivate(
procedure,
"OnNodeComplete",
null,
NodeCompleteEventArgs.Create(
"testrun",
901,
RunNodeType.BossCombat,
0,
RunNodeCompletionStatus.Completed,
true,
brokenSnapshot));
BackpackInventoryData replacedInventory = inventoryComponent.GetInventorySnapshot();
DialogFormRawData dialogRawData = recorderRouter.DialogController.LastOpenedUserData as DialogFormRawData;
Assert.That(GetFlowPhase(procedure), Is.EqualTo(ProcedureMainFlowPhase.RunCompletedPendingFinish));
Assert.That(runState.IsCompleted, Is.True);
Assert.That(replacedInventory.ParticipantTowerInstanceIds.Count, Is.EqualTo(0));
Assert.That(recorderRouter.NodeMapController.CloseCount, Is.EqualTo(1));
Assert.That(recorderRouter.MainController.CloseCount, Is.EqualTo(1));
Assert.That(recorderRouter.DialogController.OpenCount, Is.EqualTo(1));
Assert.That(dialogRawData, Is.Not.Null);
Assert.That(dialogRawData.Title, Is.EqualTo("Run Complete"));
Assert.That(dialogRawData.Message, Is.EqualTo("Boss node completed. This run is finished and will return to the main menu."));
Assert.That(dialogRawData.OnClickConfirm, Is.Not.Null);
dialogRawData.OnClickConfirm(null);
Assert.That(GetReturnToMenuPending(procedure), Is.True);
}
[Test]
public void OnNodeMapNodeEnterRequested_Ignores_Request_When_FlowPhase_Is_Not_Hub()
{
RecorderUIRouter recorderRouter = CreateBoundUIRouter();
ProcedureMain procedure = CreateProcedureMain(CreateTwoNodeRun(), ProcedureMainFlowPhase.NodeActive);
InvokePrivate(
procedure,
"OnNodeMapNodeEnterRequested",
CreateNodeMapFormSender(),
NodeMapNodeEnterRequestedEventArgs.Create("testrun", 101, RunNodeType.Combat, 0));
Assert.That(GetFlowPhase(procedure), Is.EqualTo(ProcedureMainFlowPhase.NodeActive));
Assert.That(recorderRouter.DialogController.OpenCount, Is.EqualTo(0));
Assert.That(recorderRouter.NodeMapController.CloseCount, Is.EqualTo(0));
Assert.That(recorderRouter.MainController.CloseCount, Is.EqualTo(0));
}
[Test]
public void OnNodeMapNodeEnterRequested_Ignores_Request_When_Target_Node_Does_Not_Match_Current_Node()
{
RecorderUIRouter recorderRouter = CreateBoundUIRouter();
ProcedureMain procedure = CreateProcedureMain(CreateTwoNodeRun(), ProcedureMainFlowPhase.Hub);
InvokePrivate(
procedure,
"OnNodeMapNodeEnterRequested",
CreateNodeMapFormSender(),
NodeMapNodeEnterRequestedEventArgs.Create("testrun", 102, RunNodeType.Event, 1));
Assert.That(GetFlowPhase(procedure), Is.EqualTo(ProcedureMainFlowPhase.Hub));
Assert.That(recorderRouter.DialogController.OpenCount, Is.EqualTo(0));
Assert.That(recorderRouter.NodeMapController.CloseCount, Is.EqualTo(0));
Assert.That(recorderRouter.MainController.CloseCount, Is.EqualTo(0));
}
[Test]
public void OnNodeMapNodeEnterRequested_Blocked_Combat_With_Null_Inventory_Opens_Block_Dialog()
{
RecorderUIRouter recorderRouter = CreateBoundUIRouter();
ProcedureMain procedure = CreateProcedureMain(CreateTwoNodeRun(), ProcedureMainFlowPhase.Hub);
InvokePrivate(
procedure,
"OnNodeMapNodeEnterRequested",
CreateNodeMapFormSender(),
NodeMapNodeEnterRequestedEventArgs.Create("testrun", 101, RunNodeType.Combat, 0));
DialogFormRawData dialogRawData = recorderRouter.DialogController.LastOpenedUserData as DialogFormRawData;
Assert.That(GetFlowPhase(procedure), Is.EqualTo(ProcedureMainFlowPhase.Hub));
Assert.That(recorderRouter.DialogController.CloseCount, Is.EqualTo(1));
Assert.That(recorderRouter.DialogController.OpenCount, Is.EqualTo(1));
Assert.That(dialogRawData, Is.Not.Null);
Assert.That(dialogRawData.Title, Is.EqualTo("无法进入战斗"));
Assert.That(dialogRawData.Message, Is.EqualTo("当前无法读取库存快照,暂时不能进入战斗。请重新进入本轮流程后再试。"));
}
[Test]
public void OnNodeMapNodeEnterRequested_Blocked_Combat_With_No_Valid_Participant_Tower_Opens_Block_Dialog()
{
RecorderUIRouter recorderRouter = CreateBoundUIRouter();
PlayerInventoryComponent inventoryComponent = CreateBoundPlayerInventory(CreateParticipantInventory(100f));
BackpackInventoryData invalidInventory = inventoryComponent.GetInventorySnapshot();
invalidInventory.ParticipantTowerInstanceIds.Clear();
inventoryComponent.ReplaceInventorySnapshot(invalidInventory);
ProcedureMain procedure = CreateProcedureMain(CreateTwoNodeRun(), ProcedureMainFlowPhase.Hub);
InvokePrivate(
procedure,
"OnNodeMapNodeEnterRequested",
CreateNodeMapFormSender(),
NodeMapNodeEnterRequestedEventArgs.Create("testrun", 101, RunNodeType.Combat, 0));
DialogFormRawData dialogRawData = recorderRouter.DialogController.LastOpenedUserData as DialogFormRawData;
Assert.That(GetFlowPhase(procedure), Is.EqualTo(ProcedureMainFlowPhase.Hub));
Assert.That(recorderRouter.DialogController.CloseCount, Is.EqualTo(1));
Assert.That(recorderRouter.DialogController.OpenCount, Is.EqualTo(1));
Assert.That(dialogRawData, Is.Not.Null);
Assert.That(dialogRawData.Title, Is.EqualTo("无法进入战斗"));
Assert.That(
dialogRawData.Message,
Is.EqualTo("参战区至少需要 1 座完整装配了枪口、轴承、底座的塔,才能进入战斗。"));
}
private RecorderUIRouter CreateBoundUIRouter()
{
_originalUIRouter = GameEntry.UIRouter;
_uiRouterObject = new GameObject("ProcedureMainFlowTests-UIRouter");
UIRouterComponent uiRouter = _uiRouterObject.AddComponent<UIRouterComponent>();
SetStaticUIRouter(uiRouter);
RecorderUIRouter recorderRouter = new RecorderUIRouter(uiRouter);
recorderRouter.Bind(UIFormType.NodeMapForm);
recorderRouter.Bind(UIFormType.MainForm);
recorderRouter.Bind(UIFormType.DialogForm);
recorderRouter.Bind(UIFormType.RepoForm);
return recorderRouter;
}
private PlayerInventoryComponent CreateBoundPlayerInventory(BackpackInventoryData inventory)
{
_originalPlayerInventory = GameEntry.PlayerInventory;
_inventoryObject = new GameObject("ProcedureMainFlowTests-Inventory");
PlayerInventoryComponent inventoryComponent = _inventoryObject.AddComponent<PlayerInventoryComponent>();
SetStaticPlayerInventory(inventoryComponent);
inventoryComponent.ReplaceInventorySnapshot(inventory);
return inventoryComponent;
}
private NodeMapForm CreateNodeMapFormSender()
{
_nodeMapFormObject = new GameObject("ProcedureMainFlowTests-NodeMapForm");
return _nodeMapFormObject.AddComponent<NodeMapForm>();
}
private static ProcedureMain CreateProcedureMain(RunState runState, ProcedureMainFlowPhase flowPhase)
{
ProcedureMain procedure = new ProcedureMain();
NodeMapFormUseCase nodeMapUseCase = new NodeMapFormUseCase();
nodeMapUseCase.SetRunState(runState);
SetPrivateField(procedure, "_currentRunState", runState);
SetPrivateField(procedure, "_nodeMapFormUseCase", nodeMapUseCase);
SetPrivateField(procedure, "_flowPhase", flowPhase);
SetPrivateField(procedure, "_isRunCompleteDialogShown", false);
SetPrivateField(procedure, "_isReturnToMenuPending", false);
return procedure;
}
private static void SetStaticUIRouter(UIRouterComponent uiRouter)
{
FieldInfo backingField = typeof(GameEntry).GetField(
"<UIRouter>k__BackingField",
BindingFlags.Static | BindingFlags.NonPublic);
Assert.That(backingField, Is.Not.Null);
backingField.SetValue(null, uiRouter);
}
private static void SetStaticPlayerInventory(PlayerInventoryComponent inventoryComponent)
{
FieldInfo backingField = typeof(GameEntry).GetField(
"<PlayerInventory>k__BackingField",
BindingFlags.Static | BindingFlags.NonPublic);
Assert.That(backingField, Is.Not.Null);
backingField.SetValue(null, inventoryComponent);
}
private static void InvokePrivate(object instance, string methodName, params object[] args)
{
MethodInfo method = instance.GetType().GetMethod(
methodName,
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(method, Is.Not.Null);
method.Invoke(instance, args);
}
private static void SetPrivateField(object instance, string fieldName, object value)
{
FieldInfo field = instance.GetType().GetField(
fieldName,
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(field, Is.Not.Null);
field.SetValue(instance, value);
}
private static T GetPrivateField<T>(object instance, string fieldName)
{
FieldInfo field = instance.GetType().GetField(
fieldName,
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(field, Is.Not.Null);
return (T)field.GetValue(instance);
}
private static ProcedureMainFlowPhase GetFlowPhase(ProcedureMain procedure)
{
return GetPrivateField<ProcedureMainFlowPhase>(procedure, "_flowPhase");
}
private static bool GetReturnToMenuPending(ProcedureMain procedure)
{
return GetPrivateField<bool>(procedure, "_isReturnToMenuPending");
}
private static NodeMapFormUseCase GetNodeMapUseCase(ProcedureMain procedure)
{
return GetPrivateField<NodeMapFormUseCase>(procedure, "_nodeMapFormUseCase");
}
private static RunState CreateTwoNodeRun()
{
return RunStateFactory.Create(
LevelThemeType.Plain,
new BackpackInventoryData { Gold = 50 },
new[]
{
new RunNodeSeed { NodeId = 101, NodeType = RunNodeType.Combat, LinkedLevelId = 1, SequenceIndex = 0 },
new RunNodeSeed { NodeId = 102, NodeType = RunNodeType.Event, SequenceIndex = 1 }
},
"testrun");
}
private static BackpackInventoryData CreateParticipantInventory(float endurance)
{
BackpackInventoryData inventory = new BackpackInventoryData();
inventory.MuzzleComponents.Add(new MuzzleCompItemData
{
InstanceId = 10001,
Name = "枪口",
Endurance = endurance
});
inventory.BearingComponents.Add(new BearingCompItemData
{
InstanceId = 20001,
Name = "轴承",
Endurance = endurance
});
inventory.BaseComponents.Add(new BaseCompItemData
{
InstanceId = 30001,
Name = "底座",
Endurance = endurance
});
inventory.Towers.Add(new TowerItemData
{
InstanceId = 90001,
Name = "合法塔",
MuzzleComponentInstanceId = 10001,
BearingComponentInstanceId = 20001,
BaseComponentInstanceId = 30001
});
inventory.ParticipantTowerInstanceIds.Add(90001);
return inventory;
}
private sealed class RecorderUIRouter
{
private readonly Dictionary<UIFormType, RecorderController> _controllers = new();
public RecorderUIRouter(UIRouterComponent uiRouter)
{
UIRouter = uiRouter;
}
public UIRouterComponent UIRouter { get; }
public RecorderController NodeMapController => _controllers[UIFormType.NodeMapForm];
public RecorderController MainController => _controllers[UIFormType.MainForm];
public RecorderController DialogController => _controllers[UIFormType.DialogForm];
public void Bind(UIFormType formType)
{
RecorderController controller = new RecorderController();
_controllers[formType] = controller;
FieldInfo routeControllersField = typeof(UIRouterComponent).GetField(
"_routeControllers",
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(routeControllersField, Is.Not.Null);
Dictionary<UIFormType, IUIFormController> routeControllers =
(Dictionary<UIFormType, IUIFormController>)routeControllersField.GetValue(UIRouter);
Assert.That(routeControllers, Is.Not.Null);
routeControllers[formType] = controller;
}
}
private sealed class RecorderController : IUIFormController
{
public int OpenCount { get; private set; }
public int CloseCount { get; private set; }
public object LastOpenedUserData { get; private set; }
public IUIUseCase BoundUseCase { get; private set; }
public int? OpenUI(object userData = null)
{
OpenCount++;
LastOpenedUserData = userData;
return OpenCount;
}
public void CloseUI()
{
CloseCount++;
}
public void BindUseCase(IUIUseCase useCase)
{
BoundUseCase = useCase;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3b622fd9e87fa514da333f9d8313fdd2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -50,6 +50,32 @@ namespace GeometryTD.Tests.EditMode
Assert.That(rawData.ConfirmText, Is.EqualTo("知道了"));
}
[Test]
public void RemoveBrokenParticipantTowers_Does_Not_Remove_Towers_With_Missing_Components()
{
BackpackInventoryData inventory = CreateInventory();
inventory.BaseComponents.RemoveAt(0);
ProcedureMainParticipantTowerCleanupResult result =
ProcedureMainParticipantTowerCleanupService.RemoveBrokenParticipantTowers(inventory, 4);
Assert.That(result.HasAnyRemovedTower, Is.False);
CollectionAssert.AreEqual(new long[] { 90001, 90002 }, inventory.ParticipantTowerInstanceIds);
Assert.That(inventory.Towers[0].IsParticipatingInCombat, Is.True);
}
[Test]
public void BuildRemovedTowerDialogRawData_Returns_Default_Message_When_No_Tower_Was_Removed()
{
DialogFormRawData rawData =
ProcedureMainParticipantTowerCleanupService.BuildRemovedTowerDialogRawData(
new ProcedureMainParticipantTowerCleanupResult());
Assert.That(rawData.Title, Is.EqualTo("出战塔已损坏"));
Assert.That(rawData.Message, Is.EqualTo("当前没有需要移出参战区的损坏防御塔。"));
Assert.That(rawData.ConfirmText, Is.EqualTo("知道了"));
}
private static BackpackInventoryData CreateInventory()
{
BackpackInventoryData inventory = new BackpackInventoryData();
@@ -90,6 +90,22 @@ namespace GeometryTD.Tests.EditMode
Assert.That(completedRun.RunInventorySnapshot.Gold, Is.EqualTo(10));
}
[Test]
public void TryAdvanceRun_Returns_NoChange_But_Still_Replaces_Snapshot_For_NonTerminal_Status()
{
RunState runState = CreateTwoNodeRun();
ProcedureMainRunAdvanceResult result = ProcedureMainRunFlowService.TryAdvanceRun(
runState,
RunNodeCompletionStatus.None,
new BackpackInventoryData { Gold = 999 });
Assert.That(result, Is.EqualTo(ProcedureMainRunAdvanceResult.NoChange));
Assert.That(runState.CurrentNodeIndex, Is.EqualTo(0));
Assert.That(runState.CurrentNode.Status, Is.EqualTo(RunNodeStatus.Available));
Assert.That(runState.RunInventorySnapshot.Gold, Is.EqualTo(999));
}
[Test]
public void MatchesCurrentNode_Returns_True_For_Current_Node_And_Default_Wildcards()
{
@@ -136,6 +152,23 @@ namespace GeometryTD.Tests.EditMode
Assert.That(sequenceMismatch, Is.False);
}
[Test]
public void MatchesCurrentNode_Returns_False_When_Run_Has_No_Current_Node()
{
RunState completedRun = RunStateFactory.Create(
LevelThemeType.Plain,
new BackpackInventoryData { Gold = 10 },
new RunNodeSeed[0]);
bool match = ProcedureMainNodeEventGuardService.MatchesCurrentNode(
completedRun,
1,
RunNodeType.Combat,
0);
Assert.That(match, Is.False);
}
[Test]
public void TryEnterCompletedPendingFinish_Shows_Dialog_Only_Once()
{
@@ -90,6 +90,41 @@ namespace GeometryTD.Tests.EditMode
Assert.That(hitContext.IsCriticalHit, Is.True);
Assert.That(hitContext.FinalDamage, Is.EqualTo(281));
}
[Test]
public void GetTagStack_Returns_Zero_For_Missing_Invalid_Or_Empty_Data()
{
Assert.That(TagEffectResolver.GetTagStack(null, TagType.Fire), Is.EqualTo(0));
Assert.That(TagEffectResolver.GetTagStack(System.Array.Empty<TagRuntimeData>(), TagType.Fire), Is.EqualTo(0));
Assert.That(TagEffectResolver.GetTagStack(
new[]
{
new TagRuntimeData { TagType = TagType.Fire, TotalStack = 0 },
new TagRuntimeData { TagType = TagType.Ice, TotalStack = 2 }
},
TagType.Fire), Is.EqualTo(0));
Assert.That(TagEffectResolver.GetTagStack(
new[]
{
new TagRuntimeData { TagType = TagType.Ice, TotalStack = 2 }
},
TagType.None), Is.EqualTo(0));
}
[Test]
public void GetTagStack_Returns_First_Positive_Matching_Stack()
{
int stack = TagEffectResolver.GetTagStack(
new[]
{
null,
new TagRuntimeData { TagType = TagType.Fire, TotalStack = 2 },
new TagRuntimeData { TagType = TagType.Fire, TotalStack = 4 }
},
TagType.Fire);
Assert.That(stack, Is.EqualTo(2));
}
}
public sealed class EnemyTagStatusRuntimeTests
@@ -273,6 +308,35 @@ namespace GeometryTD.Tests.EditMode
Assert.That(totalDamage, Is.EqualTo(0));
Assert.That(runtime.GetMoveSpeedMultiplier(), Is.EqualTo(1f).Within(0.001f));
}
[Test]
public void SampleInventory_TowerFireTags_Flow_From_ComponentAggregation_To_BurnDamage()
{
BackpackInventoryData inventory = InventorySeedUtility.CreateSampleInventory();
TowerItemData tower = inventory.Towers[0];
Assert.That(tower.Stats, Is.Not.Null);
Assert.That(tower.Stats.Tags, Is.EqualTo(new[] { TagType.Fire }));
Assert.That(tower.Stats.TagRuntimes, Has.Length.EqualTo(1));
Assert.That(tower.Stats.TagRuntimes[0].TagType, Is.EqualTo(TagType.Fire));
Assert.That(tower.Stats.TagRuntimes[0].TotalStack, Is.EqualTo(3));
EnemyTagStatusRuntime runtime = new EnemyTagStatusRuntime();
int totalDamage = 0;
TagEffectResolver.ApplyAfterHit(new AttackPayload
{
BaseDamage = tower.Stats.AttackDamage[0],
AttackPropertyType = tower.Stats.AttackPropertyType,
TagRuntimes = tower.Stats.TagRuntimes
}, runtime);
runtime.Tick(1f, damage => totalDamage += damage);
runtime.Tick(1f, damage => totalDamage += damage);
runtime.Tick(1f, damage => totalDamage += damage);
Assert.That(runtime.HasStatus(TagType.Fire), Is.False);
Assert.That(totalDamage, Is.EqualTo(180));
}
}
public sealed class EnemyStatusTagRegistryTests
@@ -49,6 +49,38 @@ namespace GeometryTD.Tests.EditMode
Assert.That(tags, Is.EqualTo(new[] { TagType.Fire, TagType.Crit }));
}
[Test]
public void BuildRuntimeTagsFromUniqueTags_Filters_Invalid_Values_And_Assigns_One_Stack()
{
TagRuntimeData[] runtimes = TowerTagAggregationService.BuildRuntimeTagsFromUniqueTags(new[]
{
TagType.None,
TagType.Ice,
TagType.Fire,
TagType.Ice,
(TagType)99
});
Assert.That(runtimes, Has.Length.EqualTo(2));
Assert.That(runtimes[0].TagType, Is.EqualTo(TagType.Fire));
Assert.That(runtimes[0].TotalStack, Is.EqualTo(1));
Assert.That(runtimes[1].TagType, Is.EqualTo(TagType.Ice));
Assert.That(runtimes[1].TotalStack, Is.EqualTo(1));
}
[Test]
public void FlattenUniqueTags_Ignores_Runtime_Entries_With_NonPositive_Stack()
{
TagType[] tags = TowerTagAggregationService.FlattenUniqueTags(new[]
{
new TagRuntimeData { TagType = TagType.Fire, TotalStack = 0 },
new TagRuntimeData { TagType = TagType.Ice, TotalStack = -1 },
new TagRuntimeData { TagType = TagType.Crit, TotalStack = 1 }
});
Assert.That(tags, Is.EqualTo(new[] { TagType.Crit }));
}
[Test]
public void BuildTowerTagTexts_Uses_Runtime_Stacks_For_Display()
{