feat: Introduce new services and refactor existing code for better structure

- Added ISpriteCacheService, IStaticDataService, and ITextService interfaces for improved service management.
- Implemented RunContracts for managing run states and node execution contexts.
- Created EnemyTagStatusRuntime to handle enemy status effects and their application.
- Refactored ShopPriceRuleService to utilize static data service for shop prices.
- Enhanced InventoryTowerEnduranceUtility to manage tower endurance reduction.
- Updated EventFormUseCase to work with new EventOptionExecutionContext.
- Removed unused dictionary loading code from ProcedurePreload.
- Cleaned up various utility classes and ensured consistent naming conventions.
- Updated tests to reflect changes in context and service usage.
This commit is contained in:
2026-05-13 22:18:47 +08:00
parent 9fc0ef2216
commit 7106e0adbe
45 changed files with 590 additions and 426 deletions
@@ -1,12 +1,21 @@
using System;
using System.Collections.Generic;
using GeometryTD.CustomComponent;
using GeometryTD.Core;
using GeometryTD.Procedure;
using UnityEngine;
namespace GeometryTD.Core
{
public sealed class EventOptionExecutionContext
{
public EventOptionExecutionContext(int runSeed, int sequenceIndex)
{
RunSeed = runSeed;
SequenceIndex = sequenceIndex;
}
public int RunSeed { get; }
public int SequenceIndex { get; }
}
public sealed class EventOptionExecutor
{
public EventOptionAvailability EvaluateOption(EventOption option, BackpackInventoryData inventory)
@@ -37,7 +46,7 @@ namespace GeometryTD.Core
public EventOptionExecutionResult Execute(
EventItem eventItem,
int optionIndex,
RunNodeExecutionContext context,
EventOptionExecutionContext context,
BackpackInventoryData workingInventory)
{
if (eventItem == null || workingInventory == null)
@@ -72,7 +81,7 @@ namespace GeometryTD.Core
EventEffectBase[] effects,
EventItem eventItem,
int optionIndex,
RunNodeExecutionContext context,
EventOptionExecutionContext context,
BackpackInventoryData workingInventory,
bool isRewardPhase)
{
@@ -143,7 +152,7 @@ namespace GeometryTD.Core
RemoveRandomCompsParam param,
int eventId,
int optionIndex,
RunNodeExecutionContext context,
EventOptionExecutionContext context,
BackpackInventoryData workingInventory,
int effectIndex)
{
@@ -171,7 +180,7 @@ namespace GeometryTD.Core
AddRandomCompsParam param,
int eventId,
int optionIndex,
RunNodeExecutionContext context,
EventOptionExecutionContext context,
BackpackInventoryData workingInventory,
int effectIndex)
{
@@ -181,12 +190,12 @@ namespace GeometryTD.Core
return;
}
if (GameEntry.InventoryGeneration == null)
if (CoreServiceHub.InventoryGeneration == null)
{
throw new InvalidOperationException("Event component generation requires InventoryGenerationComponent.");
}
IReadOnlyList<TowerCompItemData> generatedComponents = GameEntry.InventoryGeneration.BuildEventRewardComponents(
IReadOnlyList<TowerCompItemData> generatedComponents = CoreServiceHub.InventoryGeneration.BuildEventRewardComponents(
addCount,
param.MinRarity,
param.MaxRarity,
@@ -205,7 +214,7 @@ namespace GeometryTD.Core
DamageRandomTowerEnduranceParam param,
int eventId,
int optionIndex,
RunNodeExecutionContext context,
EventOptionExecutionContext context,
BackpackInventoryData workingInventory,
int effectIndex)
{
@@ -272,7 +281,7 @@ namespace GeometryTD.Core
}
}
private static bool RollProbability(int eventId, int optionIndex, RunNodeExecutionContext context, float probability)
private static bool RollProbability(int eventId, int optionIndex, EventOptionExecutionContext context, float probability)
{
float clampedProbability = Mathf.Clamp01(probability);
if (clampedProbability >= 1f)
@@ -290,7 +299,7 @@ namespace GeometryTD.Core
}
private static System.Random CreateRandom(
RunNodeExecutionContext context,
EventOptionExecutionContext context,
int eventId,
int optionIndex,
int effectIndex,
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9aeb92ccac2068f489dcbd0200417463
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,31 @@
using System;
namespace GeometryTD.Core
{
public static class CoreServiceHub
{
public static IPlayerInventoryManager PlayerInventory { get; private set; }
public static IInventoryGenerationManager InventoryGeneration { get; private set; }
public static IEventFlowManager EventFlow { get; private set; }
public static ISpriteCacheService SpriteCache { get; private set; }
public static IStaticDataService StaticData { get; private set; }
public static ITextService TextService { get; private set; }
public static void Bind(
IPlayerInventoryManager playerInventory,
IInventoryGenerationManager inventoryGeneration,
IEventFlowManager eventFlow,
ISpriteCacheService spriteCache,
IStaticDataService staticData,
ITextService textService)
{
PlayerInventory = playerInventory ?? throw new ArgumentNullException(nameof(playerInventory));
InventoryGeneration = inventoryGeneration ?? throw new
ArgumentNullException(nameof(inventoryGeneration));
EventFlow = eventFlow ?? throw new ArgumentNullException(nameof(eventFlow));
SpriteCache = spriteCache ?? throw new ArgumentNullException(nameof(spriteCache));
StaticData = staticData ?? throw new ArgumentNullException(nameof(staticData));
TextService = textService ?? throw new ArgumentNullException(nameof(textService));
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 95f8a75dcf78e4545b36f1e24b92d82e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,7 @@
namespace GeometryTD.Core
{
public interface IEventFlowManager
{
void EndEvent();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c14fccc4cf6d239489afdc5b302c03b9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,17 @@
using System.Collections.Generic;
namespace GeometryTD.Core
{
public interface IInventoryGenerationManager
{
IReadOnlyList<TowerCompItemData> BuildEventRewardComponents(
int count,
RarityType minRarity,
RarityType maxRarity,
int runSeed,
int sequenceIndex,
int eventId,
int optionIndex,
int effectIndex);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ba83daa081633074ab92ff9ad75c9c52
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
namespace GeometryTD.Core
{
public interface IPlayerInventoryManager
{
BackpackInventoryData GetInventorySnapshot();
void ReplaceInventorySnapshot(BackpackInventoryData snapshot);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0ce0e4176795db941a0f7ed57065f73d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,11 @@
using System;
using UnityEngine;
namespace GeometryTD.Core
{
public interface ISpriteCacheService
{
void Get(string key, Action<Sprite> callback);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 675e9ac521922ac46b3b7afe4973a706
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,12 @@
namespace GeometryTD.Core
{
public interface IStaticDataService
{
DRLevel GetLevel(int id);
DRTag GetTag(int id);
DRShopPrice[] GetAllShopPrices();
DRBearingComp[] GetAllBearingComps();
DRBaseComp[] GetAllBaseComps();
DRMuzzleComp[] GetAllMuzzleComps();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: db37b943863752b41a8a0deef641dca6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,7 @@
namespace GeometryTD.Core
{
public interface ITextService
{
string Get(string key, params object[] args);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9440fb32740fa50409ce370f9eab4d6d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f9d6ff9eff8570b4a96f296f9b107baa
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,150 @@
using System;
using System.Collections.Generic;
namespace GeometryTD.Core
{
public enum RunNodeType
{
None = 0,
Combat = 1,
Event = 2,
Shop = 3,
BossCombat = 4
}
public enum RunNodeCompletionStatus
{
None = 0,
Completed = 1,
Exception = 2
}
[Serializable]
public sealed class RunItemState
{
public int ItemId { get; set; }
public int StackCount { get; set; }
public RunItemState Clone()
{
return new RunItemState
{
ItemId = ItemId,
StackCount = StackCount
};
}
}
[Serializable]
public sealed class RunNodeExecutionContext
{
public string RunId { get; set; }
public int RunSeed { get; set; }
public int NodeId { get; set; }
public RunNodeType NodeType { get; set; }
public int SequenceIndex { get; set; }
public LevelThemeType ThemeType { get; set; }
public int ThemeStageIndex { get; set; }
public List<LevelThemeType> CurrentThemePool { get; set; } = new List<LevelThemeType>();
public List<LevelThemeType> ThemeHistory { get; set; } = new List<LevelThemeType>();
public int CurrentNodeContinueChallengeLayer { get; set; }
public List<RunItemState> RunItems { get; set; } = new List<RunItemState>();
public RunNodeExecutionContext Clone()
{
return new RunNodeExecutionContext
{
RunId = RunId,
RunSeed = RunSeed,
NodeId = NodeId,
NodeType = NodeType,
SequenceIndex = SequenceIndex,
ThemeType = ThemeType,
ThemeStageIndex = ThemeStageIndex,
CurrentThemePool = RunStateCloneUtility.CloneThemeList(CurrentThemePool),
ThemeHistory = RunStateCloneUtility.CloneThemeList(ThemeHistory),
CurrentNodeContinueChallengeLayer = CurrentNodeContinueChallengeLayer,
RunItems = RunStateCloneUtility.CloneRunItems(RunItems)
};
}
public RunNodeCompletionSnapshot CreateCompletionSnapshot(BackpackInventoryData inventorySnapshot)
{
return new RunNodeCompletionSnapshot
{
InventorySnapshot = InventoryCloneUtility.CloneInventory(inventorySnapshot),
ThemeType = ThemeType,
ThemeStageIndex = ThemeStageIndex,
CurrentThemePool = RunStateCloneUtility.CloneThemeList(CurrentThemePool),
ThemeHistory = RunStateCloneUtility.CloneThemeList(ThemeHistory),
CurrentNodeContinueChallengeLayer = CurrentNodeContinueChallengeLayer,
RunItems = RunStateCloneUtility.CloneRunItems(RunItems)
};
}
}
[Serializable]
public sealed class RunNodeCompletionSnapshot
{
public BackpackInventoryData InventorySnapshot { get; set; }
public LevelThemeType ThemeType { get; set; }
public int ThemeStageIndex { get; set; }
public List<LevelThemeType> CurrentThemePool { get; set; } = new List<LevelThemeType>();
public List<LevelThemeType> ThemeHistory { get; set; } = new List<LevelThemeType>();
public int CurrentNodeContinueChallengeLayer { get; set; }
public List<RunItemState> RunItems { get; set; } = new List<RunItemState>();
public RunNodeCompletionSnapshot Clone()
{
return new RunNodeCompletionSnapshot
{
InventorySnapshot = InventoryCloneUtility.CloneInventory(InventorySnapshot),
ThemeType = ThemeType,
ThemeStageIndex = ThemeStageIndex,
CurrentThemePool = RunStateCloneUtility.CloneThemeList(CurrentThemePool),
ThemeHistory = RunStateCloneUtility.CloneThemeList(ThemeHistory),
CurrentNodeContinueChallengeLayer = CurrentNodeContinueChallengeLayer,
RunItems = RunStateCloneUtility.CloneRunItems(RunItems)
};
}
}
public static class RunStateCloneUtility
{
public static List<LevelThemeType> CloneThemeList(IReadOnlyList<LevelThemeType> source)
{
List<LevelThemeType> cloned = new List<LevelThemeType>();
if (source == null)
{
return cloned;
}
for (int i = 0; i < source.Count; i++)
{
cloned.Add(source[i]);
}
return cloned;
}
public static List<RunItemState> CloneRunItems(IReadOnlyList<RunItemState> source)
{
List<RunItemState> cloned = new List<RunItemState>();
if (source == null)
{
return cloned;
}
for (int i = 0; i < source.Count; i++)
{
RunItemState item = source[i];
if (item != null)
{
cloned.Add(item.Clone());
}
}
return cloned;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e73cd16e1283cc94f903bb3d7728f543
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using GeometryTD.Core;
using UnityEngine;
namespace GeometryTD.Core
{
public sealed class EnemyTagStatusRuntime
{
private readonly Dictionary<TagType, EnemyStatusTagStateBase> _statesByTag = new();
private readonly List<TagType> _activeTags = new();
public bool HasStatus(TagType tagType)
{
return _statesByTag.ContainsKey(tagType);
}
public TagType[] GetActiveTagSnapshot()
{
if (_activeTags.Count <= 0)
{
return Array.Empty<TagType>();
}
return _activeTags.ToArray();
}
public void Reset()
{
_statesByTag.Clear();
_activeTags.Clear();
}
public void Activate(TagType tagType)
{
if (_activeTags.Contains(tagType))
{
return;
}
_activeTags.Add(tagType);
}
public TState GetState<TState>(TagType tagType) where TState : EnemyStatusTagStateBase
{
if (!_statesByTag.TryGetValue(tagType, out EnemyStatusTagStateBase state))
{
return null;
}
Debug.Assert(state is TState);
return state as TState;
}
public TState GetOrCreateState<TState>(TagType tagType) where TState : EnemyStatusTagStateBase, new()
{
if (_statesByTag.TryGetValue(tagType, out EnemyStatusTagStateBase existingState))
{
Debug.Assert(existingState is TState);
return existingState as TState;
}
TState state = new TState();
_statesByTag[tagType] = state;
return state;
}
public void Tick(float deltaTime, Action<int> applyDamage)
{
float resolvedDeltaTime = Mathf.Max(0f, deltaTime);
if (resolvedDeltaTime <= 0f)
{
return;
}
for (int i = _activeTags.Count - 1; i >= 0; i--)
{
TagType tagType = _activeTags[i];
Debug.Assert(EnemyStatusTagRegistry.TryGetEffect(tagType, out IEnemyStatusTagEffect effect));
if (effect.Tick(this, resolvedDeltaTime, applyDamage))
{
continue;
}
_statesByTag.Remove(tagType);
_activeTags.RemoveAt(i);
}
}
public float GetMoveSpeedMultiplier()
{
float multiplier = 1f;
for (int i = 0; i < _activeTags.Count; i++)
{
TagType tagType = _activeTags[i];
Debug.Assert(EnemyStatusTagRegistry.TryGetEffect(tagType, out IEnemyStatusTagEffect effect));
multiplier = Mathf.Min(multiplier, effect.GetMoveSpeedMultiplier(this));
}
return multiplier;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fe0965914fd71e34ea696bfd546efc66
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,5 +1,4 @@
using System;
using GeometryTD.Entity;
namespace GeometryTD.Core
{
@@ -1,5 +1,4 @@
using System;
using GeometryTD.Entity;
using UnityEngine;
namespace GeometryTD.Core
@@ -1,5 +1,4 @@
using System;
using GeometryTD.Entity;
namespace GeometryTD.Core
{
@@ -1,5 +1,4 @@
using System;
using GeometryTD.Entity;
using UnityEngine;
namespace GeometryTD.Core
@@ -13,17 +13,10 @@ namespace GeometryTD.Core
return string.Empty;
}
if (GameEntry.DataTable != null)
DRTag tagRow = CoreServiceHub.StaticData.GetTag((int)tagType);
if (tagRow != null && !string.IsNullOrWhiteSpace(tagRow.Name))
{
var tagTable = GameEntry.DataTable.GetDataTable<DRTag>();
if (tagTable != null)
{
DRTag tagRow = tagTable.GetDataRow((int)tagType);
if (tagRow != null && !string.IsNullOrWhiteSpace(tagRow.Name))
{
return tagRow.Name;
}
}
return tagRow.Name;
}
return tagType.ToString();
@@ -171,4 +164,4 @@ namespace GeometryTD.Core
return string.Empty;
}
}
}
}