补全部分 EventComponent 设计

This commit is contained in:
2026-03-16 11:53:24 +08:00
parent c76fd85a2c
commit dc2aa59d58
26 changed files with 2028 additions and 59 deletions
@@ -15,6 +15,7 @@ namespace GeometryTD.CustomComponent
public class EventNodeComponent : GameFrameworkComponent
{
private RunNodeExecutionContext _activeContext;
private EventItem _activeEvent;
private readonly List<EventItem> _eventItems = new List<EventItem>();
@@ -64,17 +65,21 @@ namespace GeometryTD.CustomComponent
return;
}
int index = Random.Range(0, _eventItems.Count);
EventItem randomEvent = _eventItems[index];
if (_eventFormUseCase == null)
{
Log.Warning("EventNodeComponent StartEvent failed. Event form is not initialized.");
return;
}
_activeContext = context != null ? context.Clone() : null;
_eventFormUseCase.SetCurrentEvent(randomEvent);
_activeEvent = SelectActiveEvent(_activeContext);
if (_activeEvent == null)
{
Log.Warning("EventNodeComponent StartEvent failed. No active event could be resolved.");
return;
}
_eventFormUseCase.BindEvent(_activeEvent, _activeContext);
GameEntry.UIRouter.OpenUI(UIFormType.EventForm);
GameEntry.Event.Fire(
this,
@@ -107,6 +112,8 @@ namespace GeometryTD.CustomComponent
private void ClearActiveNodeContext()
{
_activeContext = null;
_activeEvent = null;
_eventFormUseCase?.Clear();
}
private static EventOption[] ParseOptions(string optionsRaw)
@@ -200,5 +207,34 @@ namespace GeometryTD.CustomComponent
return effects.ToArray();
}
private EventItem SelectActiveEvent(RunNodeExecutionContext context)
{
if (_eventItems.Count <= 0)
{
return null;
}
if (context == null)
{
int randomIndex = Random.Range(0, _eventItems.Count);
return _eventItems[randomIndex];
}
System.Random random = new System.Random(BuildSelectionSeed(context));
return _eventItems[random.Next(0, _eventItems.Count)];
}
private static int BuildSelectionSeed(RunNodeExecutionContext context)
{
unchecked
{
int seed = 17;
seed = seed * 31 + context.RunSeed;
seed = seed * 31 + context.SequenceIndex;
seed = seed * 31 + context.NodeId;
return seed;
}
}
}
}
@@ -102,18 +102,78 @@ namespace GeometryTD.CustomComponent
}
}
public IReadOnlyList<TowerCompItemData> BuildEventRewardComponents(
int count,
RarityType rarity,
int runSeed,
int sequenceIndex,
int eventId,
int optionIndex,
int effectIndex)
{
return BuildEventRewardComponents(
count,
rarity,
rarity,
runSeed,
sequenceIndex,
eventId,
optionIndex,
effectIndex);
}
public IReadOnlyList<TowerCompItemData> BuildEventRewardComponents(
int count,
RarityType minRarity,
RarityType maxRarity,
int runSeed,
int sequenceIndex,
int eventId,
int optionIndex,
int effectIndex)
{
EnsureComponentTables();
int resolvedCount = Mathf.Max(0, count);
if (resolvedCount <= 0)
{
return System.Array.Empty<TowerCompItemData>();
}
RarityType normalizedMinRarity = InventoryRarityRuleService.NormalizeComponentRarity(minRarity);
RarityType normalizedMaxRarity = InventoryRarityRuleService.NormalizeComponentRarity(maxRarity);
if (normalizedMinRarity > normalizedMaxRarity)
{
throw new System.InvalidOperationException(
$"Event reward rarity range is invalid: {normalizedMinRarity} > {normalizedMaxRarity}.");
}
List<TowerCompItemData> result = new List<TowerCompItemData>(resolvedCount);
for (int i = 0; i < resolvedCount; i++)
{
InventoryGenerationRandomContext randomContext = CreateEventRandomContext(
runSeed,
sequenceIndex,
eventId,
optionIndex,
effectIndex,
i);
Random random = randomContext.CreateRandom();
result.Add(BuildRandomEventComponentItem(normalizedMinRarity, normalizedMaxRarity, randomContext, random));
}
return result;
}
private void EnsureShopTables()
{
_shopPriceTable ??= GameEntry.DataTable.GetDataTable<DRShopPrice>();
_muzzleCompTable ??= GameEntry.DataTable.GetDataTable<DRMuzzleComp>();
_bearingCompTable ??= GameEntry.DataTable.GetDataTable<DRBearingComp>();
_baseCompTable ??= GameEntry.DataTable.GetDataTable<DRBaseComp>();
EnsureComponentTables();
if (_shopPriceTable == null || _muzzleCompTable == null || _bearingCompTable == null ||
_baseCompTable == null)
if (_shopPriceTable == null)
{
throw new System.InvalidOperationException(
"InventoryGenerationComponent requires ShopPrice, MuzzleComp, BearingComp, and BaseComp data tables.");
"InventoryGenerationComponent requires ShopPrice data table.");
}
if (_shopPriceRows.Count > 0)
@@ -155,14 +215,12 @@ namespace GeometryTD.CustomComponent
private void EnsureDropTables()
{
_dropPoolTable ??= GameEntry.DataTable.GetDataTable<DROutGameDropPool>();
_muzzleCompTable ??= GameEntry.DataTable.GetDataTable<DRMuzzleComp>();
_bearingCompTable ??= GameEntry.DataTable.GetDataTable<DRBearingComp>();
_baseCompTable ??= GameEntry.DataTable.GetDataTable<DRBaseComp>();
EnsureComponentTables();
if (_dropPoolTable == null || _muzzleCompTable == null || _bearingCompTable == null || _baseCompTable == null)
if (_dropPoolTable == null)
{
throw new System.InvalidOperationException(
"InventoryGenerationComponent requires OutGameDropPool, MuzzleComp, BearingComp, and BaseComp data tables.");
"InventoryGenerationComponent requires OutGameDropPool data table.");
}
}
@@ -224,5 +282,118 @@ namespace GeometryTD.CustomComponent
return droppedItem;
}
private void EnsureComponentTables()
{
_muzzleCompTable ??= GameEntry.DataTable.GetDataTable<DRMuzzleComp>();
_bearingCompTable ??= GameEntry.DataTable.GetDataTable<DRBearingComp>();
_baseCompTable ??= GameEntry.DataTable.GetDataTable<DRBaseComp>();
if (_muzzleCompTable == null || _bearingCompTable == null || _baseCompTable == null)
{
throw new System.InvalidOperationException(
"InventoryGenerationComponent requires MuzzleComp, BearingComp, and BaseComp data tables.");
}
}
private TowerCompItemData BuildRandomEventComponentItem(
RarityType minRarity,
RarityType maxRarity,
InventoryGenerationRandomContext randomContext,
Random random)
{
RarityType rarity = ResolveEventRewardRarity(minRarity, maxRarity, random);
int slotRoll = random.Next(0, 3);
return slotRoll switch
{
0 => BuildRandomEventMuzzleItem(rarity, randomContext, random),
1 => BuildRandomEventBearingItem(rarity, randomContext, random),
_ => BuildRandomEventBaseItem(rarity, randomContext, random)
};
}
private static RarityType ResolveEventRewardRarity(
RarityType minRarity,
RarityType maxRarity,
Random random)
{
if (minRarity >= maxRarity)
{
return minRarity;
}
int rarityValue = random.Next((int)minRarity, (int)maxRarity + 1);
return (RarityType)rarityValue;
}
private MuzzleCompItemData BuildRandomEventMuzzleItem(
RarityType rarity,
InventoryGenerationRandomContext randomContext,
Random random)
{
DRMuzzleComp[] rows = _muzzleCompTable.GetAllDataRows();
DRMuzzleComp config = rows[random.Next(0, rows.Length)];
return ComponentItemFactory.CreateMuzzle(
config,
randomContext.CreateStableItemInstanceId(),
rarity,
randomContext.CreateTagRandomContext(config.Id));
}
private BearingCompItemData BuildRandomEventBearingItem(
RarityType rarity,
InventoryGenerationRandomContext randomContext,
Random random)
{
DRBearingComp[] rows = _bearingCompTable.GetAllDataRows();
DRBearingComp config = rows[random.Next(0, rows.Length)];
return ComponentItemFactory.CreateBearing(
config,
randomContext.CreateStableItemInstanceId(),
rarity,
randomContext.CreateTagRandomContext(config.Id));
}
private BaseCompItemData BuildRandomEventBaseItem(
RarityType rarity,
InventoryGenerationRandomContext randomContext,
Random random)
{
DRBaseComp[] rows = _baseCompTable.GetAllDataRows();
DRBaseComp config = rows[random.Next(0, rows.Length)];
return ComponentItemFactory.CreateBase(
config,
randomContext.CreateStableItemInstanceId(),
rarity,
randomContext.CreateTagRandomContext(config.Id));
}
private static InventoryGenerationRandomContext CreateEventRandomContext(
int runSeed,
int sequenceIndex,
int eventId,
int optionIndex,
int effectIndex,
int itemIndex)
{
return new InventoryGenerationRandomContext(
runSeed,
sequenceIndex,
InventoryTagSourceType.Event,
BuildEventLocalOrdinal(eventId, optionIndex, effectIndex, itemIndex));
}
private static int BuildEventLocalOrdinal(int eventId, int optionIndex, int effectIndex, int itemIndex)
{
unchecked
{
int value = 17;
value = value * 31 + eventId;
value = value * 31 + optionIndex;
value = value * 31 + effectIndex;
value = value * 31 + itemIndex;
return value & int.MaxValue;
}
}
}
}
@@ -44,6 +44,7 @@ namespace GeometryTD.Definition
{
InventoryTagSourceType.Shop => InventoryTagRandomContext.CreateShop(RunSeed, NodeSequenceIndex, LocalOrdinal, configId),
InventoryTagSourceType.Reward => InventoryTagRandomContext.CreateReward(RunSeed, NodeSequenceIndex, LocalOrdinal, configId),
InventoryTagSourceType.Event => InventoryTagRandomContext.CreateEvent(RunSeed, NodeSequenceIndex, LocalOrdinal, configId),
_ => InventoryTagRandomContext.CreateDrop(RunSeed, NodeSequenceIndex, LocalOrdinal, configId)
};
}
@@ -35,14 +35,27 @@ namespace GeometryTD.CustomComponent
}
public int ReduceTowerEndurance(IReadOnlyList<long> towerInstanceIds, float enduranceLoss)
{
return InventoryTowerEnduranceUtility.ReduceTowerEndurance(
_queryModel.Inventory,
towerInstanceIds,
enduranceLoss);
}
}
public static class InventoryTowerEnduranceUtility
{
public static int ReduceTowerEndurance(
BackpackInventoryData inventory,
IReadOnlyList<long> towerInstanceIds,
float enduranceLoss)
{
float resolvedLoss = Mathf.Max(0f, enduranceLoss);
BackpackInventoryData inventory = _queryModel.Inventory;
if (resolvedLoss <= 0f ||
if (inventory?.Towers == null ||
inventory.Towers.Count <= 0 ||
resolvedLoss <= 0f ||
towerInstanceIds == null ||
towerInstanceIds.Count <= 0 ||
inventory.Towers == null ||
inventory.Towers.Count <= 0)
towerInstanceIds.Count <= 0)
{
return 0;
}
+32 -2
View File
@@ -1,4 +1,5 @@
using UnityGameFramework.Runtime;
using Newtonsoft.Json.Linq;
namespace GeometryTD.DataTable
{
@@ -30,6 +31,14 @@ namespace GeometryTD.DataTable
/// <remarks>原始字符串(如 JSON 文本),不在此处做解析。</remarks>
public string OptionsRaw { get; private set; }
public string Option1Raw { get; private set; }
public string Option2Raw { get; private set; }
public string Option3Raw { get; private set; }
public string Option4Raw { get; private set; }
public override bool ParseDataRow(string dataRowString, object userData)
{
string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators);
@@ -44,9 +53,30 @@ namespace GeometryTD.DataTable
index++;
Title = columnStrings[index++];
Description = columnStrings[index++];
OptionsRaw = columnStrings[index++];
Option1Raw = columnStrings[index++];
Option2Raw = columnStrings[index++];
Option3Raw = columnStrings[index++];
Option4Raw = columnStrings[index++];
OptionsRaw = BuildOptionsRaw(Option1Raw, Option2Raw, Option3Raw, Option4Raw);
return true;
}
private static string BuildOptionsRaw(params string[] optionColumns)
{
JArray array = new JArray();
for (int i = 0; i < optionColumns.Length; i++)
{
string optionRaw = optionColumns[i];
if (string.IsNullOrWhiteSpace(optionRaw))
{
continue;
}
array.Add(JObject.Parse(optionRaw));
}
return array.ToString(Newtonsoft.Json.Formatting.None);
}
}
}
}
@@ -6,5 +6,6 @@ namespace GeometryTD.Definition
Shop = 2,
Drop = 3,
Reward = 4,
Event = 5,
}
}
@@ -18,11 +18,31 @@ namespace GeometryTD.Definition
{
public int Count;
public RarityType Rarity;
public RarityType MinRarity;
public RarityType MaxRarity;
public AddRandomCompsParam(int count, RarityType rarity)
{
Count = count;
Rarity = rarity;
RarityType normalizedRarity = InventoryRarityRuleService.NormalizeComponentRarity(rarity);
Rarity = normalizedRarity;
MinRarity = normalizedRarity;
MaxRarity = normalizedRarity;
}
public AddRandomCompsParam(int count, RarityType minRarity, RarityType maxRarity)
{
Count = count;
MinRarity = InventoryRarityRuleService.NormalizeComponentRarity(minRarity);
MaxRarity = InventoryRarityRuleService.NormalizeComponentRarity(maxRarity);
if (MinRarity > MaxRarity)
{
throw new System.ArgumentOutOfRangeException(
nameof(minRarity),
$"AddRandomComps rarity range is invalid: {MinRarity} > {MaxRarity}.");
}
Rarity = MinRarity == MaxRarity ? MinRarity : RarityType.None;
}
}
}
@@ -0,0 +1,490 @@
using System;
using System.Collections.Generic;
using GeometryTD.CustomComponent;
using GeometryTD.CustomUtility;
using GeometryTD.Procedure;
using UnityEngine;
namespace GeometryTD.Definition
{
public sealed class EventOptionExecutor
{
public EventOptionAvailability EvaluateOption(EventOption option, BackpackInventoryData inventory)
{
if (option == null)
{
return EventOptionAvailability.Blocked("选项配置无效");
}
EventRequirementBase[] requirements = option.Requirements ?? Array.Empty<EventRequirementBase>();
for (int i = 0; i < requirements.Length; i++)
{
EventRequirementBase requirement = requirements[i];
if (requirement == null)
{
continue;
}
if (!IsRequirementSatisfied(requirement, inventory))
{
return EventOptionAvailability.Blocked(BuildBlockedReason(requirement));
}
}
return EventOptionAvailability.Selectable();
}
public EventOptionExecutionResult Execute(
EventItem eventItem,
int optionIndex,
RunNodeExecutionContext context,
BackpackInventoryData workingInventory)
{
if (eventItem == null || workingInventory == null)
{
return EventOptionExecutionResult.Rejected("事件数据无效");
}
if (optionIndex < 0 || optionIndex >= eventItem.Options.Length)
{
return EventOptionExecutionResult.Rejected("事件选项索引无效");
}
EventOption option = eventItem.Options[optionIndex];
EventOptionAvailability availability = EvaluateOption(option, workingInventory);
if (!availability.IsSelectable)
{
return EventOptionExecutionResult.Rejected(availability.BlockedReason);
}
ApplyEffects(option.CostEffects, eventItem, optionIndex, context, workingInventory, isRewardPhase: false);
bool isProbabilitySuccess = RollProbability(eventItem.Id, optionIndex, context, option.Probability);
if (isProbabilitySuccess)
{
ApplyEffects(option.RewardEffects, eventItem, optionIndex, context, workingInventory, isRewardPhase: true);
}
return EventOptionExecutionResult.Accepted(isProbabilitySuccess);
}
private void ApplyEffects(
EventEffectBase[] effects,
EventItem eventItem,
int optionIndex,
RunNodeExecutionContext context,
BackpackInventoryData workingInventory,
bool isRewardPhase)
{
if (effects == null || effects.Length <= 0)
{
return;
}
for (int i = 0; i < effects.Length; i++)
{
EventEffectBase effect = effects[i];
if (effect == null)
{
continue;
}
switch (effect.EffectType)
{
case EventEffectType.AddGold:
ApplyAddGoldEffect((AddGoldParam)effect.Param, workingInventory);
break;
case EventEffectType.RemoveRandomComps:
ApplyRemoveRandomComponentsEffect(
(RemoveRandomCompsParam)effect.Param,
eventItem.Id,
optionIndex,
context,
workingInventory,
i);
break;
case EventEffectType.AddRandomComps:
ApplyAddRandomComponentsEffect(
(AddRandomCompsParam)effect.Param,
eventItem.Id,
optionIndex,
context,
workingInventory,
i);
break;
case EventEffectType.DamageRandomTowersEndurance:
ApplyDamageRandomTowerEnduranceEffect(
(DamageRandomTowerEnduranceParam)effect.Param,
eventItem.Id,
optionIndex,
context,
workingInventory,
i);
break;
default:
throw new InvalidOperationException(
$"Unsupported event effect at runtime: {effect.EffectType} (rewardPhase={isRewardPhase}).");
}
}
}
private static void ApplyAddGoldEffect(AddGoldParam param, BackpackInventoryData workingInventory)
{
int nextGold = workingInventory.Gold + (param?.Count ?? 0);
if (nextGold < 0)
{
throw new InvalidOperationException("Event gold effect would reduce gold below zero.");
}
workingInventory.Gold = nextGold;
}
private static void ApplyRemoveRandomComponentsEffect(
RemoveRandomCompsParam param,
int eventId,
int optionIndex,
RunNodeExecutionContext context,
BackpackInventoryData workingInventory,
int effectIndex)
{
int removeCount = Mathf.Max(0, param?.Count ?? 0);
if (removeCount <= 0)
{
return;
}
List<TowerCompItemData> candidates = CollectLooseComponents(workingInventory, param.Rarity);
if (candidates.Count < removeCount)
{
throw new InvalidOperationException("Event component removal effect does not have enough candidates.");
}
System.Random random = CreateRandom(context, eventId, optionIndex, effectIndex, 101);
Shuffle(candidates, random);
for (int i = 0; i < removeCount; i++)
{
RemoveComponentByInstanceId(workingInventory, candidates[i]);
}
}
private static void ApplyAddRandomComponentsEffect(
AddRandomCompsParam param,
int eventId,
int optionIndex,
RunNodeExecutionContext context,
BackpackInventoryData workingInventory,
int effectIndex)
{
int addCount = Mathf.Max(0, param?.Count ?? 0);
if (addCount <= 0)
{
return;
}
if (GameEntry.InventoryGeneration == null)
{
throw new InvalidOperationException("Event component generation requires InventoryGenerationComponent.");
}
IReadOnlyList<TowerCompItemData> generatedComponents = GameEntry.InventoryGeneration.BuildEventRewardComponents(
addCount,
param.MinRarity,
param.MaxRarity,
context?.RunSeed ?? 0,
context?.SequenceIndex ?? -1,
eventId,
optionIndex,
effectIndex);
for (int i = 0; i < generatedComponents.Count; i++)
{
AddComponentToInventory(workingInventory, generatedComponents[i]);
}
}
private static void ApplyDamageRandomTowerEnduranceEffect(
DamageRandomTowerEnduranceParam param,
int eventId,
int optionIndex,
RunNodeExecutionContext context,
BackpackInventoryData workingInventory,
int effectIndex)
{
int towerCount = Mathf.Max(0, param?.Count ?? 0);
float enduranceLoss = Mathf.Max(0, param?.Amount ?? 0);
if (towerCount <= 0 || enduranceLoss <= 0f || workingInventory.Towers == null || workingInventory.Towers.Count <= 0)
{
return;
}
List<long> candidateTowerIds = new List<long>(workingInventory.Towers.Count);
for (int i = 0; i < workingInventory.Towers.Count; i++)
{
TowerItemData tower = workingInventory.Towers[i];
if (tower != null && tower.InstanceId > 0)
{
candidateTowerIds.Add(tower.InstanceId);
}
}
if (candidateTowerIds.Count <= 0)
{
return;
}
System.Random random = CreateRandom(context, eventId, optionIndex, effectIndex, 211);
Shuffle(candidateTowerIds, random);
int resolvedCount = Mathf.Min(towerCount, candidateTowerIds.Count);
List<long> selectedTowerIds = candidateTowerIds.GetRange(0, resolvedCount);
InventoryTowerEnduranceUtility.ReduceTowerEndurance(workingInventory, selectedTowerIds, enduranceLoss);
}
private static bool IsRequirementSatisfied(EventRequirementBase requirement, BackpackInventoryData inventory)
{
switch (requirement.RequirementType)
{
case EventRequirementType.GoldAtLeast:
return (inventory?.Gold ?? 0) >= ((GoldAtLeastParam)requirement.Param).Gold;
case EventRequirementType.CompCountAtLeast:
CompCountAtLeastParam compParam = (CompCountAtLeastParam)requirement.Param;
return CollectLooseComponents(inventory, compParam.Rarity).Count >= compParam.Count;
case EventRequirementType.TowerCountAtLeast:
return (inventory?.Towers?.Count ?? 0) >= ((TowerCountAtLeastParam)requirement.Param).Count;
default:
throw new InvalidOperationException(
$"Unsupported event requirement at runtime: {requirement.RequirementType}.");
}
}
private static string BuildBlockedReason(EventRequirementBase requirement)
{
switch (requirement.RequirementType)
{
case EventRequirementType.GoldAtLeast:
return $"需要至少 {((GoldAtLeastParam)requirement.Param).Gold} 金币";
case EventRequirementType.CompCountAtLeast:
CompCountAtLeastParam compParam = (CompCountAtLeastParam)requirement.Param;
return $"需要至少 {compParam.Count} 个未装配的{GetRarityText(compParam.Rarity)}组件";
case EventRequirementType.TowerCountAtLeast:
return $"需要至少 {((TowerCountAtLeastParam)requirement.Param).Count} 座防御塔";
default:
throw new InvalidOperationException(
$"Unsupported event requirement at runtime: {requirement.RequirementType}.");
}
}
private static bool RollProbability(int eventId, int optionIndex, RunNodeExecutionContext context, float probability)
{
float clampedProbability = Mathf.Clamp01(probability);
if (clampedProbability >= 1f)
{
return true;
}
if (clampedProbability <= 0f)
{
return false;
}
System.Random random = CreateRandom(context, eventId, optionIndex, 0, 17);
return random.NextDouble() <= clampedProbability;
}
private static System.Random CreateRandom(
RunNodeExecutionContext context,
int eventId,
int optionIndex,
int effectIndex,
int salt)
{
unchecked
{
int seed = 17;
seed = seed * 31 + (context?.RunSeed ?? 0);
seed = seed * 31 + (context?.SequenceIndex ?? -1);
seed = seed * 31 + eventId;
seed = seed * 31 + optionIndex;
seed = seed * 31 + effectIndex;
seed = seed * 31 + salt;
return new System.Random(seed);
}
}
private static List<TowerCompItemData> CollectLooseComponents(
BackpackInventoryData inventory,
RarityType rarity)
{
List<TowerCompItemData> result = new List<TowerCompItemData>();
if (inventory == null)
{
return result;
}
RarityType normalizedRarity = InventoryRarityRuleService.NormalizeComponentRarity(rarity);
CollectFromList(result, inventory.MuzzleComponents, normalizedRarity);
CollectFromList(result, inventory.BearingComponents, normalizedRarity);
CollectFromList(result, inventory.BaseComponents, normalizedRarity);
return result;
}
private static void CollectFromList<TComp>(
List<TowerCompItemData> destination,
List<TComp> source,
RarityType rarity)
where TComp : TowerCompItemData
{
if (source == null)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
TComp component = source[i];
if (component == null || component.IsAssembledIntoTower)
{
continue;
}
if (InventoryRarityRuleService.NormalizeComponentRarity(component.Rarity) != rarity)
{
continue;
}
destination.Add(component);
}
}
private static void RemoveComponentByInstanceId(BackpackInventoryData inventory, TowerCompItemData component)
{
switch (component)
{
case MuzzleCompItemData muzzleComp:
RemoveByInstanceId(inventory.MuzzleComponents, muzzleComp.InstanceId);
break;
case BearingCompItemData bearingComp:
RemoveByInstanceId(inventory.BearingComponents, bearingComp.InstanceId);
break;
case BaseCompItemData baseComp:
RemoveByInstanceId(inventory.BaseComponents, baseComp.InstanceId);
break;
default:
throw new InvalidOperationException($"Unsupported component type for event removal: {component?.GetType().Name}");
}
}
private static void AddComponentToInventory(BackpackInventoryData inventory, TowerCompItemData component)
{
if (component == null)
{
return;
}
switch (component)
{
case MuzzleCompItemData muzzleComp:
inventory.MuzzleComponents.Add(InventoryCloneUtility.CloneMuzzleComp(muzzleComp));
break;
case BearingCompItemData bearingComp:
inventory.BearingComponents.Add(InventoryCloneUtility.CloneBearingComp(bearingComp));
break;
case BaseCompItemData baseComp:
inventory.BaseComponents.Add(InventoryCloneUtility.CloneBaseComp(baseComp));
break;
default:
throw new InvalidOperationException($"Unsupported component type for event addition: {component.GetType().Name}");
}
}
private static void RemoveByInstanceId<TComp>(List<TComp> components, long instanceId)
where TComp : TowerCompItemData
{
if (components == null)
{
return;
}
for (int i = 0; i < components.Count; i++)
{
TComp component = components[i];
if (component != null && component.InstanceId == instanceId)
{
components.RemoveAt(i);
return;
}
}
throw new InvalidOperationException($"Failed to remove component instance #{instanceId} from inventory.");
}
private static void Shuffle<T>(IList<T> values, System.Random random)
{
for (int i = values.Count - 1; i > 0; i--)
{
int swapIndex = random.Next(0, i + 1);
(values[i], values[swapIndex]) = (values[swapIndex], values[i]);
}
}
private static string GetRarityText(RarityType rarity)
{
return InventoryRarityRuleService.NormalizeComponentRarity(rarity) switch
{
RarityType.White => "白色",
RarityType.Green => "绿色",
RarityType.Blue => "蓝色",
RarityType.Purple => "紫色",
RarityType.Red => "红色",
_ => "未知"
};
}
}
public sealed class EventOptionAvailability
{
private EventOptionAvailability(bool isSelectable, string blockedReason)
{
IsSelectable = isSelectable;
BlockedReason = blockedReason ?? string.Empty;
}
public bool IsSelectable { get; }
public string BlockedReason { get; }
public static EventOptionAvailability Selectable()
{
return new EventOptionAvailability(true, string.Empty);
}
public static EventOptionAvailability Blocked(string blockedReason)
{
return new EventOptionAvailability(false, blockedReason);
}
}
public sealed class EventOptionExecutionResult
{
private EventOptionExecutionResult(bool isAccepted, bool isProbabilitySuccess, string failureReason)
{
IsAccepted = isAccepted;
IsProbabilitySuccess = isProbabilitySuccess;
FailureReason = failureReason ?? string.Empty;
}
public bool IsAccepted { get; }
public bool IsProbabilitySuccess { get; }
public string FailureReason { get; }
public static EventOptionExecutionResult Accepted(bool isProbabilitySuccess)
{
return new EventOptionExecutionResult(true, isProbabilitySuccess, string.Empty);
}
public static EventOptionExecutionResult Rejected(string failureReason)
{
return new EventOptionExecutionResult(false, false, failureReason);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8e4d7125aa7b4cb9aa62cb0bf9301d5b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -53,6 +53,15 @@ namespace GeometryTD.Definition
configId);
}
public static InventoryTagRandomContext CreateEvent(int runSeed, int nodeSequenceIndex, int eventOrdinal, int configId)
{
return new InventoryTagRandomContext(
runSeed,
InventoryTagSourceType.Event,
ComposeLocalItemInstanceId(nodeSequenceIndex, eventOrdinal),
configId);
}
public static long ComposeLocalItemInstanceId(int nodeSequenceIndex, int localOrdinal)
{
long normalizedSequence = Math.Max(0, nodeSequenceIndex) + 1L;
@@ -31,6 +31,21 @@ namespace GeometryTD.Factory
case EventEffectType.AddRandomComps:
{
int count = GetInt(param, "Count");
bool hasMinRarity = TryGetString(param, "MinRarity", out string minRarityRaw);
bool hasMaxRarity = TryGetString(param, "MaxRarity", out string maxRarityRaw);
if (hasMinRarity || hasMaxRarity)
{
if (!hasMinRarity || !hasMaxRarity)
{
throw new System.InvalidOperationException(
"AddRandomComps requires both MinRarity and MaxRarity when using ranged rarity config.");
}
RarityType minRarity = EnumUtility<RarityType>.Get(minRarityRaw);
RarityType maxRarity = EnumUtility<RarityType>.Get(maxRarityRaw);
return new AddRandomCompsEffect(new AddRandomCompsParam(count, minRarity, maxRarity), probability);
}
RarityType rarity = EnumUtility<RarityType>.Get(GetString(param, "Rarity"));
return new AddRandomCompsEffect(new AddRandomCompsParam(count, rarity), probability);
}
@@ -73,5 +88,17 @@ namespace GeometryTD.Factory
return token.Value<string>();
}
private static bool TryGetString(JObject param, string key, out string value)
{
value = string.Empty;
if (param == null || !param.TryGetValue(key, out JToken token) || token.Type == JTokenType.Null)
{
return false;
}
value = token.Value<string>();
return !string.IsNullOrWhiteSpace(value);
}
}
}
@@ -4,5 +4,7 @@ namespace GeometryTD.UI
{
public int OptionIndex;
public string OptionText;
public bool IsSelectable;
public string BlockedReason;
}
}
@@ -4,6 +4,7 @@ namespace GeometryTD.UI
{
public class RepoFormContext : UIContext
{
public string GoldText;
public CombineAreaContext CombineAreaContext;
public CompAreaContext CompAreaContext;
public ParticipantAreaContext ParticipantAreaContext;
@@ -140,6 +140,7 @@ namespace GeometryTD.UI
return new RepoFormContext
{
GoldText = $"金币: {rawData.Inventory.Gold}",
CombineAreaContext = new CombineAreaContext(),
CompAreaContext = new CompAreaContext
{
@@ -322,6 +323,7 @@ namespace GeometryTD.UI
return;
}
Form.RefreshGoldText(latestContext.GoldText);
Form.RefreshParticipantArea(latestContext.ParticipantAreaContext);
ApplyParticipantSelection();
}
@@ -73,7 +73,7 @@ namespace GeometryTD.UI
private static EventFormContext BuildContext(EventFormRawData rawData)
{
if (rawData?.EventItem == null)
if (rawData == null)
{
return null;
}
@@ -82,24 +82,30 @@ namespace GeometryTD.UI
for (int i = 0; i < options.Length; i++)
{
string optionText = string.Empty;
if (rawData.EventItem.Options != null && i < rawData.EventItem.Options.Length &&
rawData.EventItem.Options[i] != null)
bool isSelectable = false;
string blockedReason = string.Empty;
if (rawData.OptionItems != null && i < rawData.OptionItems.Length &&
rawData.OptionItems[i] != null)
{
optionText = rawData.EventItem.Options[i].OptionText;
optionText = rawData.OptionItems[i].OptionText;
isSelectable = rawData.OptionItems[i].IsSelectable;
blockedReason = rawData.OptionItems[i].BlockedReason;
}
options[i] = new EventOptionItemContext
{
OptionIndex = i,
OptionText = optionText
OptionText = optionText,
IsSelectable = isSelectable,
BlockedReason = blockedReason
};
}
return new EventFormContext
{
EventId = rawData.EventItem.Id,
Title = rawData.EventItem.Title,
Description = rawData.EventItem.Description,
EventId = rawData.EventId,
Title = rawData.Title,
Description = rawData.Description,
OptionItems = options
};
}
@@ -117,7 +123,7 @@ namespace GeometryTD.UI
return;
}
m_UseCase.SelectOption(args.SelectedItemId);
m_UseCase.TrySelectOption(args.SelectedItemId);
}
}
}
}
@@ -245,8 +245,8 @@ namespace GeometryTD.UI
RepoFormContext latestContext = BuildContext(latestRawData);
if (latestContext != null)
{
Form.RefreshUI(latestContext);
ApplyParticipantSelection();
SetContext(latestContext);
RefreshCurrentUI();
}
}
@@ -1,9 +1,18 @@
using GeometryTD.Definition;
namespace GeometryTD.UI
{
public class EventFormRawData
{
public EventItem EventItem;
public int EventId;
public string Title;
public string Description;
public EventOptionRawData[] OptionItems;
}
public sealed class EventOptionRawData
{
public int OptionIndex;
public string OptionText;
public bool IsSelectable;
public string BlockedReason;
}
}
@@ -1,6 +1,5 @@
using GeometryTD.CustomComponent;
using GeometryTD.CustomEvent;
using GeometryTD.Definition;
using GeometryTD.Procedure;
using UnityGameFramework.Runtime;
namespace GeometryTD.UI
@@ -8,10 +7,21 @@ namespace GeometryTD.UI
public class EventFormUseCase : IUIUseCase
{
private EventItem _currentEvent;
private RunNodeExecutionContext _currentContext;
private readonly EventOptionExecutor _executor = new EventOptionExecutor();
public void SetCurrentEvent(EventItem eventItem)
public void BindEvent(
EventItem eventItem,
RunNodeExecutionContext context)
{
_currentEvent = eventItem;
_currentContext = context != null ? context.Clone() : null;
}
public void Clear()
{
_currentEvent = null;
_currentContext = null;
}
public EventFormRawData CreateInitialModel()
@@ -23,26 +33,74 @@ namespace GeometryTD.UI
return new EventFormRawData
{
EventItem = _currentEvent
EventId = _currentEvent.Id,
Title = _currentEvent.Title,
Description = _currentEvent.Description,
OptionItems = BuildOptionItems()
};
}
public EventOption SelectOption(int optionIndex)
public bool TrySelectOption(int optionIndex)
{
if (_currentEvent == null || _currentEvent.Options == null)
{
return null;
return false;
}
if (optionIndex < 0 || optionIndex >= _currentEvent.Options.Length)
{
Log.Warning("EventFormUseCase.SelectOption() option index is invalid: {0}", optionIndex);
return null;
Log.Warning("EventFormUseCase.TrySelectOption() option index is invalid: {0}", optionIndex);
return false;
}
BackpackInventoryData workingInventory = GameEntry.PlayerInventory != null
? GameEntry.PlayerInventory.GetInventorySnapshot()
: new BackpackInventoryData();
EventOptionExecutionResult executionResult = _executor.Execute(
_currentEvent,
optionIndex,
_currentContext,
workingInventory);
if (!executionResult.IsAccepted)
{
Log.Warning(
"EventFormUseCase.TrySelectOption() rejected. EventId={0}, OptionIndex={1}, Reason={2}",
_currentEvent.Id,
optionIndex,
executionResult.FailureReason);
return false;
}
if (GameEntry.PlayerInventory != null)
{
GameEntry.PlayerInventory.ReplaceInventorySnapshot(workingInventory);
}
// TODO: 执行 Requirements 校验、CostEffects/RewardEffects 结算与后续节点推进。
GameEntry.EventNode.EndEvent();
return _currentEvent.Options[optionIndex];
return true;
}
private EventOptionRawData[] BuildOptionItems()
{
EventOption[] options = _currentEvent.Options ?? System.Array.Empty<EventOption>();
EventOptionRawData[] result = new EventOptionRawData[options.Length];
BackpackInventoryData inventory = GameEntry.PlayerInventory != null
? GameEntry.PlayerInventory.GetInventorySnapshot()
: new BackpackInventoryData();
for (int i = 0; i < options.Length; i++)
{
EventOption option = options[i];
EventOptionAvailability availability = _executor.EvaluateOption(option, inventory);
result[i] = new EventOptionRawData
{
OptionIndex = i,
OptionText = option?.OptionText ?? string.Empty,
IsSelectable = availability.IsSelectable,
BlockedReason = availability.BlockedReason
};
}
return result;
}
}
}
}
@@ -7,8 +7,18 @@ namespace GeometryTD.UI
public class EventOptionItem : MonoBehaviour
{
[SerializeField] private TMP_Text _optionText;
private static readonly Color SelectableTextColor = Color.white;
private static readonly Color BlockedTextColor = new Color(0.75f, 0.75f, 0.75f, 1f);
private int _optionIndex;
private bool _isSelectable;
private CommonButton _commonButton;
private void Awake()
{
_commonButton = GetComponent<CommonButton>();
}
public void OnInit(EventOptionItemContext context)
{
@@ -20,9 +30,16 @@ namespace GeometryTD.UI
}
_optionIndex = context.OptionIndex;
_isSelectable = context.IsSelectable;
if (_optionText != null)
{
_optionText.text = context.OptionText ?? string.Empty;
_optionText.text = BuildDisplayText(context);
_optionText.color = _isSelectable ? SelectableTextColor : BlockedTextColor;
}
if (_commonButton != null)
{
_commonButton.Interactive = _isSelectable;
}
gameObject.SetActive(!string.IsNullOrEmpty(context.OptionText));
@@ -31,20 +48,38 @@ namespace GeometryTD.UI
public void OnReset()
{
_optionIndex = -1;
_isSelectable = false;
if (_optionText != null)
{
_optionText.text = string.Empty;
_optionText.color = SelectableTextColor;
}
if (_commonButton != null)
{
_commonButton.Interactive = true;
}
}
public void OnClick()
{
if (_optionIndex < 0)
if (_optionIndex < 0 || !_isSelectable)
{
return;
}
GameEntry.Event.Fire(this, EventOptionItemSelectedEventArgs.Create(_optionIndex));
}
private static string BuildDisplayText(EventOptionItemContext context)
{
string optionText = context.OptionText ?? string.Empty;
if (context.IsSelectable || string.IsNullOrEmpty(context.BlockedReason))
{
return optionText;
}
return $"{optionText}\n<size=75%>{context.BlockedReason}</size>";
}
}
}
@@ -1,4 +1,5 @@
using GeometryTD.CustomEvent;
using TMPro;
using UnityEngine;
using UnityGameFramework.Runtime;
@@ -12,6 +13,8 @@ namespace GeometryTD.UI
[SerializeField] private ParticipantArea _participantArea;
[SerializeField] private TMP_Text _goldText;
public void RefreshUI(RepoFormContext context)
{
if (context == null)
@@ -19,11 +22,21 @@ namespace GeometryTD.UI
return;
}
RefreshGoldText(context.GoldText);
_combineArea?.OnInit(context.CombineAreaContext);
_compArea?.OnInit(context.CompAreaContext);
_participantArea?.OnInit(context.ParticipantAreaContext);
}
public void RefreshGoldText(string goldText)
{
if (_goldText != null)
{
_goldText.text = goldText ?? string.Empty;
}
}
public bool TryAssignItemToCombineArea(RepoItemContext itemContext)
{
if (_combineArea == null)
@@ -69,6 +82,11 @@ namespace GeometryTD.UI
protected override void OnClose(bool isShutdown, object userData)
{
if (_goldText != null)
{
_goldText.text = string.Empty;
}
_combineArea?.OnReset();
_compArea?.OnReset();
_participantArea?.OnReset();