Attack Boon Changes

This commit is contained in:
2026-07-13 18:30:41 -07:00
parent 972e0d5b4f
commit 24800f4bcb
34 changed files with 1306 additions and 112 deletions
@@ -21,6 +21,13 @@ namespace ProjectM.Simulation
/// snapshotted into the spawned Projectile, so the downstream move/damage systems are unchanged and
/// predicted + server projectiles match (both folded the same replicated modifiers).
///
/// Phase 1.7 mechanic-changer boons ride the owner-replicated <see cref="BoonEffects"/> (SendToOwner, so the
/// predicting owner has it — the .WithAll&lt;Simulate&gt;() filter means only the owner's own player is processed
/// client-side), read via a ComponentLookup keyed by the player (the query is already at the 7-type SystemAPI
/// limit). FORK fans <c>Fork</c> extra predicted projectiles in a symmetric spread, each with a UNIQUE
/// deterministic SpawnId (fork index packed into the low bits) so classification predicts each. PIERCE/CHAIN/PULL
/// are seeded into the server-only <see cref="ProjectileEffectState"/> at spawn (resolved by ProjectileDamageSystem).
///
/// Determinism / idempotency: the prediction loop re-runs this system on rollback, so all
/// non-idempotent effects (spawning, cooldown advance) are gated behind
/// NetworkTime.IsFirstTimeFullyPredictingTick so they happen exactly once per tick. The absolute
@@ -40,6 +47,11 @@ namespace ProjectM.Simulation
// C3/A4: knockback stamp for the Warrior CONE (guarded HasComponent + boss-immune). Server-only use.
ComponentLookup<KnockbackState> m_KnockbackLookup;
ComponentLookup<BossState> m_BossLookup;
// Phase 1.7: owner-replicated mechanic-changer boons, read by the player entity (query is at the 7-type cap).
ComponentLookup<BoonEffects> m_BoonEffectsLookup;
/// <summary>~9° gap between adjacent Split-Shot projectiles (tunable).</summary>
const float k_ForkSpreadRad = 0.157f;
[BurstCompile]
public void OnCreate(ref SystemState state)
@@ -48,6 +60,7 @@ namespace ProjectM.Simulation
state.RequireForUpdate<NetworkTime>();
m_KnockbackLookup = state.GetComponentLookup<KnockbackState>(isReadOnly: false);
m_BossLookup = state.GetComponentLookup<BossState>(isReadOnly: true);
m_BoonEffectsLookup = state.GetComponentLookup<BoonEffects>(isReadOnly: true);
}
[BurstCompile]
@@ -71,6 +84,7 @@ namespace ProjectM.Simulation
bool isServer = state.WorldUnmanaged.IsServer();
m_KnockbackLookup.Update(ref state);
m_BossLookup.Update(ref state);
m_BoonEffectsLookup.Update(ref state);
// Server-only target set (LIVING enemies/dummies), collected once: positions feed the gamepad
// auto-target assist, and entities+positions feed the Warrior CONE archetype's server-only cleave.
@@ -113,6 +127,10 @@ namespace ProjectM.Simulation
continue; // still cooling down
}
// Phase 1.7 mechanic-changer boons (owner-replicated; read by entity — see class doc for the 7-type cap).
BoonEffects bfx = m_BoonEffectsLookup.HasComponent(entity) ? m_BoonEffectsLookup[entity] : default;
bool pull = (bfx.Flags & BoonFlag.KnockToPull) != 0;
// MC-4 spike for MC-6: dispatch on the authored ability ARCHETYPE byte (baked in the blob, read here -- NOT
// folded through EffectiveAbilityStats; it is static identity, not a tunable stat). All current
// abilities are Projectile (0); hitscan/cone/aoe archetypes plug in at this point in MC-6.
@@ -143,9 +161,10 @@ namespace ProjectM.Simulation
});
// C3: the cone reads as weak vs the melee cleave without knockback — stamp it like melee
// (guarded: dummies lack KnockbackState → ECB throw; the boss is knockback-immune, A4).
// Phase 1.7 Gravity Pull: drag toward the player instead of away.
KnockbackUtil.Stamp(ref m_KnockbackLookup, m_BossLookup, coneTargets[ci],
xform.ValueRO.Position, coneTargetPos[ci], cFace, Tuning.KnockbackSpeed,
TickUtil.NonZero(serverTick.TickIndexForValidTick + (uint)math.max(1, Tuning.KnockbackDurationTicks)));
TickUtil.NonZero(serverTick.TickIndexForValidTick + (uint)math.max(1, Tuning.KnockbackDurationTicks)), pull);
}
}
uint coneCd = (uint)math.max(1, eff.ValueRO.CooldownTicks);
@@ -195,28 +214,50 @@ namespace ProjectM.Simulation
candidates);
}
uint spawnId = (uint)owner.ValueRO.NetworkId << 16 | absoluteFireCount;
// Phase 1.7 mechanic-changer seeds. Fork fans (1 + Fork) shots in a symmetric spread; each carries the
// pierce/chain/pull state into its server-only ProjectileEffectState.
byte pierce = bfx.Pierce;
byte chain = bfx.Chain;
byte projFlags = pull ? ProjectileEffectFlag.Pull : (byte)0;
int shots = 1 + math.min((int)bfx.Fork, 8); // cap forks: forkIndex is 4 spawnId bits (no wrap) + a sane spread ceiling
var projectile = ecb.Instantiate(prefab);
float3 planarDir = new float3(dir.x, 0f, dir.y);
float3 spawnPos = xform.ValueRO.Position + planarDir * 0.6f;
spawnPos.y = xform.ValueRO.Position.y;
quaternion rot = quaternion.LookRotationSafe(planarDir, math.up());
ecb.SetComponent(projectile, LocalTransform.FromPositionRotation(spawnPos, rot));
ecb.SetComponent(projectile, new GhostOwner { NetworkId = owner.ValueRO.NetworkId });
// Snapshot the effective ability stats into the projectile (base + modifiers, computed
// identically on both worlds), so the move/damage systems need no modifier lookup.
ecb.SetComponent(projectile, new Projectile
for (int s = 0; s < shots; s++)
{
Direction = math.normalize(dir),
SpawnId = spawnId,
Speed = eff.ValueRO.ProjectileSpeed,
Damage = eff.ValueRO.Damage,
Range = eff.ValueRO.Range,
DistanceTravelled = 0f,
});
// Symmetric fan around the (assisted) aim heading; s=0 is centred when there is no fork.
float offset = (s - (shots - 1) * 0.5f) * k_ForkSpreadRad;
math.sincos(offset, out float sa, out float ca);
float2 sdir = math.normalize(new float2(dir.x * ca - dir.y * sa, dir.x * sa + dir.y * ca));
// Unique deterministic classification key: owner(16) | fireCount(12) | forkIndex(4).
uint spawnId = (((uint)owner.ValueRO.NetworkId) << 16) | ((absoluteFireCount & 0x0FFFu) << 4) | (uint)(s & 0xF);
var projectile = ecb.Instantiate(prefab);
float3 planarDir = new float3(sdir.x, 0f, sdir.y);
float3 spawnPos = xform.ValueRO.Position + planarDir * 0.6f;
spawnPos.y = xform.ValueRO.Position.y;
quaternion rot = quaternion.LookRotationSafe(planarDir, math.up());
ecb.SetComponent(projectile, LocalTransform.FromPositionRotation(spawnPos, rot));
ecb.SetComponent(projectile, new GhostOwner { NetworkId = owner.ValueRO.NetworkId });
// Snapshot the effective ability stats into the projectile (base + modifiers, computed
// identically on both worlds), so the move/damage systems need no modifier lookup.
ecb.SetComponent(projectile, new Projectile
{
Direction = sdir,
SpawnId = spawnId,
Speed = eff.ValueRO.ProjectileSpeed,
Damage = eff.ValueRO.Damage,
Range = eff.ValueRO.Range,
DistanceTravelled = 0f,
});
// Server-only pierce/chain/pull seed (baked inert on the prefab; harmless on the client copy).
ecb.SetComponent(projectile, new ProjectileEffectState
{
PierceRemaining = pierce,
ChainRemaining = chain,
Flags = projFlags,
});
}
// Earliest raw tick the player may fire again. Clamp cooldown to >= 1 tick.
uint cooldownTicks = (uint)math.max(1, eff.ValueRO.CooldownTicks);