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:
@@ -13,12 +13,6 @@ namespace ProjectM.Authoring
|
||||
/// </summary>
|
||||
public class StructureCatalogAuthoring : MonoBehaviour
|
||||
{
|
||||
[Tooltip("Turret structure ghost prefab (TurretAuthoring + GhostAuthoring).")]
|
||||
public GameObject TurretPrefab;
|
||||
|
||||
[Tooltip("Ore cost to build a turret.")]
|
||||
[Min(0)] public int TurretCostOre = 40; // DR-042 combat pass: was 10 (~1/3 node) -> 40 (~1.3 nodes); a real investment
|
||||
|
||||
[Tooltip("Wall structure ghost prefab (StructureAuthoring{Wall} + GhostAuthoring).")]
|
||||
public GameObject WallPrefab;
|
||||
|
||||
@@ -39,16 +33,6 @@ namespace ProjectM.Authoring
|
||||
AddComponent<StructureCatalog>(entity);
|
||||
var buf = AddBuffer<StructureCatalogEntry>(entity);
|
||||
|
||||
if (authoring.TurretPrefab != null)
|
||||
{
|
||||
buf.Add(new StructureCatalogEntry
|
||||
{
|
||||
Type = StructureType.Turret,
|
||||
Prefab = GetEntity(authoring.TurretPrefab, TransformUsageFlags.Dynamic),
|
||||
CostResourceId = ResourceId.Ore,
|
||||
CostAmount = authoring.TurretCostOre,
|
||||
});
|
||||
}
|
||||
if (authoring.WallPrefab != null)
|
||||
{
|
||||
buf.Add(new StructureCatalogEntry
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Authoring
|
||||
{
|
||||
/// <summary>
|
||||
/// Authoring for the turret structure ghost prefab (duplicate UpgradePickup.prefab so the ownerless
|
||||
/// interpolated GhostAuthoringComponent comes free). Bakes <see cref="PlacedStructure"/>{Type=Turret} +
|
||||
/// <see cref="Turret"/> stats. BuildPlaceSystem stamps Cell + LastProcessedTick at placement.
|
||||
/// </summary>
|
||||
public class TurretAuthoring : MonoBehaviour
|
||||
{
|
||||
[Min(1f)] public float Range = 10f;
|
||||
[Min(1)] public int CooldownTicks = 30;
|
||||
[Min(1f)] public float Damage = 12f;
|
||||
[Min(1f)] public float MaxHp = 120f;
|
||||
|
||||
private class TurretBaker : Baker<TurretAuthoring>
|
||||
{
|
||||
public override void Bake(TurretAuthoring authoring)
|
||||
{
|
||||
var entity = GetEntity(authoring, TransformUsageFlags.Dynamic);
|
||||
this.AddPlacedStructure(entity, StructureType.Turret);
|
||||
AddComponent(entity, new Turret
|
||||
{
|
||||
Range = authoring.Range,
|
||||
CooldownTicks = authoring.CooldownTicks,
|
||||
Damage = authoring.Damage,
|
||||
});
|
||||
// EB-1: structures are damageable + destructible (Husks push for them; HealthApplyDamageSystem
|
||||
// destroys a Destructible at Health<=0). The DamageEvent buffer MUST exist on the archetype or an
|
||||
// AI/turret strike crashes at ECB playback. NO HitRadius on purpose -> ProjectileDamageSystem (needs
|
||||
// Health+HitRadius) ignores structures, so player shots never friendly-fire your own turret.
|
||||
this.AddDamageable(entity, authoring.MaxHp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dfb1e0820cbf41b4bbbe99066dfadd40
|
||||
@@ -27,7 +27,6 @@ namespace ProjectM.Client
|
||||
// Dev hotkey fallback: key -> buildable.
|
||||
static readonly (UnityEngine.InputSystem.Key Key, byte Type)[] s_BuildHotkeys =
|
||||
{
|
||||
(UnityEngine.InputSystem.Key.B, StructureType.Turret),
|
||||
(UnityEngine.InputSystem.Key.V, StructureType.Wall),
|
||||
// LANTERN purge: the automation buildables (Fabricator/Harvester/Conveyor) are deleted; Pylon stays
|
||||
// hidden from the hotkey fallback to match the build palette. PlaceStructure execute_code statics remain.
|
||||
@@ -54,8 +53,6 @@ namespace ProjectM.Client
|
||||
public static void PlaceStructure(byte type, int cellX, int cellZ, byte direction = 0) =>
|
||||
s_PendingBuild.Enqueue(new PendingBuild { Type = type, CellX = cellX, CellZ = cellZ, Direction = direction });
|
||||
|
||||
/// <summary>EDITOR / execute_code hook: queue a turret placement at a specific cell.</summary>
|
||||
public static void PlaceTurret(int cellX, int cellZ) => PlaceStructure(StructureType.Turret, cellX, cellZ);
|
||||
|
||||
/// <summary>EDITOR / execute_code hook: queue a wall placement at a specific cell.</summary>
|
||||
public static void PlaceWall(int cellX, int cellZ) => PlaceStructure(StructureType.Wall, cellX, cellZ);
|
||||
|
||||
@@ -33,7 +33,6 @@ namespace ProjectM.Client
|
||||
static readonly Color AetherCyan = new(0.30f, 0.85f, 1f);
|
||||
static readonly Color OreAmber = new(1f, 0.72f, 0.35f);
|
||||
static readonly Color BioGreen = new(0.55f, 0.85f, 0.45f);
|
||||
static readonly Color ChargeViolet = new(0.80f, 0.45f, 1f);
|
||||
static readonly Color CoreRed = new(1f, 0.40f, 0.32f); // END-1 Engine Core integrity bar
|
||||
|
||||
static readonly Color PanelDark = new(0.08f, 0.11f, 0.15f, 0.90f);
|
||||
@@ -93,7 +92,7 @@ namespace ProjectM.Client
|
||||
readonly List<VisualElement> _pips = new();
|
||||
|
||||
// resources
|
||||
Label _aetherNum, _oreNum, _bioNum, _chargeNum;
|
||||
Label _aetherNum, _oreNum, _bioNum;
|
||||
|
||||
// build palette + hints
|
||||
VisualElement _paletteRow, _hintBar, _buildDiscoveryChip;
|
||||
@@ -384,7 +383,7 @@ namespace ProjectM.Client
|
||||
}
|
||||
|
||||
// ---- Resources (feed palette affordability) ----
|
||||
int aether = 0, ore = 0, bio = 0, charge = 0;
|
||||
int aether = 0, ore = 0, bio = 0;
|
||||
if (SystemAPI.TryGetSingletonEntity<ResourceLedger>(out var ledgerE))
|
||||
{
|
||||
var buf = SystemAPI.GetBuffer<StorageEntry>(ledgerE);
|
||||
@@ -394,25 +393,13 @@ namespace ProjectM.Client
|
||||
if (en.ItemId == ResourceId.Aether) aether = en.Count;
|
||||
else if (en.ItemId == ResourceId.Ore) ore = en.Count;
|
||||
else if (en.ItemId == ResourceId.Biomass) bio = en.Count;
|
||||
else if (en.ItemId == ResourceId.Charge) charge = en.Count;
|
||||
}
|
||||
}
|
||||
_aetherNum.text = aether.ToString();
|
||||
_oreNum.text = ore.ToString();
|
||||
_bioNum.text = bio.ToString();
|
||||
_chargeNum.text = charge.ToString();
|
||||
|
||||
|
||||
// DR-042 C6a: dim the Aether upgrade button when it isn't affordable (cost is a compile-time const).
|
||||
// (Step 11: upgrade-button affordability tint retired with the button.)
|
||||
// EB-2 quiet-turret cue (GLOBAL, not per-turret, so the deterministic Charge split never reads as one
|
||||
// broken turret): a dry base during a siege tells the player to build a Fabricator.
|
||||
if (siege && charge == 0 && !onExpedition)
|
||||
{
|
||||
_locationText.text = "TURRETS OUT OF AMMO - build a Fabricator (Ore -> ammo)";
|
||||
_locationText.style.color = new Color(1f, 0.4f, 0.9f);
|
||||
}
|
||||
|
||||
// ---- Engine Core integrity (END-1): a red base-heart bar; an overrun stamps a transient pulse we flash ----
|
||||
if (SystemAPI.TryGetSingleton<CoreIntegrity>(out var core) && core.Max > 0)
|
||||
{
|
||||
@@ -1026,7 +1013,6 @@ namespace ProjectM.Client
|
||||
strip.Add(ResourceChip(theme != null ? theme.AetherIcon : null, AetherCyan, "0", out _aetherNum, 26, 20));
|
||||
strip.Add(ResourceChip(theme != null ? theme.OreIcon : null, OreAmber, "0", out _oreNum, 30, 22));
|
||||
strip.Add(ResourceChip(theme != null ? theme.BioIcon : null, BioGreen, "0", out _bioNum, 26, 20));
|
||||
strip.Add(ResourceChip(null, ChargeViolet, "0", out _chargeNum, 26, 20)); // EB-2 turret ammo (flat violet, no icon)
|
||||
// DR-042 C6a: the only Aether sink (ability-damage upgrade) gets a visible, clickable button (was U-key
|
||||
// only). The Button element handles its own picking even though the HUD root Ignores clicks.
|
||||
// (Step 11: the Aether UPGRADE-DMG button was RETIRED with AbilityUpgradeRequest — the choice-of-3
|
||||
@@ -1318,7 +1304,6 @@ namespace ProjectM.Client
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case StructureType.Turret: return "Turret";
|
||||
case StructureType.Wall: return "Wall";
|
||||
case StructureType.Pylon: return "Pylon";
|
||||
default: return "?";
|
||||
|
||||
@@ -48,12 +48,10 @@ namespace ProjectM.Client
|
||||
public Sprite BioIcon;
|
||||
|
||||
[Header("Structure icons")]
|
||||
public Sprite TurretIcon;
|
||||
public Sprite WallIcon;
|
||||
public Sprite PylonIcon;
|
||||
|
||||
[Header("Build-ghost preview meshes (authored real-size, ground pivot — BuildSendSystem)")]
|
||||
public Mesh TurretGhostMesh;
|
||||
public Mesh WallGhostMesh;
|
||||
|
||||
[Header("Build-mode control glyphs")]
|
||||
@@ -82,7 +80,6 @@ namespace ProjectM.Client
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case StructureType.Turret: return TurretIcon;
|
||||
case StructureType.Wall: return WallIcon;
|
||||
case StructureType.Pylon: return PylonIcon;
|
||||
default: return null;
|
||||
@@ -94,7 +91,6 @@ namespace ProjectM.Client
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case StructureType.Turret: return TurretGhostMesh;
|
||||
case StructureType.Wall: return WallGhostMesh;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -5,20 +5,19 @@ using Unity.NetCode;
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Structure type ids (a byte, not an enum, per the cross-assembly enum-in-Burst hazard). Turret is the
|
||||
/// first concrete structure; Harvester/Fabricator/Conveyor are RESERVED now (free) for the M7 automation
|
||||
/// pillar (production chains) so adding them later is purely additive.
|
||||
/// Structure type ids (a byte, not an enum, per the cross-assembly enum-in-Burst hazard). Ids 1-4 are
|
||||
/// RETIRED (EB-2 turret defense + M7 automation machines, deleted in the LANTERN purge) and stay reserved
|
||||
/// so PlacedStructure.Type's [GhostField] serializer + old save bytes never re-mean.
|
||||
/// </summary>
|
||||
public static class StructureType
|
||||
{
|
||||
public const byte None = 0;
|
||||
// RETIRED ids — reserved, do not reuse:
|
||||
public const byte Turret = 1;
|
||||
// Reserved for M7 automation — do not reuse these codes:
|
||||
public const byte Harvester = 2;
|
||||
public const byte Fabricator = 3;
|
||||
public const byte Conveyor = 4;
|
||||
// World-pass structural/cosmetic build-out (additive; these byte values keep PlacedStructure.Type's
|
||||
// [GhostField] serializer identical — no re-bake of existing ghosts). Do NOT reuse 2-4 (M7 automation).
|
||||
// Live buildables:
|
||||
public const byte Wall = 5;
|
||||
public const byte Pylon = 6;
|
||||
}
|
||||
@@ -29,10 +28,8 @@ namespace ProjectM.Simulation
|
||||
/// (a cheap byte for client visual branching); <see cref="Cell"/> is server-only (clients derive it from
|
||||
/// the replicated LocalTransform via <see cref="BaseGridMath.WorldToCell"/>, so it stays off the wire).
|
||||
/// <see cref="NextTick"/> / <see cref="LastProcessedTick"/> are server-only raw NetworkTick values
|
||||
/// (<see cref="TickUtil.NonZero"/>-guarded; 0 = inactive): the Turret reuses <see cref="NextTick"/> as its
|
||||
/// fire cooldown NOW (not dead weight), and M7 production reuses both as the next-production-tick +
|
||||
/// deterministic offline catch-up linchpin (produced = floor((now - LastProcessedTick)/period)). These two
|
||||
/// tick fields are the IDENTITY/TIMING that cannot be reconstructed retroactively, so they are baked now.
|
||||
/// (<see cref="TickUtil.NonZero"/>-guarded; 0 = inactive), kept for future timed structures (the turret
|
||||
/// cooldown + production catch-up that used them are retired — LANTERN purge).
|
||||
/// </summary>
|
||||
public struct PlacedStructure : IComponentData
|
||||
{
|
||||
@@ -42,29 +39,16 @@ namespace ProjectM.Simulation
|
||||
/// <summary>Occupied grid cell (server-only; clients derive it from LocalTransform).</summary>
|
||||
public int2 Cell;
|
||||
|
||||
/// <summary>Next action tick (server-only; turret cooldown now / next production tick in M7). 0 = inactive.</summary>
|
||||
/// <summary>Next action tick (server-only). 0 = inactive.</summary>
|
||||
public uint NextTick;
|
||||
|
||||
/// <summary>Last tick this structure was processed (server-only; M7 offline-catch-up baseline). Stamped at spawn.</summary>
|
||||
/// <summary>Last tick this structure was processed (server-only). Stamped at spawn.</summary>
|
||||
public uint LastProcessedTick;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A buildable defense turret (the first structure). Hitscan: <c>TurretFireSystem</c> applies a direct
|
||||
/// <c>DamageEvent</c> to the nearest in-range living Husk on cooldown (reusing
|
||||
/// <see cref="PlacedStructure.NextTick"/>) — reuses HealthApplyDamageSystem, no projectile/friendly-fire.
|
||||
/// </summary>
|
||||
public struct Turret : IComponentData
|
||||
{
|
||||
public float Range;
|
||||
public int CooldownTicks;
|
||||
public float Damage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One row of the build catalog: cost + prefab per structure type. Modeled on AbilityPrefabElement
|
||||
/// (prefab baked via GetEntity, NEVER inside a blob — blobs don't remap entity refs). M7 adds a recipe
|
||||
/// column to this element additively (the catalog is baked, not replicated → no format break).
|
||||
/// (prefab baked via GetEntity, NEVER inside a blob — blobs don't remap entity refs).
|
||||
/// </summary>
|
||||
public struct StructureCatalogEntry : IBufferElementData
|
||||
{
|
||||
@@ -85,3 +69,4 @@ namespace ProjectM.Simulation
|
||||
/// </summary>
|
||||
public struct RuntimePlacedTag : IComponentData { }
|
||||
}
|
||||
|
||||
|
||||
@@ -56,18 +56,6 @@ namespace ProjectM.Simulation
|
||||
/// baked telegraph can't drift from the server windup.</summary>
|
||||
public const int ChargerWindupTicks = 30;
|
||||
|
||||
// ---- Base defense (EB-2) ----
|
||||
|
||||
|
||||
/// <summary>EB-2: Charge (turret munition) consumed per turret shot, withdrawn from the global ledger. A
|
||||
/// turret with 0 Charge SOFT-FAILS (no shot, no cooldown advance). A ledger-fed Fabricator mints Charge from
|
||||
/// Ore. Operator feel-fork: keep generous so turrets stay fed while you keep mining.</summary>
|
||||
public const int TurretChargeCostPerShot = 1;
|
||||
|
||||
/// <summary>Max turrets buildable per base (server-enforced in BuildPlaceSystem; the client preview goes red at
|
||||
/// the cap). Turrets are a deliberate fortress investment, not spammable — paired with the 40-Ore build cost.</summary>
|
||||
public const int TurretCap = 6;
|
||||
|
||||
// ---- Cold start (CycleDirectorSpawnSystem seeds the shared ledger on a NEW game) ----
|
||||
|
||||
/// <summary>DR-042 C6c: Ore deposited into the shared ledger at spawn on a NEW game ONLY (a restored save
|
||||
|
||||
Reference in New Issue
Block a user