diff --git a/Assets/_Project/Prefabs/Turret.prefab b/Assets/_Project/Prefabs/Turret.prefab index 0dbcf9c32..a301cd4e2 100644 --- a/Assets/_Project/Prefabs/Turret.prefab +++ b/Assets/_Project/Prefabs/Turret.prefab @@ -28,7 +28,7 @@ Transform: serializedVersion: 2 m_LocalRotation: {x: 0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1.6, y: 1.6, z: 1.6} + m_LocalScale: {x: 0.8, y: 0.8, z: 0.8} m_ConstrainProportionsScale: 0 m_Children: - {fileID: 8624793677999475166} @@ -67,7 +67,7 @@ MeshRenderer: m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: - - {fileID: 2100000, guid: 53c360a5b4f92c04caad3273e9bb5bef, type: 2} + - {fileID: 2100000, guid: 71e41e43b459a244abaf5acca76b89ee, type: 2} m_StaticBatchInfo: firstSubMesh: 0 subMeshCount: 0 @@ -157,7 +157,7 @@ MeshRenderer: m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: - - {fileID: 2100000, guid: 53c360a5b4f92c04caad3273e9bb5bef, type: 2} + - {fileID: 2100000, guid: 71e41e43b459a244abaf5acca76b89ee, type: 2} m_StaticBatchInfo: firstSubMesh: 0 subMeshCount: 0 @@ -248,7 +248,7 @@ MeshRenderer: m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: - - {fileID: 2100000, guid: 53c360a5b4f92c04caad3273e9bb5bef, type: 2} + - {fileID: 2100000, guid: 71e41e43b459a244abaf5acca76b89ee, type: 2} m_StaticBatchInfo: firstSubMesh: 0 subMeshCount: 0 @@ -342,7 +342,7 @@ MeshRenderer: m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: - - {fileID: 2100000, guid: 53c360a5b4f92c04caad3273e9bb5bef, type: 2} + - {fileID: 2100000, guid: 71e41e43b459a244abaf5acca76b89ee, type: 2} m_StaticBatchInfo: firstSubMesh: 0 subMeshCount: 0 @@ -475,8 +475,8 @@ BoxCollider: m_ProvidesContacts: 0 m_Enabled: 1 serializedVersion: 3 - m_Size: {x: 1.6, y: 2, z: 1.6} - m_Center: {x: 0, y: 1, z: 0} + m_Size: {x: 0.8, y: 1.2, z: 0.8} + m_Center: {x: 0, y: 0.6, z: 0} --- !u!1 &4051895978514069616 GameObject: m_ObjectHideFlags: 0 @@ -543,7 +543,7 @@ MeshRenderer: m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: - - {fileID: 2100000, guid: 53c360a5b4f92c04caad3273e9bb5bef, type: 2} + - {fileID: 2100000, guid: 71e41e43b459a244abaf5acca76b89ee, type: 2} m_StaticBatchInfo: firstSubMesh: 0 subMeshCount: 0 @@ -633,7 +633,7 @@ MeshRenderer: m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: - - {fileID: 2100000, guid: 53c360a5b4f92c04caad3273e9bb5bef, type: 2} + - {fileID: 2100000, guid: 71e41e43b459a244abaf5acca76b89ee, type: 2} m_StaticBatchInfo: firstSubMesh: 0 subMeshCount: 0 @@ -723,7 +723,7 @@ MeshRenderer: m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: - - {fileID: 2100000, guid: 53c360a5b4f92c04caad3273e9bb5bef, type: 2} + - {fileID: 2100000, guid: 71e41e43b459a244abaf5acca76b89ee, type: 2} m_StaticBatchInfo: firstSubMesh: 0 subMeshCount: 0 diff --git a/Assets/_Project/Scripts/Authoring/Building/StructureCatalogAuthoring.cs b/Assets/_Project/Scripts/Authoring/Building/StructureCatalogAuthoring.cs index 3f9e15a5c..1104e01ba 100644 --- a/Assets/_Project/Scripts/Authoring/Building/StructureCatalogAuthoring.cs +++ b/Assets/_Project/Scripts/Authoring/Building/StructureCatalogAuthoring.cs @@ -18,7 +18,7 @@ namespace ProjectM.Authoring public GameObject TurretPrefab; [Tooltip("Ore cost to build a turret.")] - [Min(0)] public int TurretCostOre = 10; + [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; diff --git a/Assets/_Project/Scripts/Server/Building/BuildPlaceSystem.cs b/Assets/_Project/Scripts/Server/Building/BuildPlaceSystem.cs index cccea02ff..adb452a5e 100644 --- a/Assets/_Project/Scripts/Server/Building/BuildPlaceSystem.cs +++ b/Assets/_Project/Scripts/Server/Building/BuildPlaceSystem.cs @@ -50,10 +50,14 @@ namespace ProjectM.Server var catalog = SystemAPI.GetBuffer(SystemAPI.GetSingletonEntity()); var ledger = SystemAPI.GetBuffer(SystemAPI.GetSingletonEntity()); - // Derive occupancy from the live structure set (authoritative source of truth). + // Derive occupancy from the live structure set (authoritative); also count turrets for the per-base cap. 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); @@ -67,7 +71,9 @@ namespace ProjectM.Server for (int i = 0; i < catalog.Length; i++) if (catalog[i].Type == req.StructureType) { entryIdx = i; break; } - if (entryIdx >= 0 && catalog[entryIdx].Prefab != Entity.Null + // 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 && BuildPlacementMath.CanPlace(anchor, occupied, cell)) { var entry = catalog[entryIdx]; @@ -81,6 +87,7 @@ 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/Simulation/Tuning.cs b/Assets/_Project/Scripts/Simulation/Tuning.cs index 4698e163c..0ad55bc0f 100644 --- a/Assets/_Project/Scripts/Simulation/Tuning.cs +++ b/Assets/_Project/Scripts/Simulation/Tuning.cs @@ -63,6 +63,10 @@ namespace ProjectM.Simulation /// 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 keeps diff --git a/Assets/_Project/Subscenes/Gameplay.unity b/Assets/_Project/Subscenes/Gameplay.unity index 88f1ee428..d42b4ced6 100644 --- a/Assets/_Project/Subscenes/Gameplay.unity +++ b/Assets/_Project/Subscenes/Gameplay.unity @@ -894,7 +894,7 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.StructureCatalogAuthoring TurretPrefab: {fileID: 3885353946372160549, guid: 5459c9edea89bd94fa6f5043ae00eb40, type: 3} - TurretCostOre: 10 + TurretCostOre: 40 WallPrefab: {fileID: 3885353946372160549, guid: 1e321aea244cc484f99c1cdd68cb01c4, type: 3} WallCostOre: 4 PylonPrefab: {fileID: 3885353946372160549, guid: 7d0637ef90f120a4c9e2ba637dfc00af, type: 3} diff --git a/Assets/_Project/Tests/EditMode/DashSystemTests.cs b/Assets/_Project/Tests/EditMode/DashSystemTests.cs index c0206a069..9827133f9 100644 --- a/Assets/_Project/Tests/EditMode/DashSystemTests.cs +++ b/Assets/_Project/Tests/EditMode/DashSystemTests.cs @@ -35,8 +35,8 @@ namespace ProjectM.Tests return (world, group); } - // Dash speed derived from the baked knobs: 4.0 units / (12 ticks / 60) = 20 units/s. - const float ExpectedDashSpeed = 20f; + // Dash speed derived from the live knobs: DashDistance / (IFrameWindowTicks/60). Tracks TuningConfig.Defaults(). + static readonly float ExpectedDashSpeed = TuningConfig.Defaults().DashDistance / (TuningConfig.Defaults().IFrameWindowTicks / 60f); static Entity MakeDasher(EntityManager em, float2 facing) { @@ -69,7 +69,7 @@ namespace ProjectM.Tests var ctrl = em.GetComponentData(e).MoveVelocity; Assert.AreEqual(0f, ctrl.x, 1e-3f, "Dash heading (0,1) overrides X to 0."); Assert.AreEqual(0f, ctrl.y, 1e-3f, "Planar dash keeps Y at 0."); - Assert.AreEqual(ExpectedDashSpeed, ctrl.z, 1e-3f, "i-frame window overrides velocity to Dir*dashSpeed (20)."); + Assert.AreEqual(ExpectedDashSpeed, ctrl.z, 1e-3f, "i-frame window overrides velocity to Dir*dashSpeed (derived from the dash knobs)."); Assert.AreEqual(200f, em.GetComponentData(e).GroundedMovementSharpness, 1e-3f, "i-frame window raises GroundedMovementSharpness to ~200 (the blink)."); } @@ -133,9 +133,9 @@ namespace ProjectM.Tests var ds = em.GetComponentData(e); Assert.AreEqual(100u, ds.StartTick, "StartTick = now."); - Assert.AreEqual(112u, ds.IFrameUntilTick, "IFrameUntilTick = now + 12."); - Assert.AreEqual(121u, ds.RecoverUntilTick, "RecoverUntilTick = now + 12 + 9."); - Assert.AreEqual(145u, em.GetComponentData(e).NextTick, "Cooldown = now + 45."); + Assert.AreEqual(114u, ds.IFrameUntilTick, "IFrameUntilTick = now + 14."); + Assert.AreEqual(123u, ds.RecoverUntilTick, "RecoverUntilTick = now + 14 + 9."); + Assert.AreEqual(136u, em.GetComponentData(e).NextTick, "Cooldown = now + 36."); } } diff --git a/Assets/_Project/Tests/EditMode/TuningConfigTests.cs b/Assets/_Project/Tests/EditMode/TuningConfigTests.cs index 7d10d1ae3..f6f82bb95 100644 --- a/Assets/_Project/Tests/EditMode/TuningConfigTests.cs +++ b/Assets/_Project/Tests/EditMode/TuningConfigTests.cs @@ -24,9 +24,9 @@ namespace ProjectM.Tests { var d = TuningConfig.Defaults(); Assert.AreEqual(4.0f, d.DashDistance, 1e-6f, "DashDistance"); - Assert.AreEqual(12f, d.IFrameWindowTicks, 1e-6f, "IFrameWindowTicks"); + Assert.AreEqual(14f, d.IFrameWindowTicks, 1e-6f, "IFrameWindowTicks"); Assert.AreEqual(9f, d.RecoverTailTicks, 1e-6f, "RecoverTailTicks"); - Assert.AreEqual(45f, d.DashCooldownTicks, 1e-6f, "DashCooldownTicks"); + Assert.AreEqual(36f, d.DashCooldownTicks, 1e-6f, "DashCooldownTicks"); Assert.AreEqual(200f, d.DashSharpness, 1e-6f, "DashSharpness"); Assert.AreEqual(30f, d.ChargerWindupTicks, 1e-6f, "ChargerWindupTicks"); Assert.AreEqual(16f, d.ChargerLungeSpeed, 1e-6f, "ChargerLungeSpeed");