LANTERN purge B2: delete EB-2 turret defense
TurretFireSystem, TurretAuthoring, Turret.prefab, Turret component, turret cap, B hotkey, HUD Charge chip + out-of-ammo cue, catalog entry, Tuning consts, tests (-7, 452 green). StructureType.Turret byte + ResourceId.Charge stay reserved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -47,14 +47,10 @@ namespace ProjectM.Server
|
||||
var catalog = SystemAPI.GetBuffer<StructureCatalogEntry>(SystemAPI.GetSingletonEntity<StructureCatalog>());
|
||||
var ledger = SystemAPI.GetBuffer<StorageEntry>(SystemAPI.GetSingletonEntity<ResourceLedger>());
|
||||
|
||||
// Derive occupancy from the live structure set (authoritative); also count turrets for the per-base cap.
|
||||
// Derive occupancy from the live structure set (authoritative).
|
||||
var occupied = new NativeHashSet<int2>(64, Allocator.Temp);
|
||||
int turretCount = 0;
|
||||
foreach (var ps in SystemAPI.Query<RefRO<PlacedStructure>>())
|
||||
{
|
||||
occupied.Add(ps.ValueRO.Cell);
|
||||
if (ps.ValueRO.Type == StructureType.Turret) turretCount++;
|
||||
}
|
||||
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
|
||||
@@ -68,9 +64,7 @@ namespace ProjectM.Server
|
||||
for (int i = 0; i < catalog.Length; i++)
|
||||
if (catalog[i].Type == req.StructureType) { entryIdx = i; break; }
|
||||
|
||||
// DR-042 combat pass: cap turrets per base (server-authoritative) so they can't be spammed.
|
||||
bool turretCapOk = req.StructureType != StructureType.Turret || turretCount < Tuning.TurretCap;
|
||||
if (entryIdx >= 0 && catalog[entryIdx].Prefab != Entity.Null && turretCapOk
|
||||
if (entryIdx >= 0 && catalog[entryIdx].Prefab != Entity.Null
|
||||
&& BuildPlacementMath.CanPlace(anchor, occupied, cell))
|
||||
{
|
||||
var entry = catalog[entryIdx];
|
||||
@@ -84,7 +78,6 @@ namespace ProjectM.Server
|
||||
// Commit IN-PLACE so a second same-tick request sees the spend + reservation.
|
||||
StorageMath.Withdraw(ledger, entry.CostResourceId, entry.CostAmount);
|
||||
occupied.Add(cell);
|
||||
if (req.StructureType == StructureType.Turret) turretCount++; // keep same-tick turret requests under the cap
|
||||
|
||||
var structure = ecb.Instantiate(entry.Prefab);
|
||||
var xform = m_TransformLookup[entry.Prefab];
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.NetCode;
|
||||
using Unity.Transforms;
|
||||
|
||||
namespace ProjectM.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-only turret fire (hitscan) — EnemyAISystem reversed. Snapshots living Husks once (entity, planar
|
||||
/// pos, region); each turret picks the nearest Husk in ITS region within Range and, on a NetworkTick
|
||||
/// cooldown stored in <see cref="PlacedStructure.NextTick"/>, appends a direct <c>DamageEvent</c>
|
||||
/// (SourceNetworkId=-1) to it. Reuses HealthApplyDamageSystem (already destroys EnemyTag at HP<=0) — no
|
||||
/// projectile, no tunnelling, no friendly-fire. Plain server SimulationSystemGroup
|
||||
/// <c>[UpdateAfter(PredictedSimulationSystemGroup)]</c> (the predicted group is OrderFirst → UpdateBefore is
|
||||
/// ignored); the appended DamageEvent drains next tick (~16ms), consistent with EnemyAISystem. Self-gates:
|
||||
/// Husks only exist during the Defend wave.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
[UpdateInGroup(typeof(SimulationSystemGroup))]
|
||||
[UpdateAfter(typeof(PredictedSimulationSystemGroup))]
|
||||
public partial struct TurretFireSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<NetworkTime>();
|
||||
state.RequireForUpdate<ResourceLedger>();
|
||||
state.RequireForUpdate(state.GetEntityQuery(ComponentType.ReadOnly<Turret>()));
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var serverTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick;
|
||||
if (!serverTick.IsValid)
|
||||
return;
|
||||
uint now = serverTick.TickIndexForValidTick;
|
||||
|
||||
var huskEntities = new NativeList<Entity>(Allocator.Temp);
|
||||
var huskPos = new NativeList<float2>(Allocator.Temp);
|
||||
var huskRegion = new NativeList<byte>(Allocator.Temp);
|
||||
foreach (var (xform, health, region, e) in
|
||||
SystemAPI.Query<RefRO<LocalTransform>, RefRO<Health>, RefRO<RegionTag>>()
|
||||
.WithAll<EnemyTag>().WithNone<Dying>().WithEntityAccess()) // LIVING targets only (B3)
|
||||
{
|
||||
if (health.ValueRO.Current <= 0f)
|
||||
continue;
|
||||
huskEntities.Add(e);
|
||||
huskPos.Add(xform.ValueRO.Position.xz);
|
||||
huskRegion.Add(region.ValueRO.Region);
|
||||
}
|
||||
|
||||
if (huskEntities.Length == 0)
|
||||
{
|
||||
huskEntities.Dispose(); huskPos.Dispose(); huskRegion.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
// EB-2: resolve the shared ledger ONCE (NEVER GetSingleton<StorageEntry> — a 2nd StorageEntry buffer
|
||||
// exists on the base container). Turrets withdraw Charge from it sequentially (a finite pool split in
|
||||
// query order; later turrets soft-fail when it empties).
|
||||
var ledgerEntity = SystemAPI.GetSingletonEntity<ResourceLedger>();
|
||||
var ledger = SystemAPI.GetBuffer<StorageEntry>(ledgerEntity);
|
||||
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
|
||||
foreach (var (ps, turret, xform, region) in
|
||||
SystemAPI.Query<RefRW<PlacedStructure>, RefRO<Turret>, RefRO<LocalTransform>, RefRO<RegionTag>>())
|
||||
{
|
||||
uint nextRaw = ps.ValueRO.NextTick;
|
||||
if (nextRaw != 0)
|
||||
{
|
||||
var nextTick = new NetworkTick(nextRaw);
|
||||
if (nextTick.IsValid && nextTick.IsNewerThan(serverTick))
|
||||
continue; // still cooling down
|
||||
}
|
||||
|
||||
float2 tp = xform.ValueRO.Position.xz;
|
||||
byte treg = region.ValueRO.Region;
|
||||
float rangeSq = turret.ValueRO.Range * turret.ValueRO.Range;
|
||||
|
||||
int best = -1;
|
||||
float bestSq = float.MaxValue;
|
||||
for (int i = 0; i < huskEntities.Length; i++)
|
||||
{
|
||||
if (huskRegion[i] != treg)
|
||||
continue;
|
||||
float sq = math.distancesq(huskPos[i], tp);
|
||||
if (sq <= rangeSq && sq < bestSq)
|
||||
{
|
||||
bestSq = sq;
|
||||
best = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (best >= 0)
|
||||
{
|
||||
// EB-2 felt spend: a shot costs Charge from the shared ledger. Gate BOTH the damage AND the
|
||||
// cooldown advance on a SUCCESSFUL withdraw — out of Charge = SOFT-FAIL (no shot, no cooldown
|
||||
// burn, so the turret fires the instant Charge returns). Refund a partial (cost>1 underflow).
|
||||
int cost = math.max(1, Tuning.TurretChargeCostPerShot);
|
||||
int got = StorageMath.Withdraw(ledger, ResourceId.Charge, cost);
|
||||
if (got >= cost)
|
||||
{
|
||||
ecb.AppendToBuffer(huskEntities[best], new DamageEvent
|
||||
{
|
||||
Amount = turret.ValueRO.Damage,
|
||||
SourceNetworkId = -1,
|
||||
SourceTick = TickUtil.NonZero(now),
|
||||
});
|
||||
uint cd = (uint)math.max(1, turret.ValueRO.CooldownTicks);
|
||||
ps.ValueRW.NextTick = TickUtil.NonZero(now + cd);
|
||||
}
|
||||
else if (got > 0)
|
||||
{
|
||||
StorageMath.Deposit(ledger, ResourceId.Charge, got); // never consume Charge without firing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ecb.Playback(state.EntityManager);
|
||||
ecb.Dispose();
|
||||
huskEntities.Dispose(); huskPos.Dispose(); huskRegion.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 53cc7669bd6cc5d4e8307b18732897bc
|
||||
Reference in New Issue
Block a user