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:
@@ -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
|
||||
Reference in New Issue
Block a user