using System; using BumpCombat.Defense; using UnityEngine; namespace BumpCombat.Constants { [Serializable] public sealed class Defense25DFacilityEffectStyle { [SerializeField, Min(.001f)] private float targetDurationSeconds = .6f; [SerializeField, Min(.001f)] private float groundScale = 1f; [SerializeField, Min(.001f)] private float verticalScale = 1f; [SerializeField] private float groundYOffset = .04f; [SerializeField, Min(0f)] private float verticalHeight = .7f; [SerializeField] private Color groundTint = Color.white; [SerializeField] private Color verticalTint = Color.white; public float TargetDurationSeconds => targetDurationSeconds; public float GroundScale => groundScale; public float VerticalScale => verticalScale; public float GroundYOffset => groundYOffset; public float VerticalHeight => verticalHeight; public Color GroundTint => groundTint; public Color VerticalTint => verticalTint; internal bool TryValidate(string name, out string error) { if (!IsFinite(targetDurationSeconds) || targetDurationSeconds <= 0f || !IsFinite(groundScale) || groundScale <= 0f || !IsFinite(verticalScale) || verticalScale <= 0f || !IsFinite(groundYOffset) || !IsFinite(verticalHeight) || verticalHeight < 0f || !IsFinite(groundTint) || !IsFinite(verticalTint)) { error = $"{name} has an invalid duration, layer transform, scale, or tint."; return false; } error = null; return true; } private static bool IsFinite(float value) => !float.IsNaN(value) && !float.IsInfinity(value); private static bool IsFinite(Color color) => IsFinite(color.r) && IsFinite(color.g) && IsFinite(color.b) && IsFinite(color.a); } [CreateAssetMenu(menuName = "BumpCombat/Constants/Defense 2.5D Facility Visuals")] public sealed class Defense25DFacilityVisualConstants : ScriptableObject { [SerializeField] private string impactWaveResourceKey = "Effects/Lightning/ImpactWave"; [SerializeField] private string meteorImpactResourceKey = "Effects/Fire/Meteor-Impact"; [SerializeField] private string imbuingResourceKey = "Effects/Support/Imbuing"; [SerializeField] private int groundSortingOrder = 13; [SerializeField] private int verticalSortingOrder = 14; [SerializeField] private Defense25DFacilityEffectStyle portalSpawn = new(); [SerializeField] private Defense25DFacilityEffectStyle portalBurst = new(); [SerializeField] private Defense25DFacilityEffectStyle homeSpawn = new(); [SerializeField] private Defense25DFacilityEffectStyle homeBurst = new(); [SerializeField] private Defense25DFacilityEffectStyle towerPadSpawn = new(); [SerializeField] private Defense25DFacilityEffectStyle towerUpgrade = new(); public string ImpactWaveResourceKey => impactWaveResourceKey; public string MeteorImpactResourceKey => meteorImpactResourceKey; public string ImbuingResourceKey => imbuingResourceKey; public int GroundSortingOrder => groundSortingOrder; public int VerticalSortingOrder => verticalSortingOrder; public Defense25DFacilityEffectStyle PortalSpawn => portalSpawn; public Defense25DFacilityEffectStyle PortalBurst => portalBurst; public Defense25DFacilityEffectStyle HomeSpawn => homeSpawn; public Defense25DFacilityEffectStyle HomeBurst => homeBurst; public Defense25DFacilityEffectStyle TowerPadSpawn => towerPadSpawn; public Defense25DFacilityEffectStyle TowerUpgrade => towerUpgrade; public bool TryValidate(out string error) { if (!ValidateResource(impactWaveResourceKey, "ImpactWave", out error) || !ValidateResource(meteorImpactResourceKey, "Meteor-Impact", out error) || !ValidateResource(imbuingResourceKey, "Imbuing", out error)) return false; if (!ValidateStyle(portalSpawn, nameof(portalSpawn), out error) || !ValidateStyle(portalBurst, nameof(portalBurst), out error) || !ValidateStyle(homeSpawn, nameof(homeSpawn), out error) || !ValidateStyle(homeBurst, nameof(homeBurst), out error) || !ValidateStyle(towerPadSpawn, nameof(towerPadSpawn), out error) || !ValidateStyle(towerUpgrade, nameof(towerUpgrade), out error)) return false; error = null; return true; } public bool TryGetPlaybackScale(string resourceKey, float targetDurationSeconds, out float durationScale, out string error) { durationScale = 1f; if (!IsFinite(targetDurationSeconds) || targetDurationSeconds <= 0f) { error = "A positive finite target duration is required."; return false; } int expectedCount = DefenseTowerArt.ExpectedFrameCount(resourceKey); Sprite[] frames = DefenseTowerArt.Load(resourceKey); if (expectedCount <= 0 || frames == null || frames.Length != expectedCount) { error = $"Resource '{resourceKey}' resolved {frames?.Length ?? 0} frames; expected {expectedCount}."; return false; } float sourceDuration = 0f; for (int i = 0; i < frames.Length; i++) sourceDuration += DefenseTowerArt.FrameSeconds(resourceKey, i); if (!IsFinite(sourceDuration) || sourceDuration <= 0f) { error = $"Resource '{resourceKey}' has no positive authored frame duration."; return false; } durationScale = targetDurationSeconds / sourceDuration; error = null; return true; } private static bool ValidateResource(string resourceKey, string displayName, out string error) { if (string.IsNullOrWhiteSpace(resourceKey)) { error = $"{displayName} resource key is required."; return false; } int expectedCount = DefenseTowerArt.ExpectedFrameCount(resourceKey); Sprite[] frames = DefenseTowerArt.Load(resourceKey); if (expectedCount <= 0 || frames == null || frames.Length != expectedCount) { error = $"{displayName} resource '{resourceKey}' resolved {frames?.Length ?? 0} frames; expected {expectedCount}."; return false; } error = null; return true; } private static bool ValidateStyle(Defense25DFacilityEffectStyle style, string name, out string error) { if (style == null) { error = $"{name} style is required."; return false; } return style.TryValidate(name, out error); } private static bool IsFinite(float value) => !float.IsNaN(value) && !float.IsInfinity(value); } }