Slice Combat Depth (MC-2): enemy-variety server spine — Spitter, Swarmer, 4-type mix (DR-041)
Adds the server-authoritative mechanics for three new enemy archetypes on top of the Grunt/Charger base, plus the weighted wave-composition that introduces them: - Spitter: a ranged Husk variant (SpitterState) that holds a preferred range-band (advance/retreat/hold via EnemyAIMath.BandVelocity) and fires a telegraphed, dodgeable EnemyProjectile. New server EnemyProjectileMoveSystem (integrate + store LastStep) + EnemyProjectileDamageSystem (region-filtered swept hit-test rebuilt from LastStep — DR-018 anti-tunnelling; players use HitRadius, structures a const radius; at-most-once destroy). Concurrent-spit soft cap, soft-fail retry. - Swarmer: marker tag + deterministic cluster spawn (1 slot = 1 pack; EnemyAIMath.ClusterOffset), MaxAlive counts ENTITIES so a pack defers if it won't fit. - 4-type weighted mix: MixBands -> ZoneEnemyMath.WaveSlots/KindForSlot/ PackSizeForSlot drives both the expedition director and (fork-4a) the base siege, with a mandatory MaxAlive cap. Legacy WaveSize/IsChargerSlot kept + parity-tested. - Discriminator stays component-presence (no enum in Bursted systems): query- partition guards keep each enemy moved by exactly one EnemyAISystem pass (sole-Position-writer). EnemyTelegraph.IsCharger -> Kind byte for the client cue. New authoring (Spitter/Swarmer/EnemyProjectile) + expanded director authorings with tunable mix/cluster defaults. 13 new EditMode tests (mix composition + legacy parity, band/cluster math, projectile move + cross-region + swept anti-tunnelling regressions); full suite green before commit. Dormant until the prefab/subscene wiring lands (next): the new systems guard on TryGetSingleton/RequireForUpdate, so with no prefabs wired the new types stay inert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -56,12 +56,15 @@ namespace ProjectM.Authoring
|
||||
// EnemyTelegraph even on a Charger (the prefab composes both authorings on one entity); reading the
|
||||
// sibling ChargerAuthoring here avoids a double-AddComponent. WindupTicks = the client danger-ramp
|
||||
// denominator per variant; IsCharger lets the client pick the Charger look (LungeState is server-only).
|
||||
bool isCharger = GetComponent<ChargerAuthoring>() != null;
|
||||
AddComponent(entity, new EnemyTelegraph
|
||||
{
|
||||
WindupTicks = (byte)(isCharger ? 30 : Tuning.AttackWindupTicks),
|
||||
IsCharger = (byte)(isCharger ? 1 : 0),
|
||||
});
|
||||
// Kind byte (client telegraph look) — derived from the sibling variant authoring (EnemyBaker is the
|
||||
// SOLE EnemyTelegraph writer). Grunt=0 / Charger=1 / Spitter=2 / Swarmer=3 (ZoneEnemyMath.Kind*).
|
||||
byte kind = ZoneEnemyMath.KindGrunt;
|
||||
byte windup = (byte)Tuning.AttackWindupTicks;
|
||||
var spitter = GetComponent<SpitterAuthoring>();
|
||||
if (GetComponent<ChargerAuthoring>() != null) { kind = ZoneEnemyMath.KindCharger; windup = 30; }
|
||||
else if (spitter != null) { kind = ZoneEnemyMath.KindSpitter; windup = (byte)Mathf.Clamp(spitter.WindupTicks, 1, 255); }
|
||||
else if (GetComponent<SwarmerAuthoring>() != null) { kind = ZoneEnemyMath.KindSwarmer; windup = 6; }
|
||||
AddComponent(entity, new EnemyTelegraph { WindupTicks = windup, Kind = kind });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Authoring
|
||||
{
|
||||
/// <summary>
|
||||
/// MC-2 — authoring for the hostile Spitter projectile prefab (an ownerless INTERPOLATED ghost, duplicated from an
|
||||
/// existing interpolated ghost so the GhostAuthoringComponent comes free). Bakes <see cref="EnemyProjectile"/> with
|
||||
/// the spit's default Speed/Damage/Range; the firing Spitter OVERRIDES Direction + Speed + Damage + Region at spawn
|
||||
/// and ADDS the <c>RegionTag</c> (so this prefab MUST NOT bake RegionTag — AddComponent would throw on a baked one).
|
||||
/// NO Health (so it is invisible to every player hit-test) and NO <c>[GhostField]</c> beyond the stock LocalTransform.
|
||||
/// </summary>
|
||||
public class EnemyProjectileAuthoring : MonoBehaviour
|
||||
{
|
||||
[Min(0f), Tooltip("Default muzzle speed (the firing Spitter overrides this per-variant).")]
|
||||
public float Speed = 11f;
|
||||
|
||||
[Min(0f), Tooltip("Default damage (the firing Spitter overrides this from its AttackDamage).")]
|
||||
public float Damage = 8f;
|
||||
|
||||
[Min(0f), Tooltip("Max travel distance before the spit expires (world units).")]
|
||||
public float Range = 16f;
|
||||
|
||||
private class EnemyProjectileBaker : Baker<EnemyProjectileAuthoring>
|
||||
{
|
||||
public override void Bake(EnemyProjectileAuthoring authoring)
|
||||
{
|
||||
var entity = GetEntity(authoring, TransformUsageFlags.Dynamic);
|
||||
AddComponent(entity, new EnemyProjectile
|
||||
{
|
||||
Speed = authoring.Speed,
|
||||
Damage = authoring.Damage,
|
||||
Range = authoring.Range,
|
||||
Direction = new float2(0f, 1f),
|
||||
DistanceTravelled = 0f,
|
||||
LastStep = 0f,
|
||||
Region = 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ff79c8fbcacb8c34faad37d59836b5ac
|
||||
@@ -0,0 +1,49 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Authoring
|
||||
{
|
||||
/// <summary>
|
||||
/// MC-2 — marks a Husk prefab as a SPITTER variant (the ranged "reposition" question). Compose WITH
|
||||
/// <see cref="EnemyAuthoring"/> on the prefab root: EnemyAuthoring bakes the common Husk components + the spit's
|
||||
/// damage/cooldown (EnemyStats.AttackDamage / AttackCooldownTicks), this bakes the server-only
|
||||
/// <see cref="SpitterState"/> (zeroed NextShotTick = ready). Component-PRESENCE is the discriminator EnemyAISystem
|
||||
/// branches on (no enum); the Grunt + Charger passes exclude it via <c>.WithNone<SpitterState>()</c>. The
|
||||
/// actual spit projectile is a SEPARATE ghost configured by the SpitterProjectilePrefab subscene singleton.
|
||||
/// </summary>
|
||||
public class SpitterAuthoring : MonoBehaviour
|
||||
{
|
||||
[Min(0f), Tooltip("Distance the Spitter tries to hold from its target (band centre).")]
|
||||
public float PreferredRange = 9f;
|
||||
|
||||
[Min(0f), Tooltip("Half-width dead-zone around PreferredRange where it holds and fires.")]
|
||||
public float RangeTolerance = 1.5f;
|
||||
|
||||
[Min(0f), Tooltip("Muzzle speed of the spit (world units/second). Slow enough to be dodgeable at range.")]
|
||||
public float ProjectileSpeed = 11f;
|
||||
|
||||
[Min(0f), Tooltip("If the target closes within this AND the Spitter can't retreat, it fires point-blank.")]
|
||||
public float CorneredRange = 3f;
|
||||
|
||||
[Min(1), Tooltip("Telegraph wind-up before the spit fires (ticks). Keep >= ~24 (> interp delay) to stay dodgeable.")]
|
||||
public int WindupTicks = 26;
|
||||
|
||||
private class SpitterBaker : Baker<SpitterAuthoring>
|
||||
{
|
||||
public override void Bake(SpitterAuthoring authoring)
|
||||
{
|
||||
var entity = GetEntity(authoring, TransformUsageFlags.Dynamic);
|
||||
AddComponent(entity, new SpitterState
|
||||
{
|
||||
PreferredRange = authoring.PreferredRange,
|
||||
RangeTolerance = authoring.RangeTolerance,
|
||||
ProjectileSpeed = authoring.ProjectileSpeed,
|
||||
CorneredRange = authoring.CorneredRange,
|
||||
WindupTicks = authoring.WindupTicks,
|
||||
NextShotTick = 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 55fe00810b31aa54abd577b6a07192e2
|
||||
@@ -0,0 +1,25 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Authoring
|
||||
{
|
||||
/// <summary>
|
||||
/// MC-2 — marks a Husk prefab as a SWARMER variant (the "surround" question). Compose WITH
|
||||
/// <see cref="EnemyAuthoring"/> on the prefab root, tuned fast + low-HP + fast frequent low-chip bites (via the
|
||||
/// EnemyAuthoring fields). This bakes only the <see cref="SwarmerTag"/> marker: a Swarmer has NO AI branch (it
|
||||
/// falls through the Grunt seek+strike pass); the tag drives the director's CLUSTER spawn (a pack per slot) + a
|
||||
/// client tint. Keeps EnemyTag + RegionTag like every Husk.
|
||||
/// </summary>
|
||||
public class SwarmerAuthoring : MonoBehaviour
|
||||
{
|
||||
private class SwarmerBaker : Baker<SwarmerAuthoring>
|
||||
{
|
||||
public override void Bake(SwarmerAuthoring authoring)
|
||||
{
|
||||
var entity = GetEntity(authoring, TransformUsageFlags.Dynamic);
|
||||
AddComponent<SwarmerTag>(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b6a84b442d0535642abc303c01546a15
|
||||
@@ -22,6 +22,17 @@ namespace ProjectM.Authoring
|
||||
[Min(0), Tooltip("Additional Husks per subsequent wave.")] public int CountPerWave = 2;
|
||||
[Min(1), Tooltip("Ticks between individual spawns within a wave (~60/sec).")] public int SpawnIntervalTicks = 24;
|
||||
[Min(1), Tooltip("Ticks of calm between waves (~60/sec).")] public int LullTicks = 240;
|
||||
[Min(1), Tooltip("Max concurrent live BASE husks (mandatory fork-4a cap; bounds spits + swarmer packs at the climax).")] public int MaxAlive = 14;
|
||||
[Min(0), Tooltip("Charger base count.")] public int ChargerBase = 0;
|
||||
[Min(0), Tooltip("Spitter base count.")] public int SpitterBase = 0;
|
||||
[Min(0), Tooltip("Swarmer SLOT base count (each slot = one pack).")] public int SwarmerSlotBase = 0;
|
||||
[Min(0), Tooltip("Extra chargers per wave.")] public int ChargerPerEpoch = 1;
|
||||
[Min(0), Tooltip("Extra spitters per wave (the base gains the ranged question as it escalates).")] public int SpitterPerEpoch = 1;
|
||||
[Min(0), Tooltip("Extra swarmer slots per wave (0 = no base swarms in v1; bounded by MaxAlive when enabled).")] public int SwarmerSlotPerEpoch = 0;
|
||||
[Min(1), Tooltip("Swarmers per swarmer-slot cluster.")] public int SwarmerPackSize = 3;
|
||||
[Min(0), Tooltip("Swarmer pack-size ramp per wave (0 = fixed).")] public int SwarmerPackPerEpoch = 0;
|
||||
[Min(0f), Tooltip("Tight ring radius for a swarmer pack.")] public float ClusterTightRadius = 2.5f;
|
||||
|
||||
|
||||
private class WaveDirectorBaker : Baker<WaveDirectorAuthoring>
|
||||
{
|
||||
@@ -37,6 +48,16 @@ namespace ProjectM.Authoring
|
||||
CountPerWave = authoring.CountPerWave,
|
||||
SpawnIntervalTicks = authoring.SpawnIntervalTicks,
|
||||
LullTicks = authoring.LullTicks,
|
||||
MaxAlive = authoring.MaxAlive,
|
||||
ChargerBase = authoring.ChargerBase,
|
||||
SpitterBase = authoring.SpitterBase,
|
||||
SwarmerSlotBase = authoring.SwarmerSlotBase,
|
||||
ChargerPerEpoch = authoring.ChargerPerEpoch,
|
||||
SpitterPerEpoch = authoring.SpitterPerEpoch,
|
||||
SwarmerSlotPerEpoch = authoring.SwarmerSlotPerEpoch,
|
||||
SwarmerPackSize = authoring.SwarmerPackSize,
|
||||
SwarmerPackPerEpoch = authoring.SwarmerPackPerEpoch,
|
||||
ClusterTightRadius = authoring.ClusterTightRadius,
|
||||
});
|
||||
|
||||
var buffer = AddBuffer<WaveEnemyPrefab>(entity);
|
||||
|
||||
@@ -14,15 +14,23 @@ namespace ProjectM.Authoring
|
||||
/// </summary>
|
||||
public class ZoneEnemyDirectorAuthoring : MonoBehaviour
|
||||
{
|
||||
[Tooltip("Zone-enemy variant prefabs. Index 0 = Grunt, index 1 = Charger. Each must carry EnemyAuthoring + an interpolated GhostAuthoringComponent.")]
|
||||
[Tooltip("Zone-enemy variant prefabs by Kind: [0]=Grunt, [1]=Charger, [2]=Spitter, [3]=Swarmer. Each must carry EnemyAuthoring (+ its variant authoring) + an interpolated GhostAuthoringComponent.")]
|
||||
public GameObject[] EnemyPrefabs;
|
||||
|
||||
[Min(1), Tooltip("Max concurrent live zone enemies (the v1 ghost-relevancy budget).")] public int MaxAlive = 12;
|
||||
[Min(1), Tooltip("Max concurrent live zone enemies (the ghost-relevancy budget; also caps swarmer packs).")] public int MaxAlive = 12;
|
||||
[Min(0f)] public float RingRadius = 14f;
|
||||
[Min(1)] public int RingSlots = 10;
|
||||
[Min(1), Tooltip("Ticks between individual spawns within a wave (~60/sec).")] public int SpawnIntervalTicks = 30;
|
||||
[Min(0), Tooltip("Grunts in the epoch-1 wave (held roughly constant as the epoch climbs).")] public int GruntsPerWave = 4;
|
||||
[Min(0), Tooltip("Chargers in the epoch-1 wave (grows ~1 per epoch -> charger-heavy).")] public int ChargersPerWave = 1;
|
||||
[Min(1), Tooltip("Ticks between individual slot spawns within a wave (~60/sec).")] public int SpawnIntervalTicks = 30;
|
||||
[Min(0), Tooltip("Grunt base count (the fixed floor = remainder of the slots).")] public int GruntsPerWave = 4;
|
||||
[Min(0), Tooltip("Charger base count.")] public int ChargersPerWave = 1;
|
||||
[Min(0), Tooltip("Spitter base count (0 = introduced via the per-epoch ramp).")] public int SpitterBase = 0;
|
||||
[Min(0), Tooltip("Swarmer SLOT base count (each slot = one pack).")] public int SwarmerSlotBase = 0;
|
||||
[Min(0), Tooltip("Extra chargers per epoch.")] public int ChargerPerEpoch = 1;
|
||||
[Min(0), Tooltip("Extra spitters per epoch (1 -> Spitter first appears at epoch 2).")] public int SpitterPerEpoch = 1;
|
||||
[Min(0), Tooltip("Extra swarmer slots per epoch.")] public int SwarmerSlotPerEpoch = 1;
|
||||
[Min(1), Tooltip("Swarmers per swarmer-slot cluster.")] public int SwarmerPackSize = 4;
|
||||
[Min(0), Tooltip("Swarmer pack-size ramp per epoch (0 = fixed).")] public int SwarmerPackPerEpoch = 0;
|
||||
[Min(0f), Tooltip("Tight ring radius for a swarmer pack.")] public float ClusterTightRadius = 2.5f;
|
||||
[Min(0), Tooltip("Flat Ore banked to the shared ledger on a real clear, once per sortie.")] public int RewardOre = 25;
|
||||
|
||||
private class ZoneEnemyDirectorBaker : Baker<ZoneEnemyDirectorAuthoring>
|
||||
@@ -40,6 +48,14 @@ namespace ProjectM.Authoring
|
||||
GruntsPerWave = authoring.GruntsPerWave,
|
||||
ChargersPerWave = authoring.ChargersPerWave,
|
||||
RewardOre = authoring.RewardOre,
|
||||
SpitterBase = authoring.SpitterBase,
|
||||
SwarmerSlotBase = authoring.SwarmerSlotBase,
|
||||
ChargerPerEpoch = authoring.ChargerPerEpoch,
|
||||
SpitterPerEpoch = authoring.SpitterPerEpoch,
|
||||
SwarmerSlotPerEpoch = authoring.SwarmerSlotPerEpoch,
|
||||
SwarmerPackSize = authoring.SwarmerPackSize,
|
||||
SwarmerPackPerEpoch = authoring.SwarmerPackPerEpoch,
|
||||
ClusterTightRadius = authoring.ClusterTightRadius,
|
||||
});
|
||||
|
||||
var buffer = AddBuffer<ZoneEnemyPrefab>(entity);
|
||||
|
||||
Reference in New Issue
Block a user