Further Tests & Progress
This commit is contained in:
@@ -18,9 +18,9 @@ namespace ProjectM.Server
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
public partial struct AbilityUpgradeSystem : ISystem
|
||||
{
|
||||
const uint UpgradeSourceId = 0x00A0E711u; // distinct sentinel so the upgrade modifier is found + grown
|
||||
const float TierStep = 0.25f; // +25% damage per tier
|
||||
const int CostAmount = 20; // Aether per tier
|
||||
const uint UpgradeSourceId = Tuning.AbilityUpgradeSourceId; // distinct sentinel so the upgrade modifier is found + grown
|
||||
const float TierStep = Tuning.AbilityUpgradeTierStep; // +25% damage per tier
|
||||
const int CostAmount = Tuning.AbilityUpgradeCostAmount; // Aether per tier
|
||||
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
|
||||
@@ -59,14 +59,32 @@ namespace ProjectM.Server
|
||||
|
||||
float dt = SystemAPI.Time.DeltaTime;
|
||||
var serverTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick;
|
||||
uint now = serverTick.TickIndexForValidTick;
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
|
||||
foreach (var (xform, stats, cooldown) in
|
||||
SystemAPI.Query<RefRW<LocalTransform>, RefRO<EnemyStats>, RefRW<EnemyAttackCooldown>>()
|
||||
foreach (var (xform, stats, cooldown, knockback, windup) in
|
||||
SystemAPI.Query<RefRW<LocalTransform>, RefRO<EnemyStats>, RefRW<EnemyAttackCooldown>,
|
||||
RefRW<KnockbackState>, RefRW<AttackWindup>>()
|
||||
.WithAll<EnemyTag>())
|
||||
{
|
||||
float3 pos = xform.ValueRO.Position;
|
||||
|
||||
// Knockback overrides seek/strike for its window — EnemyAISystem stays the SOLE writer of Position.
|
||||
var kb = knockback.ValueRO;
|
||||
if (kb.UntilTick != 0)
|
||||
{
|
||||
var kbTick = new NetworkTick(kb.UntilTick);
|
||||
if (kbTick.IsValid && kbTick.IsNewerThan(serverTick))
|
||||
{
|
||||
float3 kpos = pos + new float3(kb.Dir.x, 0f, kb.Dir.y) * (kb.Speed * dt);
|
||||
kpos.y = pos.y;
|
||||
xform.ValueRW.Position = kpos;
|
||||
windup.ValueRW.WindUpUntilTick = 0; // a recoiling Husk does not wind up
|
||||
continue; // recoiling: skip seek + strike this tick
|
||||
}
|
||||
knockback.ValueRW.UntilTick = 0; // window elapsed
|
||||
}
|
||||
|
||||
// Nearest living player (planar XZ).
|
||||
int best = -1;
|
||||
float bestSq = float.MaxValue;
|
||||
@@ -96,8 +114,35 @@ namespace ProjectM.Server
|
||||
if (math.lengthsq(toTarget) > 1e-6f)
|
||||
xform.ValueRW.Rotation = quaternion.LookRotationSafe(math.normalize(toTarget), math.up());
|
||||
|
||||
// Strike on contact once the cooldown has elapsed.
|
||||
if (EnemyAIMath.InAttackRange(pos, targetPos, stats.ValueRO.AttackRange))
|
||||
// Two-phase strike with a telegraph wind-up: commit a wind-up when first in-range + cooldown-ready,
|
||||
// then strike when it elapses. WindUpUntilTick is a [GhostField] so the client can cue the ~0.3s
|
||||
// tell; leaving range mid-windup cancels it. Tuning.AttackWindupTicks = 0/1 -> near-instant (legacy).
|
||||
bool inRange = EnemyAIMath.InAttackRange(pos, targetPos, stats.ValueRO.AttackRange);
|
||||
uint windRaw = windup.ValueRO.WindUpUntilTick;
|
||||
|
||||
if (windRaw != 0)
|
||||
{
|
||||
if (!inRange)
|
||||
{
|
||||
windup.ValueRW.WindUpUntilTick = 0; // target left range -> cancel the wind-up
|
||||
}
|
||||
else
|
||||
{
|
||||
var windTick = new NetworkTick(windRaw);
|
||||
if (!(windTick.IsValid && windTick.IsNewerThan(serverTick)))
|
||||
{
|
||||
ecb.AppendToBuffer(playerEntities[best], new DamageEvent
|
||||
{
|
||||
Amount = stats.ValueRO.AttackDamage,
|
||||
SourceNetworkId = -1, // environment / Husk, not a player
|
||||
});
|
||||
uint cooldownTicks = (uint)math.max(1, stats.ValueRO.AttackCooldownTicks);
|
||||
cooldown.ValueRW.NextAttackTick = TickUtil.NonZero(now + cooldownTicks);
|
||||
windup.ValueRW.WindUpUntilTick = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (inRange)
|
||||
{
|
||||
uint nextRaw = cooldown.ValueRO.NextAttackTick;
|
||||
bool ready = true;
|
||||
@@ -107,16 +152,10 @@ namespace ProjectM.Server
|
||||
if (nextTick.IsValid && nextTick.IsNewerThan(serverTick))
|
||||
ready = false;
|
||||
}
|
||||
|
||||
if (ready)
|
||||
{
|
||||
ecb.AppendToBuffer(playerEntities[best], new DamageEvent
|
||||
{
|
||||
Amount = stats.ValueRO.AttackDamage,
|
||||
SourceNetworkId = -1, // environment / Husk, not a player
|
||||
});
|
||||
uint cooldownTicks = (uint)math.max(1, stats.ValueRO.AttackCooldownTicks);
|
||||
cooldown.ValueRW.NextAttackTick = TickUtil.NonZero(serverTick.TickIndexForValidTick + cooldownTicks);
|
||||
uint windupTicks = (uint)math.max(1, Tuning.AttackWindupTicks);
|
||||
windup.ValueRW.WindUpUntilTick = TickUtil.NonZero(now + windupTicks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,9 @@ namespace ProjectM.Server
|
||||
/// <summary>Lookup used to read a target's owner so a projectile never hits its own caster.</summary>
|
||||
ComponentLookup<GhostOwner> m_GhostOwnerLookup;
|
||||
|
||||
/// <summary>RW lookup to stamp server-only knockback on a hit Husk (Husks bake KnockbackState; players/dummies don't).</summary>
|
||||
ComponentLookup<KnockbackState> m_KnockbackLookup;
|
||||
|
||||
/// <summary>Extra forgiveness added to a target's hit radius to approximate the projectile's own size.</summary>
|
||||
const float k_ProjectileRadius = 0.2f;
|
||||
|
||||
@@ -47,6 +50,7 @@ namespace ProjectM.Server
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
m_GhostOwnerLookup = state.GetComponentLookup<GhostOwner>(isReadOnly: true);
|
||||
m_KnockbackLookup = state.GetComponentLookup<KnockbackState>(isReadOnly: false);
|
||||
|
||||
// No projectiles → nothing to expire or hit-test; skip the tick (and its allocations) entirely.
|
||||
state.RequireForUpdate<Projectile>();
|
||||
@@ -56,6 +60,8 @@ namespace ProjectM.Server
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
m_GhostOwnerLookup.Update(ref state);
|
||||
m_KnockbackLookup.Update(ref state);
|
||||
bool haveTick = SystemAPI.TryGetSingleton<NetworkTime>(out var nt);
|
||||
|
||||
float dt = SystemAPI.Time.DeltaTime;
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
@@ -125,6 +131,16 @@ namespace ProjectM.Server
|
||||
Amount = proj.ValueRO.Damage,
|
||||
SourceNetworkId = projOwnerId,
|
||||
});
|
||||
var hitTarget = targetEntities[bestIdx];
|
||||
if (haveTick && Tuning.KnockbackSpeed > 0f && m_KnockbackLookup.HasComponent(hitTarget))
|
||||
{
|
||||
m_KnockbackLookup[hitTarget] = new KnockbackState
|
||||
{
|
||||
Dir = proj.ValueRO.Direction,
|
||||
Speed = Tuning.KnockbackSpeed,
|
||||
UntilTick = TickUtil.NonZero(nt.ServerTick.TickIndexForValidTick + (uint)math.max(1, Tuning.KnockbackDurationTicks)),
|
||||
};
|
||||
}
|
||||
ecb.DestroyEntity(projectileEntity);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Burst;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-authoritative expiry of TIMED <see cref="StatModifier"/>s. Each tick it walks every entity's
|
||||
/// server-only <see cref="TimedModifier"/> buffer; for any row whose <see cref="TimedModifier.UntilTick"/> has
|
||||
/// elapsed (wrap-safe <see cref="Unity.NetCode.NetworkTick.IsNewerThan"/> compare, never raw uint<) it removes
|
||||
/// the matching StatModifier(s) by SourceId and the timed row. The shortened StatModifier [GhostField] buffer
|
||||
/// auto-replicates (OwnerSendType.All), so StatRecomputeSystem reverts the effective stat on both worlds with no
|
||||
/// change. Runs in the plain server <see cref="SimulationSystemGroup"/> (NOT the predicted loop) so it is applied
|
||||
/// exactly once and never double-removed on rollback; a DynamicBuffer mutation is not a structural change, so it
|
||||
/// is safe to mutate the iterated entity's own buffers in place.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
[UpdateInGroup(typeof(SimulationSystemGroup))]
|
||||
public partial struct TimedModifierExpirySystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<NetworkTime>();
|
||||
state.RequireForUpdate(state.GetEntityQuery(ComponentType.ReadOnly<TimedModifier>()));
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var serverTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick;
|
||||
if (!serverTick.IsValid)
|
||||
return;
|
||||
|
||||
foreach (var (timed, mods) in
|
||||
SystemAPI.Query<DynamicBuffer<TimedModifier>, DynamicBuffer<StatModifier>>())
|
||||
{
|
||||
for (int i = timed.Length - 1; i >= 0; i--)
|
||||
{
|
||||
uint until = timed[i].UntilTick;
|
||||
if (until == 0)
|
||||
continue; // inert (no expiry scheduled)
|
||||
|
||||
var untilTick = new NetworkTick(until);
|
||||
if (untilTick.IsValid && untilTick.IsNewerThan(serverTick))
|
||||
continue; // not yet due
|
||||
|
||||
TimedModifierUtil.RemoveBySourceId(mods, timed[i].SourceId);
|
||||
timed.RemoveAtSwapBack(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 94c6107954fa4d94f8ead51cfe4de3b7
|
||||
@@ -28,7 +28,7 @@ namespace ProjectM.Server
|
||||
[UpdateAfter(typeof(PredictedSimulationSystemGroup))]
|
||||
public partial struct ResourceHarvestSystem : ISystem
|
||||
{
|
||||
const float k_ProjectileRadius = 0.2f;
|
||||
const float k_ProjectileRadius = Tuning.HarvestProjectileRadius;
|
||||
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
|
||||
@@ -82,6 +82,10 @@ namespace ProjectM.Server
|
||||
break;
|
||||
}
|
||||
|
||||
// Surface the live wave number on the replicated CycleState for the HUD (single writer).
|
||||
if (SystemAPI.TryGetSingleton<WaveState>(out var waveSync))
|
||||
cycle.WaveNumber = waveSync.WaveNumber;
|
||||
|
||||
SystemAPI.SetComponent(cycleEntity, cycle);
|
||||
SystemAPI.SetComponent(cycleEntity, runtime);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user