G6+G4: Cone damage-at-contact + zone fill telegraph (ZoneEffect GhostFields) + co-op saturation budget
Cone/SpecialSlam damage lands at its visual contact via ConeContactPending (knob 32, 0=legacy; early-flush + resolve re-validate; death + BOTH class-swap paths drop armed pendings — fixes the shipped melee death-strand in the same stroke). Zones: [GhostField] Caster/Radius/NextTick + ZoneTelegraphSystem (Geyser latch contract on InterpolationTick; rim = true radius; arm grows, persistent phase drains). Cone cues latch to contact (FireStartRaw, C14); TuningConfig.Defaults() fallback at client cue sites (release-build timing fix). G4: SaturationMath ally-FX degrade (living-enemy census, solo-exempt; enemy telegraphs structurally exempt) + CombatStressDebug + overlay saturation rows. Reviews wf_98bf1268 (13 confirmed folded) / wf_9757d214 (5 confirmed fixed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
using Unity.Entities;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>07-21 G6 (socket honesty, design review wf_98bf1268): SERVER-ONLY scheduled Cone-socket cleave —
|
||||
/// the <see cref="MeleeCleavePending"/> schedule-and-consume idiom applied to the SpecialSlam (Cone archetype)
|
||||
/// socket, so its damage lands at the animation's visual contact instead of the fire tick. Stamped by
|
||||
/// AbilityFireSystem when a Cone socket fires (ResolveTick = fire + TuningConfig.ConeContactTicks; knob 0 =
|
||||
/// legacy immediate, no stamp), fired on a wrap-safe elapsed compare from LIVE state (cast-turn steers the
|
||||
/// cone until contact; stats re-folded at resolve), CONSUMED by zeroing. A still-armed pending is FLUSHED
|
||||
/// (fired early) before a recast overwrites it. Cleared on death (PlayerDeathStateSystem — an armed pending
|
||||
/// must never fire from the respawn position) and by the dev SetClass swap (socket identity changes).
|
||||
/// At resolve the socket is re-validated (bounds + still a Cone Spark) — consume-drop on mismatch.
|
||||
/// Not replicated; client contact cues are presentation-side (CombatFeedbackSystem). Baked zeroed.</summary>
|
||||
public struct ConeContactPending : IComponentData
|
||||
{
|
||||
/// <summary>NonZero tick the armed cone's slam lands; 0 = nothing pending.</summary>
|
||||
public uint ResolveTick;
|
||||
/// <summary>Socket index (0..SocketId.Count-1) of the ARMED cast — stats/aim re-read live at resolve.</summary>
|
||||
public byte Socket;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8e1f7f8852ab4e548babedafccbc6084
|
||||
@@ -1,32 +1,42 @@
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// SERVER-ONLY periodic-AoE state on a player-cast zone ghost (Vortex / LightZone) — the LANTERN Aoe/zone
|
||||
/// archetype. NOT a <c>[GhostField]</c>: the zone is a server-spawned INTERPOLATED ownerless ghost whose
|
||||
/// position replicates via the stock LocalTransform variant; only its damage schedule lives here, server-side.
|
||||
/// <c>ZonePulseSystem</c> borrows the <c>GeyserEruptSystem</c> skeleton (the INVERTED invalid-tick guard so a
|
||||
/// born-0 tick never storm-fires + the <c>= now + period</c> reschedule) but is ENEMY-ONLY (no friendly fire),
|
||||
/// stamps <see cref="DamageEvent.SourceNetworkId"/> = <see cref="CasterNetworkId"/> (kills credit the caster,
|
||||
/// feeding KillRewardSystem — NOT the -1 environment convention), and has NO region gate (a no-world gym).
|
||||
/// DestroyEntity when <see cref="ExpireTick"/> elapses.
|
||||
/// Periodic-AoE state on a player-cast zone ghost (Vortex / LightZone) — the LANTERN Aoe/zone archetype.
|
||||
/// The zone is a server-spawned INTERPOLATED ownerless ghost; position replicates via the stock LocalTransform
|
||||
/// variant. 07-21 G6 (design review wf_98bf1268): THREE fields are now <c>[GhostField]</c> so the client can
|
||||
/// draw an HONEST fill telegraph (ZoneTelegraphSystem): <see cref="CasterNetworkId"/> (G3 ownership tint),
|
||||
/// <see cref="Radius"/> (rim = the TRUE folded damage radius) and <see cref="NextTick"/> (fill arrival = the
|
||||
/// damage moment — server mutations propagate to all clients on an ownerless ghost). Damage schedule/amount
|
||||
/// stay server-only. <c>ZonePulseSystem</c> borrows the <c>GeyserEruptSystem</c> skeleton (the INVERTED
|
||||
/// invalid-tick guard so a born-0 tick never storm-fires + the <c>= now + period</c> reschedule) but is
|
||||
/// ENEMY-ONLY (no friendly fire), stamps <see cref="DamageEvent.SourceNetworkId"/> = <see cref="CasterNetworkId"/>
|
||||
/// (kills credit the caster, feeding KillRewardSystem — NOT the -1 environment convention), and has NO region
|
||||
/// gate (a no-world gym). DestroyEntity when <see cref="ExpireTick"/> elapses.
|
||||
/// </summary>
|
||||
public struct ZoneEffect : IComponentData
|
||||
{
|
||||
/// <summary>NetworkId of the casting player (damage/kill attribution).</summary>
|
||||
public int CasterNetworkId;
|
||||
/// <summary>Pulse cadence in ticks — single source for ZonePulseSystem (damage) and ZoneTelegraphSystem
|
||||
/// (the client fill window). Hoisted 07-21 G6 (was ZonePulseSystem.k_PulsePeriodTicks).</summary>
|
||||
public const uint PulsePeriodTicks = 30;
|
||||
|
||||
/// <summary>Planar (XZ) damage radius, world units.</summary>
|
||||
public float Radius;
|
||||
/// <summary>NetworkId of the casting player (damage/kill attribution + the client's mine-vs-ally tint).</summary>
|
||||
[GhostField] public int CasterNetworkId;
|
||||
|
||||
/// <summary>Damage dealt to each living enemy in radius per pulse.</summary>
|
||||
/// <summary>Planar (XZ) damage radius, world units. Replicated so the drawn rim is the TRUE folded radius.</summary>
|
||||
[GhostField(Quantization = 100)] public float Radius;
|
||||
|
||||
/// <summary>Raw next-pulse tick (NonZero). <c>0</c> = unscheduled → lazy-stamped born-correct (GeyserErupt H2
|
||||
/// rule, never storm-fires). Replicated for the client fill: ride the ABSOLUTE tick + a value-latch + a
|
||||
/// was-counting-down arm-guard (the Geyser ★ rule — never edge-detect the re-stamp).</summary>
|
||||
[GhostField] public uint NextTick;
|
||||
|
||||
/// <summary>Damage dealt to each living enemy in radius per pulse. Server-only.</summary>
|
||||
public float DamagePerPulse;
|
||||
|
||||
/// <summary>Raw next-pulse tick (NonZero). <c>0</c> = unscheduled → lazy-stamped born-correct (GeyserErupt H2 rule, never storm-fires).</summary>
|
||||
public uint NextTick;
|
||||
|
||||
/// <summary>Raw tick the zone despawns (NonZero). Active while .IsNewerThan(serverTick).</summary>
|
||||
/// <summary>Raw tick the zone despawns (NonZero). Active while .IsNewerThan(serverTick). Server-only.</summary>
|
||||
public uint ExpireTick;
|
||||
|
||||
/// <summary>See <see cref="ZoneEffectFlag"/>. bit0 = Vortex (also pull enemies toward the zone center each pulse).</summary>
|
||||
|
||||
@@ -35,12 +35,22 @@ namespace ProjectM.Simulation
|
||||
public static bool FireActive(uint nextFireRaw, int cooldownTicks, NetworkTick serverTick, uint animTicks)
|
||||
{
|
||||
if (nextFireRaw == 0u || cooldownTicks <= 0 || !serverTick.IsValid) return false;
|
||||
uint startRaw = TickUtil.NonZero(nextFireRaw - (uint)cooldownTicks);
|
||||
uint startRaw = FireStartRaw(nextFireRaw, cooldownTicks);
|
||||
var start = new NetworkTick(startRaw);
|
||||
var end = new NetworkTick(TickUtil.NonZero(startRaw + animTicks));
|
||||
return start.IsValid && end.IsValid && !start.IsNewerThan(serverTick) && end.IsNewerThan(serverTick);
|
||||
}
|
||||
|
||||
/// <summary>Reconstructed fire-window START (raw NonZero tick) from the replicated per-socket cooldown
|
||||
/// stamp minus the locally-derived CooldownTicks; 0 = no window (unstamped / degenerate inputs). The ONE
|
||||
/// home of this reconstruction (07-21 G6 review wf_98bf1268: the cone connect-cue latches
|
||||
/// contact = FireStartRaw + ConeContactTicks at the fire edge — never re-derive it inline).</summary>
|
||||
public static uint FireStartRaw(uint nextFireRaw, int cooldownTicks)
|
||||
{
|
||||
if (nextFireRaw == 0u || cooldownTicks <= 0) return 0u;
|
||||
return TickUtil.NonZero(nextFireRaw - (uint)cooldownTicks);
|
||||
}
|
||||
|
||||
/// <summary>LANTERN 4-socket fire/cone resolution (any-socket model): firing if ANY socketed,
|
||||
/// NON-Movement Spark's per-socket window is mid-fire; cone if any such active socket holds a
|
||||
/// Cone-archetype Spark. Movement sockets are skipped OUTRIGHT (blink = dodge, not cast). Without a
|
||||
|
||||
@@ -53,6 +53,10 @@ namespace ProjectM.Simulation
|
||||
public float MeleeBufferTicks;
|
||||
/// <summary>Base contact delay (ticks from swing start to the blade landing; per-step via MeleeTiming). 0 = IMMEDIATE (legacy same-tick cleave).</summary>
|
||||
public float MeleeContactTicks;
|
||||
/// <summary>07-21 G6 (socket honesty): ticks from the Cone-socket fire tick to its damage landing (the
|
||||
/// SpecialSlam visual contact). 0 = IMMEDIATE (legacy at-fire cleave). Keep < CastFacingTicks (26) or a
|
||||
/// resting gamepad stick resolves a movement-facing cone; default 21 sits 1 tick under WarriorCone cooldown 22.</summary>
|
||||
public float ConeContactTicks;
|
||||
|
||||
// EB-1 fortress aggro: a <1 multiplier on a Husk's SQUARED distance to a structure (so structures are
|
||||
// preferred targets); a closer player 'in the way' still wins. Read server-side by EnemyAISystem.
|
||||
@@ -96,6 +100,7 @@ namespace ProjectM.Simulation
|
||||
MeleeFinisherRangeMult = 1.25f, // 07-20 G2.2: finisher REACH (damage/recover/knockback keep MeleeFinisherMult)
|
||||
MeleeBufferTicks = 10f, // 07-21 HEAVY LOCK: wider buffer fits the slower cadence (0 = off)
|
||||
MeleeContactTicks = 20f, // 07-21 HEAVY LOCK: blade lands 20/12/25t per step -- clip speeds retuned to MATCH (wire tool 1.0/0.85/0.88; audit must show zero drift). Step-3 (25t) sits 1 tick under CastFacingTicks 26 -- do NOT raise without raising the cast window.
|
||||
ConeContactTicks = 21f, // 07-21 G6: SpecialSlam contact ~0.35s after fire; keep < CastFacingTicks 26
|
||||
StructureAggroWeight = 0.7f, // EB-1: <1 prefers structures (fortress aggro); live-tunable
|
||||
StaggerKnockbackSpeed = 7f, // B2 poise: kb.Speed >= this interrupts windups/lunges; below = nudge only
|
||||
SeparationMaxSpeed = 3f, // B1: max separation push (units/s) so soft-collision can't fling
|
||||
@@ -130,6 +135,7 @@ namespace ProjectM.Simulation
|
||||
case TuningKnob.MeleeFinisherRangeMult:
|
||||
case TuningKnob.MeleeBufferTicks: // 07-20: 0 = buffer OFF sentinel (must survive ClampKnob — review C10)
|
||||
case TuningKnob.MeleeContactTicks: // 07-20: 0 = IMMEDIATE sentinel (legacy same-tick cleave)
|
||||
case TuningKnob.ConeContactTicks: // 07-21 G6: 0 = IMMEDIATE sentinel (legacy at-fire cone)
|
||||
return math.max(0f, value);
|
||||
// tick knobs: >= 1 (a 0 tick count is degenerate; a 0 i-frame window divides-by-zero in DashSystem).
|
||||
// FinalSiegeMultiplier also lands here on purpose — a final siege should never be < 1x a normal one.
|
||||
@@ -172,6 +178,7 @@ namespace ProjectM.Simulation
|
||||
case TuningKnob.MeleeBufferTicks: c.MeleeBufferTicks = value; break;
|
||||
case TuningKnob.MeleeFinisherRangeMult: c.MeleeFinisherRangeMult = value; break;
|
||||
case TuningKnob.MeleeContactTicks: c.MeleeContactTicks = value; break;
|
||||
case TuningKnob.ConeContactTicks: c.ConeContactTicks = value; break;
|
||||
|
||||
// unknown index -> no-op (matches the no-default switch convention in DebugCommandReceiveSystem)
|
||||
}
|
||||
@@ -210,6 +217,7 @@ namespace ProjectM.Simulation
|
||||
case TuningKnob.MeleeBufferTicks: return c.MeleeBufferTicks;
|
||||
case TuningKnob.MeleeFinisherRangeMult: return c.MeleeFinisherRangeMult;
|
||||
case TuningKnob.MeleeContactTicks: return c.MeleeContactTicks;
|
||||
case TuningKnob.ConeContactTicks: return c.ConeContactTicks;
|
||||
|
||||
default: return 0f;
|
||||
}
|
||||
@@ -246,6 +254,7 @@ namespace ProjectM.Simulation
|
||||
MeleeBufferTicks = c.MeleeBufferTicks,
|
||||
MeleeFinisherRangeMult = c.MeleeFinisherRangeMult,
|
||||
MeleeContactTicks = c.MeleeContactTicks,
|
||||
ConeContactTicks = c.ConeContactTicks,
|
||||
|
||||
};
|
||||
|
||||
@@ -280,6 +289,7 @@ namespace ProjectM.Simulation
|
||||
MeleeBufferTicks = r.MeleeBufferTicks,
|
||||
MeleeFinisherRangeMult = r.MeleeFinisherRangeMult,
|
||||
MeleeContactTicks = r.MeleeContactTicks,
|
||||
ConeContactTicks = r.ConeContactTicks,
|
||||
|
||||
};
|
||||
}
|
||||
@@ -318,9 +328,10 @@ namespace ProjectM.Simulation
|
||||
public const byte MeleeBufferTicks = 29; // 07-20 G7 (0 = off sentinel)
|
||||
public const byte MeleeFinisherRangeMult = 30; // 07-20 G2.2
|
||||
public const byte MeleeContactTicks = 31; // 07-20 G2.1 (0 = immediate sentinel)
|
||||
public const byte ConeContactTicks = 32; // 07-21 G6 (0 = immediate sentinel)
|
||||
|
||||
/// <summary>Knob count (overlay iteration bound).</summary>
|
||||
public const byte Count = 32;
|
||||
public const byte Count = 33;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -359,6 +370,7 @@ namespace ProjectM.Simulation
|
||||
public float MeleeBufferTicks; // 07-20 melee feel forks (dev-protocol bump: rebuild both peers)
|
||||
public float MeleeFinisherRangeMult;
|
||||
public float MeleeContactTicks;
|
||||
public float ConeContactTicks; // 07-21 G6 socket honesty (dev-protocol bump: rebuild both peers)
|
||||
}
|
||||
|
||||
// NOTE: appending fields = a DEV-PROTOCOL BUMP (RpcCollection hash) — rebuild both peers together.
|
||||
|
||||
@@ -45,6 +45,14 @@ namespace ProjectM.Simulation
|
||||
// MC-4: clear any in-flight combo so a death mid-combo leaves no stale lock/step on respawn.
|
||||
if (SystemAPI.HasComponent<MeleeCombo>(entity))
|
||||
SystemAPI.SetComponent(entity, default(MeleeCombo));
|
||||
// 07-21 G6 (review wf_98bf1268): zero any ARMED scheduled cleave — a pending surviving death
|
||||
// fires from the RESPAWN position once Dead re-disables (the elapsed compare passes instantly).
|
||||
// Fixes the shipped melee variant + the new cone in one stroke. Idempotent default-writes;
|
||||
// client copies are always zero, so the both-world predicted write is rollback-safe.
|
||||
if (SystemAPI.HasComponent<MeleeCleavePending>(entity))
|
||||
SystemAPI.SetComponent(entity, default(MeleeCleavePending));
|
||||
if (SystemAPI.HasComponent<ConeContactPending>(entity))
|
||||
SystemAPI.SetComponent(entity, default(ConeContactPending));
|
||||
if (SystemAPI.HasComponent<CharacterComponent>(entity))
|
||||
{
|
||||
var cc = SystemAPI.GetComponent<CharacterComponent>(entity);
|
||||
|
||||
Reference in New Issue
Block a user