Attack Boon Changes
This commit is contained in:
@@ -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<Simulate>() 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);
|
||||
|
||||
Reference in New Issue
Block a user