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
@@ -0,0 +1,42 @@
using Unity.Entities;
using Unity.NetCode;
namespace ProjectM.Simulation
{
/// <summary>
/// Enableable, LOCAL (not replicated) "is dead" gate for a player. Derived every predicted tick from the
/// replicated <see cref="Health"/> by <see cref="PlayerDeathStateSystem"/> (Dead == Health.Current &lt;= 0) —
/// exactly the derive-don't-replicate idiom of <see cref="EffectiveCharacterStats"/>/<c>StatRecomputeSystem</c>,
/// so it is identical on server + owner-predicted client and rollback-correct with no replicated enabled bit.
/// Baked DISABLED (the player spawns alive). Movement/aim/fire systems query <c>.WithDisabled&lt;Dead&gt;()</c>
/// so a dead player is frozen and can't act.
/// </summary>
public struct Dead : IComponentData, IEnableableComponent { }
/// <summary>
/// Server-only respawn timer for a player. NOT replicated — recovery is server-authoritative and the refilled
/// <see cref="Health"/> (GhostField) + repositioned LocalTransform replicate instead. <see cref="RespawnTick"/>
/// == 0 means "no respawn pending / alive"; <see cref="DelayTicks"/> is the baked down-time before recovery.
/// </summary>
public struct RespawnState : IComponentData
{
/// <summary>Raw server tick at which to respawn; 0 = none pending.</summary>
public uint RespawnTick;
/// <summary>Ticks the player stays down before recovering (~60 ticks/sec).</summary>
public int DelayTicks;
/// <summary>Ticks of post-respawn damage immunity granted on recovery (~60 ticks/sec).</summary>
public int InvulnTicks;
}
/// <summary>
/// Replicated post-respawn damage-immunity window. <c>UntilTick</c> = the raw server tick until which the
/// player ignores damage; 0 = none. Set server-side by <c>PlayerRespawnSystem</c> on recovery, enforced by
/// <c>HealthApplyDamageSystem</c>, and a <c>[GhostField]</c> so the client HUD can show a SHIELDED cue.
/// </summary>
public struct RespawnInvuln : IComponentData
{
[GhostField] public uint UntilTick;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3bd1e4b63cf826446b85d11124bc291b
@@ -22,7 +22,7 @@ namespace ProjectM.Simulation
{
foreach (var (facing, transform, input) in
SystemAPI.Query<RefRW<PlayerFacing>, RefRW<LocalTransform>, RefRO<PlayerInput>>()
.WithAll<Simulate>())
.WithAll<Simulate>().WithDisabled<Dead>())
{
float2 aim = input.ValueRO.Aim;
if (math.lengthsq(aim) < 1e-6f)
@@ -24,7 +24,7 @@ namespace ProjectM.Simulation
{
foreach (var (control, input, stats) in
SystemAPI.Query<RefRW<CharacterControl>, RefRO<PlayerInput>, RefRO<EffectiveCharacterStats>>()
.WithAll<Simulate>())
.WithAll<Simulate>().WithDisabled<Dead>())
{
control.ValueRW.MoveVelocity =
CharacterControlMath.DesiredMovement(input.ValueRO.Move, stats.ValueRO.MoveSpeed);
@@ -0,0 +1,42 @@
using Unity.Burst;
using Unity.Entities;
using Unity.Mathematics;
using Unity.NetCode;
namespace ProjectM.Simulation
{
/// <summary>
/// Derives the LOCAL enableable <see cref="Dead"/> gate from the replicated <see cref="Health"/> every predicted
/// tick (Dead == Health.Current &lt;= 0). Runs in BOTH worlds inside
/// <see cref="PredictedSimulationSystemGroup"/>, BEFORE movement/aim/fire, so a dead player is excluded from
/// those systems (which query <c>.WithDisabled&lt;Dead&gt;()</c>) on the server AND the owner-predicting client.
/// Because it is a pure function of the already-replicated, reconciled Health (the same derive-don't-replicate
/// pattern as <see cref="StatRecomputeSystem"/>), the gate is identical across server, owner-client, and rollback
/// — no replicated enabled bit required. Also zeroes <see cref="CharacterControl.MoveVelocity"/> while dead so
/// the kinematic character holds still (the movement system is skipped and would otherwise coast on stale
/// velocity). The authoritative recovery (Health refill + reposition) is owned server-side by
/// <c>PlayerRespawnSystem</c>. Visits dead players too via <c>.WithPresent&lt;Dead&gt;()</c> (required to write
/// the enabled bit on an entity whose Dead is currently disabled).
/// </summary>
[UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
[UpdateBefore(typeof(PlayerControlSystem))]
[UpdateBefore(typeof(PlayerAimSystem))]
[BurstCompile]
public partial struct PlayerDeathStateSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
foreach (var (health, control, deadEnabled) in
SystemAPI.Query<RefRO<Health>, RefRW<CharacterControl>, EnabledRefRW<Dead>>()
.WithAll<PlayerTag, Simulate>()
.WithPresent<Dead>())
{
bool isDead = health.ValueRO.Current <= 0f;
deadEnabled.ValueRW = isDead;
if (isDead)
control.ValueRW.MoveVelocity = float3.zero;
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 65930c35d657ce84fbce6f1130efb441
@@ -0,0 +1,36 @@
using Unity.Mathematics;
namespace ProjectM.Simulation
{
/// <summary>
/// Pure, deterministic respawn-timer math (no RNG, no wall-clock) — unit-testable in EditMode without a netcode
/// world (mirrors <see cref="PlayerSpawnMath"/> / <see cref="EnemyAIMath"/>). Ticks are the server's monotonic
/// simulation ticks; a stored value of 0 means "no respawn pending / alive". Comparisons use a wrap-safe signed-delta (modular) compare — matching
/// <see cref="Unity.NetCode.NetworkTick.IsNewerThan"/> semantics — so they hold across the uint tick wraparound.
/// </summary>
public static class RespawnMath
{
/// <summary>
/// The tick at which a death at <paramref name="deathTick"/> should respawn, given
/// <paramref name="delayTicks"/> (clamped to &gt;= 1). Never returns 0 (0 is the "no respawn pending"
/// sentinel), so a death exactly at tick 0 still schedules a recovery.
/// </summary>
public static uint RespawnTick(uint deathTick, int delayTicks)
{
uint delay = (uint)math.max(1, delayTicks);
uint t = deathTick + delay;
return t == 0u ? 1u : t;
}
/// <summary>
/// True when <paramref name="now"/> has reached/passed a scheduled <paramref name="respawnTick"/> (and one
/// is actually scheduled, i.e. non-zero).
/// </summary>
public static bool IsDue(uint now, uint respawnTick)
{
// Wrap-safe modular compare (signed delta), NOT a raw `now >= respawnTick` (which is unsafe at the
// uint tick wraparound). Matches NetworkTick.IsNewerThan; keeps this helper pure-uint + unit-testable.
return respawnTick != 0u && (int)(now - respawnTick) >= 0;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 128d07bd94a16ab40b8a71bdf4f3e4cf