InventoryGenerationComponent G2
This commit is contained in:
parent
f67888a8da
commit
53b6b261dd
|
|
@ -100,7 +100,7 @@ namespace GeometryTD.CustomComponent
|
||||||
_runtime.NodeId = nodeId;
|
_runtime.NodeId = nodeId;
|
||||||
_runtime.NodeType = nodeType;
|
_runtime.NodeType = nodeType;
|
||||||
_runtime.SequenceIndex = sequenceIndex;
|
_runtime.SequenceIndex = sequenceIndex;
|
||||||
_runtime.EnemyDropResolver.ConfigureRunContext(runSeed, sequenceIndex);
|
GameEntry.InventoryGeneration.ConfigureRunContext(runSeed, sequenceIndex);
|
||||||
_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 +201,7 @@ namespace GeometryTD.CustomComponent
|
||||||
enemy,
|
enemy,
|
||||||
_runtime.PhaseLoopRuntime.DisplayPhaseIndex,
|
_runtime.PhaseLoopRuntime.DisplayPhaseIndex,
|
||||||
_coordinator.ResolveCurrentThemeType());
|
_coordinator.ResolveCurrentThemeType());
|
||||||
EnemyDropResult result = _runtime.EnemyDropResolver.Resolve(context);
|
EnemyDropResult result = GameEntry.InventoryGeneration.ResolveEnemyDrop(context);
|
||||||
_runtime.CombatRunResourceStore.AddEnemyDefeatedReward(result.Coin, result.Gold);
|
_runtime.CombatRunResourceStore.AddEnemyDefeatedReward(result.Coin, result.Gold);
|
||||||
_runtime.CombatRunResourceStore.AddEnemyDefeatedLoot(result.LootItem);
|
_runtime.CombatRunResourceStore.AddEnemyDefeatedLoot(result.LootItem);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,6 @@ namespace GeometryTD.CustomComponent
|
||||||
_runtime.PhaseLoopRuntime.Reset();
|
_runtime.PhaseLoopRuntime.Reset();
|
||||||
_runtime.LoadSession.Reset();
|
_runtime.LoadSession.Reset();
|
||||||
_runtime.CombatRunResourceStore.Reset();
|
_runtime.CombatRunResourceStore.Reset();
|
||||||
_runtime.EnemyDropResolver.Reset();
|
|
||||||
_runtime.SettlementContext = null;
|
_runtime.SettlementContext = null;
|
||||||
_runtime.CurrentLevel = null;
|
_runtime.CurrentLevel = null;
|
||||||
_runtime.DidCombatWin = true;
|
_runtime.DidCombatWin = true;
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ namespace GeometryTD.CustomComponent
|
||||||
public CombatLoadSession LoadSession { get; } = new();
|
public CombatLoadSession LoadSession { get; } = new();
|
||||||
public CombatEventBridge EventBridge { get; } = new();
|
public CombatEventBridge EventBridge { get; } = new();
|
||||||
public CombatRunResourceStore CombatRunResourceStore { get; } = new();
|
public CombatRunResourceStore CombatRunResourceStore { get; } = new();
|
||||||
public EnemyDropResolver EnemyDropResolver { get; } = new();
|
|
||||||
public CombatSettlementService CombatSettlementService { get; } = new();
|
public CombatSettlementService CombatSettlementService { get; } = new();
|
||||||
|
|
||||||
public EntityComponent Entity { get; set; }
|
public EntityComponent Entity { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -83,19 +83,18 @@ namespace GeometryTD.CustomComponent
|
||||||
|
|
||||||
public bool TryPrepareRewardSelection(
|
public bool TryPrepareRewardSelection(
|
||||||
CombatSettlementContext settlementContext,
|
CombatSettlementContext settlementContext,
|
||||||
EnemyDropResolver enemyDropResolver,
|
|
||||||
int displayPhaseIndex,
|
int displayPhaseIndex,
|
||||||
LevelThemeType themeType,
|
LevelThemeType themeType,
|
||||||
RewardSelectFormUseCase rewardSelectFormUseCase,
|
RewardSelectFormUseCase rewardSelectFormUseCase,
|
||||||
Action<RewardSelectItemRawData> onRewardSelected,
|
Action<RewardSelectItemRawData> onRewardSelected,
|
||||||
Action onGiveUp)
|
Action onGiveUp)
|
||||||
{
|
{
|
||||||
if (settlementContext == null || enemyDropResolver == null || rewardSelectFormUseCase == null)
|
if (settlementContext == null || rewardSelectFormUseCase == null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
IReadOnlyList<TowerCompItemData> candidateItems = enemyDropResolver.RollSettlementRewardCandidates(
|
IReadOnlyList<TowerCompItemData> candidateItems = GameEntry.InventoryGeneration.BuildRewardCandidates(
|
||||||
displayPhaseIndex,
|
displayPhaseIndex,
|
||||||
themeType,
|
themeType,
|
||||||
RewardSelectDisplayCount);
|
RewardSelectDisplayCount);
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ namespace GeometryTD.CustomComponent
|
||||||
Coordinator.EnsureRewardSelectFormUseCaseBound();
|
Coordinator.EnsureRewardSelectFormUseCaseBound();
|
||||||
if (!Runtime.CombatSettlementService.TryPrepareRewardSelection(
|
if (!Runtime.CombatSettlementService.TryPrepareRewardSelection(
|
||||||
Runtime.SettlementContext,
|
Runtime.SettlementContext,
|
||||||
Runtime.EnemyDropResolver,
|
|
||||||
Runtime.PhaseLoopRuntime.DisplayPhaseIndex,
|
Runtime.PhaseLoopRuntime.DisplayPhaseIndex,
|
||||||
Coordinator.ResolveCurrentThemeType(),
|
Coordinator.ResolveCurrentThemeType(),
|
||||||
Runtime.RewardSelectFormUseCase,
|
Runtime.RewardSelectFormUseCase,
|
||||||
|
|
|
||||||
|
|
@ -1,493 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using GameFramework.DataTable;
|
|
||||||
using GeometryTD.DataTable;
|
|
||||||
using GeometryTD.Definition;
|
|
||||||
using UnityEngine;
|
|
||||||
using Random = UnityEngine.Random;
|
|
||||||
|
|
||||||
namespace GeometryTD.CustomComponent
|
|
||||||
{
|
|
||||||
public sealed class EnemyDropResolver
|
|
||||||
{
|
|
||||||
private const float DropChanceBase = 0.05f;
|
|
||||||
private const float DropChancePerPhase = 0.2f;
|
|
||||||
private const float DropChanceCap = 0.2f;
|
|
||||||
private const float RarityCurveScalePhase = 30f;
|
|
||||||
|
|
||||||
private readonly List<DROutGameDropPool> _eligibleDropPoolBuffer = new();
|
|
||||||
private readonly Dictionary<RarityType, float> _rarityRollWeightBuffer = new();
|
|
||||||
private IDataTable<DROutGameDropPool> _drOutGameDropPool;
|
|
||||||
private IDataTable<DRMuzzleComp> _drMuzzleComp;
|
|
||||||
private IDataTable<DRBearingComp> _drBearingComp;
|
|
||||||
private IDataTable<DRBaseComp> _drBaseComp;
|
|
||||||
private long _nextDropItemInstanceId = 1;
|
|
||||||
private int _runSeed;
|
|
||||||
private int _nodeSequenceIndex = -1;
|
|
||||||
private int _nextDropTagOrdinal;
|
|
||||||
private int _nextRewardTagOrdinal;
|
|
||||||
|
|
||||||
public void Reset()
|
|
||||||
{
|
|
||||||
_eligibleDropPoolBuffer.Clear();
|
|
||||||
_rarityRollWeightBuffer.Clear();
|
|
||||||
_nextDropItemInstanceId = 1;
|
|
||||||
_runSeed = 0;
|
|
||||||
_nodeSequenceIndex = -1;
|
|
||||||
_nextDropTagOrdinal = 0;
|
|
||||||
_nextRewardTagOrdinal = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ConfigureRunContext(int runSeed, int nodeSequenceIndex)
|
|
||||||
{
|
|
||||||
_runSeed = runSeed;
|
|
||||||
_nodeSequenceIndex = nodeSequenceIndex;
|
|
||||||
_nextDropTagOrdinal = 0;
|
|
||||||
_nextRewardTagOrdinal = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public EnemyDropResult Resolve(in EnemyDropContext context)
|
|
||||||
{
|
|
||||||
DREnemy enemy = context.Enemy;
|
|
||||||
if (enemy == null)
|
|
||||||
{
|
|
||||||
return EnemyDropResult.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
int coin = Mathf.Max(0, enemy.DropCoin);
|
|
||||||
int gold = 0;
|
|
||||||
|
|
||||||
float dropRate = enemy.DropPercent > 1f
|
|
||||||
? Mathf.Clamp01(enemy.DropPercent * 0.01f)
|
|
||||||
: Mathf.Clamp01(enemy.DropPercent);
|
|
||||||
|
|
||||||
if (enemy.DropGold > 0 && dropRate > 0f && Random.value <= dropRate)
|
|
||||||
{
|
|
||||||
gold = Mathf.Max(0, enemy.DropGold);
|
|
||||||
}
|
|
||||||
|
|
||||||
TowerCompItemData lootItem = null;
|
|
||||||
if (ShouldRollOutGameItem(context.DisplayPhaseIndex) &&
|
|
||||||
TryRollOutGameItem(context.DisplayPhaseIndex, context.ThemeType, out TowerCompItemData droppedItem))
|
|
||||||
{
|
|
||||||
lootItem = droppedItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new EnemyDropResult(coin, gold, lootItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IReadOnlyList<TowerCompItemData> RollSettlementRewardCandidates(
|
|
||||||
int displayPhaseIndex,
|
|
||||||
LevelThemeType themeType,
|
|
||||||
int candidateCount)
|
|
||||||
{
|
|
||||||
int resolvedCount = Mathf.Max(0, candidateCount);
|
|
||||||
if (resolvedCount <= 0)
|
|
||||||
{
|
|
||||||
return Array.Empty<TowerCompItemData>();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<TowerCompItemData> candidates = new List<TowerCompItemData>(resolvedCount);
|
|
||||||
HashSet<int> selectedPoolRowIds = new HashSet<int>();
|
|
||||||
int maxAttempts = Mathf.Max(resolvedCount * 6, resolvedCount);
|
|
||||||
int phaseIndex = Mathf.Max(1, displayPhaseIndex);
|
|
||||||
|
|
||||||
int attempts = 0;
|
|
||||||
while (candidates.Count < resolvedCount && attempts < maxAttempts)
|
|
||||||
{
|
|
||||||
attempts++;
|
|
||||||
if (!TryPickDropPoolRow(phaseIndex, themeType, out DROutGameDropPool selectedRow) || selectedRow == null)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selectedPoolRowIds.Add(selectedRow.Id))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryBuildDropItem(selectedRow, InventoryTagSourceType.Reward, AllocateRewardTagOrdinal(), out TowerCompItemData droppedItem) || droppedItem == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
candidates.Add(droppedItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
attempts = 0;
|
|
||||||
while (candidates.Count < resolvedCount && attempts < maxAttempts)
|
|
||||||
{
|
|
||||||
attempts++;
|
|
||||||
if (!TryRollOutGameItem(phaseIndex, themeType, out TowerCompItemData droppedItem) || droppedItem == null)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
candidates.Add(droppedItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
return candidates;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool ShouldRollOutGameItem(int displayPhaseIndex)
|
|
||||||
{
|
|
||||||
int phaseIndex = Mathf.Max(1, displayPhaseIndex);
|
|
||||||
float dropChance = Mathf.Clamp(DropChanceBase + (phaseIndex - 1) * DropChancePerPhase, 0f, DropChanceCap);
|
|
||||||
return Random.value <= dropChance;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryRollOutGameItem(int displayPhaseIndex, LevelThemeType themeType, out TowerCompItemData droppedItem)
|
|
||||||
{
|
|
||||||
droppedItem = null;
|
|
||||||
int phaseIndex = Mathf.Max(1, displayPhaseIndex);
|
|
||||||
if (!TryPickDropPoolRow(phaseIndex, themeType, out DROutGameDropPool selectedRow))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return TryBuildDropItem(selectedRow, InventoryTagSourceType.Drop, AllocateDropTagOrdinal(), out droppedItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryPickDropPoolRow(int displayPhaseIndex, LevelThemeType themeType, out DROutGameDropPool selectedRow)
|
|
||||||
{
|
|
||||||
selectedRow = null;
|
|
||||||
IDataTable<DROutGameDropPool> dropTable = EnsureOutGameDropPoolTable();
|
|
||||||
if (dropTable == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_eligibleDropPoolBuffer.Clear();
|
|
||||||
DROutGameDropPool[] allRows = dropTable.GetAllDataRows();
|
|
||||||
for (int i = 0; i < allRows.Length; i++)
|
|
||||||
{
|
|
||||||
DROutGameDropPool row = allRows[i];
|
|
||||||
if (row == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (row.LevelThemeType != themeType)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (displayPhaseIndex < row.MinPhase || displayPhaseIndex > row.MaxPhase)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
_eligibleDropPoolBuffer.Add(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_eligibleDropPoolBuffer.Count <= 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
RarityType selectedRarity = RollRarity(displayPhaseIndex, _eligibleDropPoolBuffer);
|
|
||||||
if (selectedRarity == RarityType.None)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int totalWeight = 0;
|
|
||||||
for (int i = 0; i < _eligibleDropPoolBuffer.Count; i++)
|
|
||||||
{
|
|
||||||
DROutGameDropPool row = _eligibleDropPoolBuffer[i];
|
|
||||||
if (row.Rarity != selectedRarity)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
totalWeight += Mathf.Max(1, row.Weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalWeight <= 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int randomWeight = Random.Range(1, totalWeight + 1);
|
|
||||||
int cumulativeWeight = 0;
|
|
||||||
for (int i = 0; i < _eligibleDropPoolBuffer.Count; i++)
|
|
||||||
{
|
|
||||||
DROutGameDropPool row = _eligibleDropPoolBuffer[i];
|
|
||||||
if (row.Rarity != selectedRarity)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
cumulativeWeight += Mathf.Max(1, row.Weight);
|
|
||||||
if (randomWeight <= cumulativeWeight)
|
|
||||||
{
|
|
||||||
selectedRow = row;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedRow = _eligibleDropPoolBuffer[_eligibleDropPoolBuffer.Count - 1];
|
|
||||||
return selectedRow != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private RarityType RollRarity(int displayPhaseIndex, List<DROutGameDropPool> candidates)
|
|
||||||
{
|
|
||||||
_rarityRollWeightBuffer.Clear();
|
|
||||||
float phaseT = Mathf.Clamp01((displayPhaseIndex - 1) / RarityCurveScalePhase);
|
|
||||||
for (int i = 0; i < candidates.Count; i++)
|
|
||||||
{
|
|
||||||
DROutGameDropPool row = candidates[i];
|
|
||||||
if (row == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
float curveWeight = GetRarityCurveWeight(row.Rarity, phaseT);
|
|
||||||
if (curveWeight <= 0f)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_rarityRollWeightBuffer.TryGetValue(row.Rarity, out float existingWeight))
|
|
||||||
{
|
|
||||||
_rarityRollWeightBuffer[row.Rarity] = existingWeight + Mathf.Max(1, row.Weight) * curveWeight;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_rarityRollWeightBuffer[row.Rarity] = Mathf.Max(1, row.Weight) * curveWeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
float totalWeight = 0f;
|
|
||||||
foreach (var pair in _rarityRollWeightBuffer)
|
|
||||||
{
|
|
||||||
totalWeight += Mathf.Max(0f, pair.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalWeight <= 0f)
|
|
||||||
{
|
|
||||||
return RarityType.None;
|
|
||||||
}
|
|
||||||
|
|
||||||
float randomWeight = Random.value * totalWeight;
|
|
||||||
float cumulativeWeight = 0f;
|
|
||||||
foreach (var pair in _rarityRollWeightBuffer)
|
|
||||||
{
|
|
||||||
cumulativeWeight += Mathf.Max(0f, pair.Value);
|
|
||||||
if (randomWeight <= cumulativeWeight)
|
|
||||||
{
|
|
||||||
return pair.Key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var pair in _rarityRollWeightBuffer)
|
|
||||||
{
|
|
||||||
return pair.Key;
|
|
||||||
}
|
|
||||||
|
|
||||||
return RarityType.None;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static float GetRarityCurveWeight(RarityType rarityType, float phaseT)
|
|
||||||
{
|
|
||||||
float hump = Mathf.Exp(-Mathf.Pow((phaseT - 0.35f) / 0.28f, 2f));
|
|
||||||
switch (rarityType)
|
|
||||||
{
|
|
||||||
case RarityType.White:
|
|
||||||
return Mathf.Max(0.05f, 0.18f + 1.25f * hump);
|
|
||||||
case RarityType.Green:
|
|
||||||
return Mathf.Max(0.05f, 0.35f + 0.55f * hump);
|
|
||||||
case RarityType.Blue:
|
|
||||||
return 0.18f + 0.55f * phaseT;
|
|
||||||
case RarityType.Purple:
|
|
||||||
return 0.05f + 0.22f * phaseT;
|
|
||||||
case RarityType.Red:
|
|
||||||
return 0.01f + 0.08f * phaseT * phaseT;
|
|
||||||
default:
|
|
||||||
return 0f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private IDataTable<DROutGameDropPool> EnsureOutGameDropPoolTable()
|
|
||||||
{
|
|
||||||
if (_drOutGameDropPool != null)
|
|
||||||
{
|
|
||||||
return _drOutGameDropPool;
|
|
||||||
}
|
|
||||||
|
|
||||||
_drOutGameDropPool = GameEntry.DataTable.GetDataTable<DROutGameDropPool>();
|
|
||||||
return _drOutGameDropPool;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryBuildDropItem(
|
|
||||||
DROutGameDropPool row,
|
|
||||||
InventoryTagSourceType sourceType,
|
|
||||||
int localOrdinal,
|
|
||||||
out TowerCompItemData droppedItem)
|
|
||||||
{
|
|
||||||
droppedItem = null;
|
|
||||||
if (row == null || row.ItemId <= 0 || string.IsNullOrWhiteSpace(row.ItemType))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
string itemType = row.ItemType.Trim();
|
|
||||||
if (itemType.Equals("MuzzleComp", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return TryBuildMuzzleCompItem(row, sourceType, localOrdinal, out droppedItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (itemType.Equals("BearingComp", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return TryBuildBearingCompItem(row, sourceType, localOrdinal, out droppedItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (itemType.Equals("BaseComp", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return TryBuildBaseCompItem(row, sourceType, localOrdinal, out droppedItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryBuildMuzzleCompItem(
|
|
||||||
DROutGameDropPool row,
|
|
||||||
InventoryTagSourceType sourceType,
|
|
||||||
int localOrdinal,
|
|
||||||
out TowerCompItemData droppedItem)
|
|
||||||
{
|
|
||||||
droppedItem = null;
|
|
||||||
_drMuzzleComp ??= GameEntry.DataTable.GetDataTable<DRMuzzleComp>();
|
|
||||||
if (_drMuzzleComp == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
DRMuzzleComp config = _drMuzzleComp.GetDataRow(row.ItemId);
|
|
||||||
if (config == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
long instanceId = _nextDropItemInstanceId++;
|
|
||||||
RarityType rarity = InventoryRarityRuleService.NormalizeComponentRarity(row.Rarity);
|
|
||||||
droppedItem = new MuzzleCompItemData
|
|
||||||
{
|
|
||||||
InstanceId = instanceId,
|
|
||||||
ConfigId = config.Id,
|
|
||||||
Name = config.Name,
|
|
||||||
Rarity = rarity,
|
|
||||||
Endurance = 100f,
|
|
||||||
Constraint = config.Constraint,
|
|
||||||
Tags = ComponentTagGenerationService.ResolveComponentTags(
|
|
||||||
config.PossibleTag,
|
|
||||||
rarity,
|
|
||||||
CreateRandomContext(sourceType, localOrdinal, config.Id)),
|
|
||||||
AttackDamage = config.AttackDamage != null ? (int[])config.AttackDamage.Clone() : Array.Empty<int>(),
|
|
||||||
DamageRandomRate = config.DamageRandomRate,
|
|
||||||
AttackMethodType = config.AttackMethodType
|
|
||||||
};
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryBuildBearingCompItem(
|
|
||||||
DROutGameDropPool row,
|
|
||||||
InventoryTagSourceType sourceType,
|
|
||||||
int localOrdinal,
|
|
||||||
out TowerCompItemData droppedItem)
|
|
||||||
{
|
|
||||||
droppedItem = null;
|
|
||||||
_drBearingComp ??= GameEntry.DataTable.GetDataTable<DRBearingComp>();
|
|
||||||
if (_drBearingComp == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
DRBearingComp config = _drBearingComp.GetDataRow(row.ItemId);
|
|
||||||
if (config == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
long instanceId = _nextDropItemInstanceId++;
|
|
||||||
RarityType rarity = InventoryRarityRuleService.NormalizeComponentRarity(row.Rarity);
|
|
||||||
droppedItem = new BearingCompItemData
|
|
||||||
{
|
|
||||||
InstanceId = instanceId,
|
|
||||||
ConfigId = config.Id,
|
|
||||||
Name = config.Name,
|
|
||||||
Rarity = rarity,
|
|
||||||
Endurance = 100f,
|
|
||||||
Constraint = config.Constraint,
|
|
||||||
Tags = ComponentTagGenerationService.ResolveComponentTags(
|
|
||||||
config.PossibleTag,
|
|
||||||
rarity,
|
|
||||||
CreateRandomContext(sourceType, localOrdinal, config.Id)),
|
|
||||||
RotateSpeed = config.RotateSpeed != null ? (float[])config.RotateSpeed.Clone() : Array.Empty<float>(),
|
|
||||||
AttackRange = config.AttackRange != null ? (float[])config.AttackRange.Clone() : Array.Empty<float>()
|
|
||||||
};
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryBuildBaseCompItem(
|
|
||||||
DROutGameDropPool row,
|
|
||||||
InventoryTagSourceType sourceType,
|
|
||||||
int localOrdinal,
|
|
||||||
out TowerCompItemData droppedItem)
|
|
||||||
{
|
|
||||||
droppedItem = null;
|
|
||||||
_drBaseComp ??= GameEntry.DataTable.GetDataTable<DRBaseComp>();
|
|
||||||
if (_drBaseComp == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
DRBaseComp config = _drBaseComp.GetDataRow(row.ItemId);
|
|
||||||
if (config == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
long instanceId = _nextDropItemInstanceId++;
|
|
||||||
RarityType rarity = InventoryRarityRuleService.NormalizeComponentRarity(row.Rarity);
|
|
||||||
droppedItem = new BaseCompItemData
|
|
||||||
{
|
|
||||||
InstanceId = instanceId,
|
|
||||||
ConfigId = config.Id,
|
|
||||||
Name = config.Name,
|
|
||||||
Rarity = rarity,
|
|
||||||
Endurance = 100f,
|
|
||||||
Constraint = config.Constraint,
|
|
||||||
Tags = ComponentTagGenerationService.ResolveComponentTags(
|
|
||||||
config.PossibleTag,
|
|
||||||
rarity,
|
|
||||||
CreateRandomContext(sourceType, localOrdinal, config.Id)),
|
|
||||||
AttackSpeed = config.AttackSpeed != null ? (float[])config.AttackSpeed.Clone() : Array.Empty<float>(),
|
|
||||||
AttackPropertyType = config.AttackPropertyType
|
|
||||||
};
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private InventoryTagRandomContext CreateRandomContext(
|
|
||||||
InventoryTagSourceType sourceType,
|
|
||||||
int localOrdinal,
|
|
||||||
int configId)
|
|
||||||
{
|
|
||||||
return sourceType switch
|
|
||||||
{
|
|
||||||
InventoryTagSourceType.Reward => InventoryTagRandomContext.CreateReward(_runSeed, _nodeSequenceIndex, localOrdinal, configId),
|
|
||||||
_ => InventoryTagRandomContext.CreateDrop(_runSeed, _nodeSequenceIndex, localOrdinal, configId)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private int AllocateDropTagOrdinal()
|
|
||||||
{
|
|
||||||
return _nextDropTagOrdinal++;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int AllocateRewardTagOrdinal()
|
|
||||||
{
|
|
||||||
return _nextRewardTagOrdinal++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,186 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using GameFramework.DataTable;
|
||||||
|
using GeometryTD.DataTable;
|
||||||
|
using GeometryTD.Definition;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace GeometryTD.CustomComponent
|
||||||
|
{
|
||||||
|
public sealed class DropPoolRoller
|
||||||
|
{
|
||||||
|
private const float RarityCurveScalePhase = 30f;
|
||||||
|
|
||||||
|
private readonly List<DROutGameDropPool> _eligibleRowBuffer = new();
|
||||||
|
private readonly Dictionary<RarityType, float> _rarityWeightBuffer = new();
|
||||||
|
private readonly IDataTable<DROutGameDropPool> _dropPoolTable;
|
||||||
|
|
||||||
|
public DropPoolRoller(IDataTable<DROutGameDropPool> dropPoolTable)
|
||||||
|
{
|
||||||
|
_dropPoolTable = dropPoolTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryRollRow(int displayPhaseIndex, LevelThemeType themeType, out DROutGameDropPool selectedRow)
|
||||||
|
{
|
||||||
|
selectedRow = null;
|
||||||
|
|
||||||
|
DROutGameDropPool[] allRows = _dropPoolTable.GetAllDataRows();
|
||||||
|
if (allRows == null || allRows.Length <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectEligibleRows(allRows, displayPhaseIndex, themeType);
|
||||||
|
if (_eligibleRowBuffer.Count <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RarityType selectedRarity = RollRarity(displayPhaseIndex);
|
||||||
|
if (selectedRarity == RarityType.None)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int totalWeight = 0;
|
||||||
|
for (int i = 0; i < _eligibleRowBuffer.Count; i++)
|
||||||
|
{
|
||||||
|
DROutGameDropPool row = _eligibleRowBuffer[i];
|
||||||
|
if (row.Rarity != selectedRarity)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalWeight += Mathf.Max(1, row.Weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalWeight <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int randomWeight = Random.Range(1, totalWeight + 1);
|
||||||
|
int cumulativeWeight = 0;
|
||||||
|
foreach (var row in _eligibleRowBuffer)
|
||||||
|
{
|
||||||
|
if (row.Rarity != selectedRarity)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cumulativeWeight += Mathf.Max(1, row.Weight);
|
||||||
|
if (randomWeight <= cumulativeWeight)
|
||||||
|
{
|
||||||
|
selectedRow = row;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedRow = _eligibleRowBuffer[_eligibleRowBuffer.Count - 1];
|
||||||
|
return selectedRow != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CollectEligibleRows(
|
||||||
|
DROutGameDropPool[] allRows,
|
||||||
|
int displayPhaseIndex,
|
||||||
|
LevelThemeType themeType)
|
||||||
|
{
|
||||||
|
_eligibleRowBuffer.Clear();
|
||||||
|
|
||||||
|
for (int i = 0; i < allRows.Length; i++)
|
||||||
|
{
|
||||||
|
DROutGameDropPool row = allRows[i];
|
||||||
|
if (row == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.LevelThemeType != themeType)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (displayPhaseIndex < row.MinPhase || displayPhaseIndex > row.MaxPhase)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_eligibleRowBuffer.Add(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private RarityType RollRarity(int displayPhaseIndex)
|
||||||
|
{
|
||||||
|
_rarityWeightBuffer.Clear();
|
||||||
|
float phaseT = Mathf.Clamp01((displayPhaseIndex - 1) / RarityCurveScalePhase);
|
||||||
|
|
||||||
|
for (int i = 0; i < _eligibleRowBuffer.Count; i++)
|
||||||
|
{
|
||||||
|
DROutGameDropPool row = _eligibleRowBuffer[i];
|
||||||
|
float curveWeight = GetRarityCurveWeight(row.Rarity, phaseT);
|
||||||
|
if (curveWeight <= 0f)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
float rowWeight = Mathf.Max(1, row.Weight) * curveWeight;
|
||||||
|
if (_rarityWeightBuffer.TryGetValue(row.Rarity, out float existingWeight))
|
||||||
|
{
|
||||||
|
_rarityWeightBuffer[row.Rarity] = existingWeight + rowWeight;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_rarityWeightBuffer[row.Rarity] = rowWeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float totalWeight = 0f;
|
||||||
|
foreach (var pair in _rarityWeightBuffer)
|
||||||
|
{
|
||||||
|
totalWeight += Mathf.Max(0f, pair.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalWeight <= 0f)
|
||||||
|
{
|
||||||
|
return RarityType.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
float randomWeight = Random.value * totalWeight;
|
||||||
|
float cumulativeWeight = 0f;
|
||||||
|
foreach (var pair in _rarityWeightBuffer)
|
||||||
|
{
|
||||||
|
cumulativeWeight += Mathf.Max(0f, pair.Value);
|
||||||
|
if (randomWeight <= cumulativeWeight)
|
||||||
|
{
|
||||||
|
return pair.Key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var pair in _rarityWeightBuffer)
|
||||||
|
{
|
||||||
|
return pair.Key;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RarityType.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float GetRarityCurveWeight(RarityType rarityType, float phaseT)
|
||||||
|
{
|
||||||
|
float hump = Mathf.Exp(-Mathf.Pow((phaseT - 0.35f) / 0.28f, 2f));
|
||||||
|
switch (rarityType)
|
||||||
|
{
|
||||||
|
case RarityType.White:
|
||||||
|
return Mathf.Max(0.05f, 0.18f + 1.25f * hump);
|
||||||
|
case RarityType.Green:
|
||||||
|
return Mathf.Max(0.05f, 0.35f + 0.55f * hump);
|
||||||
|
case RarityType.Blue:
|
||||||
|
return 0.18f + 0.55f * phaseT;
|
||||||
|
case RarityType.Purple:
|
||||||
|
return 0.05f + 0.22f * phaseT;
|
||||||
|
case RarityType.Red:
|
||||||
|
return 0.01f + 0.08f * phaseT * phaseT;
|
||||||
|
default:
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: b7dc48b97505d0242bf5846ba835f3b3
|
guid: 807de988c6a1431ab7b4b6d4d5a7d92f
|
||||||
MonoImporter:
|
MonoImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
|
|
@ -3,21 +3,43 @@ using System.Collections.Generic;
|
||||||
using GameFramework.DataTable;
|
using GameFramework.DataTable;
|
||||||
using GeometryTD.DataTable;
|
using GeometryTD.DataTable;
|
||||||
using GeometryTD.Definition;
|
using GeometryTD.Definition;
|
||||||
|
using GeometryTD.Factory;
|
||||||
using GeometryTD.UI;
|
using GeometryTD.UI;
|
||||||
|
using UnityEngine;
|
||||||
using UnityGameFramework.Runtime;
|
using UnityGameFramework.Runtime;
|
||||||
|
using Random = UnityEngine.Random;
|
||||||
|
|
||||||
namespace GeometryTD.CustomComponent
|
namespace GeometryTD.CustomComponent
|
||||||
{
|
{
|
||||||
public sealed class InventoryGenerationComponent : GameFrameworkComponent
|
public sealed class InventoryGenerationComponent : GameFrameworkComponent
|
||||||
{
|
{
|
||||||
|
private const float DropChanceBase = 0.05f;
|
||||||
|
private const float DropChancePerPhase = 0.2f;
|
||||||
|
private const float DropChanceCap = 0.2f;
|
||||||
|
|
||||||
private long _nextTempInstanceId = 1000000;
|
private long _nextTempInstanceId = 1000000;
|
||||||
|
private int _runSeed;
|
||||||
|
private int _nodeSequenceIndex = -1;
|
||||||
|
private int _nextDropTagOrdinal;
|
||||||
|
private int _nextRewardTagOrdinal;
|
||||||
|
|
||||||
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<DRMuzzleComp> _muzzleCompTable;
|
private IDataTable<DRMuzzleComp> _muzzleCompTable;
|
||||||
private IDataTable<DRBearingComp> _bearingCompTable;
|
private IDataTable<DRBearingComp> _bearingCompTable;
|
||||||
private IDataTable<DRBaseComp> _baseCompTable;
|
private IDataTable<DRBaseComp> _baseCompTable;
|
||||||
private ShopGoodsBuilder _shopGoodsBuilder;
|
private ShopGoodsBuilder _shopGoodsBuilder;
|
||||||
|
private DropPoolRoller _dropPoolRoller;
|
||||||
|
private RewardCandidateBuilder _rewardCandidateBuilder;
|
||||||
|
|
||||||
|
public void ConfigureRunContext(int runSeed, int sequenceIndex)
|
||||||
|
{
|
||||||
|
_runSeed = runSeed;
|
||||||
|
_nodeSequenceIndex = sequenceIndex;
|
||||||
|
_nextDropTagOrdinal = 0;
|
||||||
|
_nextRewardTagOrdinal = 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)
|
||||||
{
|
{
|
||||||
|
|
@ -27,8 +49,31 @@ namespace GeometryTD.CustomComponent
|
||||||
|
|
||||||
public EnemyDropResult ResolveEnemyDrop(in EnemyDropContext context)
|
public EnemyDropResult ResolveEnemyDrop(in EnemyDropContext context)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException(
|
DREnemy enemy = context.Enemy;
|
||||||
"InventoryGenerationComponent.ResolveEnemyDrop() will be implemented in G2.");
|
if (enemy == null)
|
||||||
|
{
|
||||||
|
return EnemyDropResult.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
int coin = Mathf.Max(0, enemy.DropCoin);
|
||||||
|
int gold = 0;
|
||||||
|
float dropRate = enemy.DropPercent > 1f
|
||||||
|
? Mathf.Clamp01(enemy.DropPercent * 0.01f)
|
||||||
|
: Mathf.Clamp01(enemy.DropPercent);
|
||||||
|
|
||||||
|
if (enemy.DropGold > 0 && dropRate > 0f && Random.value <= dropRate)
|
||||||
|
{
|
||||||
|
gold = Mathf.Max(0, enemy.DropGold);
|
||||||
|
}
|
||||||
|
|
||||||
|
TowerCompItemData lootItem = null;
|
||||||
|
if (ShouldRollOutGameItem(context.DisplayPhaseIndex) &&
|
||||||
|
TryRollOutGameItem(context.DisplayPhaseIndex, context.ThemeType, out TowerCompItemData droppedItem))
|
||||||
|
{
|
||||||
|
lootItem = droppedItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new EnemyDropResult(coin, gold, lootItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IReadOnlyList<TowerCompItemData> BuildRewardCandidates(
|
public IReadOnlyList<TowerCompItemData> BuildRewardCandidates(
|
||||||
|
|
@ -36,8 +81,12 @@ namespace GeometryTD.CustomComponent
|
||||||
LevelThemeType themeType,
|
LevelThemeType themeType,
|
||||||
int candidateCount)
|
int candidateCount)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException(
|
RewardCandidateBuilder rewardCandidateBuilder = EnsureRewardCandidateBuilder();
|
||||||
"InventoryGenerationComponent.BuildRewardCandidates() will be implemented in G2.");
|
return rewardCandidateBuilder.BuildCandidates(
|
||||||
|
displayPhaseIndex,
|
||||||
|
themeType,
|
||||||
|
candidateCount,
|
||||||
|
BuildRewardCandidateItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EnsureShopTables()
|
private void EnsureShopTables()
|
||||||
|
|
@ -94,5 +143,148 @@ namespace GeometryTD.CustomComponent
|
||||||
{
|
{
|
||||||
return _nextTempInstanceId++;
|
return _nextTempInstanceId++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void EnsureDropTables()
|
||||||
|
{
|
||||||
|
_dropPoolTable ??= GameEntry.DataTable.GetDataTable<DROutGameDropPool>();
|
||||||
|
_muzzleCompTable ??= GameEntry.DataTable.GetDataTable<DRMuzzleComp>();
|
||||||
|
_bearingCompTable ??= GameEntry.DataTable.GetDataTable<DRBearingComp>();
|
||||||
|
_baseCompTable ??= GameEntry.DataTable.GetDataTable<DRBaseComp>();
|
||||||
|
|
||||||
|
if (_dropPoolTable == null || _muzzleCompTable == null || _bearingCompTable == null || _baseCompTable == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"InventoryGenerationComponent requires OutGameDropPool, MuzzleComp, BearingComp, and BaseComp data tables.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DropPoolRoller EnsureDropPoolRoller()
|
||||||
|
{
|
||||||
|
EnsureDropTables();
|
||||||
|
_dropPoolRoller ??= new DropPoolRoller(_dropPoolTable);
|
||||||
|
return _dropPoolRoller;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RewardCandidateBuilder EnsureRewardCandidateBuilder()
|
||||||
|
{
|
||||||
|
_rewardCandidateBuilder ??= new RewardCandidateBuilder(EnsureDropPoolRoller());
|
||||||
|
return _rewardCandidateBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldRollOutGameItem(int displayPhaseIndex)
|
||||||
|
{
|
||||||
|
int phaseIndex = Mathf.Max(1, displayPhaseIndex);
|
||||||
|
float dropChance = Mathf.Clamp(DropChanceBase + (phaseIndex - 1) * DropChancePerPhase, 0f, DropChanceCap);
|
||||||
|
return Random.value <= dropChance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryRollOutGameItem(int displayPhaseIndex, LevelThemeType themeType, out TowerCompItemData droppedItem)
|
||||||
|
{
|
||||||
|
droppedItem = null;
|
||||||
|
DropPoolRoller dropPoolRoller = EnsureDropPoolRoller();
|
||||||
|
int phaseIndex = Mathf.Max(1, displayPhaseIndex);
|
||||||
|
if (!dropPoolRoller.TryRollRow(phaseIndex, themeType, out DROutGameDropPool selectedRow) || selectedRow == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TryBuildDropItem(selectedRow, InventoryTagSourceType.Drop, AllocateDropTagOrdinal(), out droppedItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryBuildDropItem(
|
||||||
|
DROutGameDropPool row,
|
||||||
|
InventoryTagSourceType sourceType,
|
||||||
|
int localOrdinal,
|
||||||
|
out TowerCompItemData droppedItem)
|
||||||
|
{
|
||||||
|
droppedItem = null;
|
||||||
|
if (row.ItemId <= 0 || string.IsNullOrWhiteSpace(row.ItemType))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
string itemType = row.ItemType.Trim();
|
||||||
|
if (itemType.Equals("MuzzleComp", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
DRMuzzleComp config = _muzzleCompTable.GetDataRow(row.ItemId);
|
||||||
|
if (config == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
droppedItem = ComponentItemFactory.CreateMuzzle(
|
||||||
|
config,
|
||||||
|
AllocateTempInstanceId(),
|
||||||
|
row.Rarity,
|
||||||
|
CreateRandomContext(sourceType, localOrdinal, config.Id));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemType.Equals("BearingComp", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
DRBearingComp config = _bearingCompTable.GetDataRow(row.ItemId);
|
||||||
|
if (config == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
droppedItem = ComponentItemFactory.CreateBearing(
|
||||||
|
config,
|
||||||
|
AllocateTempInstanceId(),
|
||||||
|
row.Rarity,
|
||||||
|
CreateRandomContext(sourceType, localOrdinal, config.Id));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemType.Equals("BaseComp", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
DRBaseComp config = _baseCompTable.GetDataRow(row.ItemId);
|
||||||
|
if (config == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
droppedItem = ComponentItemFactory.CreateBase(
|
||||||
|
config,
|
||||||
|
AllocateTempInstanceId(),
|
||||||
|
row.Rarity,
|
||||||
|
CreateRandomContext(sourceType, localOrdinal, config.Id));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int AllocateDropTagOrdinal()
|
||||||
|
{
|
||||||
|
return _nextDropTagOrdinal++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int AllocateRewardTagOrdinal()
|
||||||
|
{
|
||||||
|
return _nextRewardTagOrdinal++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TowerCompItemData BuildRewardCandidateItem(DROutGameDropPool row)
|
||||||
|
{
|
||||||
|
if (!TryBuildDropItem(row, InventoryTagSourceType.Reward, AllocateRewardTagOrdinal(), out TowerCompItemData droppedItem))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return droppedItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
private InventoryTagRandomContext CreateRandomContext(
|
||||||
|
InventoryTagSourceType sourceType,
|
||||||
|
int localOrdinal,
|
||||||
|
int configId)
|
||||||
|
{
|
||||||
|
return sourceType switch
|
||||||
|
{
|
||||||
|
InventoryTagSourceType.Reward => InventoryTagRandomContext.CreateReward(_runSeed, _nodeSequenceIndex, localOrdinal, configId),
|
||||||
|
_ => InventoryTagRandomContext.CreateDrop(_runSeed, _nodeSequenceIndex, localOrdinal, configId)
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using GeometryTD.DataTable;
|
||||||
|
using GeometryTD.Definition;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace GeometryTD.CustomComponent
|
||||||
|
{
|
||||||
|
public sealed class RewardCandidateBuilder
|
||||||
|
{
|
||||||
|
private readonly DropPoolRoller _dropPoolRoller;
|
||||||
|
|
||||||
|
public RewardCandidateBuilder(DropPoolRoller dropPoolRoller)
|
||||||
|
{
|
||||||
|
_dropPoolRoller = dropPoolRoller;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<TowerCompItemData> BuildCandidates(
|
||||||
|
int displayPhaseIndex,
|
||||||
|
LevelThemeType themeType,
|
||||||
|
int candidateCount,
|
||||||
|
Func<DROutGameDropPool, TowerCompItemData> buildRewardItem)
|
||||||
|
{
|
||||||
|
int resolvedCount = Mathf.Max(0, candidateCount);
|
||||||
|
if (resolvedCount <= 0)
|
||||||
|
{
|
||||||
|
return Array.Empty<TowerCompItemData>();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<TowerCompItemData> candidates = new List<TowerCompItemData>(resolvedCount);
|
||||||
|
HashSet<int> selectedPoolRowIds = new HashSet<int>();
|
||||||
|
int maxAttempts = Mathf.Max(resolvedCount * 6, resolvedCount);
|
||||||
|
int phaseIndex = Mathf.Max(1, displayPhaseIndex);
|
||||||
|
|
||||||
|
int attempts = 0;
|
||||||
|
while (candidates.Count < resolvedCount && attempts < maxAttempts)
|
||||||
|
{
|
||||||
|
attempts++;
|
||||||
|
if (!_dropPoolRoller.TryRollRow(phaseIndex, themeType, out DROutGameDropPool selectedRow) || selectedRow == null)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedPoolRowIds.Add(selectedRow.Id))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
TowerCompItemData candidate = buildRewardItem(selectedRow);
|
||||||
|
if (candidate == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates.Add(candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
attempts = 0;
|
||||||
|
while (candidates.Count < resolvedCount && attempts < maxAttempts)
|
||||||
|
{
|
||||||
|
attempts++;
|
||||||
|
if (!_dropPoolRoller.TryRollRow(phaseIndex, themeType, out DROutGameDropPool selectedRow) || selectedRow == null)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
TowerCompItemData candidate = buildRewardItem(selectedRow);
|
||||||
|
if (candidate == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates.Add(candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5fe3f5eaa2194d13b4f6f2ff4b2f7de2
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -186,10 +186,9 @@ namespace GeometryTD.Tests.EditMode
|
||||||
|
|
||||||
bool prepared = new CombatSettlementService().TryPrepareRewardSelection(
|
bool prepared = new CombatSettlementService().TryPrepareRewardSelection(
|
||||||
settlementContext,
|
settlementContext,
|
||||||
null,
|
|
||||||
displayPhaseIndex: 1,
|
displayPhaseIndex: 1,
|
||||||
themeType: LevelThemeType.Plain,
|
themeType: LevelThemeType.Plain,
|
||||||
rewardSelectFormUseCase: new RewardSelectFormUseCase(),
|
rewardSelectFormUseCase: null,
|
||||||
onRewardSelected: _ => { },
|
onRewardSelected: _ => { },
|
||||||
onGiveUp: null);
|
onGiveUp: null);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -111,10 +111,10 @@
|
||||||
|
|
||||||
| 状态 | ID | 任务 | 目标 |
|
| 状态 | ID | 任务 | 目标 |
|
||||||
|-----|-------|-----------------------------------|------------|
|
|-----|-------|-----------------------------------|------------|
|
||||||
| [ ] | G1-01 | 新增 `InventoryGenerationComponent` | 建立统一组件产出入口 |
|
| [x] | G1-01 | 新增 `InventoryGenerationComponent` | 建立统一组件产出入口 |
|
||||||
| [ ] | G1-02 | 新增 `ComponentItemFactory` | 收口组件实例构造 |
|
| [x] | G1-02 | 新增 `ComponentItemFactory` | 收口组件实例构造 |
|
||||||
| [ ] | G1-03 | 新增 `ShopGoodsBuilder` | 收口商店货物生成 |
|
| [x] | G1-03 | 新增 `ShopGoodsBuilder` | 收口商店货物生成 |
|
||||||
| [ ] | G1-04 | 让 `ShopFormUseCase` 改用组件入口 | 商店不再自己生成组件 |
|
| [x] | G1-04 | 让 `ShopFormUseCase` 改用组件入口 | 商店不再自己生成组件 |
|
||||||
|
|
||||||
### G1 验收标准
|
### G1 验收标准
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# CombatNode 设计规范(开发约束)
|
# CombatNode 设计规范(开发约束)
|
||||||
|
|
||||||
最后更新:2026-03-06
|
最后更新:2026-03-12
|
||||||
|
|
||||||
## 1. 适用范围与目标
|
## 1. 适用范围与目标
|
||||||
|
|
||||||
|
|
@ -296,22 +296,25 @@
|
||||||
- `Coin / BaseHp` 变化事件同时携带“当前值”和“变化量”。
|
- `Coin / BaseHp` 变化事件同时携带“当前值”和“变化量”。
|
||||||
- `Gold` 只是结算累计值,不要求战斗内实时事件驱动。
|
- `Gold` 只是结算累计值,不要求战斗内实时事件驱动。
|
||||||
|
|
||||||
### 4.5 EnemyDropResolver
|
### 4.5 InventoryGenerationComponent
|
||||||
|
|
||||||
文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropResolver.cs`
|
文件:`Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/InventoryGenerationComponent.cs`
|
||||||
|
|
||||||
目标职责:
|
目标职责:
|
||||||
- 只负责敌人死亡后的掉落判定。
|
- 作为局外组件产出的统一运行时入口。
|
||||||
- 输入:
|
- 对外提供:
|
||||||
- `DREnemy`
|
- `BuildShopGoods(...)`
|
||||||
- 当前阶段索引
|
- `ResolveEnemyDrop(...)`
|
||||||
- 当前主题或关卡上下文
|
- `BuildRewardCandidates(...)`
|
||||||
- 输出:
|
- 在内部编排:
|
||||||
- 掉落结果对象(`coin / gold / loot`)
|
- `DropPoolRoller`
|
||||||
|
- `RewardCandidateBuilder`
|
||||||
|
- `ComponentItemFactory`
|
||||||
|
|
||||||
约束:
|
约束:
|
||||||
- 不直接修改资源状态。
|
- `CombatNode` 域不直接持有或复制组件产出规则。
|
||||||
- 不直接读取 `CombatNodeComponent`、`MapEntity`、`EnemyManager` 内部状态。
|
- `CombatScheduler` 与结算状态链只调用统一入口,不直接访问掉落池滚动或组件实例构造细节。
|
||||||
|
- `InventoryGenerationComponent` 负责组件实例生成、Tag 随机上下文以及 `runSeed/sequenceIndex` 相关上下文。
|
||||||
|
|
||||||
### 4.6 IPhaseEndCondition
|
### 4.6 IPhaseEndCondition
|
||||||
|
|
||||||
|
|
@ -406,7 +409,7 @@ Boss 识别规则:
|
||||||
- `OnEnemyDefeated(DREnemy enemy)`
|
- `OnEnemyDefeated(DREnemy enemy)`
|
||||||
- `OnEnemyReachedBase(DREnemy enemy)`
|
- `OnEnemyReachedBase(DREnemy enemy)`
|
||||||
- `CombatScheduler` 公共层负责处理敌人事件的通用副作用:
|
- `CombatScheduler` 公共层负责处理敌人事件的通用副作用:
|
||||||
- 击杀:调用 `EnemyDropResolver`,再调用局内资源管理器入账。
|
- 击杀:调用 `GameEntry.InventoryGeneration.ResolveEnemyDrop(...)`,再调用局内资源管理器入账。
|
||||||
- 到家:调用局内资源管理器扣减 `BaseHp`。
|
- 到家:调用局内资源管理器扣减 `BaseHp`。
|
||||||
|
|
||||||
约束:
|
约束:
|
||||||
|
|
@ -467,7 +470,7 @@ Boss 识别规则:
|
||||||
1. `CombatScheduler` 只做状态机管理与共享运行时收口,不继续吸收具体业务细节。
|
1. `CombatScheduler` 只做状态机管理与共享运行时收口,不继续吸收具体业务细节。
|
||||||
2. `CombatNodeComponent` 不再持有战斗内资源真值。
|
2. `CombatNodeComponent` 不再持有战斗内资源真值。
|
||||||
3. 局内 `Coin / Gold / BaseHp / Loot Backpack / BuildTowerSnapshots` 以 `CombatRunResourceStore` 为唯一真值来源。
|
3. 局内 `Coin / Gold / BaseHp / Loot Backpack / BuildTowerSnapshots` 以 `CombatRunResourceStore` 为唯一真值来源。
|
||||||
4. 敌人死亡掉落判定以 `EnemyDropResolver` 为唯一判定入口。
|
4. 组件产出规则以 `InventoryGenerationComponent` 为统一运行时入口;战斗掉落与奖励候选都通过它生成。
|
||||||
5. 存活敌人数与 `HasAliveBoss` 以 `EnemyLifecycleTracker` 为唯一真值来源。
|
5. 存活敌人数与 `HasAliveBoss` 以 `EnemyLifecycleTracker` 为唯一真值来源。
|
||||||
6. Phase 运行时信息与统一结束标记以 `PhaseLoopRuntime` 为唯一真值来源。
|
6. Phase 运行时信息与统一结束标记以 `PhaseLoopRuntime` 为唯一真值来源。
|
||||||
7. `PhaseEndType` 的退出条件以 `IPhaseEndCondition` 实现类为唯一判定入口。
|
7. `PhaseEndType` 的退出条件以 `IPhaseEndCondition` 实现类为唯一判定入口。
|
||||||
|
|
@ -498,7 +501,11 @@ Boss 识别规则:
|
||||||
|
|
||||||
### 10.3 新增敌人掉落规则
|
### 10.3 新增敌人掉落规则
|
||||||
|
|
||||||
优先改 `EnemyDropResolver`,不要在 `EnemyManager` 或状态类里直接计算掉落。
|
优先改 `InventoryGenerationComponent` 及其下层规则模块,不要在 `EnemyManager`、`CombatScheduler` 或状态类里直接计算掉落。
|
||||||
|
|
||||||
|
### 10.3.x 新增奖励候选规则
|
||||||
|
|
||||||
|
优先改 `InventoryGenerationComponent`、`RewardCandidateBuilder` 或 `DropPoolRoller`,不要在结算状态链里复制一套候选生成规则。
|
||||||
|
|
||||||
### 10.4 新增战斗内资源或建塔快照规则
|
### 10.4 新增战斗内资源或建塔快照规则
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue