Further Tests & Progress

This commit is contained in:
2026-06-04 11:35:57 -07:00
parent 5c11ff4fad
commit 51401d2c2b
65 changed files with 2784 additions and 45 deletions
@@ -0,0 +1,19 @@
using Unity.Entities;
using Unity.NetCode;
namespace ProjectM.Simulation
{
/// <summary>
/// Replicated Husk attack-telegraph signal. While <see cref="WindUpUntilTick"/> is non-zero the Husk is
/// "winding up" to strike; EnemyAISystem sets it ~<see cref="Tuning.AttackWindupTicks"/> before the strike
/// lands, and the strike fires when the tick elapses. This is a [GhostField] (the only replicated Husk field
/// beyond the stock LocalTransform) so the CLIENT can play a ~0.3s pre-strike cue — the client has none of the
/// server-only timing inputs (EnemyStats / EnemyAttackCooldown), so the wind-up MUST be replicated. A uint tick
/// (not a [GhostEnabledBit]) so the cue can ramp/countdown and survive a missed snapshot (absolute, not an edge).
/// </summary>
public struct AttackWindup : IComponentData
{
/// <summary>Server tick the wind-up completes + the strike lands (0 = not winding up; scheduled via TickUtil.NonZero).</summary>
[GhostField] public uint WindUpUntilTick;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f2c9d899714758b4baefe6c1cbb3be0a
@@ -0,0 +1,26 @@
using Unity.Entities;
using Unity.Mathematics;
namespace ProjectM.Simulation
{
/// <summary>
/// SERVER-ONLY transient knockback on a Husk. While <see cref="UntilTick"/> has not elapsed, EnemyAISystem
/// moves the Husk along <see cref="Dir"/> at <see cref="Speed"/> (REPLACING its seek) and suppresses its strike.
/// Stamped by ProjectileDamageSystem on a hit (Dir = the projectile's heading). NOT a [GhostField] — the Husk's
/// displaced position already replicates via the stock LocalTransform default variant, so knockback adds NO
/// replicated surface (no ghost re-bake). EnemyAISystem must remain the SOLE writer of the Husk's Position, so
/// knockback is applied INSIDE it (never a competing system). Force/duration live in <see cref="Tuning"/>
/// (KnockbackSpeed = 0 disables knockback globally).
/// </summary>
public struct KnockbackState : IComponentData
{
/// <summary>Planar (XZ) knockback heading — the projectile's direction at impact.</summary>
public float2 Dir;
/// <summary>Knockback speed (world units/sec) applied for the window; 0 = not knocked.</summary>
public float Speed;
/// <summary>Server tick until which the knockback is active (0 = none; scheduled via TickUtil.NonZero).</summary>
public uint UntilTick;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9fa07e28f83ad6b43a8164b8c673a6b1
@@ -0,0 +1,35 @@
using Unity.Entities;
namespace ProjectM.Simulation
{
/// <summary>
/// SERVER-ONLY expiry tracker paired with a <see cref="StatModifier"/> by <see cref="SourceId"/>. It is NOT a
/// [GhostField] and lives in a SEPARATE buffer so the replicated <see cref="StatModifier"/> layout stays
/// byte-identical — adding ANY member (even non-ghost) to a [GhostField] buffer element regenerates its
/// serializer/stride/hash = an effective ghost re-bake. To grant a TIMED buff, append both a StatModifier and a
/// TimedModifier sharing one unique SourceId; <c>TimedModifierExpirySystem</c> removes the matching StatModifier
/// when <see cref="UntilTick"/> elapses, and that removal replicates for free via the StatModifier [GhostField]
/// buffer (OwnerSendType.All), so StatRecomputeSystem reverts the effective stat on both worlds with no change.
/// </summary>
public struct TimedModifier : IBufferElementData
{
/// <summary>Matches the <see cref="StatModifier.SourceId"/> this row governs.</summary>
public uint SourceId;
/// <summary>Server tick at which the paired StatModifier expires (0 = no expiry / inert; schedule via TickUtil.NonZero).</summary>
public uint UntilTick;
}
/// <summary>Pure helpers for removing modifiers by provenance (clear-by-type / timed expiry). Deterministic, no RNG/wall-clock.</summary>
public static class TimedModifierUtil
{
/// <summary>Remove every <see cref="StatModifier"/> row whose SourceId matches (RemoveAtSwapBack). Returns the count removed.</summary>
public static int RemoveBySourceId(DynamicBuffer<StatModifier> mods, uint sourceId)
{
int removed = 0;
for (int j = mods.Length - 1; j >= 0; j--)
if (mods[j].SourceId == sourceId) { mods.RemoveAtSwapBack(j); removed++; }
return removed;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 67465323b013e6a4cb59519111b1b9e5