Import art/VFX asset packs + game-feel systems; normalize texture extensions to lowercase for LFS

Add BefourStudios SciFi environment packs, Gabriel Aguiar VFX, and the
ShaderCrew Toon Shader embedded packages, plus combat/enemy/wave/death
gameplay systems and supporting vault docs/screenshots.

Rename 11 vendor textures from uppercase .PNG/.HDR to lowercase so the
case-sensitive Git LFS filters (*.png/*.hdr) match on case-sensitive
filesystems (Linux CI, case-sensitive macOS), not just locally where
core.ignorecase=true masks the gap. Each .meta moved with its asset so
GUID references are preserved. All ~1000 binaries tracked via LFS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 22:50:43 -07:00
parent dd0064c377
commit e362aaeb43
4830 changed files with 1293057 additions and 38 deletions
@@ -68,7 +68,7 @@ namespace ProjectM.Simulation
if (isServer)
{
foreach (var dummyTransform in
SystemAPI.Query<RefRO<LocalTransform>>().WithAll<TrainingDummyTag>())
SystemAPI.Query<RefRO<LocalTransform>>().WithAny<TrainingDummyTag, EnemyTag>())
{
candidatePositions.Add(dummyTransform.ValueRO.Position);
}
@@ -81,7 +81,7 @@ namespace ProjectM.Simulation
SystemAPI.Query<RefRO<PlayerInput>, RefRO<PlayerFacing>, RefRO<LocalTransform>,
RefRO<EffectiveAbilityStats>, RefRO<AbilityRef>, RefRW<AbilityCooldown>,
RefRO<GhostOwner>>()
.WithAll<Simulate>()
.WithAll<Simulate>().WithDisabled<Dead>()
.WithEntityAccess())
{
// The InputEvent on the component carries the per-tick delta: set => fired this tick.
@@ -162,7 +162,7 @@ namespace ProjectM.Simulation
// Earliest raw tick the player may fire again. Clamp cooldown to >= 1 tick.
uint cooldownTicks = (uint)math.max(1, eff.ValueRO.CooldownTicks);
cd.ValueRW.NextFireTick = serverTick.TickIndexForValidTick + cooldownTicks;
cd.ValueRW.NextFireTick = TickUtil.NonZero(serverTick.TickIndexForValidTick + cooldownTicks);
}
ecb.Playback(state.EntityManager);
@@ -0,0 +1,54 @@
using Unity.Mathematics;
namespace ProjectM.Simulation
{
/// <summary>
/// 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 <see cref="PlayerSpawnMath"/> /
/// <c>StatMath</c>).
/// </summary>
public static class EnemyAIMath
{
/// <summary>
/// Planar (XZ) seek velocity from <paramref name="from"/> toward <paramref name="to"/> at
/// <paramref name="speed"/>. Y is forced to 0 (top-down plane). Returns zero once within
/// <paramref name="stopDistance"/> (so the Husk halts at strike range instead of jittering through the
/// target) or when the two points coincide.
/// </summary>
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;
}
/// <summary>
/// True when <paramref name="to"/> is within <paramref name="range"/> of <paramref name="from"/> on the
/// XZ plane.
/// </summary>
public static bool InAttackRange(float3 from, float3 to, float range)
{
float3 d = to - from;
d.y = 0f;
return math.lengthsq(d) <= range * range;
}
/// <summary>
/// Deterministic planar ring position around <paramref name="center"/> for spawn
/// <paramref name="index"/>: evenly spaced over <paramref name="slots"/> angles at
/// <paramref name="radius"/>. Stable per index so a replayed spawn lands identically.
/// </summary>
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);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7db18ae1e3adab1448a9769b002fb9cd
@@ -0,0 +1,43 @@
using Unity.Entities;
namespace ProjectM.Simulation
{
/// <summary>
/// Marks a Husk enemy: a server-simulated, OWNERLESS INTERPOLATED ghost that seeks the nearest player
/// and strikes on contact. Damageable like the training dummy (Health/HitRadius/DamageEvent), but unlike
/// the dummy it IS a replicated ghost, so every client sees it. Movement is server-authoritative — written
/// to LocalTransform in the plain <c>SimulationSystemGroup</c> (interpolated ghosts are NOT predicted) and
/// replicated via the stock LocalTransform default variant (no hand-written <c>[GhostField]</c>). Spawned by
/// the <see cref="WaveDirector"/> (round-robin over Husk variants).
/// </summary>
public struct EnemyTag : IComponentData { }
/// <summary>
/// Baked Husk tunables — identical on both worlds, not replicated (only server systems read them). Different
/// Husk variants (Grunt / Swarmer / Brute) are just different baked values of this component.
/// </summary>
public struct EnemyStats : IComponentData
{
/// <summary>Planar seek speed toward the target, world units/second.</summary>
public float MoveSpeed;
/// <summary>Centre-to-centre distance at which the Husk can strike a player.</summary>
public float AttackRange;
/// <summary>Damage dealt per strike.</summary>
public float AttackDamage;
/// <summary>Simulation ticks between strikes.</summary>
public int AttackCooldownTicks;
}
/// <summary>
/// Server-only per-Husk attack gate. Raw tick value of the earliest tick it may strike again; <c>0</c> =
/// ready. Compared by wrapping into a <see cref="Unity.NetCode.NetworkTick"/> (raw subtraction is unsafe
/// across tick wraparound), mirroring <see cref="AbilityCooldown"/>. Not replicated.
/// </summary>
public struct EnemyAttackCooldown : IComponentData
{
public uint NextAttackTick;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d06ea439174f0cd45b95e9a6e02c14bf
@@ -0,0 +1,17 @@
namespace ProjectM.Simulation
{
/// <summary>
/// Shared tick helpers for the project-wide convention that a stored raw "next tick" value of <c>0</c> means
/// "ready / nothing pending". A computed absolute tick (<c>ServerTick + delay</c>) can legitimately equal 0 at
/// <see cref="uint"/> wraparound, which would be misread as "ready"; <see cref="NonZero"/> coerces it to 1 (a
/// 1-tick error only at the single wrap instant). Mirrors the guard already inlined in
/// <see cref="RespawnMath.RespawnTick"/>, applied consistently at every cooldown/spawn "next tick" write
/// (<c>AbilityCooldown.NextFireTick</c>, <c>EnemyAttackCooldown.NextAttackTick</c>,
/// <c>EnemySpawner.NextSpawnTick</c>).
/// </summary>
public static class TickUtil
{
/// <summary>Coerce a computed raw tick of 0 to 1 so it never collides with the "0 = ready" sentinel.</summary>
public static uint NonZero(uint tick) => tick == 0u ? 1u : tick;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3cea93829504a584fabb291b2d559f2f
@@ -0,0 +1,47 @@
using Unity.Entities;
namespace ProjectM.Simulation
{
/// <summary>
/// Baked config for the Husk wave/threat director (singleton). The director escalates: wave N spawns
/// <c>BaseCount + (N-1)*CountPerWave</c> Husks (round-robin over the <see cref="WaveEnemyPrefab"/> pool) at a
/// deterministic ring around the <see cref="BaseAnchor"/>, one every <c>SpawnIntervalTicks</c>; once a wave is
/// cleared the field stays calm for <c>LullTicks</c> before the next, bigger wave. Replaces the flat sustain.
/// </summary>
public struct WaveDirector : IComponentData
{
public float RingRadius;
public int RingSlots;
public int BaseCount;
public int CountPerWave;
public int SpawnIntervalTicks;
public int LullTicks;
}
/// <summary>Baked pool of Husk prefab variants the director draws from round-robin (Grunt / Swarmer / Brute / ...).</summary>
public struct WaveEnemyPrefab : IBufferElementData
{
public Entity Prefab;
}
/// <summary>Phase constants for <see cref="WaveState.Phase"/>.</summary>
public static class WavePhase
{
public const byte Lull = 0;
public const byte Spawning = 1;
}
/// <summary>
/// Runtime state of the wave director (server-only singleton; not replicated). Tracks the current wave, the
/// phase (lull vs spawning), the next action tick, how many Husks remain to spawn this wave, and a monotonic
/// spawn counter (drives the ring slot + the round-robin prefab pick).
/// </summary>
public struct WaveState : IComponentData
{
public int WaveNumber;
public byte Phase;
public uint NextActionTick;
public int RemainingToSpawn;
public int SpawnCounter;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1999bc9f9b3f2ac4da59d1e8f51849be