geometry-tower-defense/Assets/GameMain/Scripts/Definition/Event/EventOptionExecutor.cs

491 lines
18 KiB
C#

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);
}
}
}