using Unity.Entities;
namespace ProjectM.Simulation
{
///
/// SERVER-ONLY expiry tracker paired with a by . It is NOT a
/// [GhostField] and lives in a SEPARATE buffer so the replicated layout stays
/// byte-identical — adding ANY member (even non-ghost) to a [GhostField] buffer element regenerates its
/// serializer/stride/hash = an effective ghost re-bake. To grant a TIMED buff, append both a StatModifier and a
/// TimedModifier sharing one unique SourceId; TimedModifierExpirySystem removes the matching StatModifier
/// when elapses, and that removal replicates for free via the StatModifier [GhostField]
/// buffer (OwnerSendType.All), so StatRecomputeSystem reverts the effective stat on both worlds with no change.
///
public struct TimedModifier : IBufferElementData
{
/// Matches the this row governs.
public uint SourceId;
/// Server tick at which the paired StatModifier expires (0 = no expiry / inert; schedule via TickUtil.NonZero).
public uint UntilTick;
}
/// Pure helpers for removing modifiers by provenance (clear-by-type / timed expiry). Deterministic, no RNG/wall-clock.
public static class TimedModifierUtil
{
/// Remove every row whose SourceId matches (RemoveAtSwapBack). Returns the count removed.
public static int RemoveBySourceId(DynamicBuffer mods, uint sourceId)
{
int removed = 0;
for (int j = mods.Length - 1; j >= 0; j--)
if (mods[j].SourceId == sourceId) { mods.RemoveAtSwapBack(j); removed++; }
return removed;
}
/// Remove every row whose SourceId lies in [lo, hiExclusive) — the
/// run-scoped BOON strip (per-pick distinct ids share the band; one call clears the whole run's boons while
/// class/meta/equip bands survive untouched). Idempotent; RemoveAtSwapBack. Returns the count removed.
public static int RemoveBySourceIdRange(DynamicBuffer mods, uint lo, uint hiExclusive)
{
int removed = 0;
for (int j = mods.Length - 1; j >= 0; j--)
if (mods[j].SourceId >= lo && mods[j].SourceId < hiExclusive) { mods.RemoveAtSwapBack(j); removed++; }
return removed;
}
/// Phase 1.7: guarantee EXACTLY ONE row per in BOTH the replicated
/// buffer and this server-only buffer (remove-then-add) so a
/// timed buff REFRESHES (re-stamps ) rather than stacking on a repeat grant. Used by
/// KillRewardSystem for Frenzy so successive kills extend the surge instead of compounding the modifier.
public static void Upsert(DynamicBuffer mods, DynamicBuffer timed,
uint sourceId, byte target, byte op, float value, uint untilTick)
{
RemoveBySourceId(mods, sourceId);
for (int j = timed.Length - 1; j >= 0; j--)
if (timed[j].SourceId == sourceId) timed.RemoveAtSwapBack(j);
mods.Add(new StatModifier { Target = target, Op = op, Value = value, SourceId = sourceId });
timed.Add(new TimedModifier { SourceId = sourceId, UntilTick = untilTick });
}
/// Phase 1.7: remove every server-only row matching
/// (the paired is cleared separately — e.g. the Returning boon-band range-strip). This
/// closes the cross-run gap where a stale Frenzy timed row could outlive its StatModifier. Returns the count removed.
public static int RemoveBySourceId(DynamicBuffer timed, uint sourceId)
{
int removed = 0;
for (int j = timed.Length - 1; j >= 0; j--)
if (timed[j].SourceId == sourceId) { timed.RemoveAtSwapBack(j); removed++; }
return removed;
}
}
}