using System; using System.Collections.Generic; using BumpCombat.Defense.Comparison; using BumpCombat.Enemies; using UnityEngine; namespace BumpCombat.Constants { [Serializable] public sealed class Defense25DBattlefieldDefinition { public string Id = "open-field"; public Rect Bounds = new(-40f, -40f, 80f, 80f); public Vector2[] HomeCandidates = CreateHomeCandidates(); [Min(0f)] public float MinBranchClearance = 28f; public float[] BranchRotationCandidates = { 0f, 60f, 120f, 180f, 240f, 300f }; public Defense25DLocalRoute[] LocalRoutes = CreateLocalRoutes(); public Vector2[] LocalPads = CreateLocalPads(); public static Defense25DBattlefieldDefinition CreateDefault() => new(); public bool TryCreateLayout(int layoutSeed, out Defense25DRunLayout layout, out string error) { layout = null; if (Bounds.width <= 0f || Bounds.height <= 0f) { error = $"Battlefield '{Id}' has invalid bounds."; return false; } if (HomeCandidates == null || HomeCandidates.Length == 0 || BranchRotationCandidates == null || BranchRotationCandidates.Length == 0 || LocalRoutes == null || LocalRoutes.Length != 12 || LocalPads == null || LocalPads.Length != 18) { error = $"Battlefield '{Id}' must define home/rotation candidates, 12 routes and 18 pads."; return false; } List validHomes = new(); foreach (Vector2 candidate in HomeCandidates) { if (candidate == Vector2.zero || !ContainsClearance(candidate)) continue; if (!AllLayoutPointsFit(candidate, 0f)) continue; validHomes.Add(candidate); } if (validHomes.Count == 0) { error = $"Battlefield '{Id}' has no home candidate with {MinBranchClearance:0.##} units of branch clearance."; return false; } Defense25DDeterministicRandom random = new(layoutSeed, 0x4C41594F); Vector2 home = validHomes[random.NextInt(validHomes.Count)]; float rotation = BranchRotationCandidates[random.NextInt(BranchRotationCandidates.Length)]; if (!AllLayoutPointsFit(home, rotation)) { error = $"Battlefield '{Id}' selected a layout point outside its bounds."; return false; } layout = Defense25DRunLayout.Create(this, layoutSeed, home, rotation); error = string.Empty; return true; } private bool ContainsClearance(Vector2 home) { float clearance = Mathf.Max(0f, MinBranchClearance); return home.x - Bounds.xMin >= clearance && Bounds.xMax - home.x >= clearance && home.y - Bounds.yMin >= clearance && Bounds.yMax - home.y >= clearance; } private bool AllLayoutPointsFit(Vector2 home, float rotation) { foreach (Defense25DLocalRoute route in LocalRoutes) { if (route.Points == null || route.Points.Length < 2) return false; foreach (Vector2 point in route.Points) if (!Bounds.Contains(home + Rotate(point, rotation))) return false; } foreach (Vector2 pad in LocalPads) if (!Bounds.Contains(home + Rotate(pad, rotation))) return false; return Bounds.Contains(home + Rotate(new Vector2(0f, -2f), rotation)); } internal static Vector2 Rotate(Vector2 value, float degrees) { float radians = degrees * Mathf.Deg2Rad; float sine = Mathf.Sin(radians); float cosine = Mathf.Cos(radians); return new Vector2(value.x * cosine - value.y * sine, value.x * sine + value.y * cosine); } private static Vector2[] CreateHomeCandidates() { List candidates = new(120); for (int x = -10; x <= 10; x += 2) for (int z = -10; z <= 10; z += 2) { Vector2 candidate = new(x, z); if (candidate != Vector2.zero) candidates.Add(candidate); } return candidates.ToArray(); } private static Defense25DLocalRoute[] CreateLocalRoutes() { int[] branchOrder = { 0, 3, 1, 4, 2, 5 }; Defense25DLocalRoute[] routes = new Defense25DLocalRoute[12]; for (int portalId = 0; portalId < routes.Length; portalId++) { int branch = branchOrder[portalId % 6]; float angle = branch * 60f; float portalAngle = angle + (portalId < 6 ? -12f : 12f); Vector2 endpoint = Direction(portalAngle) * 24f; Vector2 branchOuter = Direction(angle) * 16f; Vector2 branchInner = Direction(angle) * 8f; routes[portalId] = new Defense25DLocalRoute(portalId, new[] { endpoint, branchOuter, branchInner, Vector2.zero }); } return routes; } private static Vector2[] CreateLocalPads() { Vector2[] pads = new Vector2[18]; for (int branch = 0; branch < 6; branch++) { float angle = branch * 60f; int offset = branch * 3; pads[offset] = Direction(angle - 20f) * 12f; pads[offset + 1] = Direction(angle + 20f) * 12f; pads[offset + 2] = Direction(angle + 30f) * 6f; } return pads; } private static Vector2 Direction(float degrees) { float radians = degrees * Mathf.Deg2Rad; return new Vector2(Mathf.Cos(radians), Mathf.Sin(radians)); } } [Serializable] public struct Defense25DLocalRoute { public int PortalId; public Vector2[] Points; public Defense25DLocalRoute(int portalId, Vector2[] points) { PortalId = portalId; Points = points; } } public enum Defense25DTowerKindId { None = 0, Fire = 1, Summon = 2, Lightning = 3, Cold = 4, Seismic = 5, } [Serializable] public struct Defense25DPortalDefinition { public int PortalId; [Min(0f)] public float OpenAt; [Min(0f)] public float TelegraphSeconds; public int RouteId; public bool HasDamageImmunity; public Defense25DTowerKindId ImmuneTowerKindId; public static Defense25DPortalDefinition Create(int portalId, float openAt, float telegraphSeconds, int routeId, bool hasDamageImmunity, Defense25DTowerKindId immuneTowerKindId) => new() { PortalId = portalId, OpenAt = openAt, TelegraphSeconds = telegraphSeconds, RouteId = routeId, HasDamageImmunity = hasDamageImmunity, ImmuneTowerKindId = immuneTowerKindId, }; } [CreateAssetMenu(menuName = "BumpCombat/Defense/2.5D Long Defense Run")] public sealed class Defense25DRunDefinition : ScriptableObject { public string ProfileId = "LongDefense-v2"; [Min(0f)] public float PreparationSeconds = 20f; [Min(0f)] public float FinalBossAtSeconds = 720f; public Defense25DBattlefieldDefinition[] BattlefieldDefinitions = { Defense25DBattlefieldDefinition.CreateDefault() }; public Defense25DPortalDefinition[] PortalDefinitions = CreateDefaultPortals(); public Defense25DWaveDirectorConstants WaveDirectorConstants; public Defense25DMonsterPoolDefinition MonsterPoolDefinition; public Defense25DMonsterSourceMotionCatalog MonsterSourceMotionCatalog; public int[] PersonalMissionRequiredPortalCounts = { 3, 6, 3 }; public Defense25DArtifactDropConstants ArtifactDropConstants; public Defense25DRewardConstants RewardConstants; public Defense25DFacilityVisualConstants FacilityVisualConstants; public EnemyDefinition GlobalEliteDefinition; public EnemyDefinition GlobalMidBossDefinition; public EnemyDefinition FinalBossDefinition; public GameObject FinalBossVisualPrefab; public bool Validate(out string error) { if (string.IsNullOrWhiteSpace(ProfileId) || !IsFinite(PreparationSeconds) || PreparationSeconds < 0f || !IsFinite(FinalBossAtSeconds) || FinalBossAtSeconds < 0f || BattlefieldDefinitions == null || BattlefieldDefinitions.Length == 0 || WaveDirectorConstants == null || MonsterPoolDefinition == null || MonsterSourceMotionCatalog == null || ArtifactDropConstants == null || RewardConstants == null || FacilityVisualConstants == null || GlobalEliteDefinition == null || GlobalMidBossDefinition == null || FinalBossDefinition == null || FinalBossVisualPrefab == null) { error = "The long-defense profile, preparation/final time, battlefield, wave constants, monster pool, source-motion catalog, special enemy definitions, final-boss visual source, and artifact/reward constants are required."; return false; } if (!WaveDirectorConstants.Validate(out error)) return false; if (!MonsterPoolDefinition.TryValidate(out error)) return false; if (!MonsterSourceMotionCatalog.TryValidateAgainst(MonsterPoolDefinition, out error)) return false; if (!ArtifactDropConstants.Validate(out error)) return false; if (!RewardConstants.Validate(out error)) return false; if (!FacilityVisualConstants.TryValidate(out error)) return false; if (!ValidatePortals(PortalDefinitions, FinalBossAtSeconds, BattlefieldDefinitions[0]?.LocalRoutes?.Length ?? 0, PersonalMissionRequiredPortalCounts, out error)) return false; foreach (Defense25DBattlefieldDefinition battlefield in BattlefieldDefinitions) { if (battlefield == null || !battlefield.TryCreateLayout(0, out _, out error)) return false; } error = string.Empty; return true; } public static bool ValidatePortals(Defense25DPortalDefinition[] portals, float finalBossAtSeconds, int routeCount, IReadOnlyList missionPortalCounts, out string error) { if (!IsFinite(finalBossAtSeconds) || finalBossAtSeconds < 0f || portals == null || portals.Length == 0 || routeCount <= 0) { error = "Portal definitions and their route definitions are required."; return false; } if (portals.Length != routeCount) { error = $"Every portal must own exactly one route ({portals.Length} portals, {routeCount} routes)."; return false; } HashSet ids = new(); HashSet routeIds = new(); bool opensForFinalBoss = false; foreach (Defense25DPortalDefinition portal in portals) { if (!ids.Add(portal.PortalId) || portal.PortalId < 0 || !IsFinite(portal.OpenAt) || portal.OpenAt < 0f || !IsFinite(portal.TelegraphSeconds) || portal.TelegraphSeconds < 0f || portal.RouteId < 0 || portal.RouteId >= routeCount || !Enum.IsDefined(typeof(Defense25DTowerKindId), portal.ImmuneTowerKindId) || (portal.HasDamageImmunity == (portal.ImmuneTowerKindId == Defense25DTowerKindId.None))) { error = $"Portal {portal.PortalId} has a duplicate ID or invalid route, time or immunity kind."; return false; } if (!routeIds.Add(portal.RouteId)) { error = $"Portal route {portal.RouteId} is assigned more than once."; return false; } opensForFinalBoss |= portal.OpenAt <= finalBossAtSeconds; } if (!opensForFinalBoss) { error = "At least one portal must be open when the final boss is scheduled."; return false; } if (missionPortalCounts != null) foreach (int required in missionPortalCounts) if (required < 0 || required > portals.Length) { error = $"A mission requires {required} portals, but only {portals.Length} are defined."; return false; } error = string.Empty; return true; } public static Defense25DPortalDefinition[] CreateDefaultPortals() { float[] openTimes = { 0f, 0f, 120f, 120f, 240f, 240f, 360f, 360f, 480f, 480f, 600f, 600f }; Defense25DTowerKindId[] immunityOrder = { Defense25DTowerKindId.Fire, Defense25DTowerKindId.Summon, Defense25DTowerKindId.Lightning, Defense25DTowerKindId.Cold, Defense25DTowerKindId.Seismic, Defense25DTowerKindId.None, Defense25DTowerKindId.Cold, Defense25DTowerKindId.Lightning, Defense25DTowerKindId.Summon, Defense25DTowerKindId.Fire, Defense25DTowerKindId.None, Defense25DTowerKindId.Seismic, }; Defense25DPortalDefinition[] portals = new Defense25DPortalDefinition[openTimes.Length]; for (int id = 0; id < portals.Length; id++) portals[id] = Defense25DPortalDefinition.Create(id, openTimes[id], 10f, id, immunityOrder[id] != Defense25DTowerKindId.None, immunityOrder[id]); return portals; } private static bool IsFinite(float value) => !float.IsNaN(value) && !float.IsInfinity(value); } internal sealed class Defense25DDeterministicRandom { private uint state; public Defense25DDeterministicRandom(int seed, uint stream) { uint value = unchecked((uint)seed) ^ (stream * 0x9E3779B9u) ^ 0xA511E9B3u; value ^= value >> 16; value *= 0x7FEB352Du; value ^= value >> 15; value *= 0x846CA68Bu; value ^= value >> 16; state = value == 0 ? 0x6D2B79F5u : value; } public uint NextUInt() { uint value = state; value ^= value << 13; value ^= value >> 17; value ^= value << 5; state = value; return value; } public int NextInt(int exclusiveMaximum) { if (exclusiveMaximum <= 0) throw new ArgumentOutOfRangeException(nameof(exclusiveMaximum)); return (int)(NextUInt() % (uint)exclusiveMaximum); } public float NextFloat() => (NextUInt() >> 8) * (1f / 16777216f); public float Range(float minimum, float maximum) => minimum + (maximum - minimum) * NextFloat(); } }