2.5 update
This commit is contained in:
@@ -0,0 +1,352 @@
|
||||
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<Vector2> 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<Vector2> 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<int> 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<int> ids = new();
|
||||
HashSet<int> 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user