修复已知问题
This commit is contained in:
parent
aa44170d56
commit
777c58b812
|
|
@ -100,7 +100,8 @@ namespace GeometryTD.CustomComponent
|
|||
_runtime.NodeId = nodeId;
|
||||
_runtime.NodeType = nodeType;
|
||||
_runtime.SequenceIndex = sequenceIndex;
|
||||
GameEntry.InventoryGeneration.ConfigureRunContext(runSeed, sequenceIndex);
|
||||
_runtime.NextDropOrdinal = 0;
|
||||
_runtime.NextRewardOrdinal = 0;
|
||||
_runtime.CombatRunResourceStore.InitializeForCombat(level);
|
||||
for (int i = 0; i < phases.Count; i++)
|
||||
{
|
||||
|
|
@ -201,7 +202,13 @@ namespace GeometryTD.CustomComponent
|
|||
enemy,
|
||||
_runtime.PhaseLoopRuntime.DisplayPhaseIndex,
|
||||
_coordinator.ResolveCurrentThemeType());
|
||||
EnemyDropResult result = GameEntry.InventoryGeneration.ResolveEnemyDrop(context);
|
||||
int nextDropOrdinal = _runtime.NextDropOrdinal;
|
||||
EnemyDropResult result = GameEntry.InventoryGeneration.ResolveEnemyDrop(
|
||||
context,
|
||||
_runtime.RunSeed,
|
||||
_runtime.SequenceIndex,
|
||||
ref nextDropOrdinal);
|
||||
_runtime.NextDropOrdinal = nextDropOrdinal;
|
||||
_runtime.CombatRunResourceStore.AddEnemyDefeatedReward(result.Coin, result.Gold);
|
||||
_runtime.CombatRunResourceStore.AddEnemyDefeatedLoot(result.LootItem);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@ namespace GeometryTD.CustomComponent
|
|||
_runtime.NodeId = 0;
|
||||
_runtime.NodeType = RunNodeType.None;
|
||||
_runtime.SequenceIndex = -1;
|
||||
_runtime.NextDropOrdinal = 0;
|
||||
_runtime.NextRewardOrdinal = 0;
|
||||
}
|
||||
|
||||
public void CleanupAllCombatEntities()
|
||||
|
|
|
|||
|
|
@ -34,5 +34,7 @@ namespace GeometryTD.CustomComponent
|
|||
public int NodeId { get; set; }
|
||||
public RunNodeType NodeType { get; set; }
|
||||
public int SequenceIndex { get; set; }
|
||||
public int NextDropOrdinal { get; set; }
|
||||
public int NextRewardOrdinal { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,9 @@ namespace GeometryTD.CustomComponent
|
|||
CombatSettlementContext settlementContext,
|
||||
int displayPhaseIndex,
|
||||
LevelThemeType themeType,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
ref int nextRewardOrdinal,
|
||||
RewardSelectFormUseCase rewardSelectFormUseCase,
|
||||
Action<RewardSelectItemRawData> onRewardSelected,
|
||||
Action onGiveUp)
|
||||
|
|
@ -48,7 +51,10 @@ namespace GeometryTD.CustomComponent
|
|||
IReadOnlyList<TowerCompItemData> candidateItems = GameEntry.InventoryGeneration.BuildRewardCandidates(
|
||||
displayPhaseIndex,
|
||||
themeType,
|
||||
RewardSelectDisplayCount);
|
||||
RewardSelectDisplayCount,
|
||||
runSeed,
|
||||
sequenceIndex,
|
||||
ref nextRewardOrdinal);
|
||||
if (candidateItems == null || candidateItems.Count <= 0)
|
||||
{
|
||||
settlementContext.Flags.ShouldOpenRewardSelection = false;
|
||||
|
|
@ -60,7 +66,7 @@ namespace GeometryTD.CustomComponent
|
|||
candidateItems,
|
||||
displayCount: RewardSelectDisplayCount,
|
||||
refreshCost: 0,
|
||||
allowRefreshOnce: false,
|
||||
allowRotateOnce: false,
|
||||
allowGiveUp: false,
|
||||
tipText: "基地满血奖励:请选择 1 个组件");
|
||||
|
||||
|
|
|
|||
|
|
@ -16,16 +16,23 @@ namespace GeometryTD.CustomComponent
|
|||
}
|
||||
|
||||
Coordinator.EnsureRewardSelectFormUseCaseBound();
|
||||
int nextRewardOrdinal = Runtime.NextRewardOrdinal;
|
||||
if (!Runtime.CombatSettlementService.TryPrepareRewardSelection(
|
||||
Runtime.SettlementContext,
|
||||
Runtime.PhaseLoopRuntime.DisplayPhaseIndex,
|
||||
Coordinator.ResolveCurrentThemeType(),
|
||||
Runtime.RunSeed,
|
||||
Runtime.SequenceIndex,
|
||||
ref nextRewardOrdinal,
|
||||
Runtime.RewardSelectFormUseCase,
|
||||
Coordinator.OnFullBaseHpRewardSelected,
|
||||
Coordinator.OnFullBaseHpRewardGiveUp))
|
||||
{
|
||||
Coordinator.ChangeState(new CombatFinishFormState(Runtime, Coordinator));
|
||||
return;
|
||||
}
|
||||
|
||||
Runtime.NextRewardOrdinal = nextRewardOrdinal;
|
||||
}
|
||||
|
||||
public override void OnExit()
|
||||
|
|
|
|||
|
|
@ -10,9 +10,17 @@ namespace GeometryTD.CustomComponent
|
|||
public sealed class DropPoolRoller
|
||||
{
|
||||
private const float RarityCurveScalePhase = 30f;
|
||||
private static readonly RarityType[] OrderedRarities =
|
||||
{
|
||||
RarityType.White,
|
||||
RarityType.Green,
|
||||
RarityType.Blue,
|
||||
RarityType.Purple,
|
||||
RarityType.Red
|
||||
};
|
||||
|
||||
private readonly List<DROutGameDropPool> _eligibleRowBuffer = new();
|
||||
private readonly Dictionary<RarityType, float> _rarityWeightBuffer = new();
|
||||
private readonly float[] _rarityWeightBuffer = new float[OrderedRarities.Length];
|
||||
private readonly IDataTable<DROutGameDropPool> _dropPoolTable;
|
||||
|
||||
public DropPoolRoller(IDataTable<DROutGameDropPool> dropPoolTable)
|
||||
|
|
@ -50,9 +58,8 @@ namespace GeometryTD.CustomComponent
|
|||
|
||||
int totalWeight = 0;
|
||||
DROutGameDropPool fallbackRow = null;
|
||||
for (int i = 0; i < _eligibleRowBuffer.Count; i++)
|
||||
foreach (var row in _eligibleRowBuffer)
|
||||
{
|
||||
DROutGameDropPool row = _eligibleRowBuffer[i];
|
||||
if (!IsEligibleAtPhase(row, selectedRarity, displayPhaseIndex))
|
||||
{
|
||||
continue;
|
||||
|
|
@ -107,9 +114,8 @@ namespace GeometryTD.CustomComponent
|
|||
{
|
||||
_eligibleRowBuffer.Clear();
|
||||
|
||||
for (int i = 0; i < allRows.Length; i++)
|
||||
foreach (var row in allRows)
|
||||
{
|
||||
DROutGameDropPool row = allRows[i];
|
||||
if (row == null)
|
||||
{
|
||||
continue;
|
||||
|
|
@ -131,15 +137,18 @@ namespace GeometryTD.CustomComponent
|
|||
|
||||
private RarityType RollRarity(int displayPhaseIndex, Random random)
|
||||
{
|
||||
_rarityWeightBuffer.Clear();
|
||||
for (int i = 0; i < _rarityWeightBuffer.Length; i++)
|
||||
{
|
||||
_rarityWeightBuffer[i] = 0f;
|
||||
}
|
||||
|
||||
float phaseT = Mathf.Clamp01((displayPhaseIndex - 1) / RarityCurveScalePhase);
|
||||
|
||||
for (int i = 0; i < _eligibleRowBuffer.Count; i++)
|
||||
foreach (var row in _eligibleRowBuffer)
|
||||
{
|
||||
DROutGameDropPool row = _eligibleRowBuffer[i];
|
||||
for (int rarityIndex = (int)RarityType.White; rarityIndex <= (int)RarityType.Red; rarityIndex++)
|
||||
for (int rarityIndex = 0; rarityIndex < OrderedRarities.Length; rarityIndex++)
|
||||
{
|
||||
RarityType rarity = (RarityType)rarityIndex;
|
||||
RarityType rarity = OrderedRarities[rarityIndex];
|
||||
if (!IsEligibleAtPhase(row, rarity, displayPhaseIndex))
|
||||
{
|
||||
continue;
|
||||
|
|
@ -157,22 +166,14 @@ namespace GeometryTD.CustomComponent
|
|||
continue;
|
||||
}
|
||||
|
||||
float rarityWeight = rowWeight * curveWeight;
|
||||
if (_rarityWeightBuffer.TryGetValue(rarity, out float existingWeight))
|
||||
{
|
||||
_rarityWeightBuffer[rarity] = existingWeight + rarityWeight;
|
||||
}
|
||||
else
|
||||
{
|
||||
_rarityWeightBuffer[rarity] = rarityWeight;
|
||||
}
|
||||
_rarityWeightBuffer[rarityIndex] += rowWeight * curveWeight;
|
||||
}
|
||||
}
|
||||
|
||||
float totalWeight = 0f;
|
||||
foreach (var pair in _rarityWeightBuffer)
|
||||
for (int rarityIndex = 0; rarityIndex < _rarityWeightBuffer.Length; rarityIndex++)
|
||||
{
|
||||
totalWeight += Mathf.Max(0f, pair.Value);
|
||||
totalWeight += Mathf.Max(0f, _rarityWeightBuffer[rarityIndex]);
|
||||
}
|
||||
|
||||
if (totalWeight <= 0f)
|
||||
|
|
@ -182,18 +183,21 @@ namespace GeometryTD.CustomComponent
|
|||
|
||||
float randomWeight = (float)(random.NextDouble() * totalWeight);
|
||||
float cumulativeWeight = 0f;
|
||||
foreach (var pair in _rarityWeightBuffer)
|
||||
for (int rarityIndex = 0; rarityIndex < _rarityWeightBuffer.Length; rarityIndex++)
|
||||
{
|
||||
cumulativeWeight += Mathf.Max(0f, pair.Value);
|
||||
cumulativeWeight += Mathf.Max(0f, _rarityWeightBuffer[rarityIndex]);
|
||||
if (randomWeight <= cumulativeWeight)
|
||||
{
|
||||
return pair.Key;
|
||||
return OrderedRarities[rarityIndex];
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var pair in _rarityWeightBuffer)
|
||||
for (int rarityIndex = 0; rarityIndex < _rarityWeightBuffer.Length; rarityIndex++)
|
||||
{
|
||||
return pair.Key;
|
||||
if (_rarityWeightBuffer[rarityIndex] > 0f)
|
||||
{
|
||||
return OrderedRarities[rarityIndex];
|
||||
}
|
||||
}
|
||||
|
||||
return RarityType.None;
|
||||
|
|
@ -201,9 +205,9 @@ namespace GeometryTD.CustomComponent
|
|||
|
||||
private static bool IsEligibleAtPhase(DROutGameDropPool row, int displayPhaseIndex)
|
||||
{
|
||||
for (int rarityIndex = (int)RarityType.White; rarityIndex <= (int)RarityType.Red; rarityIndex++)
|
||||
for (int rarityIndex = 0; rarityIndex < OrderedRarities.Length; rarityIndex++)
|
||||
{
|
||||
RarityType rarity = (RarityType)rarityIndex;
|
||||
RarityType rarity = OrderedRarities[rarityIndex];
|
||||
if (IsEligibleAtPhase(row, rarity, displayPhaseIndex))
|
||||
{
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -12,11 +12,6 @@ namespace GeometryTD.CustomComponent
|
|||
{
|
||||
public sealed class InventoryGenerationComponent : GameFrameworkComponent
|
||||
{
|
||||
private int _runSeed;
|
||||
private int _nodeSequenceIndex = -1;
|
||||
private int _nextDropOrdinal;
|
||||
private int _nextRewardOrdinal;
|
||||
|
||||
private readonly List<DRShopPrice> _shopPriceRows = new();
|
||||
private IDataTable<DRShopPrice> _shopPriceTable;
|
||||
private IDataTable<DROutGameDropPool> _dropPoolTable;
|
||||
|
|
@ -28,21 +23,17 @@ namespace GeometryTD.CustomComponent
|
|||
private RewardCandidateBuilder _rewardCandidateBuilder;
|
||||
private OutGameDropItemBuilder _outGameDropItemBuilder;
|
||||
|
||||
public void ConfigureRunContext(int runSeed, int sequenceIndex)
|
||||
{
|
||||
_runSeed = runSeed;
|
||||
_nodeSequenceIndex = sequenceIndex;
|
||||
_nextDropOrdinal = 0;
|
||||
_nextRewardOrdinal = 0;
|
||||
}
|
||||
|
||||
public List<GoodsItemRawData> BuildShopGoods(int goodsCount, int runSeed = 0, int sequenceIndex = -1)
|
||||
{
|
||||
EnsureShopBuilder();
|
||||
return _shopGoodsBuilder.BuildGoods(goodsCount, runSeed, sequenceIndex);
|
||||
}
|
||||
|
||||
public EnemyDropResult ResolveEnemyDrop(in EnemyDropContext context)
|
||||
public EnemyDropResult ResolveEnemyDrop(
|
||||
in EnemyDropContext context,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
ref int nextDropOrdinal)
|
||||
{
|
||||
DREnemy enemy = context.Enemy;
|
||||
if (enemy == null)
|
||||
|
|
@ -56,9 +47,10 @@ namespace GeometryTD.CustomComponent
|
|||
? Mathf.Clamp01(enemy.DropPercent * 0.01f)
|
||||
: Mathf.Clamp01(enemy.DropPercent);
|
||||
|
||||
int dropOrdinal = AllocateDropOrdinal();
|
||||
int dropOrdinal = nextDropOrdinal;
|
||||
nextDropOrdinal++;
|
||||
InventoryGenerationRandomContext randomContext =
|
||||
new(_runSeed, _nodeSequenceIndex, InventoryTagSourceType.Drop, dropOrdinal);
|
||||
new(runSeed, sequenceIndex, InventoryTagSourceType.Drop, dropOrdinal);
|
||||
Random random = randomContext.CreateRandom();
|
||||
|
||||
if (enemy.DropGold > 0 && dropRate > 0f && random.NextDouble() <= dropRate)
|
||||
|
|
@ -84,15 +76,30 @@ namespace GeometryTD.CustomComponent
|
|||
public IReadOnlyList<TowerCompItemData> BuildRewardCandidates(
|
||||
int displayPhaseIndex,
|
||||
LevelThemeType themeType,
|
||||
int candidateCount)
|
||||
int candidateCount,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
ref int nextRewardOrdinal)
|
||||
{
|
||||
RewardCandidateBuilder rewardCandidateBuilder = EnsureRewardCandidateBuilder();
|
||||
return rewardCandidateBuilder.BuildCandidates(
|
||||
int rewardOrdinal = nextRewardOrdinal;
|
||||
IReadOnlyList<TowerCompItemData> candidates = rewardCandidateBuilder.BuildCandidates(
|
||||
displayPhaseIndex,
|
||||
themeType,
|
||||
candidateCount,
|
||||
CreateNextRewardRandomContext,
|
||||
BuildRewardCandidateItem);
|
||||
nextRewardOrdinal = rewardOrdinal;
|
||||
return candidates;
|
||||
|
||||
InventoryGenerationRandomContext CreateNextRewardRandomContext()
|
||||
{
|
||||
return new InventoryGenerationRandomContext(
|
||||
runSeed,
|
||||
sequenceIndex,
|
||||
InventoryTagSourceType.Reward,
|
||||
rewardOrdinal++);
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureShopTables()
|
||||
|
|
@ -205,25 +212,6 @@ namespace GeometryTD.CustomComponent
|
|||
return EnsureOutGameDropItemBuilder().TryBuildItem(selectedRow, selectedRarity, randomContext, out droppedItem);
|
||||
}
|
||||
|
||||
private int AllocateDropOrdinal()
|
||||
{
|
||||
return _nextDropOrdinal++;
|
||||
}
|
||||
|
||||
private int AllocateRewardOrdinal()
|
||||
{
|
||||
return _nextRewardOrdinal++;
|
||||
}
|
||||
|
||||
private InventoryGenerationRandomContext CreateNextRewardRandomContext()
|
||||
{
|
||||
return new InventoryGenerationRandomContext(
|
||||
_runSeed,
|
||||
_nodeSequenceIndex,
|
||||
InventoryTagSourceType.Reward,
|
||||
AllocateRewardOrdinal());
|
||||
}
|
||||
|
||||
private TowerCompItemData BuildRewardCandidateItem(
|
||||
DROutGameDropPool row,
|
||||
RarityType rarity,
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ namespace GeometryTD.UI
|
|||
return;
|
||||
}
|
||||
|
||||
RewardSelectFormRawData nextRawData = _useCase.TryRefresh();
|
||||
RewardSelectFormRawData nextRawData = _useCase.TryRotateSelection();
|
||||
if (nextRawData == null)
|
||||
{
|
||||
CloseUI();
|
||||
|
|
@ -159,4 +159,4 @@ namespace GeometryTD.UI
|
|||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,17 +15,20 @@ namespace GeometryTD.UI
|
|||
|
||||
private int _displayCount = 3;
|
||||
private int _refreshCost;
|
||||
private bool _allowRefreshOnce = true;
|
||||
private bool _allowRotateOnce = true;
|
||||
private bool _allowGiveUp = true;
|
||||
private bool _hasRefreshed;
|
||||
private bool _hasRotated;
|
||||
private int _selectionOffset;
|
||||
private string _tipText = "Select one reward";
|
||||
|
||||
// RewardSelectForm keeps a fixed reward pool for the current node.
|
||||
// The "refresh" action only rotates which slice of that pool is shown so save/load
|
||||
// can reopen the same node with the exact same reward contents.
|
||||
public void ConfigureRewardPool(
|
||||
IReadOnlyList<RewardSelectItemRawData> rewardPool,
|
||||
int displayCount = 3,
|
||||
int refreshCost = 0,
|
||||
bool allowRefreshOnce = true,
|
||||
bool allowRotateOnce = true,
|
||||
bool allowGiveUp = true,
|
||||
string tipText = null)
|
||||
{
|
||||
|
|
@ -45,10 +48,10 @@ namespace GeometryTD.UI
|
|||
|
||||
_displayCount = Mathf.Max(1, displayCount);
|
||||
_refreshCost = Mathf.Max(0, refreshCost);
|
||||
_allowRefreshOnce = allowRefreshOnce;
|
||||
_allowRotateOnce = allowRotateOnce;
|
||||
_allowGiveUp = allowGiveUp;
|
||||
_tipText = string.IsNullOrWhiteSpace(tipText) ? "Select one reward" : tipText;
|
||||
_hasRefreshed = false;
|
||||
_hasRotated = false;
|
||||
_selectionOffset = 0;
|
||||
_currentModel = null;
|
||||
}
|
||||
|
|
@ -57,7 +60,7 @@ namespace GeometryTD.UI
|
|||
IReadOnlyList<TowerCompItemData> rewardCandidates,
|
||||
int displayCount = 3,
|
||||
int refreshCost = 0,
|
||||
bool allowRefreshOnce = true,
|
||||
bool allowRotateOnce = true,
|
||||
bool allowGiveUp = true,
|
||||
string tipText = null)
|
||||
{
|
||||
|
|
@ -77,10 +80,10 @@ namespace GeometryTD.UI
|
|||
|
||||
_displayCount = Mathf.Max(1, displayCount);
|
||||
_refreshCost = Mathf.Max(0, refreshCost);
|
||||
_allowRefreshOnce = allowRefreshOnce;
|
||||
_allowRotateOnce = allowRotateOnce;
|
||||
_allowGiveUp = allowGiveUp;
|
||||
_tipText = string.IsNullOrWhiteSpace(tipText) ? "Select one reward" : tipText;
|
||||
_hasRefreshed = false;
|
||||
_hasRotated = false;
|
||||
_selectionOffset = 0;
|
||||
_currentModel = null;
|
||||
}
|
||||
|
|
@ -93,30 +96,30 @@ namespace GeometryTD.UI
|
|||
|
||||
public RewardSelectFormRawData CreateInitialModel()
|
||||
{
|
||||
_hasRefreshed = false;
|
||||
_hasRotated = false;
|
||||
_currentModel = BuildModel();
|
||||
return _currentModel;
|
||||
}
|
||||
|
||||
public RewardSelectFormRawData TryRefresh()
|
||||
public RewardSelectFormRawData TryRotateSelection()
|
||||
{
|
||||
if (_currentModel == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!CanRefreshInternal())
|
||||
if (!CanRotateSelectionInternal())
|
||||
{
|
||||
return _currentModel;
|
||||
}
|
||||
|
||||
if (!TryConsumeRefreshCost())
|
||||
if (!TryConsumeRotateCost())
|
||||
{
|
||||
_currentModel.CanRefresh = false;
|
||||
return _currentModel;
|
||||
}
|
||||
|
||||
_hasRefreshed = true;
|
||||
_hasRotated = true;
|
||||
if (_rewardPool.Count > 0)
|
||||
{
|
||||
_selectionOffset = (_selectionOffset + _displayCount) % _rewardPool.Count;
|
||||
|
|
@ -166,7 +169,7 @@ namespace GeometryTD.UI
|
|||
TipText = _tipText,
|
||||
RewardItems = selectedRewards,
|
||||
RefreshCost = _refreshCost,
|
||||
CanRefresh = selectedRewards.Length > 0 && CanRefreshInternal(),
|
||||
CanRefresh = selectedRewards.Length > 0 && CanRotateSelectionInternal(),
|
||||
CanGiveUp = _allowGiveUp
|
||||
};
|
||||
}
|
||||
|
|
@ -190,17 +193,17 @@ namespace GeometryTD.UI
|
|||
return results;
|
||||
}
|
||||
|
||||
private bool CanRefreshInternal()
|
||||
private bool CanRotateSelectionInternal()
|
||||
{
|
||||
if (!_allowRefreshOnce || _hasRefreshed)
|
||||
if (!_allowRotateOnce || _hasRotated)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return CanPayRefreshCost();
|
||||
return CanPayRotateCost();
|
||||
}
|
||||
|
||||
private bool CanPayRefreshCost()
|
||||
private bool CanPayRotateCost()
|
||||
{
|
||||
if (_refreshCost <= 0)
|
||||
{
|
||||
|
|
@ -215,7 +218,7 @@ namespace GeometryTD.UI
|
|||
return GameEntry.PlayerInventory.Gold >= _refreshCost;
|
||||
}
|
||||
|
||||
private bool TryConsumeRefreshCost()
|
||||
private bool TryConsumeRotateCost()
|
||||
{
|
||||
if (_refreshCost <= 0)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -183,11 +183,15 @@ namespace GeometryTD.Tests.EditMode
|
|||
CreateBoundPlayerInventory(CreateInventory(100f, 100f, 100f));
|
||||
CombatSettlementContext settlementContext = new CombatSettlementContext();
|
||||
settlementContext.Flags.ShouldOpenRewardSelection = true;
|
||||
int nextRewardOrdinal = 0;
|
||||
|
||||
bool prepared = new CombatSettlementService().TryPrepareRewardSelection(
|
||||
settlementContext,
|
||||
displayPhaseIndex: 1,
|
||||
themeType: LevelThemeType.Plain,
|
||||
runSeed: 1001,
|
||||
sequenceIndex: 3,
|
||||
ref nextRewardOrdinal,
|
||||
rewardSelectFormUseCase: null,
|
||||
onRewardSelected: _ => { },
|
||||
onGiveUp: null);
|
||||
|
|
|
|||
|
|
@ -110,7 +110,43 @@ namespace GeometryTD.Tests.EditMode
|
|||
}
|
||||
|
||||
[Test]
|
||||
public void RewardSelectFormUseCase_Uses_Stable_Order_And_Deterministic_Refresh_Rotation()
|
||||
public void DropPoolRoller_Is_Reproducible_When_DropPool_Row_Order_Changes()
|
||||
{
|
||||
DROutGameDropPool whiteRow = CreateDropPoolRow(1, "MuzzleComp", 1, "[100,0,0,0,0]");
|
||||
DROutGameDropPool greenRow = CreateDropPoolRow(2, "BearingComp", 1, "[0,100,0,0,0]");
|
||||
DROutGameDropPool blueRow = CreateDropPoolRow(3, "BaseComp", 1, "[0,0,100,0,0]");
|
||||
DROutGameDropPool purpleRow = CreateDropPoolRow(4, "MuzzleComp", 2, "[0,0,0,100,0]");
|
||||
DROutGameDropPool redRow = CreateDropPoolRow(5, "BearingComp", 2, "[0,0,0,0,100]");
|
||||
|
||||
DropPoolRoller forwardOrderRoller = new DropPoolRoller(
|
||||
new InsertionOrderDataTable<DROutGameDropPool>(whiteRow, greenRow, blueRow, purpleRow, redRow));
|
||||
DropPoolRoller reverseOrderRoller = new DropPoolRoller(
|
||||
new InsertionOrderDataTable<DROutGameDropPool>(redRow, purpleRow, blueRow, greenRow, whiteRow));
|
||||
|
||||
for (int seed = 1; seed <= 64; seed++)
|
||||
{
|
||||
bool forwardRolled = forwardOrderRoller.TryRollRow(
|
||||
8,
|
||||
LevelThemeType.Plain,
|
||||
new System.Random(seed),
|
||||
out DROutGameDropPool forwardRow,
|
||||
out RarityType forwardRarity);
|
||||
bool reverseRolled = reverseOrderRoller.TryRollRow(
|
||||
8,
|
||||
LevelThemeType.Plain,
|
||||
new System.Random(seed),
|
||||
out DROutGameDropPool reverseRow,
|
||||
out RarityType reverseRarity);
|
||||
|
||||
Assert.That(forwardRolled, Is.True, $"seed={seed}");
|
||||
Assert.That(reverseRolled, Is.True, $"seed={seed}");
|
||||
Assert.That(reverseRarity, Is.EqualTo(forwardRarity), $"seed={seed}");
|
||||
Assert.That(reverseRow.Id, Is.EqualTo(forwardRow.Id), $"seed={seed}");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RewardSelectFormUseCase_Uses_Stable_Order_And_Deterministic_Selection_Rotation()
|
||||
{
|
||||
RewardSelectFormUseCase useCase = new RewardSelectFormUseCase();
|
||||
useCase.ConfigureRewardPool(
|
||||
|
|
@ -123,11 +159,11 @@ namespace GeometryTD.Tests.EditMode
|
|||
},
|
||||
displayCount: 3,
|
||||
refreshCost: 0,
|
||||
allowRefreshOnce: true,
|
||||
allowRotateOnce: true,
|
||||
allowGiveUp: false);
|
||||
|
||||
RewardSelectFormRawData initialModel = useCase.CreateInitialModel();
|
||||
RewardSelectFormRawData refreshedModel = useCase.TryRefresh();
|
||||
RewardSelectFormRawData refreshedModel = useCase.TryRotateSelection();
|
||||
|
||||
Assert.That(initialModel.RewardItems.Select(item => item.Title).ToArray(), Is.EqualTo(new[] { "一号", "二号", "三号" }));
|
||||
Assert.That(refreshedModel.RewardItems.Select(item => item.Title).ToArray(), Is.EqualTo(new[] { "四号", "一号", "二号" }));
|
||||
|
|
@ -141,15 +177,25 @@ namespace GeometryTD.Tests.EditMode
|
|||
|
||||
private string BuildEnemyDropSignature(int runSeed, int sequenceIndex)
|
||||
{
|
||||
_component.ConfigureRunContext(runSeed, sequenceIndex);
|
||||
EnemyDropResult result = _component.ResolveEnemyDrop(new EnemyDropContext(CreateEnemyRow(), 8, LevelThemeType.Plain));
|
||||
int nextDropOrdinal = 0;
|
||||
EnemyDropResult result = _component.ResolveEnemyDrop(
|
||||
new EnemyDropContext(CreateEnemyRow(), 8, LevelThemeType.Plain),
|
||||
runSeed,
|
||||
sequenceIndex,
|
||||
ref nextDropOrdinal);
|
||||
return BuildDropSignaturePart(result);
|
||||
}
|
||||
|
||||
private string BuildRewardCandidateSignature(int runSeed, int sequenceIndex)
|
||||
{
|
||||
_component.ConfigureRunContext(runSeed, sequenceIndex);
|
||||
IReadOnlyList<TowerCompItemData> candidates = _component.BuildRewardCandidates(8, LevelThemeType.Plain, 3);
|
||||
int nextRewardOrdinal = 0;
|
||||
IReadOnlyList<TowerCompItemData> candidates = _component.BuildRewardCandidates(
|
||||
8,
|
||||
LevelThemeType.Plain,
|
||||
3,
|
||||
runSeed,
|
||||
sequenceIndex,
|
||||
ref nextRewardOrdinal);
|
||||
return string.Join("|", candidates.Select(BuildItemSignaturePart));
|
||||
}
|
||||
|
||||
|
|
@ -433,5 +479,185 @@ namespace GeometryTD.Tests.EditMode
|
|||
return ids;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class InsertionOrderDataTable<TRow> : IDataTable<TRow> where TRow : class, IDataRow
|
||||
{
|
||||
private readonly List<TRow> _rows = new();
|
||||
private readonly Dictionary<int, TRow> _rowsById = new();
|
||||
|
||||
public InsertionOrderDataTable(params TRow[] rows)
|
||||
{
|
||||
if (rows == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < rows.Length; i++)
|
||||
{
|
||||
TRow row = rows[i];
|
||||
if (row == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_rows.Add(row);
|
||||
_rowsById[row.Id] = row;
|
||||
}
|
||||
}
|
||||
|
||||
public string Name => typeof(TRow).Name;
|
||||
public string FullName => typeof(TRow).FullName;
|
||||
public Type Type => typeof(TRow);
|
||||
public int Count => _rows.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) => _rowsById.ContainsKey(id);
|
||||
|
||||
public bool HasDataRow(Predicate<TRow> condition) => GetDataRow(condition) != null;
|
||||
|
||||
public TRow GetDataRow(int id) => _rowsById.TryGetValue(id, out TRow row) ? row : null;
|
||||
|
||||
public TRow GetDataRow(Predicate<TRow> condition)
|
||||
{
|
||||
if (condition == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
for (int i = 0; i < _rows.Count; i++)
|
||||
{
|
||||
TRow row = _rows[i];
|
||||
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;
|
||||
}
|
||||
|
||||
for (int i = 0; i < _rows.Count; i++)
|
||||
{
|
||||
TRow row = _rows[i];
|
||||
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(_rows);
|
||||
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;
|
||||
}
|
||||
|
||||
results.AddRange(_rows);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (!_rowsById.TryGetValue(id, out TRow row))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_rowsById.Remove(id);
|
||||
return _rows.Remove(row);
|
||||
}
|
||||
|
||||
public void RemoveAllDataRows()
|
||||
{
|
||||
_rows.Clear();
|
||||
_rowsById.Clear();
|
||||
}
|
||||
|
||||
public IEnumerator<TRow> GetEnumerator()
|
||||
{
|
||||
for (int i = 0; i < _rows.Count; i++)
|
||||
{
|
||||
yield return _rows[i];
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
private int[] GetOrderedIds()
|
||||
{
|
||||
int[] ids = _rowsById.Keys.ToArray();
|
||||
Array.Sort(ids);
|
||||
return ids;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue