diff --git a/Assets/_Project/Prefabs/Turret.prefab b/Assets/_Project/Prefabs/Turret.prefab deleted file mode 100644 index 2428fd310..000000000 --- a/Assets/_Project/Prefabs/Turret.prefab +++ /dev/null @@ -1,204 +0,0 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!1 &2057690259992313649 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 4449724123933675757} - - component: {fileID: 3818890329194429404} - - component: {fileID: 6553709043537316589} - m_Layer: 0 - m_Name: Model - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!4 &4449724123933675757 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 2057690259992313649} - serializedVersion: 2 - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 3572766465862231365} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!33 &3818890329194429404 -MeshFilter: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 2057690259992313649} - m_Mesh: {fileID: -8131118695690612189, guid: 472a8342ece31c14f9d53ff7119b7857, type: 3} ---- !u!23 &6553709043537316589 -MeshRenderer: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 2057690259992313649} - m_Enabled: 1 - m_CastShadows: 1 - m_ReceiveShadows: 1 - m_DynamicOccludee: 1 - m_StaticShadowCaster: 0 - m_MotionVectors: 1 - m_LightProbeUsage: 1 - m_ReflectionProbeUsage: 1 - m_RayTracingMode: 2 - m_RayTraceProcedural: 0 - m_RayTracingAccelStructBuildFlagsOverride: 0 - m_RayTracingAccelStructBuildFlags: 1 - m_SmallMeshCulling: 1 - m_ForceMeshLod: -1 - m_MeshLodSelectionBias: 0 - m_RenderingLayerMask: 1 - m_RendererPriority: 0 - m_Materials: - - {fileID: 2100000, guid: 4cee7a5983b187943adc589b9cb1f084, type: 2} - - {fileID: 2100000, guid: e600b338198602a449203a6e59c0e794, type: 2} - m_StaticBatchInfo: - firstSubMesh: 0 - subMeshCount: 0 - m_StaticBatchRoot: {fileID: 0} - m_ProbeAnchor: {fileID: 0} - m_LightProbeVolumeOverride: {fileID: 0} - m_ScaleInLightmap: 1 - m_ReceiveGI: 1 - m_PreserveUVs: 0 - m_IgnoreNormalsForChartDetection: 0 - m_ImportantGI: 0 - m_StitchLightmapSeams: 1 - m_SelectedEditorRenderState: 3 - m_MinimumChartSize: 4 - m_AutoUVMaxDistance: 0.5 - m_AutoUVMaxAngle: 89 - m_LightmapParameters: {fileID: 0} - m_GlobalIlluminationMeshLod: 0 - m_SortingLayerID: 0 - m_SortingLayer: 0 - m_SortingOrder: 0 - m_MaskInteraction: 0 - m_AdditionalVertexStreams: {fileID: 0} ---- !u!1 &3885353946372160549 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 3572766465862231365} - - component: {fileID: 9053853372340598254} - - component: {fileID: 6834786618115927220} - - component: {fileID: 1794795016809289889} - - component: {fileID: 9049467567705961987} - m_Layer: 9 - m_Name: Turret - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!4 &3572766465862231365 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 3885353946372160549} - serializedVersion: 2 - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: - - {fileID: 4449724123933675757} - m_Father: {fileID: 0} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!114 &9053853372340598254 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 3885353946372160549} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: c16549610bfe4458aa9389201d072bb6, type: 3} - m_Name: - m_EditorClassIdentifier: Unity.Entities.Hybrid::Unity.Entities.Hybrid.Baking.LinkedEntityGroupAuthoring ---- !u!114 &6834786618115927220 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 3885353946372160549} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 7c79d771cedb4794bf100ce60df5f764, type: 3} - m_Name: - m_EditorClassIdentifier: Unity.NetCode.Authoring.Hybrid::Unity.NetCode.GhostAuthoringComponent - HasOwner: 0 - SupportAutoCommandTarget: 1 - TrackInterpolationDelay: 0 - GhostGroup: 0 - UsePreSerialization: 0 - UseSingleBaseline: 0 - RollbackPredictedSpawnedGhostState: 0 - RollbackPredictionOnStructuralChanges: 1 - DefaultGhostMode: 0 - SupportedGhostModes: 3 - OptimizationMode: 0 - Importance: 1 - MaxSendRate: 0 - prefabId: ---- !u!114 &1794795016809289889 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 3885353946372160549} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: dfb1e0820cbf41b4bbbe99066dfadd40, type: 3} - m_Name: - m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.TurretAuthoring - Range: 10 - CooldownTicks: 30 - Damage: 12 - MaxHp: 120 ---- !u!65 &9049467567705961987 -BoxCollider: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 3885353946372160549} - m_Material: {fileID: 0} - m_IncludeLayers: - serializedVersion: 2 - m_Bits: 0 - m_ExcludeLayers: - serializedVersion: 2 - m_Bits: 0 - m_LayerOverridePriority: 0 - m_IsTrigger: 0 - m_ProvidesContacts: 0 - m_Enabled: 1 - serializedVersion: 3 - m_Size: {x: 0.8, y: 1.2, z: 0.8} - m_Center: {x: 0, y: 0.6, z: 0} diff --git a/Assets/_Project/Prefabs/Turret.prefab.meta b/Assets/_Project/Prefabs/Turret.prefab.meta deleted file mode 100644 index 3e8e9f559..000000000 --- a/Assets/_Project/Prefabs/Turret.prefab.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 5459c9edea89bd94fa6f5043ae00eb40 -PrefabImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/_Project/Scripts/Authoring/Building/StructureCatalogAuthoring.cs b/Assets/_Project/Scripts/Authoring/Building/StructureCatalogAuthoring.cs index 924ba0652..e5ebbff9c 100644 --- a/Assets/_Project/Scripts/Authoring/Building/StructureCatalogAuthoring.cs +++ b/Assets/_Project/Scripts/Authoring/Building/StructureCatalogAuthoring.cs @@ -13,12 +13,6 @@ namespace ProjectM.Authoring /// 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(entity); var buf = AddBuffer(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 diff --git a/Assets/_Project/Scripts/Authoring/Building/TurretAuthoring.cs b/Assets/_Project/Scripts/Authoring/Building/TurretAuthoring.cs deleted file mode 100644 index e40be839a..000000000 --- a/Assets/_Project/Scripts/Authoring/Building/TurretAuthoring.cs +++ /dev/null @@ -1,39 +0,0 @@ -using ProjectM.Simulation; -using Unity.Entities; -using UnityEngine; - -namespace ProjectM.Authoring -{ - /// - /// Authoring for the turret structure ghost prefab (duplicate UpgradePickup.prefab so the ownerless - /// interpolated GhostAuthoringComponent comes free). Bakes {Type=Turret} + - /// stats. BuildPlaceSystem stamps Cell + LastProcessedTick at placement. - /// - 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 - { - 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); - } - } - } -} diff --git a/Assets/_Project/Scripts/Authoring/Building/TurretAuthoring.cs.meta b/Assets/_Project/Scripts/Authoring/Building/TurretAuthoring.cs.meta deleted file mode 100644 index 468df5a7b..000000000 --- a/Assets/_Project/Scripts/Authoring/Building/TurretAuthoring.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: dfb1e0820cbf41b4bbbe99066dfadd40 \ No newline at end of file diff --git a/Assets/_Project/Scripts/Client/Building/BuildSendSystem.cs b/Assets/_Project/Scripts/Client/Building/BuildSendSystem.cs index 624f2cb0c..f787e88a4 100644 --- a/Assets/_Project/Scripts/Client/Building/BuildSendSystem.cs +++ b/Assets/_Project/Scripts/Client/Building/BuildSendSystem.cs @@ -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 }); - /// EDITOR / execute_code hook: queue a turret placement at a specific cell. - public static void PlaceTurret(int cellX, int cellZ) => PlaceStructure(StructureType.Turret, cellX, cellZ); /// EDITOR / execute_code hook: queue a wall placement at a specific cell. public static void PlaceWall(int cellX, int cellZ) => PlaceStructure(StructureType.Wall, cellX, cellZ); diff --git a/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs b/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs index 5940660d3..973c0cf45 100644 --- a/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs +++ b/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs @@ -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 _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(out var ledgerE)) { var buf = SystemAPI.GetBuffer(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(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 "?"; diff --git a/Assets/_Project/Scripts/Client/UI/HudTheme.cs b/Assets/_Project/Scripts/Client/UI/HudTheme.cs index 097e357ec..cb0aa25d2 100644 --- a/Assets/_Project/Scripts/Client/UI/HudTheme.cs +++ b/Assets/_Project/Scripts/Client/UI/HudTheme.cs @@ -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; } diff --git a/Assets/_Project/Scripts/Server/Building/BuildPlaceSystem.cs b/Assets/_Project/Scripts/Server/Building/BuildPlaceSystem.cs index 5d1b5f037..7339cb44f 100644 --- a/Assets/_Project/Scripts/Server/Building/BuildPlaceSystem.cs +++ b/Assets/_Project/Scripts/Server/Building/BuildPlaceSystem.cs @@ -47,14 +47,10 @@ namespace ProjectM.Server var catalog = SystemAPI.GetBuffer(SystemAPI.GetSingletonEntity()); var ledger = SystemAPI.GetBuffer(SystemAPI.GetSingletonEntity()); - // 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(64, Allocator.Temp); - int turretCount = 0; foreach (var ps in SystemAPI.Query>()) - { 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]; diff --git a/Assets/_Project/Scripts/Server/Building/TurretFireSystem.cs b/Assets/_Project/Scripts/Server/Building/TurretFireSystem.cs deleted file mode 100644 index f08031134..000000000 --- a/Assets/_Project/Scripts/Server/Building/TurretFireSystem.cs +++ /dev/null @@ -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 -{ - /// - /// 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 , appends a direct DamageEvent - /// (SourceNetworkId=-1) to it. Reuses HealthApplyDamageSystem (already destroys EnemyTag at HP<=0) — no - /// projectile, no tunnelling, no friendly-fire. Plain server SimulationSystemGroup - /// [UpdateAfter(PredictedSimulationSystemGroup)] (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. - /// - [BurstCompile] - [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] - [UpdateInGroup(typeof(SimulationSystemGroup))] - [UpdateAfter(typeof(PredictedSimulationSystemGroup))] - public partial struct TurretFireSystem : ISystem - { - [BurstCompile] - public void OnCreate(ref SystemState state) - { - state.RequireForUpdate(); - state.RequireForUpdate(); - state.RequireForUpdate(state.GetEntityQuery(ComponentType.ReadOnly())); - } - - [BurstCompile] - public void OnUpdate(ref SystemState state) - { - var serverTick = SystemAPI.GetSingleton().ServerTick; - if (!serverTick.IsValid) - return; - uint now = serverTick.TickIndexForValidTick; - - var huskEntities = new NativeList(Allocator.Temp); - var huskPos = new NativeList(Allocator.Temp); - var huskRegion = new NativeList(Allocator.Temp); - foreach (var (xform, health, region, e) in - SystemAPI.Query, RefRO, RefRO>() - .WithAll().WithNone().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 — 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(); - var ledger = SystemAPI.GetBuffer(ledgerEntity); - - var ecb = new EntityCommandBuffer(Allocator.Temp); - - foreach (var (ps, turret, xform, region) in - SystemAPI.Query, RefRO, RefRO, RefRO>()) - { - 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(); - } - } -} diff --git a/Assets/_Project/Scripts/Server/Building/TurretFireSystem.cs.meta b/Assets/_Project/Scripts/Server/Building/TurretFireSystem.cs.meta deleted file mode 100644 index 88b909ef1..000000000 --- a/Assets/_Project/Scripts/Server/Building/TurretFireSystem.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 53cc7669bd6cc5d4e8307b18732897bc \ No newline at end of file diff --git a/Assets/_Project/Scripts/Simulation/Building/StructureComponents.cs b/Assets/_Project/Scripts/Simulation/Building/StructureComponents.cs index d095c1ae1..cd3854dc3 100644 --- a/Assets/_Project/Scripts/Simulation/Building/StructureComponents.cs +++ b/Assets/_Project/Scripts/Simulation/Building/StructureComponents.cs @@ -5,20 +5,19 @@ using Unity.NetCode; namespace ProjectM.Simulation { /// - /// 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. /// 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); is server-only (clients derive it from /// the replicated LocalTransform via , so it stays off the wire). /// / are server-only raw NetworkTick values - /// (-guarded; 0 = inactive): the Turret reuses 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. + /// (-guarded; 0 = inactive), kept for future timed structures (the turret + /// cooldown + production catch-up that used them are retired — LANTERN purge). /// public struct PlacedStructure : IComponentData { @@ -42,29 +39,16 @@ namespace ProjectM.Simulation /// Occupied grid cell (server-only; clients derive it from LocalTransform). public int2 Cell; - /// Next action tick (server-only; turret cooldown now / next production tick in M7). 0 = inactive. + /// Next action tick (server-only). 0 = inactive. public uint NextTick; - /// Last tick this structure was processed (server-only; M7 offline-catch-up baseline). Stamped at spawn. + /// Last tick this structure was processed (server-only). Stamped at spawn. public uint LastProcessedTick; } - /// - /// A buildable defense turret (the first structure). Hitscan: TurretFireSystem applies a direct - /// DamageEvent to the nearest in-range living Husk on cooldown (reusing - /// ) — reuses HealthApplyDamageSystem, no projectile/friendly-fire. - /// - public struct Turret : IComponentData - { - public float Range; - public int CooldownTicks; - public float Damage; - } - /// /// 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). /// public struct StructureCatalogEntry : IBufferElementData { @@ -85,3 +69,4 @@ namespace ProjectM.Simulation /// public struct RuntimePlacedTag : IComponentData { } } + diff --git a/Assets/_Project/Scripts/Simulation/Tuning.cs b/Assets/_Project/Scripts/Simulation/Tuning.cs index 3c403f7cf..80804e24e 100644 --- a/Assets/_Project/Scripts/Simulation/Tuning.cs +++ b/Assets/_Project/Scripts/Simulation/Tuning.cs @@ -56,18 +56,6 @@ namespace ProjectM.Simulation /// baked telegraph can't drift from the server windup. public const int ChargerWindupTicks = 30; - // ---- Base defense (EB-2) ---- - - - /// 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. - public const int TurretChargeCostPerShot = 1; - - /// 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. - public const int TurretCap = 6; - // ---- Cold start (CycleDirectorSpawnSystem seeds the shared ledger on a NEW game) ---- /// DR-042 C6c: Ore deposited into the shared ledger at spawn on a NEW game ONLY (a restored save diff --git a/Assets/_Project/Tests/EditMode/TurretFireSystemTests.cs b/Assets/_Project/Tests/EditMode/TurretFireSystemTests.cs deleted file mode 100644 index 38edd5f8d..000000000 --- a/Assets/_Project/Tests/EditMode/TurretFireSystemTests.cs +++ /dev/null @@ -1,219 +0,0 @@ -using NUnit.Framework; -using ProjectM.Server; -using ProjectM.Simulation; -using Unity.Core; -using Unity.Entities; -using Unity.Mathematics; -using Unity.NetCode; -using Unity.Transforms; - -namespace ProjectM.Tests -{ - /// - /// Plain-Entities EditMode tests for the server-only (hitscan defense turret). - /// A bare world is seeded with a NetworkTime singleton, a Charge pool (the - /// turret's EB-2 ammo), a turret (PlacedStructure + Turret + RegionTag + LocalTransform) and Husks (Health + - /// EnemyTag + RegionTag + LocalTransform + a DamageEvent buffer). The system snapshots living Husks, fires at - /// the nearest in-range one in its OWN region on the PlacedStructure.NextTick cooldown — but ONLY when - /// it can withdraw Charge from the ledger; out of Charge = - /// soft-fail (no shot, no cooldown burn, so it fires the instant Charge returns). These tests pin range - /// filtering, region gating, the wrap-safe cooldown, and the EB-2 Charge spend / soft-fail. - /// - public class TurretFireSystemTests - { - // Range/region/cooldown tests never need to run dry; the spend tests pass an explicit small pool. - const int AmpleCharge = 1_000_000; - - static (World world, SimulationSystemGroup group, Entity ledger) MakeWorld( - string name, uint serverTick, int charge = AmpleCharge) - { - var world = new World(name); - var group = world.GetOrCreateSystemManaged(); - group.AddSystemToUpdateList(world.GetOrCreateSystem()); - group.SortSystems(); - world.SetTime(new TimeData(elapsedTime: 0f, deltaTime: 1f / 60f)); - SetServerTick(world, serverTick); - var em = world.EntityManager; - var ledger = em.CreateEntity(typeof(ResourceLedger)); - var buf = em.AddBuffer(ledger); - if (charge > 0) - buf.Add(new StorageEntry { ItemId = ResourceId.Charge, Count = charge }); - return (world, group, ledger); - } - - static void SetServerTick(World world, uint tick) - { - var em = world.EntityManager; - using var q = em.CreateEntityQuery(typeof(NetworkTime)); - Entity e = q.IsEmpty ? em.CreateEntity(typeof(NetworkTime)) : q.GetSingletonEntity(); - em.SetComponentData(e, new NetworkTime { ServerTick = new NetworkTick(tick) }); - } - - static Entity MakeTurret(EntityManager em, float3 pos, byte region, float range, int cooldown, float damage) - { - var e = em.CreateEntity(); - em.AddComponentData(e, LocalTransform.FromPosition(pos)); - em.AddComponentData(e, new RegionTag { Region = region }); - em.AddComponentData(e, new PlacedStructure { Type = StructureType.Turret, NextTick = 0 }); - em.AddComponentData(e, new Turret { Range = range, CooldownTicks = cooldown, Damage = damage }); - return e; - } - - static Entity MakeHusk(EntityManager em, float3 pos, byte region, float health) - { - var e = em.CreateEntity(); - em.AddComponentData(e, LocalTransform.FromPosition(pos)); - em.AddComponentData(e, new RegionTag { Region = region }); - em.AddComponentData(e, new Health { Current = health, Max = health }); - em.AddComponent(e); - em.AddBuffer(e); - return e; - } - - static int LedgerCharge(EntityManager em, Entity ledger) - { - var buf = em.GetBuffer(ledger); - for (int i = 0; i < buf.Length; i++) - if (buf[i].ItemId == ResourceId.Charge) return buf[i].Count; - return 0; - } - - [Test] - public void Turret_Fires_At_InRange_Husk_In_Its_Region() - { - var (world, group, _) = MakeWorld("TurretFireWorld", serverTick: 200); - using (world) - { - var em = world.EntityManager; - var turret = MakeTurret(em, new float3(5, 1, 5), RegionId.Base, range: 20f, cooldown: 30, damage: 10f); - var husk = MakeHusk(em, new float3(10, 1, 10), RegionId.Base, health: 50f); - - group.Update(); - - var dmg = em.GetBuffer(husk); - Assert.AreEqual(1, dmg.Length, "An in-range, same-region Husk takes exactly one turret hit."); - Assert.AreEqual(10f, dmg[0].Amount, 1e-4f, "DamageEvent carries the turret's damage."); - Assert.AreEqual(-1, dmg[0].SourceNetworkId, "Turret damage uses the -1 (world) source id."); - Assert.AreNotEqual(0u, em.GetComponentData(turret).NextTick, - "The cooldown tick is stamped after firing."); - } - } - - [Test] - public void Turret_Ignores_Husk_Out_Of_Range() - { - var (world, group, _) = MakeWorld("TurretRangeWorld", serverTick: 200); - using (world) - { - var em = world.EntityManager; - MakeTurret(em, new float3(0, 1, 0), RegionId.Base, range: 5f, cooldown: 30, damage: 10f); - var husk = MakeHusk(em, new float3(100, 1, 0), RegionId.Base, health: 50f); - - group.Update(); - - Assert.AreEqual(0, em.GetBuffer(husk).Length, "A Husk beyond Range takes no hit."); - } - } - - [Test] - public void Turret_Ignores_Husk_In_A_Different_Region() - { - var (world, group, _) = MakeWorld("TurretRegionWorld", serverTick: 200); - using (world) - { - var em = world.EntityManager; - MakeTurret(em, new float3(0, 1, 0), RegionId.Base, range: 50f, cooldown: 30, damage: 10f); - var husk = MakeHusk(em, new float3(2, 1, 2), RegionId.Expedition, health: 50f); - - group.Update(); - - Assert.AreEqual(0, em.GetBuffer(husk).Length, - "A Husk in a different region is never targeted (region gating)."); - } - } - - [Test] - public void Turret_Respects_Cooldown_Then_Fires_Again() - { - var (world, group, _) = MakeWorld("TurretCooldownWorld", serverTick: 200); - using (world) - { - var em = world.EntityManager; - MakeTurret(em, new float3(0, 1, 0), RegionId.Base, range: 50f, cooldown: 30, damage: 10f); - var husk = MakeHusk(em, new float3(3, 1, 0), RegionId.Base, health: 5000f); - - group.Update(); // fires at tick 200, NextTick -> 230 - Assert.AreEqual(1, em.GetBuffer(husk).Length, "Fires on the first ready tick."); - - SetServerTick(world, 210); - group.Update(); // 210 < 230 -> still cooling down - Assert.AreEqual(1, em.GetBuffer(husk).Length, "No second shot while cooling down."); - - SetServerTick(world, 240); - group.Update(); // 240 >= 230 -> ready again - Assert.AreEqual(2, em.GetBuffer(husk).Length, "Fires again once the cooldown elapses."); - } - } - - // ---- EB-2: turret ammo (Charge) spend + soft-fail ---- - - [Test] - public void Turret_Out_Of_Charge_Soft_Fails_No_Shot_No_Cooldown_Burn() - { - var (world, group, ledger) = MakeWorld("TurretNoChargeWorld", serverTick: 200, charge: 0); - using (world) - { - var em = world.EntityManager; - var turret = MakeTurret(em, new float3(0, 1, 0), RegionId.Base, range: 50f, cooldown: 30, damage: 10f); - var husk = MakeHusk(em, new float3(3, 1, 0), RegionId.Base, health: 50f); - - group.Update(); - - Assert.AreEqual(0, em.GetBuffer(husk).Length, - "With an empty Charge pool the turret cannot fire (soft-fail)."); - Assert.AreEqual(0u, em.GetComponentData(turret).NextTick, - "A soft-failed shot burns no cooldown — the turret stays ready for the instant Charge returns."); - Assert.AreEqual(0, LedgerCharge(em, ledger), "No Charge is consumed when no shot is fired."); - } - } - - [Test] - public void Turret_Consumes_One_Charge_Per_Shot() - { - var (world, group, ledger) = MakeWorld("TurretSpendWorld", serverTick: 200, charge: 5); - using (world) - { - var em = world.EntityManager; - MakeTurret(em, new float3(0, 1, 0), RegionId.Base, range: 50f, cooldown: 30, damage: 10f); - var husk = MakeHusk(em, new float3(3, 1, 0), RegionId.Base, health: 50f); - - group.Update(); - - Assert.AreEqual(1, em.GetBuffer(husk).Length, "A funded turret fires."); - Assert.AreEqual(5 - Tuning.TurretChargeCostPerShot, LedgerCharge(em, ledger), - "Exactly TurretChargeCostPerShot Charge is withdrawn for the shot."); - } - } - - [Test] - public void Two_Turrets_Share_A_Finite_Charge_Pool_Second_Soft_Fails() - { - // One Charge, two turrets, one Husk: the first turret (query/creation order) spends the last Charge and - // fires; the second withdraws 0 and soft-fails. The Husk takes exactly one hit and the pool empties. - var (world, group, ledger) = MakeWorld("TurretSharedPoolWorld", serverTick: 200, charge: 1); - using (world) - { - var em = world.EntityManager; - MakeTurret(em, new float3(-2, 1, 0), RegionId.Base, range: 50f, cooldown: 30, damage: 10f); - MakeTurret(em, new float3(2, 1, 0), RegionId.Base, range: 50f, cooldown: 30, damage: 10f); - var husk = MakeHusk(em, new float3(0, 1, 0), RegionId.Base, health: 500f); - - group.Update(); - - Assert.AreEqual(1, em.GetBuffer(husk).Length, - "A single Charge funds exactly one of the two turrets this tick."); - Assert.AreEqual(0, LedgerCharge(em, ledger), "The shared Charge pool is drained to zero."); - } - } - } -} diff --git a/Assets/_Project/Tests/EditMode/TurretFireSystemTests.cs.meta b/Assets/_Project/Tests/EditMode/TurretFireSystemTests.cs.meta deleted file mode 100644 index 2b5b858ef..000000000 --- a/Assets/_Project/Tests/EditMode/TurretFireSystemTests.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: a86a8f9715eff4c45817718443589cdb \ No newline at end of file