using Unity.Collections; using Unity.Mathematics; namespace ProjectM.Simulation { /// /// Pure, deterministic Husk AI math — no RNG, no wall-clock — so server simulation stays reproducible and /// the helpers are EditMode-unit-testable without an ECS world (mirrors / /// StatMath). /// public static class EnemyAIMath { /// /// Planar (XZ) seek velocity from toward at /// . Y is forced to 0 (top-down plane). Returns zero once within /// (so the Husk halts at strike range instead of jittering through the /// target) or when the two points coincide. /// public static float3 SeekVelocity(float3 from, float3 to, float speed, float stopDistance) { float3 d = to - from; d.y = 0f; float distSq = math.lengthsq(d); if (distSq <= stopDistance * stopDistance || distSq < 1e-8f) return float3.zero; return math.normalize(d) * speed; } /// /// True when is within of on the /// XZ plane. /// public static bool InAttackRange(float3 from, float3 to, float range) { float3 d = to - from; d.y = 0f; return math.lengthsq(d) <= range * range; } /// /// Projects a planar movement onto a wall plane defined by /// (collide-and-slide): removes the component of that pushes into the surface so the /// mover glances along the wall instead of stopping dead. Both inputs are flattened to the XZ plane (top-down). /// Returns unchanged when the normal is degenerate. /// public static float3 SlideVelocity(float3 vel, float3 surfaceNormal) { surfaceNormal.y = 0f; float len = math.length(surfaceNormal); if (len < 1e-6f) return vel; float3 n = surfaceNormal / len; float3 slid = vel - math.dot(vel, n) * n; slid.y = 0f; return slid; } /// /// Deterministic planar ring position around for spawn /// : evenly spaced over angles at /// . Stable per index so a replayed spawn lands identically. /// public static float3 RingPosition(float3 center, int index, int slots, float radius) { if (slots < 1) slots = 1; int slot = ((index % slots) + slots) % slots; float angle = (2f * math.PI * slot) / slots; math.sincos(angle, out float s, out float c); return center + new float3(c * radius, 0f, s * radius); } /// /// EB-1 fortress aggro: pick a Husk's target as the weighted-nearest of the living players (weight 1) and /// the live structures (a SQUARED applied to structure distance, so <1 /// makes structures preferred while a sufficiently-closer player 'in the way' still wins). Planar XZ, /// deterministic (no RNG/wall-clock). Sets = -1 when there are no targets. Pure so /// both the Grunt and Charger passes select IDENTICALLY and it is EditMode-unit-testable. /// public static void PickWeightedNearest(float3 from, NativeList playerPositions, NativeList structurePositions, float structureWeight, out bool isStructure, out int index) { isStructure = false; index = -1; float bestSq = float.MaxValue; for (int i = 0; i < playerPositions.Length; i++) { float sq = math.lengthsq(playerPositions[i].xz - from.xz); if (sq < bestSq) { bestSq = sq; index = i; isStructure = false; } } float w = math.max(0f, structureWeight); float wsq = w * w; // applied to SQUARED distance so the weight scales true distance for (int i = 0; i < structurePositions.Length; i++) { float sq = math.lengthsq(structurePositions[i].xz - from.xz) * wsq; if (sq < bestSq) { bestSq = sq; index = i; isStructure = true; } } } } }