Boss becomes a real fight + readable-but-fair enemy threat
BossState (server-only) + BossAISystem: sole mover, seek -> telegraphed radial slam (AttackWindup) -> phase-2 speed + swarmer summon. Excluded from the EnemyAISystem Charger pass; knockback-immune at all 3 stamp sites; arena-anchored spawn; scaled Health/HitRadius/AttackRange. Grunt windup now commits in its last ~30% (whiffs out-of-range); Charger stagger roots; grunt speed 4.2->5.2; swarmer telegraph synced. SweptMove + zone-enemy spawn-stamp extracted to shared helpers. Zero new [GhostField]. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -37,11 +37,17 @@ namespace ProjectM.Simulation
|
||||
[BurstCompile]
|
||||
public partial struct AbilityFireSystem : ISystem
|
||||
{
|
||||
// C3/A4: knockback stamp for the Warrior CONE (guarded HasComponent + boss-immune). Server-only use.
|
||||
ComponentLookup<KnockbackState> m_KnockbackLookup;
|
||||
ComponentLookup<BossState> m_BossLookup;
|
||||
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<AbilityDatabase>();
|
||||
state.RequireForUpdate<NetworkTime>();
|
||||
m_KnockbackLookup = state.GetComponentLookup<KnockbackState>(isReadOnly: false);
|
||||
m_BossLookup = state.GetComponentLookup<BossState>(isReadOnly: true);
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
@@ -63,6 +69,8 @@ namespace ProjectM.Simulation
|
||||
var abilityDb = SystemAPI.GetSingleton<AbilityDatabase>();
|
||||
|
||||
bool isServer = state.WorldUnmanaged.IsServer();
|
||||
m_KnockbackLookup.Update(ref state);
|
||||
m_BossLookup.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.
|
||||
@@ -132,6 +140,19 @@ namespace ProjectM.Simulation
|
||||
SourceNetworkId = owner.ValueRO.NetworkId,
|
||||
SourceTick = cStamp,
|
||||
});
|
||||
// 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).
|
||||
if (m_KnockbackLookup.HasComponent(coneTargets[ci]) && !m_BossLookup.HasComponent(coneTargets[ci]))
|
||||
{
|
||||
float3 kd3 = coneTargetPos[ci] - xform.ValueRO.Position;
|
||||
float2 kdir = math.lengthsq(kd3.xz) > 1e-6f ? math.normalize(kd3.xz) : cFace;
|
||||
m_KnockbackLookup[coneTargets[ci]] = new KnockbackState
|
||||
{
|
||||
Dir = kdir,
|
||||
Speed = Tuning.KnockbackSpeed,
|
||||
UntilTick = TickUtil.NonZero(serverTick.TickIndexForValidTick + (uint)math.max(1, Tuning.KnockbackDurationTicks)),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
uint coneCd = (uint)math.max(1, eff.ValueRO.CooldownTicks);
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using Unity.Entities;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// SERVER-ONLY working state for the expedition BOSS (a scaled Charger that <see cref="Server"/>'s
|
||||
/// RoomEnemyDirectorSystem tags at spawn). NOT replicated and NOT baked — added at runtime via ECB on the boss
|
||||
/// entity, so it needs no ghost-hash change (a runtime-added replicated component would not replicate anyway;
|
||||
/// this one is deliberately server-only, like <see cref="LungeState"/>/<see cref="KnockbackState"/>).
|
||||
/// <para>
|
||||
/// Component PRESENCE is the boss discriminator: BossAISystem is the SOLE mover/attacker of
|
||||
/// <c>.WithAll<EnemyTag, BossState>()</c>, and EnemyAISystem's Charger MOVE pass excludes it via
|
||||
/// <c>.WithNone<BossState>()</c> so exactly one system writes the boss's Position/AttackWindup. The boss does
|
||||
/// NOT use LungeState (its signature move is a telegraphed radial SLAM, not a lunge) — so EnemyAISystem's
|
||||
/// IsLunging derive visits it but sees <c>LungeState.UntilTick==0</c> and derives the bit off (harmless single
|
||||
/// writer). <see cref="Phase"/> is a byte (never a C# enum on a Bursted path — the cross-assembly-enum ICE rule).
|
||||
/// All tick fields route through <c>TickUtil.NonZero</c> and compare with <see cref="Unity.NetCode.NetworkTick"/>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public struct BossState : IComponentData
|
||||
{
|
||||
/// <summary>1 = phase one (heavy Charger + slam), 2 = phase two (<50% HP: faster + summons adds). Byte, not enum.</summary>
|
||||
public byte Phase;
|
||||
|
||||
/// <summary>Earliest raw tick the boss may begin its next radial SLAM wind-up (NonZero; 0 = ready).</summary>
|
||||
public uint SlamReadyTick;
|
||||
|
||||
/// <summary>Earliest raw tick the boss may summon its next add pack (phase two only; NonZero; 0 = ready).</summary>
|
||||
public uint SummonReadyTick;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fdf576e1f07162e43bae89c4ccc06dec
|
||||
@@ -49,6 +49,8 @@ namespace ProjectM.Simulation
|
||||
public partial struct MeleeComboSystem : ISystem
|
||||
{
|
||||
ComponentLookup<KnockbackState> m_KnockbackLookup;
|
||||
ComponentLookup<BossState> m_BossLookup; // A4: the boss is knockback-immune (no melee stunlock out of its slams)
|
||||
|
||||
ComponentLookup<RegionTag> m_RegionLookup;
|
||||
BufferLookup<InventorySlot> m_InvLookup;
|
||||
BufferLookup<StatModifier> m_StatModLookup;
|
||||
@@ -57,6 +59,8 @@ namespace ProjectM.Simulation
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
m_KnockbackLookup = state.GetComponentLookup<KnockbackState>(isReadOnly: false);
|
||||
m_BossLookup = state.GetComponentLookup<BossState>(isReadOnly: true);
|
||||
|
||||
m_RegionLookup = state.GetComponentLookup<RegionTag>(isReadOnly: true);
|
||||
m_InvLookup = state.GetBufferLookup<InventorySlot>(isReadOnly: false);
|
||||
m_StatModLookup = state.GetBufferLookup<StatModifier>(isReadOnly: true);
|
||||
@@ -224,6 +228,8 @@ namespace ProjectM.Simulation
|
||||
|
||||
|
||||
m_KnockbackLookup.Update(ref state);
|
||||
m_BossLookup.Update(ref state);
|
||||
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
for (int s = 0; s < cleaves.Length; s++)
|
||||
{
|
||||
@@ -239,7 +245,7 @@ namespace ProjectM.Simulation
|
||||
SourceNetworkId = c.OwnerId,
|
||||
SourceTick = c.Stamp,
|
||||
});
|
||||
if (c.KnockSpeed > 0f && m_KnockbackLookup.HasComponent(target))
|
||||
if (c.KnockSpeed > 0f && m_KnockbackLookup.HasComponent(target) && !m_BossLookup.HasComponent(target))
|
||||
{
|
||||
float3 d3 = enemyPositions[i] - c.From;
|
||||
float2 kdir = new float2(d3.x, d3.z);
|
||||
|
||||
@@ -72,7 +72,7 @@ namespace ProjectM.Simulation
|
||||
/// <summary>DR-042 C6c: Ore deposited into the shared ledger at spawn on a NEW game ONLY (a restored save keeps
|
||||
/// its persisted ledger). Bootstraps the Fabricator(30)->Charge->Turret(10) chain so a turret placed before any
|
||||
/// mining isn't a silent cold deadlock. Ore-only so the 'build a Fabricator to arm turrets' lesson survives.</summary>
|
||||
public const int StartingOre = 50;
|
||||
public const int StartingOre = 90;
|
||||
|
||||
// ---- Expedition run economy (RoomFieldSystem / RunDirectorSystem) ----
|
||||
|
||||
@@ -95,6 +95,41 @@ namespace ProjectM.Simulation
|
||||
/// polish; an idle player died in ~2 s of landing). Subsequent slots keep the normal drip cadence.</summary>
|
||||
public const uint RoomEntryGraceTicks = 100;
|
||||
|
||||
// ---- Expedition BOSS (a scaled Charger given a real kit by BossAISystem; server-only feel consts) ----
|
||||
|
||||
/// <summary>Radial SLAM AoE radius (world units): a player inside this ring at wind-up elapse eats the hit
|
||||
/// unless dashing (i-frames). Sized well above melee reach so the tell reads as a boss-scale ring.</summary>
|
||||
public const float BossSlamRadius = 5.5f;
|
||||
|
||||
/// <summary>Radial SLAM damage. Heavy but survivable given generous i-frames + respawn (readable-but-fair).</summary>
|
||||
public const float BossSlamDamage = 34f;
|
||||
|
||||
/// <summary>SLAM telegraph lead (~0.75 s @60): a fair dodge window under interp lag before the ring lands.</summary>
|
||||
public const uint BossSlamWindupTicks = 45;
|
||||
|
||||
/// <summary>Ticks between SLAMs in phase one (~4 s). Phase two multiplies this by BossPhase2SlamCooldownMult.</summary>
|
||||
public const uint BossSlamCooldownTicks = 240;
|
||||
|
||||
/// <summary>Health fraction at/below which the boss enters phase two (faster + summons adds).</summary>
|
||||
public const float BossPhase2HealthFraction = 0.5f;
|
||||
|
||||
/// <summary>Phase-two move-speed multiplier over the boss's base EnemyStats.MoveSpeed (a visible gear shift).</summary>
|
||||
public const float BossPhase2SpeedMult = 1.45f;
|
||||
|
||||
/// <summary>Phase-two SLAM cooldown multiplier (<1 = slams more often when enraged).</summary>
|
||||
public const float BossPhase2SlamCooldownMult = 0.6f;
|
||||
|
||||
/// <summary>Swarmers summoned per phase-two summon (spawned around the boss, tagged as room zone enemies).</summary>
|
||||
public const int BossSummonCount = 3;
|
||||
|
||||
/// <summary>Ticks between phase-two summons (~7 s).</summary>
|
||||
public const uint BossSummonCooldownTicks = 420;
|
||||
|
||||
/// <summary>Cap on live zone enemies the boss will summon toward — it holds fire above this so a boss room
|
||||
/// never floods past readable (also bounded by the director's MaxAlive count).</summary>
|
||||
public const int BossSummonLiveCap = 8;
|
||||
|
||||
|
||||
// ---- Inventory (per-player bag; InventoryMath / ResourceHarvestSystem / InventoryDepositSystem) ----
|
||||
|
||||
/// <summary>Max stacks a player can carry; InventoryMath rejects deposits past this and the harvest remainder spills to the global ledger.</summary>
|
||||
|
||||
Reference in New Issue
Block a user