refactor 1: 新增/重命名类定义

- 将资源服务类型从 CombatResourceManager 重命名为 CombatInRunResourceManager,并同步更新现有引用。
    - CombatScheduler.cs:28
    - CombatInRunResourceManager.cs:11
    - CombatFinishFormState.cs:22
- 新增掉落解析骨架:
    - EnemyDropResolveContext.cs:6
    - EnemyDropResolveResult.cs:3
    - EnemyDropResolver.cs:7
- 新增 phase end condition 骨架:
    - IPhaseEndCondition.cs:5
    - PhaseEndConditionContext.cs:5
    - PhaseEndConditionFactory.cs:5
    - 以及 4 个实现类:
        - NonePhaseEndCondition
        - TimeElapsedPhaseEndCondition
        - EnemiesClearedPhaseEndCondition
        - BossDeadPhaseEndCondition
This commit is contained in:
SepComet 2026-03-07 11:30:44 +08:00
parent 0ff04f02f4
commit 1d7c5b80d9
25 changed files with 361 additions and 14 deletions

View File

@ -8,7 +8,7 @@ using Random = UnityEngine.Random;
namespace GeometryTD.CustomComponent
{
internal sealed class CombatResourceManager
internal sealed class CombatInRunResourceManager
{
// 每次击杀敌人后,进入“掉组件判定”的概率:
// chance = clamp(DropChanceBase + (phaseIndex - 1) * DropChancePerPhase, 0, DropChanceCap)

View File

@ -25,7 +25,7 @@ namespace GeometryTD.CustomComponent
private readonly PhaseLoopRuntime _phaseLoopRuntime = new();
private readonly CombatLoadSession _loadSession = new();
private readonly CombatEventBridge _eventBridge = new();
private readonly CombatResourceManager _combatResourceManager = new();
private readonly CombatInRunResourceManager _combatInRunResourceManager = new();
private EntityComponent _entity;
private DRLevel _currentLevel;
@ -49,8 +49,8 @@ namespace GeometryTD.CustomComponent
public int PhaseCount => _phaseLoopRuntime.PhaseCount;
public bool CanEndCombat => _phaseLoopRuntime.CanEndCombat;
public int DefeatedEnemyCount => _enemyManager.DefeatedEnemyCount;
public int GainedCoin => _combatResourceManager.GainedCoin;
public int GainedGold => _combatResourceManager.GainedGold;
public int GainedCoin => _combatInRunResourceManager.GainedCoin;
public int GainedGold => _combatInRunResourceManager.GainedGold;
public void OnInit()
{
@ -95,7 +95,7 @@ namespace GeometryTD.CustomComponent
_enemyManager.EndPhase();
_enemyManager.ResetCombatStats();
ResetRuntime();
_combatResourceManager.Reset();
_combatInRunResourceManager.Reset();
_isFinishAsVictory = true;
_currentLevel = level;
@ -192,14 +192,14 @@ namespace GeometryTD.CustomComponent
public void OnEnemyDefeatedRewardResolved(int gainedCoin, int gainedGold)
{
_combatResourceManager.AddEnemyDefeatedReward(gainedCoin, gainedGold);
_combatInRunResourceManager.AddEnemyDefeatedReward(gainedCoin, gainedGold);
if (!IsRunning)
{
return;
}
_combatResourceManager.TryRollOutGameItemDrop(
_combatInRunResourceManager.TryRollOutGameItemDrop(
_phaseLoopRuntime.DisplayPhaseIndex,
ResolveCurrentThemeType());
}
@ -222,7 +222,7 @@ namespace GeometryTD.CustomComponent
_spawnEntriesByPhaseId.Clear();
_phaseLoopRuntime.Reset();
_loadSession.Reset();
_combatResourceManager.Reset();
_combatInRunResourceManager.Reset();
_settlementContext = null;
_currentLevel = null;
_isFinishAsVictory = true;
@ -337,8 +337,8 @@ namespace GeometryTD.CustomComponent
SettlementContext settlementContext = new SettlementContext
{
DefeatedEnemyCount = Mathf.Max(0, defeatedEnemyCount),
GainedGold = Mathf.Max(0, _combatResourceManager.GainedGold),
RewardInventory = _combatResourceManager.GetRewardInventorySnapshot(),
GainedGold = Mathf.Max(0, _combatInRunResourceManager.GainedGold),
RewardInventory = _combatInRunResourceManager.GetRewardInventorySnapshot(),
ShouldOpenRewardSelection = shouldOpenFullBaseHpRewardSelect,
Reason = reason
};
@ -397,10 +397,10 @@ namespace GeometryTD.CustomComponent
}
}
int goldForBonusCalculation = Mathf.Max(0, _combatResourceManager.GainedGold) + levelRewardGold;
int goldForBonusCalculation = Mathf.Max(0, _combatInRunResourceManager.GainedGold) + levelRewardGold;
int bonusGold = bonusRate > 0f ? Mathf.FloorToInt(goldForBonusCalculation * bonusRate) : 0;
int settlementGold = levelRewardGold + bonusGold;
_combatResourceManager.AddSettlementGold(settlementGold);
_combatInRunResourceManager.AddSettlementGold(settlementGold);
Log.Info(
"Combat settlement resolved. BaseHp={0}/{1}, LevelReward={2}, BonusRate={3:P0}, BonusGold={4}, FullHpRewardSelect={5}, LowHpPenalty={6}.",
@ -437,7 +437,7 @@ namespace GeometryTD.CustomComponent
private bool TryPrepareRewardSelection(SettlementContext settlementContext)
{
IReadOnlyList<TowerCompItemData> candidateItems = _combatResourceManager.RollSettlementRewardCandidates(
IReadOnlyList<TowerCompItemData> candidateItems = _combatInRunResourceManager.RollSettlementRewardCandidates(
_phaseLoopRuntime.DisplayPhaseIndex,
ResolveCurrentThemeType(),
RewardSelectDisplayCount);

View File

@ -0,0 +1,21 @@
using GeometryTD.DataTable;
using GeometryTD.Definition;
namespace GeometryTD.CustomComponent
{
internal readonly struct EnemyDropResolveContext
{
public EnemyDropResolveContext(DREnemy enemy, int displayPhaseIndex, LevelThemeType themeType)
{
Enemy = enemy;
DisplayPhaseIndex = displayPhaseIndex;
ThemeType = themeType;
}
public DREnemy Enemy { get; }
public int DisplayPhaseIndex { get; }
public LevelThemeType ThemeType { get; }
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cc1fe871193e2594189cac3af541f354
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,20 @@
namespace GeometryTD.CustomComponent
{
internal readonly struct EnemyDropResolveResult
{
public static EnemyDropResolveResult Empty => new(0, 0, false);
public EnemyDropResolveResult(int coin, int gold, bool shouldRollOutGameItem)
{
Coin = coin;
Gold = gold;
ShouldRollOutGameItem = shouldRollOutGameItem;
}
public int Coin { get; }
public int Gold { get; }
public bool ShouldRollOutGameItem { get; }
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 754c09edf691f354aa918c4a730a9cc5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,32 @@
using GeometryTD.DataTable;
using UnityEngine;
using Random = UnityEngine.Random;
namespace GeometryTD.CustomComponent
{
internal sealed class EnemyDropResolver
{
public EnemyDropResolveResult Resolve(in EnemyDropResolveContext context)
{
DREnemy enemy = context.Enemy;
if (enemy == null)
{
return EnemyDropResolveResult.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);
}
return new EnemyDropResolveResult(coin, gold, shouldRollOutGameItem: true);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b7dc48b97505d0242bf5846ba835f3b3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9e3f30abcd9a85642b7fe0e9a73254ce
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,14 @@
using GeometryTD.Definition;
namespace GeometryTD.CustomComponent
{
internal sealed class BossDeadPhaseEndCondition : IPhaseEndCondition
{
public PhaseEndType EndType => PhaseEndType.BossDead;
public bool ShouldExit(in PhaseEndConditionContext context)
{
return context.IsPhaseSpawnCompleted && !context.HasAliveBoss;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 69d0ce2f2c3ee424985fa9775c6291d1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,14 @@
using GeometryTD.Definition;
namespace GeometryTD.CustomComponent
{
internal sealed class EnemiesClearedPhaseEndCondition : IPhaseEndCondition
{
public PhaseEndType EndType => PhaseEndType.EnemiesCleared;
public bool ShouldExit(in PhaseEndConditionContext context)
{
return context.IsPhaseSpawnCompleted && context.AliveEnemyCount <= 0;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9a9d79f496dab39479265367593921f8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,11 @@
using GeometryTD.Definition;
namespace GeometryTD.CustomComponent
{
internal interface IPhaseEndCondition
{
PhaseEndType EndType { get; }
bool ShouldExit(in PhaseEndConditionContext context);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 15e7750c40dbb6449bc1bc93370734cb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,25 @@
using GeometryTD.Definition;
namespace GeometryTD.CustomComponent
{
internal sealed class NonePhaseEndCondition : IPhaseEndCondition
{
public PhaseEndType EndType => PhaseEndType.None;
public bool ShouldExit(in PhaseEndConditionContext context)
{
if (context.Phase == null)
{
return false;
}
if (context.Phase.DurationSeconds > 0 &&
context.PhaseElapsedSeconds >= context.Phase.DurationSeconds)
{
return true;
}
return context.IsPhaseSpawnCompleted && context.AliveEnemyCount <= 0;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 27a6e6b0635d41d4d8ba336e05927bab
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,31 @@
using GeometryTD.DataTable;
namespace GeometryTD.CustomComponent
{
internal readonly struct PhaseEndConditionContext
{
public PhaseEndConditionContext(
DRLevelPhase phase,
float phaseElapsedSeconds,
bool isPhaseSpawnCompleted,
int aliveEnemyCount,
bool hasAliveBoss)
{
Phase = phase;
PhaseElapsedSeconds = phaseElapsedSeconds;
IsPhaseSpawnCompleted = isPhaseSpawnCompleted;
AliveEnemyCount = aliveEnemyCount;
HasAliveBoss = hasAliveBoss;
}
public DRLevelPhase Phase { get; }
public float PhaseElapsedSeconds { get; }
public bool IsPhaseSpawnCompleted { get; }
public int AliveEnemyCount { get; }
public bool HasAliveBoss { get; }
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d4ce36bacd2ae574dbd77a9441cbbecb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,24 @@
using GeometryTD.Definition;
namespace GeometryTD.CustomComponent
{
internal static class PhaseEndConditionFactory
{
private static readonly IPhaseEndCondition None = new NonePhaseEndCondition();
private static readonly IPhaseEndCondition TimeElapsed = new TimeElapsedPhaseEndCondition();
private static readonly IPhaseEndCondition EnemiesCleared = new EnemiesClearedPhaseEndCondition();
private static readonly IPhaseEndCondition BossDead = new BossDeadPhaseEndCondition();
public static IPhaseEndCondition Create(PhaseEndType endType)
{
return endType switch
{
PhaseEndType.TimeElapsed => TimeElapsed,
PhaseEndType.EnemiesCleared => EnemiesCleared,
PhaseEndType.BossDead => BossDead,
PhaseEndType.None => None,
_ => None
};
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 35a7aac328337414dbae929053176177
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,37 @@
using System.Globalization;
using GeometryTD.DataTable;
using GeometryTD.Definition;
namespace GeometryTD.CustomComponent
{
internal sealed class TimeElapsedPhaseEndCondition : IPhaseEndCondition
{
public PhaseEndType EndType => PhaseEndType.TimeElapsed;
public bool ShouldExit(in PhaseEndConditionContext context)
{
if (context.Phase == null)
{
return false;
}
return context.PhaseElapsedSeconds >= ResolveTimeElapsedThreshold(context.Phase);
}
private static float ResolveTimeElapsedThreshold(DRLevelPhase phase)
{
if (!string.IsNullOrWhiteSpace(phase.EndParam) &&
float.TryParse(phase.EndParam, NumberStyles.Float, CultureInfo.InvariantCulture, out float parsed))
{
return parsed;
}
if (phase.DurationSeconds > 0)
{
return phase.DurationSeconds;
}
return 0f;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 39315af578eed09408c2a088fb83d409
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -19,7 +19,7 @@ namespace GeometryTD.CustomComponent
}
Scheduler.CommitSettlementInventory(Scheduler._settlementContext);
Scheduler._settlementContext.GainedGold = Mathf.Max(0, Scheduler._combatResourceManager.GainedGold);
Scheduler._settlementContext.GainedGold = Mathf.Max(0, Scheduler._combatInRunResourceManager.GainedGold);
Scheduler.OpenCombatFinishForm(Scheduler._settlementContext);
Scheduler.ChangeState(new CombatWaitingForReturnState(Scheduler));
}