补全部分 EventComponent 设计
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user