修复已知问题
This commit is contained in:
parent
aa44170d56
commit
777c58b812
|
|
@ -100,7 +100,8 @@ namespace GeometryTD.CustomComponent
|
||||||
_runtime.NodeId = nodeId;
|
_runtime.NodeId = nodeId;
|
||||||
_runtime.NodeType = nodeType;
|
_runtime.NodeType = nodeType;
|
||||||
_runtime.SequenceIndex = sequenceIndex;
|
_runtime.SequenceIndex = sequenceIndex;
|
||||||
GameEntry.InventoryGeneration.ConfigureRunContext(runSeed, sequenceIndex);
|
_runtime.NextDropOrdinal = 0;
|
||||||
|
_runtime.NextRewardOrdinal = 0;
|
||||||
_runtime.CombatRunResourceStore.InitializeForCombat(level);
|
_runtime.CombatRunResourceStore.InitializeForCombat(level);
|
||||||
for (int i = 0; i < phases.Count; i++)
|
for (int i = 0; i < phases.Count; i++)
|
||||||
{
|
{
|
||||||
|
|
@ -201,7 +202,13 @@ namespace GeometryTD.CustomComponent
|
||||||
enemy,
|
enemy,
|
||||||
_runtime.PhaseLoopRuntime.DisplayPhaseIndex,
|
_runtime.PhaseLoopRuntime.DisplayPhaseIndex,
|
||||||
_coordinator.ResolveCurrentThemeType());
|
_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.AddEnemyDefeatedReward(result.Coin, result.Gold);
|
||||||
_runtime.CombatRunResourceStore.AddEnemyDefeatedLoot(result.LootItem);
|
_runtime.CombatRunResourceStore.AddEnemyDefeatedLoot(result.LootItem);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,8 @@ namespace GeometryTD.CustomComponent
|
||||||
_runtime.NodeId = 0;
|
_runtime.NodeId = 0;
|
||||||
_runtime.NodeType = RunNodeType.None;
|
_runtime.NodeType = RunNodeType.None;
|
||||||
_runtime.SequenceIndex = -1;
|
_runtime.SequenceIndex = -1;
|
||||||
|
_runtime.NextDropOrdinal = 0;
|
||||||
|
_runtime.NextRewardOrdinal = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CleanupAllCombatEntities()
|
public void CleanupAllCombatEntities()
|
||||||
|
|
|
||||||
|
|
@ -34,5 +34,7 @@ namespace GeometryTD.CustomComponent
|
||||||
public int NodeId { get; set; }
|
public int NodeId { get; set; }
|
||||||
public RunNodeType NodeType { get; set; }
|
public RunNodeType NodeType { get; set; }
|
||||||
public int SequenceIndex { 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,
|
CombatSettlementContext settlementContext,
|
||||||
int displayPhaseIndex,
|
int displayPhaseIndex,
|
||||||
LevelThemeType themeType,
|
LevelThemeType themeType,
|
||||||
|
int runSeed,
|
||||||
|
int sequenceIndex,
|
||||||
|
ref int nextRewardOrdinal,
|
||||||
RewardSelectFormUseCase rewardSelectFormUseCase,
|
RewardSelectFormUseCase rewardSelectFormUseCase,
|
||||||
Action<RewardSelectItemRawData> onRewardSelected,
|
Action<RewardSelectItemRawData> onRewardSelected,
|
||||||
Action onGiveUp)
|
Action onGiveUp)
|
||||||
|
|
@ -48,7 +51,10 @@ namespace GeometryTD.CustomComponent
|
||||||
IReadOnlyList<TowerCompItemData> candidateItems = GameEntry.InventoryGeneration.BuildRewardCandidates(
|
IReadOnlyList<TowerCompItemData> candidateItems = GameEntry.InventoryGeneration.BuildRewardCandidates(
|
||||||
displayPhaseIndex,
|
displayPhaseIndex,
|
||||||
themeType,
|
themeType,
|
||||||
RewardSelectDisplayCount);
|
RewardSelectDisplayCount,
|
||||||
|
runSeed,
|
||||||
|
sequenceIndex,
|
||||||
|
ref nextRewardOrdinal);
|
||||||
if (candidateItems == null || candidateItems.Count <= 0)
|
if (candidateItems == null || candidateItems.Count <= 0)
|
||||||
{
|
{
|
||||||
settlementContext.Flags.ShouldOpenRewardSelection = false;
|
settlementContext.Flags.ShouldOpenRewardSelection = false;
|
||||||
|
|
@ -60,7 +66,7 @@ namespace GeometryTD.CustomComponent
|
||||||
candidateItems,
|
candidateItems,
|
||||||
displayCount: RewardSelectDisplayCount,
|
displayCount: RewardSelectDisplayCount,
|
||||||
refreshCost: 0,
|
refreshCost: 0,
|
||||||
allowRefreshOnce: false,
|
allowRotateOnce: false,
|
||||||
allowGiveUp: false,
|
allowGiveUp: false,
|
||||||
tipText: "基地满血奖励:请选择 1 个组件");
|
tipText: "基地满血奖励:请选择 1 个组件");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,16 +16,23 @@ namespace GeometryTD.CustomComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
Coordinator.EnsureRewardSelectFormUseCaseBound();
|
Coordinator.EnsureRewardSelectFormUseCaseBound();
|
||||||
|
int nextRewardOrdinal = Runtime.NextRewardOrdinal;
|
||||||
if (!Runtime.CombatSettlementService.TryPrepareRewardSelection(
|
if (!Runtime.CombatSettlementService.TryPrepareRewardSelection(
|
||||||
Runtime.SettlementContext,
|
Runtime.SettlementContext,
|
||||||
Runtime.PhaseLoopRuntime.DisplayPhaseIndex,
|
Runtime.PhaseLoopRuntime.DisplayPhaseIndex,
|
||||||
Coordinator.ResolveCurrentThemeType(),
|
Coordinator.ResolveCurrentThemeType(),
|
||||||
|
Runtime.RunSeed,
|
||||||
|
Runtime.SequenceIndex,
|
||||||
|
ref nextRewardOrdinal,
|
||||||
Runtime.RewardSelectFormUseCase,
|
Runtime.RewardSelectFormUseCase,
|
||||||
Coordinator.OnFullBaseHpRewardSelected,
|
Coordinator.OnFullBaseHpRewardSelected,
|
||||||
Coordinator.OnFullBaseHpRewardGiveUp))
|
Coordinator.OnFullBaseHpRewardGiveUp))
|
||||||
{
|
{
|
||||||
Coordinator.ChangeState(new CombatFinishFormState(Runtime, Coordinator));
|
Coordinator.ChangeState(new CombatFinishFormState(Runtime, Coordinator));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Runtime.NextRewardOrdinal = nextRewardOrdinal;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnExit()
|
public override void OnExit()
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,17 @@ namespace GeometryTD.CustomComponent
|
||||||
public sealed class DropPoolRoller
|
public sealed class DropPoolRoller
|
||||||
{
|
{
|
||||||
private const float RarityCurveScalePhase = 30f;
|
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 List<DROutGameDropPool> _eligibleRowBuffer = new();
|
||||||
private readonly Dictionary<RarityType, float> _rarityWeightBuffer = new();
|
private readonly float[] _rarityWeightBuffer = new float[OrderedRarities.Length];
|
||||||
private readonly IDataTable<DROutGameDropPool> _dropPoolTable;
|
private readonly IDataTable<DROutGameDropPool> _dropPoolTable;
|
||||||
|
|
||||||
public DropPoolRoller(IDataTable<DROutGameDropPool> dropPoolTable)
|
public DropPoolRoller(IDataTable<DROutGameDropPool> dropPoolTable)
|
||||||
|
|
@ -50,9 +58,8 @@ namespace GeometryTD.CustomComponent
|
||||||
|
|
||||||
int totalWeight = 0;
|
int totalWeight = 0;
|
||||||
DROutGameDropPool fallbackRow = null;
|
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))
|
if (!IsEligibleAtPhase(row, selectedRarity, displayPhaseIndex))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -107,9 +114,8 @@ namespace GeometryTD.CustomComponent
|
||||||
{
|
{
|
||||||
_eligibleRowBuffer.Clear();
|
_eligibleRowBuffer.Clear();
|
||||||
|
|
||||||
for (int i = 0; i < allRows.Length; i++)
|
foreach (var row in allRows)
|
||||||
{
|
{
|
||||||
DROutGameDropPool row = allRows[i];
|
|
||||||
if (row == null)
|
if (row == null)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -131,15 +137,18 @@ namespace GeometryTD.CustomComponent
|
||||||
|
|
||||||
private RarityType RollRarity(int displayPhaseIndex, Random random)
|
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);
|
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 = 0; rarityIndex < OrderedRarities.Length; rarityIndex++)
|
||||||
for (int rarityIndex = (int)RarityType.White; rarityIndex <= (int)RarityType.Red; rarityIndex++)
|
|
||||||
{
|
{
|
||||||
RarityType rarity = (RarityType)rarityIndex;
|
RarityType rarity = OrderedRarities[rarityIndex];
|
||||||
if (!IsEligibleAtPhase(row, rarity, displayPhaseIndex))
|
if (!IsEligibleAtPhase(row, rarity, displayPhaseIndex))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -157,22 +166,14 @@ namespace GeometryTD.CustomComponent
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
float rarityWeight = rowWeight * curveWeight;
|
_rarityWeightBuffer[rarityIndex] += rowWeight * curveWeight;
|
||||||
if (_rarityWeightBuffer.TryGetValue(rarity, out float existingWeight))
|
|
||||||
{
|
|
||||||
_rarityWeightBuffer[rarity] = existingWeight + rarityWeight;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_rarityWeightBuffer[rarity] = rarityWeight;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
float totalWeight = 0f;
|
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)
|
if (totalWeight <= 0f)
|
||||||
|
|
@ -182,18 +183,21 @@ namespace GeometryTD.CustomComponent
|
||||||
|
|
||||||
float randomWeight = (float)(random.NextDouble() * totalWeight);
|
float randomWeight = (float)(random.NextDouble() * totalWeight);
|
||||||
float cumulativeWeight = 0f;
|
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)
|
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;
|
return RarityType.None;
|
||||||
|
|
@ -201,9 +205,9 @@ namespace GeometryTD.CustomComponent
|
||||||
|
|
||||||
private static bool IsEligibleAtPhase(DROutGameDropPool row, int displayPhaseIndex)
|
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))
|
if (IsEligibleAtPhase(row, rarity, displayPhaseIndex))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,6 @@ namespace GeometryTD.CustomComponent
|
||||||
{
|
{
|
||||||
public sealed class InventoryGenerationComponent : GameFrameworkComponent
|
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 readonly List<DRShopPrice> _shopPriceRows = new();
|
||||||
private IDataTable<DRShopPrice> _shopPriceTable;
|
private IDataTable<DRShopPrice> _shopPriceTable;
|
||||||
private IDataTable<DROutGameDropPool> _dropPoolTable;
|
private IDataTable<DROutGameDropPool> _dropPoolTable;
|
||||||
|
|
@ -28,21 +23,17 @@ namespace GeometryTD.CustomComponent
|
||||||
private RewardCandidateBuilder _rewardCandidateBuilder;
|
private RewardCandidateBuilder _rewardCandidateBuilder;
|
||||||
private OutGameDropItemBuilder _outGameDropItemBuilder;
|
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)
|
public List<GoodsItemRawData> BuildShopGoods(int goodsCount, int runSeed = 0, int sequenceIndex = -1)
|
||||||
{
|
{
|
||||||
EnsureShopBuilder();
|
EnsureShopBuilder();
|
||||||
return _shopGoodsBuilder.BuildGoods(goodsCount, runSeed, sequenceIndex);
|
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;
|
DREnemy enemy = context.Enemy;
|
||||||
if (enemy == null)
|
if (enemy == null)
|
||||||
|
|
@ -56,9 +47,10 @@ namespace GeometryTD.CustomComponent
|
||||||
? Mathf.Clamp01(enemy.DropPercent * 0.01f)
|
? Mathf.Clamp01(enemy.DropPercent * 0.01f)
|
||||||
: Mathf.Clamp01(enemy.DropPercent);
|
: Mathf.Clamp01(enemy.DropPercent);
|
||||||
|
|
||||||
int dropOrdinal = AllocateDropOrdinal();
|
int dropOrdinal = nextDropOrdinal;
|
||||||
|
nextDropOrdinal++;
|
||||||
InventoryGenerationRandomContext randomContext =
|
InventoryGenerationRandomContext randomContext =
|
||||||
new(_runSeed, _nodeSequenceIndex, InventoryTagSourceType.Drop, dropOrdinal);
|
new(runSeed, sequenceIndex, InventoryTagSourceType.Drop, dropOrdinal);
|
||||||
Random random = randomContext.CreateRandom();
|
Random random = randomContext.CreateRandom();
|
||||||
|
|
||||||
if (enemy.DropGold > 0 && dropRate > 0f && random.NextDouble() <= dropRate)
|
if (enemy.DropGold > 0 && dropRate > 0f && random.NextDouble() <= dropRate)
|
||||||
|
|
@ -84,15 +76,30 @@ namespace GeometryTD.CustomComponent
|
||||||
public IReadOnlyList<TowerCompItemData> BuildRewardCandidates(
|
public IReadOnlyList<TowerCompItemData> BuildRewardCandidates(
|
||||||
int displayPhaseIndex,
|
int displayPhaseIndex,
|
||||||
LevelThemeType themeType,
|
LevelThemeType themeType,
|
||||||
int candidateCount)
|
int candidateCount,
|
||||||
|
int runSeed,
|
||||||
|
int sequenceIndex,
|
||||||
|
ref int nextRewardOrdinal)
|
||||||
{
|
{
|
||||||
RewardCandidateBuilder rewardCandidateBuilder = EnsureRewardCandidateBuilder();
|
RewardCandidateBuilder rewardCandidateBuilder = EnsureRewardCandidateBuilder();
|
||||||
return rewardCandidateBuilder.BuildCandidates(
|
int rewardOrdinal = nextRewardOrdinal;
|
||||||
|
IReadOnlyList<TowerCompItemData> candidates = rewardCandidateBuilder.BuildCandidates(
|
||||||
displayPhaseIndex,
|
displayPhaseIndex,
|
||||||
themeType,
|
themeType,
|
||||||
candidateCount,
|
candidateCount,
|
||||||
CreateNextRewardRandomContext,
|
CreateNextRewardRandomContext,
|
||||||
BuildRewardCandidateItem);
|
BuildRewardCandidateItem);
|
||||||
|
nextRewardOrdinal = rewardOrdinal;
|
||||||
|
return candidates;
|
||||||
|
|
||||||
|
InventoryGenerationRandomContext CreateNextRewardRandomContext()
|
||||||
|
{
|
||||||
|
return new InventoryGenerationRandomContext(
|
||||||
|
runSeed,
|
||||||
|
sequenceIndex,
|
||||||
|
InventoryTagSourceType.Reward,
|
||||||
|
rewardOrdinal++);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EnsureShopTables()
|
private void EnsureShopTables()
|
||||||
|
|
@ -205,25 +212,6 @@ namespace GeometryTD.CustomComponent
|
||||||
return EnsureOutGameDropItemBuilder().TryBuildItem(selectedRow, selectedRarity, randomContext, out droppedItem);
|
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(
|
private TowerCompItemData BuildRewardCandidateItem(
|
||||||
DROutGameDropPool row,
|
DROutGameDropPool row,
|
||||||
RarityType rarity,
|
RarityType rarity,
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ namespace GeometryTD.UI
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
RewardSelectFormRawData nextRawData = _useCase.TryRefresh();
|
RewardSelectFormRawData nextRawData = _useCase.TryRotateSelection();
|
||||||
if (nextRawData == null)
|
if (nextRawData == null)
|
||||||
{
|
{
|
||||||
CloseUI();
|
CloseUI();
|
||||||
|
|
@ -159,4 +159,4 @@ namespace GeometryTD.UI
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,17 +15,20 @@ namespace GeometryTD.UI
|
||||||
|
|
||||||
private int _displayCount = 3;
|
private int _displayCount = 3;
|
||||||
private int _refreshCost;
|
private int _refreshCost;
|
||||||
private bool _allowRefreshOnce = true;
|
private bool _allowRotateOnce = true;
|
||||||
private bool _allowGiveUp = true;
|
private bool _allowGiveUp = true;
|
||||||
private bool _hasRefreshed;
|
private bool _hasRotated;
|
||||||
private int _selectionOffset;
|
private int _selectionOffset;
|
||||||
private string _tipText = "Select one reward";
|
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(
|
public void ConfigureRewardPool(
|
||||||
IReadOnlyList<RewardSelectItemRawData> rewardPool,
|
IReadOnlyList<RewardSelectItemRawData> rewardPool,
|
||||||
int displayCount = 3,
|
int displayCount = 3,
|
||||||
int refreshCost = 0,
|
int refreshCost = 0,
|
||||||
bool allowRefreshOnce = true,
|
bool allowRotateOnce = true,
|
||||||
bool allowGiveUp = true,
|
bool allowGiveUp = true,
|
||||||
string tipText = null)
|
string tipText = null)
|
||||||
{
|
{
|
||||||
|
|
@ -45,10 +48,10 @@ namespace GeometryTD.UI
|
||||||
|
|
||||||
_displayCount = Mathf.Max(1, displayCount);
|
_displayCount = Mathf.Max(1, displayCount);
|
||||||
_refreshCost = Mathf.Max(0, refreshCost);
|
_refreshCost = Mathf.Max(0, refreshCost);
|
||||||
_allowRefreshOnce = allowRefreshOnce;
|
_allowRotateOnce = allowRotateOnce;
|
||||||
_allowGiveUp = allowGiveUp;
|
_allowGiveUp = allowGiveUp;
|
||||||
_tipText = string.IsNullOrWhiteSpace(tipText) ? "Select one reward" : tipText;
|
_tipText = string.IsNullOrWhiteSpace(tipText) ? "Select one reward" : tipText;
|
||||||
_hasRefreshed = false;
|
_hasRotated = false;
|
||||||
_selectionOffset = 0;
|
_selectionOffset = 0;
|
||||||
_currentModel = null;
|
_currentModel = null;
|
||||||
}
|
}
|
||||||
|
|
@ -57,7 +60,7 @@ namespace GeometryTD.UI
|
||||||
IReadOnlyList<TowerCompItemData> rewardCandidates,
|
IReadOnlyList<TowerCompItemData> rewardCandidates,
|
||||||
int displayCount = 3,
|
int displayCount = 3,
|
||||||
int refreshCost = 0,
|
int refreshCost = 0,
|
||||||
bool allowRefreshOnce = true,
|
bool allowRotateOnce = true,
|
||||||
bool allowGiveUp = true,
|
bool allowGiveUp = true,
|
||||||
string tipText = null)
|
string tipText = null)
|
||||||
{
|
{
|
||||||
|
|
@ -77,10 +80,10 @@ namespace GeometryTD.UI
|
||||||
|
|
||||||
_displayCount = Mathf.Max(1, displayCount);
|
_displayCount = Mathf.Max(1, displayCount);
|
||||||
_refreshCost = Mathf.Max(0, refreshCost);
|
_refreshCost = Mathf.Max(0, refreshCost);
|
||||||
_allowRefreshOnce = allowRefreshOnce;
|
_allowRotateOnce = allowRotateOnce;
|
||||||
_allowGiveUp = allowGiveUp;
|
_allowGiveUp = allowGiveUp;
|
||||||
_tipText = string.IsNullOrWhiteSpace(tipText) ? "Select one reward" : tipText;
|
_tipText = string.IsNullOrWhiteSpace(tipText) ? "Select one reward" : tipText;
|
||||||
_hasRefreshed = false;
|
_hasRotated = false;
|
||||||
_selectionOffset = 0;
|
_selectionOffset = 0;
|
||||||
_currentModel = null;
|
_currentModel = null;
|
||||||
}
|
}
|
||||||
|
|
@ -93,30 +96,30 @@ namespace GeometryTD.UI
|
||||||
|
|
||||||
public RewardSelectFormRawData CreateInitialModel()
|
public RewardSelectFormRawData CreateInitialModel()
|
||||||
{
|
{
|
||||||
_hasRefreshed = false;
|
_hasRotated = false;
|
||||||
_currentModel = BuildModel();
|
_currentModel = BuildModel();
|
||||||
return _currentModel;
|
return _currentModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RewardSelectFormRawData TryRefresh()
|
public RewardSelectFormRawData TryRotateSelection()
|
||||||
{
|
{
|
||||||
if (_currentModel == null)
|
if (_currentModel == null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!CanRefreshInternal())
|
if (!CanRotateSelectionInternal())
|
||||||
{
|
{
|
||||||
return _currentModel;
|
return _currentModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryConsumeRefreshCost())
|
if (!TryConsumeRotateCost())
|
||||||
{
|
{
|
||||||
_currentModel.CanRefresh = false;
|
_currentModel.CanRefresh = false;
|
||||||
return _currentModel;
|
return _currentModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
_hasRefreshed = true;
|
_hasRotated = true;
|
||||||
if (_rewardPool.Count > 0)
|
if (_rewardPool.Count > 0)
|
||||||
{
|
{
|
||||||
_selectionOffset = (_selectionOffset + _displayCount) % _rewardPool.Count;
|
_selectionOffset = (_selectionOffset + _displayCount) % _rewardPool.Count;
|
||||||
|
|
@ -166,7 +169,7 @@ namespace GeometryTD.UI
|
||||||
TipText = _tipText,
|
TipText = _tipText,
|
||||||
RewardItems = selectedRewards,
|
RewardItems = selectedRewards,
|
||||||
RefreshCost = _refreshCost,
|
RefreshCost = _refreshCost,
|
||||||
CanRefresh = selectedRewards.Length > 0 && CanRefreshInternal(),
|
CanRefresh = selectedRewards.Length > 0 && CanRotateSelectionInternal(),
|
||||||
CanGiveUp = _allowGiveUp
|
CanGiveUp = _allowGiveUp
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -190,17 +193,17 @@ namespace GeometryTD.UI
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CanRefreshInternal()
|
private bool CanRotateSelectionInternal()
|
||||||
{
|
{
|
||||||
if (!_allowRefreshOnce || _hasRefreshed)
|
if (!_allowRotateOnce || _hasRotated)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return CanPayRefreshCost();
|
return CanPayRotateCost();
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CanPayRefreshCost()
|
private bool CanPayRotateCost()
|
||||||
{
|
{
|
||||||
if (_refreshCost <= 0)
|
if (_refreshCost <= 0)
|
||||||
{
|
{
|
||||||
|
|
@ -215,7 +218,7 @@ namespace GeometryTD.UI
|
||||||
return GameEntry.PlayerInventory.Gold >= _refreshCost;
|
return GameEntry.PlayerInventory.Gold >= _refreshCost;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryConsumeRefreshCost()
|
private bool TryConsumeRotateCost()
|
||||||
{
|
{
|
||||||
if (_refreshCost <= 0)
|
if (_refreshCost <= 0)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -183,11 +183,15 @@ namespace GeometryTD.Tests.EditMode
|
||||||
CreateBoundPlayerInventory(CreateInventory(100f, 100f, 100f));
|
CreateBoundPlayerInventory(CreateInventory(100f, 100f, 100f));
|
||||||
CombatSettlementContext settlementContext = new CombatSettlementContext();
|
CombatSettlementContext settlementContext = new CombatSettlementContext();
|
||||||
settlementContext.Flags.ShouldOpenRewardSelection = true;
|
settlementContext.Flags.ShouldOpenRewardSelection = true;
|
||||||
|
int nextRewardOrdinal = 0;
|
||||||
|
|
||||||
bool prepared = new CombatSettlementService().TryPrepareRewardSelection(
|
bool prepared = new CombatSettlementService().TryPrepareRewardSelection(
|
||||||
settlementContext,
|
settlementContext,
|
||||||
displayPhaseIndex: 1,
|
displayPhaseIndex: 1,
|
||||||
themeType: LevelThemeType.Plain,
|
themeType: LevelThemeType.Plain,
|
||||||
|
runSeed: 1001,
|
||||||
|
sequenceIndex: 3,
|
||||||
|
ref nextRewardOrdinal,
|
||||||
rewardSelectFormUseCase: null,
|
rewardSelectFormUseCase: null,
|
||||||
onRewardSelected: _ => { },
|
onRewardSelected: _ => { },
|
||||||
onGiveUp: null);
|
onGiveUp: null);
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,43 @@ namespace GeometryTD.Tests.EditMode
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[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();
|
RewardSelectFormUseCase useCase = new RewardSelectFormUseCase();
|
||||||
useCase.ConfigureRewardPool(
|
useCase.ConfigureRewardPool(
|
||||||
|
|
@ -123,11 +159,11 @@ namespace GeometryTD.Tests.EditMode
|
||||||
},
|
},
|
||||||
displayCount: 3,
|
displayCount: 3,
|
||||||
refreshCost: 0,
|
refreshCost: 0,
|
||||||
allowRefreshOnce: true,
|
allowRotateOnce: true,
|
||||||
allowGiveUp: false);
|
allowGiveUp: false);
|
||||||
|
|
||||||
RewardSelectFormRawData initialModel = useCase.CreateInitialModel();
|
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(initialModel.RewardItems.Select(item => item.Title).ToArray(), Is.EqualTo(new[] { "一号", "二号", "三号" }));
|
||||||
Assert.That(refreshedModel.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)
|
private string BuildEnemyDropSignature(int runSeed, int sequenceIndex)
|
||||||
{
|
{
|
||||||
_component.ConfigureRunContext(runSeed, sequenceIndex);
|
int nextDropOrdinal = 0;
|
||||||
EnemyDropResult result = _component.ResolveEnemyDrop(new EnemyDropContext(CreateEnemyRow(), 8, LevelThemeType.Plain));
|
EnemyDropResult result = _component.ResolveEnemyDrop(
|
||||||
|
new EnemyDropContext(CreateEnemyRow(), 8, LevelThemeType.Plain),
|
||||||
|
runSeed,
|
||||||
|
sequenceIndex,
|
||||||
|
ref nextDropOrdinal);
|
||||||
return BuildDropSignaturePart(result);
|
return BuildDropSignaturePart(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string BuildRewardCandidateSignature(int runSeed, int sequenceIndex)
|
private string BuildRewardCandidateSignature(int runSeed, int sequenceIndex)
|
||||||
{
|
{
|
||||||
_component.ConfigureRunContext(runSeed, sequenceIndex);
|
int nextRewardOrdinal = 0;
|
||||||
IReadOnlyList<TowerCompItemData> candidates = _component.BuildRewardCandidates(8, LevelThemeType.Plain, 3);
|
IReadOnlyList<TowerCompItemData> candidates = _component.BuildRewardCandidates(
|
||||||
|
8,
|
||||||
|
LevelThemeType.Plain,
|
||||||
|
3,
|
||||||
|
runSeed,
|
||||||
|
sequenceIndex,
|
||||||
|
ref nextRewardOrdinal);
|
||||||
return string.Join("|", candidates.Select(BuildItemSignaturePart));
|
return string.Join("|", candidates.Select(BuildItemSignaturePart));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -433,5 +479,185 @@ namespace GeometryTD.Tests.EditMode
|
||||||
return ids;
|
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