using System; using System.Collections.Generic; using System.Globalization; using BumpCombat.Defense; using UnityEngine; namespace BumpCombat.Constants { public enum Defense25DRewardSource { LevelUp = 0, GlobalElite = 1, GlobalMidBoss = 2, PersonalMissionRelic = 3, } public enum Defense25DRewardTargetKind { PlayerModifier = 0, TowerModifier = 1, PersonalMissionRelic = 2, Currency = 3, } public enum Defense25DRewardRarity { None = 0, Common = 1, Rare = 2, Epic = 3, } public enum Defense25DTowerModifierEffect { AttackDamageIncreased = 0, AreaRadiusIncreased = 1, ChainCount = 2, AdditionalProjectile = 3, AdditionalTarget = 4, SplitProjectile = 5, ReturnProjectile = 6, } [Serializable] public struct Defense25DRewardRarityWeights { [Min(0)] public int Common; [Min(0)] public int Rare; [Min(0)] public int Epic; public int Total => Common + Rare + Epic; } [Serializable] public sealed class Defense25DTowerModifierDefinition { public string DefinitionId; public string DisplayName; [TextArea] public string Description; public DefenseTowerKind Family; public Defense25DTowerModifierEffect Effect; public Defense25DRewardRarity Rarity; [Min(0f)] public float Value; [Min(1)] public int MaximumStacks = 1; } [CreateAssetMenu(menuName = "BumpCombat/Constants/Defense 2.5D Rewards")] public sealed class Defense25DRewardConstants : ScriptableObject { [Min(0)] public int RunSeed = 1; [Min(1)] public int ChoiceCount = 3; public Defense25DRewardRarityWeights LevelUpRarityWeights = new() { Common = 75, Rare = 20, Epic = 5 }; public Defense25DRewardRarityWeights GlobalEliteRarityWeights = new() { Common = 60, Rare = 30, Epic = 10 }; [Min(0f)] public float PlayerRareEffectMultiplier = 1.5f; [Min(0f)] public float PlayerEpicEffectMultiplier = 2f; [Min(0)] public int EmptyEpicFallbackCurrency = 10; public Defense25DTowerModifierDefinition[] TowerModifierCatalog = CreateFirstTowerModifierCatalog(); private const string ChoiceConfirmationText = "선택하면 즉시 적용됩니다."; public bool Validate(out string error) { if (ChoiceCount < 1 || !IsFinite(PlayerRareEffectMultiplier) || PlayerRareEffectMultiplier <= 0f || !IsFinite(PlayerEpicEffectMultiplier) || PlayerEpicEffectMultiplier <= 0f || EmptyEpicFallbackCurrency < 0) { error = "Reward choice count, player rarity multipliers, or currency fallback is invalid."; return false; } if (!ValidWeights(LevelUpRarityWeights) || !ValidWeights(GlobalEliteRarityWeights)) { error = "Level-up and Global Elite rarity weights must be non-negative and total 100."; return false; } if (TowerModifierCatalog == null) { error = "Tower modifier catalog cannot be null."; return false; } HashSet definitionIds = new(StringComparer.Ordinal); Dictionary stackCaps = new(StringComparer.Ordinal); foreach (Defense25DTowerModifierDefinition definition in TowerModifierCatalog) { if (definition == null || string.IsNullOrWhiteSpace(definition.DefinitionId) || string.IsNullOrWhiteSpace(definition.DisplayName) || !IsFinite(definition.Value) || definition.Value <= 0f || definition.MaximumStacks < 1 || !definitionIds.Add(definition.DefinitionId)) { error = "Tower modifier entries need unique IDs, display names, positive finite values, and positive stack caps."; return false; } if (!IsValidFamilyEffect(definition)) { error = $"Tower modifier '{definition.DefinitionId}' has an unsupported family/effect/rarity combination."; return false; } string stackKey = StackKey(definition.Family, definition.Effect); if (stackCaps.TryGetValue(stackKey, out int existingCap) && existingCap != definition.MaximumStacks) { error = $"Tower modifier variants in '{stackKey}' must share one stack cap."; return false; } stackCaps[stackKey] = definition.MaximumStacks; } error = string.Empty; return true; } public static string StackKey( DefenseTowerKind family, Defense25DTowerModifierEffect effect) { return $"{(int)family}:{(int)effect}"; } private static bool ValidWeights(Defense25DRewardRarityWeights weights) { return weights.Common >= 0 && weights.Rare >= 0 && weights.Epic >= 0 && weights.Total == 100; } private static bool IsValidFamilyEffect( Defense25DTowerModifierDefinition definition) { bool tieredRarity = definition.Rarity is Defense25DRewardRarity.Common or Defense25DRewardRarity.Rare or Defense25DRewardRarity.Epic; bool isEpic = definition.Rarity == Defense25DRewardRarity.Epic; return definition.Effect switch { Defense25DTowerModifierEffect.AttackDamageIncreased => definition.Family != DefenseTowerKind.Empowering && tieredRarity, Defense25DTowerModifierEffect.AreaRadiusIncreased => definition.Family is DefenseTowerKind.Direct or DefenseTowerKind.Summon or DefenseTowerKind.Cold or DefenseTowerKind.Seismic or DefenseTowerKind.Empowering && tieredRarity, Defense25DTowerModifierEffect.ChainCount => definition.Family == DefenseTowerKind.Lightning && definition.Rarity == Defense25DRewardRarity.Rare, Defense25DTowerModifierEffect.AdditionalProjectile => definition.Family == DefenseTowerKind.Cold && isEpic, Defense25DTowerModifierEffect.AdditionalTarget => definition.Family == DefenseTowerKind.Direct && isEpic, Defense25DTowerModifierEffect.SplitProjectile or Defense25DTowerModifierEffect.ReturnProjectile => definition.Family == DefenseTowerKind.Cold && isEpic, _ => false, }; } private static Defense25DTowerModifierDefinition[] CreateFirstTowerModifierCatalog() { List definitions = new(); AddTiered(definitions, DefenseTowerKind.Direct, "fire", true); AddTiered(definitions, DefenseTowerKind.Summon, "summon", false); AddTiered(definitions, DefenseTowerKind.Lightning, "lightning", false); AddTiered(definitions, DefenseTowerKind.Cold, "cold", true); AddTiered(definitions, DefenseTowerKind.Seismic, "seismic", true); AddTiered(definitions, DefenseTowerKind.Empowering, "empowering", true, includeAttack: false); definitions.Add(Definition("lightning.chain.rare", "번개 연쇄 강화", $"연쇄 대상 +1명. {ChoiceConfirmationText}", DefenseTowerKind.Lightning, Defense25DTowerModifierEffect.ChainCount, Defense25DRewardRarity.Rare, 1f, 3)); definitions.Add(Definition("cold.projectile.epic", "냉기 투사체 추가 발사", $"추가 투사체 +1발. {ChoiceConfirmationText}", DefenseTowerKind.Cold, Defense25DTowerModifierEffect.AdditionalProjectile, Defense25DRewardRarity.Epic, 1f, 2)); definitions.Add(Definition("fire.targets.epic", "화염 동시 타격 강화", $"동시 타격 대상 +1개체. {ChoiceConfirmationText}", DefenseTowerKind.Direct, Defense25DTowerModifierEffect.AdditionalTarget, Defense25DRewardRarity.Epic, 1f, 1)); definitions.Add(Definition("cold.split.epic", "냉기 투사체 분할", $"원본 최초 유효 적중 시 ±25° 방향으로 자식 투사체 2개를 발사합니다. 자식은 재분할하거나 복귀하지 않으며, 각 자식 피해는 원본 피해의 50%입니다. {ChoiceConfirmationText}", DefenseTowerKind.Cold, Defense25DTowerModifierEffect.SplitProjectile, Defense25DRewardRarity.Epic, 1f, 1)); definitions.Add(Definition("cold.return.epic", "냉기 투사체 복귀", $"원본 비행 종료 후 발사 위치로 1회 복귀합니다. 복귀 피해는 원본의 50%입니다. {ChoiceConfirmationText}", DefenseTowerKind.Cold, Defense25DTowerModifierEffect.ReturnProjectile, Defense25DRewardRarity.Epic, 1f, 1)); return definitions.ToArray(); } private static void AddTiered( List definitions, DefenseTowerKind family, string familyId, bool includeArea, bool includeAttack = true) { string familyName = GetTowerFamilyDisplayName(family); if (includeAttack) { definitions.Add(Definition($"{familyId}.attack.common", $"{familyName} 공격 강화", $"공격 피해 +{FormatPercent(.10f)}% 증가. {ChoiceConfirmationText}", family, Defense25DTowerModifierEffect.AttackDamageIncreased, Defense25DRewardRarity.Common, .10f, 5)); definitions.Add(Definition($"{familyId}.attack.rare", $"상급 {familyName} 공격 강화", $"공격 피해 +{FormatPercent(.20f)}% 증가. {ChoiceConfirmationText}", family, Defense25DTowerModifierEffect.AttackDamageIncreased, Defense25DRewardRarity.Rare, .20f, 5)); definitions.Add(Definition($"{familyId}.attack.epic", $"최상급 {familyName} 공격 강화", $"공격 피해 +{FormatPercent(.35f)}% 증가. {ChoiceConfirmationText}", family, Defense25DTowerModifierEffect.AttackDamageIncreased, Defense25DRewardRarity.Epic, .35f, 5)); } if (includeArea) { definitions.Add(Definition($"{familyId}.area.common", $"{familyName} 범위 강화", $"효과 반경 +{FormatPercent(.10f)}% 증가. {ChoiceConfirmationText}", family, Defense25DTowerModifierEffect.AreaRadiusIncreased, Defense25DRewardRarity.Common, .10f, 3)); definitions.Add(Definition($"{familyId}.area.rare", $"상급 {familyName} 범위 강화", $"효과 반경 +{FormatPercent(.15f)}% 증가. {ChoiceConfirmationText}", family, Defense25DTowerModifierEffect.AreaRadiusIncreased, Defense25DRewardRarity.Rare, .15f, 3)); definitions.Add(Definition($"{familyId}.area.epic", $"최상급 {familyName} 범위 강화", $"효과 반경 +{FormatPercent(.25f)}% 증가. {ChoiceConfirmationText}", family, Defense25DTowerModifierEffect.AreaRadiusIncreased, Defense25DRewardRarity.Epic, .25f, 3)); } } private static string GetTowerFamilyDisplayName(DefenseTowerKind family) { return family switch { DefenseTowerKind.Direct => "화염 포탑", DefenseTowerKind.Summon => "소환 포탑", DefenseTowerKind.Lightning => "번개 포탑", DefenseTowerKind.Cold => "냉기 포탑", DefenseTowerKind.Seismic => "지진 포탑", DefenseTowerKind.Empowering => "지원 포탑", _ => "포탑", }; } private static string FormatPercent(float value) { return (value * 100f).ToString("0.##", CultureInfo.InvariantCulture); } private static Defense25DTowerModifierDefinition Definition( string id, string displayName, string description, DefenseTowerKind family, Defense25DTowerModifierEffect effect, Defense25DRewardRarity rarity, float value, int maximumStacks) { return new Defense25DTowerModifierDefinition { DefinitionId = id, DisplayName = displayName, Description = description, Family = family, Effect = effect, Rarity = rarity, Value = value, MaximumStacks = maximumStacks, }; } private static bool IsFinite(float value) { return !float.IsNaN(value) && !float.IsInfinity(value); } } }