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(); 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 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 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 candidateTowerIds = new List(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 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 CollectLooseComponents( BackpackInventoryData inventory, RarityType rarity) { List result = new List(); 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( List destination, List 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(List 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(IList 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); } } }