Files
Project-M/Assets/_Project/Scripts/Simulation/Combat/TimedModifier.cs
T
2026-07-13 18:30:41 -07:00

74 lines
4.3 KiB
C#

using Unity.Entities;
namespace ProjectM.Simulation
{
/// <summary>
/// SERVER-ONLY expiry tracker paired with a <see cref="StatModifier"/> by <see cref="SourceId"/>. It is NOT a
/// [GhostField] and lives in a SEPARATE buffer so the replicated <see cref="StatModifier"/> 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; <c>TimedModifierExpirySystem</c> removes the matching StatModifier
/// when <see cref="UntilTick"/> 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.
/// </summary>
public struct TimedModifier : IBufferElementData
{
/// <summary>Matches the <see cref="StatModifier.SourceId"/> this row governs.</summary>
public uint SourceId;
/// <summary>Server tick at which the paired StatModifier expires (0 = no expiry / inert; schedule via TickUtil.NonZero).</summary>
public uint UntilTick;
}
/// <summary>Pure helpers for removing modifiers by provenance (clear-by-type / timed expiry). Deterministic, no RNG/wall-clock.</summary>
public static class TimedModifierUtil
{
/// <summary>Remove every <see cref="StatModifier"/> row whose SourceId matches (RemoveAtSwapBack). Returns the count removed.</summary>
public static int RemoveBySourceId(DynamicBuffer<StatModifier> 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;
}
/// <summary>Remove every <see cref="StatModifier"/> 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.</summary>
public static int RemoveBySourceIdRange(DynamicBuffer<StatModifier> 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;
}
/// <summary>Phase 1.7: guarantee EXACTLY ONE row per <paramref name="sourceId"/> in BOTH the replicated
/// <see cref="StatModifier"/> buffer and this server-only <see cref="TimedModifier"/> buffer (remove-then-add) so a
/// timed buff REFRESHES (re-stamps <paramref name="untilTick"/>) rather than stacking on a repeat grant. Used by
/// <c>KillRewardSystem</c> for Frenzy so successive kills extend the surge instead of compounding the modifier.</summary>
public static void Upsert(DynamicBuffer<StatModifier> mods, DynamicBuffer<TimedModifier> 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 });
}
/// <summary>Phase 1.7: remove every server-only <see cref="TimedModifier"/> row matching <paramref name="sourceId"/>
/// (the paired <see cref="StatModifier"/> 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.</summary>
public static int RemoveBySourceId(DynamicBuffer<TimedModifier> 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;
}
}
}