491 lines
18 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|