105 lines
5.0 KiB
C#
105 lines
5.0 KiB
C#
using ProjectM.Simulation;
|
|
using Unity.Burst;
|
|
using Unity.Collections;
|
|
using Unity.Entities;
|
|
using Unity.Mathematics;
|
|
using Unity.NetCode;
|
|
|
|
namespace ProjectM.Server
|
|
{
|
|
/// <summary>
|
|
/// Phase 1.7 on-kill boons. When <c>HealthApplyDamageSystem</c> stamps an enemy <see cref="Dying"/> it records the
|
|
/// crediting player's NetworkId; this system grants that killer their on-kill boons ONCE per corpse:
|
|
/// <see cref="BoonFlag.Siphon"/> heals the killer (clamped to <see cref="EffectiveCharacterStats.MaxHealth"/>) and
|
|
/// <see cref="BoonFlag.Frenzy"/> refreshes a short cooldown-reduction buff (<see cref="TimedModifierUtil.Upsert"/> —
|
|
/// re-stamped, never stacked). Idempotent via the <see cref="Dying.Rewarded"/> latch (a value write, no edge-detect).
|
|
///
|
|
/// A SEPARATE system (not folded into HealthApplyDamageSystem) because healing the killer needs RW
|
|
/// <see cref="Health"/> access, which would alias that system's <c>RefRW<Health></c> victim query. Here the
|
|
/// only query is <c>RefRW<Dying></c> over enemies, and all killer writes go through ComponentLookup/BufferLookup
|
|
/// on player entities — no aliasing. Server-only (no rollback) inside the predicted group, after damage application.
|
|
/// </summary>
|
|
[BurstCompile]
|
|
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
|
[UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
|
|
[UpdateAfter(typeof(HealthApplyDamageSystem))]
|
|
public partial struct KillRewardSystem : ISystem
|
|
{
|
|
ComponentLookup<BoonEffects> m_Fx;
|
|
ComponentLookup<Health> m_Health;
|
|
ComponentLookup<EffectiveCharacterStats> m_EffChar;
|
|
BufferLookup<StatModifier> m_Mods;
|
|
BufferLookup<TimedModifier> m_Timed;
|
|
|
|
const float k_SiphonHeal = 8f; // HP restored per kill (tunable)
|
|
|
|
[BurstCompile]
|
|
public void OnCreate(ref SystemState state)
|
|
{
|
|
m_Fx = state.GetComponentLookup<BoonEffects>(isReadOnly: true);
|
|
m_Health = state.GetComponentLookup<Health>(isReadOnly: false);
|
|
m_EffChar = state.GetComponentLookup<EffectiveCharacterStats>(isReadOnly: true);
|
|
m_Mods = state.GetBufferLookup<StatModifier>(isReadOnly: false);
|
|
m_Timed = state.GetBufferLookup<TimedModifier>(isReadOnly: false);
|
|
state.RequireForUpdate<NetworkTime>();
|
|
state.RequireForUpdate<Dying>(); // only run while a fresh corpse exists
|
|
}
|
|
|
|
[BurstCompile]
|
|
public void OnUpdate(ref SystemState state)
|
|
{
|
|
var serverTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick;
|
|
if (!serverTick.IsValid)
|
|
return;
|
|
|
|
m_Fx.Update(ref state);
|
|
m_Health.Update(ref state);
|
|
m_EffChar.Update(ref state);
|
|
m_Mods.Update(ref state);
|
|
m_Timed.Update(ref state);
|
|
|
|
// Resolve killers by NetworkId (players only).
|
|
var playerByNet = new NativeHashMap<int, Entity>(8, Allocator.Temp);
|
|
foreach (var (owner, e) in SystemAPI.Query<RefRO<GhostOwner>>().WithAll<PlayerTag>().WithEntityAccess())
|
|
playerByNet[owner.ValueRO.NetworkId] = e;
|
|
|
|
uint until = TickUtil.NonZero(serverTick.TickIndexForValidTick + (uint)math.max(1, Tuning.FrenzyDurationTicks));
|
|
|
|
foreach (var (dying, corpse) in SystemAPI.Query<RefRW<Dying>>().WithAll<EnemyTag>().WithEntityAccess())
|
|
{
|
|
if (dying.ValueRO.Rewarded != 0)
|
|
continue;
|
|
dying.ValueRW.Rewarded = 1; // mark ONCE — idempotent even when the killer can't be resolved
|
|
|
|
int killerNet = dying.ValueRO.KillerNetId;
|
|
if (killerNet < 0 || !playerByNet.TryGetValue(killerNet, out var killer))
|
|
continue;
|
|
if (!m_Fx.HasComponent(killer))
|
|
continue;
|
|
byte flags = m_Fx[killer].Flags;
|
|
|
|
// Siphon: heal the killer, clamped to their effective max (no over-heal; skip a corpse killer).
|
|
if ((flags & BoonFlag.Siphon) != 0 && m_Health.HasComponent(killer))
|
|
{
|
|
var h = m_Health[killer];
|
|
if (h.Current > 0f)
|
|
{
|
|
float max = m_EffChar.HasComponent(killer) ? m_EffChar[killer].MaxHealth : h.Max;
|
|
h.Current = math.min(h.Current + k_SiphonHeal, max);
|
|
m_Health[killer] = h;
|
|
}
|
|
}
|
|
|
|
// Frenzy: refresh (never stack) a short cooldown-reduction buff on the killer.
|
|
if ((flags & BoonFlag.Frenzy) != 0 && m_Mods.HasBuffer(killer) && m_Timed.HasBuffer(killer))
|
|
{
|
|
TimedModifierUtil.Upsert(m_Mods[killer], m_Timed[killer], Tuning.FrenzySourceId,
|
|
(byte)StatTarget.CooldownTicks, (byte)ModOp.PercentMult, Tuning.FrenzyCooldownMult, until);
|
|
}
|
|
}
|
|
|
|
playerByNet.Dispose();
|
|
}
|
|
}
|
|
}
|