Attack Boon Changes
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.NetCode;
|
||||
using Unity.Transforms;
|
||||
|
||||
namespace ProjectM.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Phase 1.7 "Blade Dash" boon (<see cref="BoonFlag.DashTrail"/>): while a player is inside its dash blink window,
|
||||
/// living enemies within <see cref="k_Radius"/> of the player take damage — one hit per enemy per dash. SERVER-ONLY
|
||||
/// (enemies are interpolated ghosts the client never predicts — mirrors the melee cleave / cone / projectile-damage
|
||||
/// pattern), inside the predicted group after <see cref="DashSystem"/> (dash state committed) and before
|
||||
/// <c>HealthApplyDamageSystem</c> (the DamageEvent drains the same tick). Enemies carry no <c>DashState</c>, so the
|
||||
/// dash-i-frame negation branch in HealthApplyDamageSystem is skipped — harmless.
|
||||
///
|
||||
/// Dedup is keyed to <see cref="DashState.StartTick"/> (which is <c>TickUtil.NonZero(now)</c> on every dash and has
|
||||
/// NO reliable clear edge on a release server): <see cref="DashTrailState.Hit"/> is cleared whenever the current
|
||||
/// StartTick differs from <see cref="DashTrailState.LastStartTick"/>. Server-only ⇒ no rollback, so persisting the
|
||||
/// accumulator across ticks is safe. A per-tick radius test (run every blink tick) approximates the swept path; the
|
||||
/// per-tick dash step (<~0.6u) is well inside the radius, so a thin enemy is not tunnelled. Hit-set overflow stops
|
||||
/// adding (a possible re-hit on a very crowded dash — accepted v1 cap).
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
[UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
|
||||
[UpdateAfter(typeof(DashSystem))]
|
||||
[UpdateBefore(typeof(HealthApplyDamageSystem))]
|
||||
public partial struct DashTrailDamageSystem : ISystem
|
||||
{
|
||||
const float k_Radius = 1.6f; // planar hit radius around the dashing player (tunable)
|
||||
const float k_Damage = 12f; // per-enemy damage for a dash pass (tunable)
|
||||
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<NetworkTime>();
|
||||
state.RequireForUpdate<DashTrailState>();
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var nt = SystemAPI.GetSingleton<NetworkTime>();
|
||||
var serverTick = nt.ServerTick;
|
||||
if (!serverTick.IsValid)
|
||||
return;
|
||||
|
||||
// Snapshot living enemies once (positions + radii + entities), stable query order.
|
||||
var enemyEntities = new NativeList<Entity>(Allocator.Temp);
|
||||
var enemyPositions = new NativeList<float3>(Allocator.Temp);
|
||||
var enemyRadii = new NativeList<float>(Allocator.Temp);
|
||||
foreach (var (tx, hr, hp, te) in
|
||||
SystemAPI.Query<RefRO<LocalTransform>, RefRO<HitRadius>, RefRO<Health>>()
|
||||
.WithAll<EnemyTag>().WithNone<Dying>().WithEntityAccess())
|
||||
{
|
||||
if (hp.ValueRO.Current <= 0f) continue;
|
||||
enemyEntities.Add(te);
|
||||
enemyPositions.Add(tx.ValueRO.Position);
|
||||
enemyRadii.Add(hr.ValueRO.Value);
|
||||
}
|
||||
|
||||
if (enemyEntities.Length == 0)
|
||||
{
|
||||
enemyEntities.Dispose(); enemyPositions.Dispose(); enemyRadii.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
uint stamp = TickUtil.NonZero(serverTick.TickIndexForValidTick);
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
|
||||
foreach (var (xform, dash, trail, owner, fx) in
|
||||
SystemAPI.Query<RefRO<LocalTransform>, RefRO<DashState>, RefRW<DashTrailState>,
|
||||
RefRO<GhostOwner>, RefRO<BoonEffects>>()
|
||||
.WithAll<PlayerTag, Simulate>())
|
||||
{
|
||||
if ((fx.ValueRO.Flags & BoonFlag.DashTrail) == 0)
|
||||
continue;
|
||||
|
||||
uint startRaw = dash.ValueRO.StartTick;
|
||||
if (startRaw == 0u)
|
||||
continue; // never dashed
|
||||
|
||||
// Inside the blink (i-frame) window [StartTick, IFrameUntilTick)?
|
||||
var startTick = new NetworkTick(startRaw);
|
||||
var untilTick = new NetworkTick(dash.ValueRO.IFrameUntilTick);
|
||||
bool dashing = startTick.IsValid && untilTick.IsValid
|
||||
&& !startTick.IsNewerThan(serverTick) && untilTick.IsNewerThan(serverTick);
|
||||
if (!dashing)
|
||||
continue;
|
||||
|
||||
// New dash → reset the per-dash hit set (StartTick changes every dash; no reliable DashState clear).
|
||||
if (trail.ValueRO.LastStartTick != startRaw)
|
||||
{
|
||||
trail.ValueRW.Hit.Clear();
|
||||
trail.ValueRW.LastStartTick = startRaw;
|
||||
}
|
||||
|
||||
float3 p = xform.ValueRO.Position;
|
||||
int ownerId = owner.ValueRO.NetworkId;
|
||||
for (int i = 0; i < enemyEntities.Length; i++)
|
||||
{
|
||||
var enemy = enemyEntities[i];
|
||||
if (HitContains(trail.ValueRO, enemy))
|
||||
continue;
|
||||
float2 d = new float2(enemyPositions[i].x - p.x, enemyPositions[i].z - p.z);
|
||||
float reach = k_Radius + enemyRadii[i];
|
||||
if (math.lengthsq(d) > reach * reach)
|
||||
continue;
|
||||
|
||||
if (trail.ValueRO.Hit.Length >= trail.ValueRO.Hit.Capacity) break; // hit-cap: never damage an enemy we can't record (else re-hit every tick)
|
||||
ecb.AppendToBuffer(enemy, new DamageEvent
|
||||
{
|
||||
Amount = k_Damage,
|
||||
SourceNetworkId = ownerId, // a real player id (legit Charger whiff-punish credit)
|
||||
SourceTick = stamp,
|
||||
});
|
||||
if (trail.ValueRO.Hit.Length < trail.ValueRO.Hit.Capacity)
|
||||
trail.ValueRW.Hit.Add(enemy);
|
||||
}
|
||||
}
|
||||
|
||||
ecb.Playback(state.EntityManager);
|
||||
ecb.Dispose();
|
||||
enemyEntities.Dispose();
|
||||
enemyPositions.Dispose();
|
||||
enemyRadii.Dispose();
|
||||
}
|
||||
|
||||
static bool HitContains(in DashTrailState trail, Entity e)
|
||||
{
|
||||
for (int i = 0; i < trail.Hit.Length; i++)
|
||||
if (trail.Hit[i] == e) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user