InventoryGenerationComponent G2
This commit is contained in:
parent
f67888a8da
commit
53b6b261dd
|
|
@ -100,7 +100,7 @@ namespace GeometryTD.CustomComponent
|
|||
_runtime.NodeId = nodeId;
|
||||
_runtime.NodeType = nodeType;
|
||||
_runtime.SequenceIndex = sequenceIndex;
|
||||
_runtime.EnemyDropResolver.ConfigureRunContext(runSeed, sequenceIndex);
|
||||
GameEntry.InventoryGeneration.ConfigureRunContext(runSeed, sequenceIndex);
|
||||
_runtime.CombatRunResourceStore.InitializeForCombat(level);
|
||||
for (int i = 0; i < phases.Count; i++)
|
||||
{
|
||||
|
|
@ -201,7 +201,7 @@ namespace GeometryTD.CustomComponent
|
|||
enemy,
|
||||
_runtime.PhaseLoopRuntime.DisplayPhaseIndex,
|
||||
_coordinator.ResolveCurrentThemeType());
|
||||
EnemyDropResult result = _runtime.EnemyDropResolver.Resolve(context);
|
||||
EnemyDropResult result = GameEntry.InventoryGeneration.ResolveEnemyDrop(context);
|
||||
_runtime.CombatRunResourceStore.AddEnemyDefeatedReward(result.Coin, result.Gold);
|
||||
_runtime.CombatRunResourceStore.AddEnemyDefeatedLoot(result.LootItem);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@ namespace GeometryTD.CustomComponent
|
|||
_runtime.PhaseLoopRuntime.Reset();
|
||||
_runtime.LoadSession.Reset();
|
||||
_runtime.CombatRunResourceStore.Reset();
|
||||
_runtime.EnemyDropResolver.Reset();
|
||||
_runtime.SettlementContext = null;
|
||||
_runtime.CurrentLevel = null;
|
||||
_runtime.DidCombatWin = true;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ namespace GeometryTD.CustomComponent
|
|||
public CombatLoadSession LoadSession { get; } = new();
|
||||
public CombatEventBridge EventBridge { get; } = new();
|
||||
public CombatRunResourceStore CombatRunResourceStore { get; } = new();
|
||||
public EnemyDropResolver EnemyDropResolver { get; } = new();
|
||||
public CombatSettlementService CombatSettlementService { get; } = new();
|
||||
|
||||
public EntityComponent Entity { get; set; }
|
||||
|
|
|
|||
|
|
@ -83,19 +83,18 @@ namespace GeometryTD.CustomComponent
|
|||
|
||||
public bool TryPrepareRewardSelection(
|
||||
CombatSettlementContext settlementContext,
|
||||
EnemyDropResolver enemyDropResolver,
|
||||
int displayPhaseIndex,
|
||||
LevelThemeType themeType,
|
||||
RewardSelectFormUseCase rewardSelectFormUseCase,
|
||||
Action<RewardSelectItemRawData> onRewardSelected,
|
||||
Action onGiveUp)
|
||||
{
|
||||
if (settlementContext == null || enemyDropResolver == null || rewardSelectFormUseCase == null)
|
||||
if (settlementContext == null || rewardSelectFormUseCase == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
IReadOnlyList<TowerCompItemData> candidateItems = enemyDropResolver.RollSettlementRewardCandidates(
|
||||
IReadOnlyList<TowerCompItemData> candidateItems = GameEntry.InventoryGeneration.BuildRewardCandidates(
|
||||
displayPhaseIndex,
|
||||
themeType,
|
||||
RewardSelectDisplayCount);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ namespace GeometryTD.CustomComponent
|
|||
Coordinator.EnsureRewardSelectFormUseCaseBound();
|
||||
if (!Runtime.CombatSettlementService.TryPrepareRewardSelection(
|
||||
Runtime.SettlementContext,
|
||||
Runtime.EnemyDropResolver,
|
||||
Runtime.PhaseLoopRuntime.DisplayPhaseIndex,
|
||||
Coordinator.ResolveCurrentThemeType(),
|
||||
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
|
||||
guid: b7dc48b97505d0242bf5846ba835f3b3
|
||||
guid: 807de988c6a1431ab7b4b6d4d5a7d92f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
|
|
@ -3,21 +3,43 @@ using System.Collections.Generic;
|
|||
using GameFramework.DataTable;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Factory;
|
||||
using GeometryTD.UI;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
using Random = UnityEngine.Random;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
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 int _runSeed;
|
||||
private int _nodeSequenceIndex = -1;
|
||||
private int _nextDropTagOrdinal;
|
||||
private int _nextRewardTagOrdinal;
|
||||
|
||||
private readonly List<DRShopPrice> _shopPriceRows = new();
|
||||
private IDataTable<DRShopPrice> _shopPriceTable;
|
||||
private IDataTable<DROutGameDropPool> _dropPoolTable;
|
||||
private IDataTable<DRMuzzleComp> _muzzleCompTable;
|
||||
private IDataTable<DRBearingComp> _bearingCompTable;
|
||||
private IDataTable<DRBaseComp> _baseCompTable;
|
||||
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)
|
||||
{
|
||||
|
|
@ -27,8 +49,31 @@ namespace GeometryTD.CustomComponent
|
|||
|
||||
public EnemyDropResult ResolveEnemyDrop(in EnemyDropContext context)
|
||||
{
|
||||
throw new NotImplementedException(
|
||||
"InventoryGenerationComponent.ResolveEnemyDrop() will be implemented in G2.");
|
||||
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> BuildRewardCandidates(
|
||||
|
|
@ -36,8 +81,12 @@ namespace GeometryTD.CustomComponent
|
|||
LevelThemeType themeType,
|
||||
int candidateCount)
|
||||
{
|
||||
throw new NotImplementedException(
|
||||
"InventoryGenerationComponent.BuildRewardCandidates() will be implemented in G2.");
|
||||
RewardCandidateBuilder rewardCandidateBuilder = EnsureRewardCandidateBuilder();
|
||||
return rewardCandidateBuilder.BuildCandidates(
|
||||
displayPhaseIndex,
|
||||
themeType,
|
||||
candidateCount,
|
||||
BuildRewardCandidateItem);
|
||||
}
|
||||
|
||||
private void EnsureShopTables()
|
||||
|
|
@ -94,5 +143,148 @@ namespace GeometryTD.CustomComponent
|
|||
{
|
||||
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(
|
||||
settlementContext,
|
||||
null,
|
||||
displayPhaseIndex: 1,
|
||||
themeType: LevelThemeType.Plain,
|
||||
rewardSelectFormUseCase: new RewardSelectFormUseCase(),
|
||||
rewardSelectFormUseCase: null,
|
||||
onRewardSelected: _ => { },
|
||||
onGiveUp: null);
|
||||
|
||||
|
|
|
|||
|
|
@ -111,10 +111,10 @@
|
|||
|
||||
| 状态 | ID | 任务 | 目标 |
|
||||
|-----|-------|-----------------------------------|------------|
|
||||
| [ ] | G1-01 | 新增 `InventoryGenerationComponent` | 建立统一组件产出入口 |
|
||||
| [ ] | G1-02 | 新增 `ComponentItemFactory` | 收口组件实例构造 |
|
||||
| [ ] | G1-03 | 新增 `ShopGoodsBuilder` | 收口商店货物生成 |
|
||||
| [ ] | G1-04 | 让 `ShopFormUseCase` 改用组件入口 | 商店不再自己生成组件 |
|
||||
| [x] | G1-01 | 新增 `InventoryGenerationComponent` | 建立统一组件产出入口 |
|
||||
| [x] | G1-02 | 新增 `ComponentItemFactory` | 收口组件实例构造 |
|
||||
| [x] | G1-03 | 新增 `ShopGoodsBuilder` | 收口商店货物生成 |
|
||||
| [x] | G1-04 | 让 `ShopFormUseCase` 改用组件入口 | 商店不再自己生成组件 |
|
||||
|
||||
### G1 验收标准
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# CombatNode 设计规范(开发约束)
|
||||
|
||||
最后更新:2026-03-06
|
||||
最后更新:2026-03-12
|
||||
|
||||
## 1. 适用范围与目标
|
||||
|
||||
|
|
@ -296,22 +296,25 @@
|
|||
- `Coin / BaseHp` 变化事件同时携带“当前值”和“变化量”。
|
||||
- `Gold` 只是结算累计值,不要求战斗内实时事件驱动。
|
||||
|
||||
### 4.5 EnemyDropResolver
|
||||
### 4.5 InventoryGenerationComponent
|
||||
|
||||
文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropResolver.cs`
|
||||
文件:`Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/InventoryGenerationComponent.cs`
|
||||
|
||||
目标职责:
|
||||
- 只负责敌人死亡后的掉落判定。
|
||||
- 输入:
|
||||
- `DREnemy`
|
||||
- 当前阶段索引
|
||||
- 当前主题或关卡上下文
|
||||
- 输出:
|
||||
- 掉落结果对象(`coin / gold / loot`)
|
||||
- 作为局外组件产出的统一运行时入口。
|
||||
- 对外提供:
|
||||
- `BuildShopGoods(...)`
|
||||
- `ResolveEnemyDrop(...)`
|
||||
- `BuildRewardCandidates(...)`
|
||||
- 在内部编排:
|
||||
- `DropPoolRoller`
|
||||
- `RewardCandidateBuilder`
|
||||
- `ComponentItemFactory`
|
||||
|
||||
约束:
|
||||
- 不直接修改资源状态。
|
||||
- 不直接读取 `CombatNodeComponent`、`MapEntity`、`EnemyManager` 内部状态。
|
||||
- `CombatNode` 域不直接持有或复制组件产出规则。
|
||||
- `CombatScheduler` 与结算状态链只调用统一入口,不直接访问掉落池滚动或组件实例构造细节。
|
||||
- `InventoryGenerationComponent` 负责组件实例生成、Tag 随机上下文以及 `runSeed/sequenceIndex` 相关上下文。
|
||||
|
||||
### 4.6 IPhaseEndCondition
|
||||
|
||||
|
|
@ -406,7 +409,7 @@ Boss 识别规则:
|
|||
- `OnEnemyDefeated(DREnemy enemy)`
|
||||
- `OnEnemyReachedBase(DREnemy enemy)`
|
||||
- `CombatScheduler` 公共层负责处理敌人事件的通用副作用:
|
||||
- 击杀:调用 `EnemyDropResolver`,再调用局内资源管理器入账。
|
||||
- 击杀:调用 `GameEntry.InventoryGeneration.ResolveEnemyDrop(...)`,再调用局内资源管理器入账。
|
||||
- 到家:调用局内资源管理器扣减 `BaseHp`。
|
||||
|
||||
约束:
|
||||
|
|
@ -467,7 +470,7 @@ Boss 识别规则:
|
|||
1. `CombatScheduler` 只做状态机管理与共享运行时收口,不继续吸收具体业务细节。
|
||||
2. `CombatNodeComponent` 不再持有战斗内资源真值。
|
||||
3. 局内 `Coin / Gold / BaseHp / Loot Backpack / BuildTowerSnapshots` 以 `CombatRunResourceStore` 为唯一真值来源。
|
||||
4. 敌人死亡掉落判定以 `EnemyDropResolver` 为唯一判定入口。
|
||||
4. 组件产出规则以 `InventoryGenerationComponent` 为统一运行时入口;战斗掉落与奖励候选都通过它生成。
|
||||
5. 存活敌人数与 `HasAliveBoss` 以 `EnemyLifecycleTracker` 为唯一真值来源。
|
||||
6. Phase 运行时信息与统一结束标记以 `PhaseLoopRuntime` 为唯一真值来源。
|
||||
7. `PhaseEndType` 的退出条件以 `IPhaseEndCondition` 实现类为唯一判定入口。
|
||||
|
|
@ -498,7 +501,11 @@ Boss 识别规则:
|
|||
|
||||
### 10.3 新增敌人掉落规则
|
||||
|
||||
优先改 `EnemyDropResolver`,不要在 `EnemyManager` 或状态类里直接计算掉落。
|
||||
优先改 `InventoryGenerationComponent` 及其下层规则模块,不要在 `EnemyManager`、`CombatScheduler` 或状态类里直接计算掉落。
|
||||
|
||||
### 10.3.x 新增奖励候选规则
|
||||
|
||||
优先改 `InventoryGenerationComponent`、`RewardCandidateBuilder` 或 `DropPoolRoller`,不要在结算状态链里复制一套候选生成规则。
|
||||
|
||||
### 10.4 新增战斗内资源或建塔快照规则
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue