Initial Combat Implementation
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a8dcd28017b9e4abead6e2dc32ef9383
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,68 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-authoritative damage application. Drains each damageable entity's
|
||||
/// <see cref="DamageEvent"/> buffer (appended by <see cref="ProjectileDamageSystem"/> earlier
|
||||
/// this tick), subtracts the summed amount from <see cref="Health"/>, then clears the buffer so
|
||||
/// each hit is applied exactly once. Entities that carry character stats (players) clamp to their
|
||||
/// data-driven <see cref="EffectiveCharacterStats.MaxHealth"/> ceiling; others (training dummies)
|
||||
/// clamp at zero. A dead <see cref="TrainingDummyTag"/> is destroyed; player death is deferred.
|
||||
/// Health.Current is a <c>[GhostField]</c>, so the new value replicates to clients for display.
|
||||
///
|
||||
/// Runs server-only (<see cref="WorldSystemFilterFlags.ServerSimulation"/>) inside the prediction
|
||||
/// group so it shares tick timing with movement/damage, where it executes once per tick. The
|
||||
/// single structural change (destroying a dead dummy) is batched through a frame-allocator
|
||||
/// <see cref="EntityCommandBuffer"/> that is played back immediately to the entity manager — so a
|
||||
/// plain-world EditMode test needs no separate ECB system.
|
||||
/// </summary>
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
[UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
|
||||
[UpdateAfter(typeof(ProjectileDamageSystem))]
|
||||
[BurstCompile]
|
||||
public partial struct HealthApplyDamageSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
|
||||
foreach (var (health, dmg, entity) in
|
||||
SystemAPI.Query<RefRW<Health>, DynamicBuffer<DamageEvent>>()
|
||||
.WithEntityAccess())
|
||||
{
|
||||
if (dmg.Length == 0)
|
||||
continue;
|
||||
|
||||
float total = 0f;
|
||||
for (int i = 0; i < dmg.Length; i++)
|
||||
total += dmg[i].Amount;
|
||||
dmg.Clear();
|
||||
|
||||
float newHp = health.ValueRO.Current - total;
|
||||
|
||||
// Effective max health (base + modifiers) is the runtime ceiling for entities that carry
|
||||
// character stats (players); others just clamp at zero. No auto-heal on a max increase.
|
||||
if (SystemAPI.HasComponent<EffectiveCharacterStats>(entity))
|
||||
newHp = math.clamp(newHp, 0f, SystemAPI.GetComponent<EffectiveCharacterStats>(entity).MaxHealth);
|
||||
else
|
||||
newHp = math.max(0f, newHp);
|
||||
|
||||
health.ValueRW.Current = newHp;
|
||||
|
||||
// Server-authoritative death: training dummies despawn; player death is deferred (clamp only).
|
||||
if (health.ValueRO.Current <= 0f && SystemAPI.HasComponent<TrainingDummyTag>(entity))
|
||||
ecb.DestroyEntity(entity);
|
||||
}
|
||||
|
||||
ecb.Playback(state.EntityManager);
|
||||
ecb.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7c1729291515a4966b83ef050554a772
|
||||
@@ -0,0 +1,145 @@
|
||||
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>
|
||||
/// Server-authoritative projectile resolution: applies hits to damageable entities and expires
|
||||
/// projectiles past their range. Runs in the server world only
|
||||
/// (<see cref="WorldSystemFilterFlags.ServerSimulation"/>) inside the
|
||||
/// <see cref="PredictedSimulationSystemGroup"/>, ordered <see cref="ProjectileMoveSystem"/> so the
|
||||
/// projectile's <see cref="LocalTransform"/> is the post-move position for this tick.
|
||||
///
|
||||
/// Hit detection is a <b>swept</b> planar (XZ) test: rather than checking the projectile's point
|
||||
/// position (which tunnels straight through a target when the per-tick step exceeds the target's
|
||||
/// radius — e.g. a fast projectile, or any projectile while the server is tick-batching under load),
|
||||
/// it reconstructs the segment the projectile traversed this tick
|
||||
/// (<c>[curPos - dir*speed*dt, curPos]</c>) and tests each target's hit radius against the closest
|
||||
/// point on that segment. The target hit earliest along the path (smallest segment parameter) wins.
|
||||
/// A target whose <see cref="GhostOwner"/> matches the projectile's owner is skipped (no self-hits);
|
||||
/// dummies carry no <see cref="GhostOwner"/> and are therefore always valid targets.
|
||||
///
|
||||
/// On a hit the system appends a <see cref="DamageEvent"/> to the target (consumed by
|
||||
/// <c>HealthApplyDamageSystem</c>) and destroys the projectile. Deferring damage to a buffer lets a
|
||||
/// single tick stack hits from multiple projectiles. All structural changes go through an
|
||||
/// <see cref="EntityCommandBuffer"/> that plays back immediately to the
|
||||
/// <see cref="EntityManager"/> (Temp allocator) — keeping this server-only, once-per-tick system
|
||||
/// self-contained and plain-world testable without a separate ECB system.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
[UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
|
||||
[UpdateAfter(typeof(ProjectileMoveSystem))]
|
||||
public partial struct ProjectileDamageSystem : ISystem
|
||||
{
|
||||
/// <summary>Lookup used to read a target's owner so a projectile never hits its own caster.</summary>
|
||||
ComponentLookup<GhostOwner> m_GhostOwnerLookup;
|
||||
|
||||
/// <summary>Extra forgiveness added to a target's hit radius to approximate the projectile's own size.</summary>
|
||||
const float k_ProjectileRadius = 0.2f;
|
||||
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
m_GhostOwnerLookup = state.GetComponentLookup<GhostOwner>(isReadOnly: true);
|
||||
|
||||
// No projectiles → nothing to expire or hit-test; skip the tick (and its allocations) entirely.
|
||||
state.RequireForUpdate<Projectile>();
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
m_GhostOwnerLookup.Update(ref state);
|
||||
|
||||
float dt = SystemAPI.Time.DeltaTime;
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
|
||||
// Snapshot all damageable targets once for this tick. Stable iteration order (query order).
|
||||
var targetEntities = new NativeList<Entity>(Allocator.Temp);
|
||||
var targetPositions = new NativeList<float3>(Allocator.Temp);
|
||||
var targetRadii = new NativeList<float>(Allocator.Temp);
|
||||
|
||||
foreach (var (xform, hitRadius, targetEntity) in
|
||||
SystemAPI.Query<RefRO<LocalTransform>, RefRO<HitRadius>>()
|
||||
.WithAll<Health>()
|
||||
.WithEntityAccess())
|
||||
{
|
||||
targetEntities.Add(targetEntity);
|
||||
targetPositions.Add(xform.ValueRO.Position);
|
||||
targetRadii.Add(hitRadius.ValueRO.Value);
|
||||
}
|
||||
|
||||
foreach (var (xform, proj, owner, projectileEntity) in
|
||||
SystemAPI.Query<RefRO<LocalTransform>, RefRO<Projectile>, RefRO<GhostOwner>>()
|
||||
.WithEntityAccess())
|
||||
{
|
||||
int projOwnerId = owner.ValueRO.NetworkId;
|
||||
|
||||
// This tick's planar travel segment: [segStart -> segEnd]. Sweeping the segment (rather
|
||||
// than testing only segEnd) is what prevents fast projectiles from tunnelling targets.
|
||||
float3 cur = xform.ValueRO.Position;
|
||||
float2 segEnd = new float2(cur.x, cur.z);
|
||||
float2 segStart = segEnd - proj.ValueRO.Direction * (proj.ValueRO.Speed * dt);
|
||||
float2 seg = segEnd - segStart;
|
||||
float segLenSq = math.lengthsq(seg);
|
||||
|
||||
int bestIdx = -1;
|
||||
float bestT = float.MaxValue;
|
||||
for (int i = 0; i < targetEntities.Length; i++)
|
||||
{
|
||||
var target = targetEntities[i];
|
||||
|
||||
// Skip the caster: a target whose GhostOwner matches the projectile owner is the
|
||||
// shooter (or another ghost they own). Dummies have no GhostOwner, so never skipped.
|
||||
if (m_GhostOwnerLookup.HasComponent(target) &&
|
||||
m_GhostOwnerLookup[target].NetworkId == projOwnerId)
|
||||
continue;
|
||||
|
||||
float2 tp = new float2(targetPositions[i].x, targetPositions[i].z);
|
||||
|
||||
// Closest point on the travel segment to the target centre.
|
||||
float t = segLenSq > 1e-8f
|
||||
? math.saturate(math.dot(tp - segStart, seg) / segLenSq)
|
||||
: 0f;
|
||||
float2 closest = segStart + t * seg;
|
||||
|
||||
float hitDist = targetRadii[i] + k_ProjectileRadius;
|
||||
if (math.distancesq(tp, closest) <= hitDist * hitDist && t < bestT)
|
||||
{
|
||||
bestT = t;
|
||||
bestIdx = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestIdx >= 0)
|
||||
{
|
||||
// Earliest target along the travel path: deal damage and consume the projectile.
|
||||
ecb.AppendToBuffer(targetEntities[bestIdx], new DamageEvent
|
||||
{
|
||||
Amount = proj.ValueRO.Damage,
|
||||
SourceNetworkId = projOwnerId,
|
||||
});
|
||||
ecb.DestroyEntity(projectileEntity);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Nothing hit this tick: expire the projectile once it has travelled its full range.
|
||||
if (proj.ValueRO.DistanceTravelled >= proj.ValueRO.Range)
|
||||
ecb.DestroyEntity(projectileEntity);
|
||||
}
|
||||
|
||||
ecb.Playback(state.EntityManager);
|
||||
|
||||
ecb.Dispose();
|
||||
targetEntities.Dispose();
|
||||
targetPositions.Dispose();
|
||||
targetRadii.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f19fa77609fa04c2ca4e293f19c052a4
|
||||
@@ -0,0 +1,52 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.Transforms;
|
||||
|
||||
namespace ProjectM.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-only, one-shot dummy spawner. On its first update it reads the baked
|
||||
/// <see cref="TrainingDummySpawner"/> singleton and instantiates <c>Count</c> training-dummy
|
||||
/// ghosts in a row, spaced <c>Spacing</c> world-units apart along +X starting at <c>Origin</c>.
|
||||
/// It then destroys the spawner singleton entity so <c>RequireForUpdate<TrainingDummySpawner></c>
|
||||
/// is no longer satisfied and the system stops running (idempotent: dummies are spawned exactly once).
|
||||
/// Runs in the default <see cref="SimulationSystemGroup"/> (NOT the prediction loop) since spawning is
|
||||
/// a non-predicted, server-authoritative event; the dummies replicate to clients as interpolated ghosts.
|
||||
/// All structural changes are batched through an <see cref="EntityCommandBuffer"/>.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
public partial struct TrainingDummySpawnSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<TrainingDummySpawner>();
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
// Grab both the singleton entity (to destroy when done) and its baked config.
|
||||
var spawnerEntity = SystemAPI.GetSingletonEntity<TrainingDummySpawner>();
|
||||
var spawner = SystemAPI.GetComponent<TrainingDummySpawner>(spawnerEntity);
|
||||
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
|
||||
for (int i = 0; i < spawner.Count; i++)
|
||||
{
|
||||
var dummy = ecb.Instantiate(spawner.Prefab);
|
||||
var position = spawner.Origin + new float3(i * spawner.Spacing, 0f, 0f);
|
||||
ecb.SetComponent(dummy, LocalTransform.FromPosition(position));
|
||||
}
|
||||
|
||||
// One-shot: remove the spawner so RequireForUpdate fails and the system idles.
|
||||
ecb.DestroyEntity(spawnerEntity);
|
||||
|
||||
ecb.Playback(state.EntityManager);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7673acbc74f7a4bddbca0679122511ea
|
||||
@@ -0,0 +1,52 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.Transforms;
|
||||
|
||||
namespace ProjectM.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-only, one-shot upgrade-pickup spawner (mirrors TrainingDummySpawnSystem). On its first
|
||||
/// update it reads the baked <see cref="UpgradePickupSpawner"/> singleton and instantiates
|
||||
/// <c>Count</c> pickup ghosts in a row, spaced <c>Spacing</c> world-units apart along +X starting at
|
||||
/// <c>Origin</c>, then destroys the singleton so the system idles (spawned exactly once). Runs in the
|
||||
/// default <see cref="SimulationSystemGroup"/> (NOT the prediction loop); pickups replicate to clients
|
||||
/// as interpolated ghosts. Structural changes are batched through an <see cref="EntityCommandBuffer"/>.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
public partial struct UpgradePickupSpawnSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<UpgradePickupSpawner>();
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var spawnerEntity = SystemAPI.GetSingletonEntity<UpgradePickupSpawner>();
|
||||
var spawner = SystemAPI.GetComponent<UpgradePickupSpawner>(spawnerEntity);
|
||||
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
|
||||
if (spawner.Prefab != Entity.Null)
|
||||
{
|
||||
for (int i = 0; i < spawner.Count; i++)
|
||||
{
|
||||
var pickup = ecb.Instantiate(spawner.Prefab);
|
||||
var position = spawner.Origin + new float3(i * spawner.Spacing, 0f, 0f);
|
||||
ecb.SetComponent(pickup, LocalTransform.FromPosition(position));
|
||||
}
|
||||
}
|
||||
|
||||
// One-shot: remove the spawner so RequireForUpdate fails and the system idles.
|
||||
ecb.DestroyEntity(spawnerEntity);
|
||||
|
||||
ecb.Playback(state.EntityManager);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 602544c80ca5649258a9d6ca9ad74f79
|
||||
@@ -0,0 +1,78 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.Transforms;
|
||||
|
||||
namespace ProjectM.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-authoritative upgrade pickup grant. When a player overlaps an <see cref="UpgradePickup"/>
|
||||
/// (planar XZ distance within the pickup's <see cref="HitRadius"/>), appends the pickup's modifier to
|
||||
/// the player's replicated <see cref="StatModifier"/> buffer (which replicates to the predicting
|
||||
/// owner, so StatRecomputeSystem folds identical effective stats on both worlds) and destroys the
|
||||
/// pickup. Runs in the default <see cref="SimulationSystemGroup"/> (NOT the prediction loop) since the
|
||||
/// grant is a non-predicted server event. The buffer append + pickup destroy are batched through an
|
||||
/// <see cref="EntityCommandBuffer"/> played back immediately — so a plain-world EditMode test needs no
|
||||
/// separate ECB system.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
public partial struct UpgradePickupSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<UpgradePickup>();
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
// Snapshot modifiable players (carrying the modifier buffer + a transform) once this tick.
|
||||
var playerEntities = new NativeList<Entity>(Allocator.Temp);
|
||||
var playerPositions = new NativeList<float3>(Allocator.Temp);
|
||||
foreach (var (xform, e) in
|
||||
SystemAPI.Query<RefRO<LocalTransform>>()
|
||||
.WithAll<PlayerTag, StatModifier>()
|
||||
.WithEntityAccess())
|
||||
{
|
||||
playerEntities.Add(e);
|
||||
playerPositions.Add(xform.ValueRO.Position);
|
||||
}
|
||||
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
|
||||
foreach (var (xform, radius, pickup, pickupEntity) in
|
||||
SystemAPI.Query<RefRO<LocalTransform>, RefRO<HitRadius>, RefRO<UpgradePickup>>()
|
||||
.WithEntityAccess())
|
||||
{
|
||||
float2 pp = new float2(xform.ValueRO.Position.x, xform.ValueRO.Position.z);
|
||||
float r = radius.ValueRO.Value;
|
||||
|
||||
for (int i = 0; i < playerEntities.Length; i++)
|
||||
{
|
||||
float2 cp = new float2(playerPositions[i].x, playerPositions[i].z);
|
||||
if (math.distancesq(pp, cp) > r * r)
|
||||
continue;
|
||||
|
||||
ecb.AppendToBuffer(playerEntities[i], new StatModifier
|
||||
{
|
||||
Target = pickup.ValueRO.Target,
|
||||
Op = pickup.ValueRO.Op,
|
||||
Value = pickup.ValueRO.Value,
|
||||
SourceId = pickup.ValueRO.SourceId,
|
||||
});
|
||||
ecb.DestroyEntity(pickupEntity);
|
||||
break; // granted to the first overlapping player, then despawns
|
||||
}
|
||||
}
|
||||
|
||||
ecb.Playback(state.EntityManager);
|
||||
ecb.Dispose();
|
||||
playerEntities.Dispose();
|
||||
playerPositions.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c3d9ef25fbc464e52aa342b531d6f35e
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 94ea01b14c1384c9f96d8f7bd6ac1a14
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,89 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.Collections.Generic;
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Editor-only debug hook (mirrors ProjectM.Client.DebugInputInjectionSystem's static-poke pattern)
|
||||
/// for driving the server-authoritative modifier stack from MCP execute_code. Because modifiers are
|
||||
/// server-authoritative, a client-side append would be stomped by the next snapshot, so this runs in
|
||||
/// the SERVER world: the change flows back through the snapshot and is prediction-correct on the
|
||||
/// client. In-editor single-process only (client + server worlds in one process). Poke from execute_code:
|
||||
/// DebugModifierInjectionSystem.AddModifier((byte)StatTarget.Damage, (byte)ModOp.Flat, 50f);
|
||||
/// DebugModifierInjectionSystem.AddModifier((byte)StatTarget.MoveSpeed, (byte)ModOp.PercentAdd, 0.5f);
|
||||
/// DebugModifierInjectionSystem.CycleAbility(); // Primary -> FastLight -> SlowHeavy -> Primary
|
||||
/// DebugModifierInjectionSystem.ClearModifiers();
|
||||
/// All applied to the first player on the next server tick.
|
||||
/// </summary>
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
public partial class DebugModifierInjectionSystem : SystemBase
|
||||
{
|
||||
struct PendingModifier { public byte Target; public byte Op; public float Value; }
|
||||
|
||||
static readonly List<PendingModifier> s_Pending = new List<PendingModifier>();
|
||||
static bool s_Clear;
|
||||
static bool s_Cycle;
|
||||
|
||||
/// <summary>Queue a modifier to append to the first player on the next server tick.</summary>
|
||||
public static void AddModifier(byte target, byte op, float value)
|
||||
{
|
||||
s_Pending.Add(new PendingModifier { Target = target, Op = op, Value = value });
|
||||
}
|
||||
|
||||
/// <summary>Clear the first player's whole modifier stack on the next server tick.</summary>
|
||||
public static void ClearModifiers() => s_Clear = true;
|
||||
|
||||
/// <summary>Cycle the first player's primary ability id on the next server tick.</summary>
|
||||
public static void CycleAbility() => s_Cycle = true;
|
||||
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
if (s_Pending.Count == 0 && !s_Clear && !s_Cycle)
|
||||
return;
|
||||
|
||||
Entity player = Entity.Null;
|
||||
foreach (var (abilityRef, e) in
|
||||
SystemAPI.Query<RefRO<AbilityRef>>().WithAll<PlayerTag, StatModifier>().WithEntityAccess())
|
||||
{
|
||||
player = e;
|
||||
break;
|
||||
}
|
||||
if (player == Entity.Null)
|
||||
return;
|
||||
|
||||
if (s_Clear)
|
||||
{
|
||||
EntityManager.GetBuffer<StatModifier>(player).Clear();
|
||||
s_Clear = false;
|
||||
}
|
||||
|
||||
if (s_Pending.Count > 0)
|
||||
{
|
||||
var buffer = EntityManager.GetBuffer<StatModifier>(player);
|
||||
for (int i = 0; i < s_Pending.Count; i++)
|
||||
{
|
||||
var m = s_Pending[i];
|
||||
buffer.Add(new StatModifier { Target = m.Target, Op = m.Op, Value = m.Value, SourceId = 0u });
|
||||
}
|
||||
s_Pending.Clear();
|
||||
}
|
||||
|
||||
if (s_Cycle)
|
||||
{
|
||||
var abilityRef = EntityManager.GetComponentData<AbilityRef>(player);
|
||||
abilityRef.Id = abilityRef.Id switch
|
||||
{
|
||||
(byte)AbilityId.Primary => (byte)AbilityId.FastLight,
|
||||
(byte)AbilityId.FastLight => (byte)AbilityId.SlowHeavy,
|
||||
_ => (byte)AbilityId.Primary,
|
||||
};
|
||||
EntityManager.SetComponentData(player, abilityRef);
|
||||
s_Cycle = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1f8906df777734bbcb3f21100d6e664e
|
||||
Reference in New Issue
Block a user