using ProjectM.Simulation;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.NetCode;
namespace ProjectM.Server
{
///
/// Phase 1.7 on-kill boons. When HealthApplyDamageSystem stamps an enemy it records the
/// crediting player's NetworkId; this system grants that killer their on-kill boons ONCE per corpse:
/// heals the killer (clamped to ) and
/// refreshes a short cooldown-reduction buff ( —
/// re-stamped, never stacked). Idempotent via the latch (a value write, no edge-detect).
///
/// A SEPARATE system (not folded into HealthApplyDamageSystem) because healing the killer needs RW
/// access, which would alias that system's RefRW<Health> victim query. Here the
/// only query is RefRW<Dying> 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.
///
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
[UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
[UpdateAfter(typeof(HealthApplyDamageSystem))]
public partial struct KillRewardSystem : ISystem
{
ComponentLookup m_Fx;
ComponentLookup m_Health;
ComponentLookup m_EffChar;
BufferLookup m_Mods;
BufferLookup m_Timed;
const float k_SiphonHeal = 8f; // HP restored per kill (tunable)
[BurstCompile]
public void OnCreate(ref SystemState state)
{
m_Fx = state.GetComponentLookup(isReadOnly: true);
m_Health = state.GetComponentLookup(isReadOnly: false);
m_EffChar = state.GetComponentLookup(isReadOnly: true);
m_Mods = state.GetBufferLookup(isReadOnly: false);
m_Timed = state.GetBufferLookup(isReadOnly: false);
state.RequireForUpdate();
state.RequireForUpdate(); // only run while a fresh corpse exists
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var serverTick = SystemAPI.GetSingleton().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(8, Allocator.Temp);
foreach (var (owner, e) in SystemAPI.Query>().WithAll().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>().WithAll().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();
}
}
}