Turrets: 40 Ore + per-base cap of 6 + fit-one-cell textured model (fix cheap/spam/massive/untextured)
Operator: turrets were "super duper cheap", spammable "unlimited", "spaced weirdly", "massive and not textured". All four were real and independently rooted (placement grid was actually fine). - Cost: TurretCostOre 10 -> 40 (authoring default + the serialized Gameplay subscene value, which overrides the code default). A node yields 30 Ore, so a turret is now ~1.3 nodes instead of 1/3 of one. - Cap: new Tuning.TurretCap=6, enforced server-authoritatively in BuildPlaceSystem (count live Base turrets while building the occupancy set; reject placement at the cap, same-tick-safe). Was unlimited. - Model: the 1.6x Synty ballista (~5m on a 1m cell, clipping neighbours) scaled to 0.8 to fit one cell; the C5 BoxCollider shrunk to match (0.8x1.2x0.8, center y 0.6); all 6 sub-renderers swapped off the flat untextured teal Mat_StructureOwned_Cyan to the Synty atlas PolygonFantasyKingdom_Mat_01_A (textured). Play-verified TurretCost=40 Ore / cap=6 baked; no exceptions. Also fixes 3 EditMode tests that pinned the old dash knobs (the prior tuning commit changed iframe 12->14 / cooldown 45->36 but I committed it without re-running tests): DashSystemTests now derives the expected dash speed from TuningConfig.Defaults() (robust to future tuning) + asserts now+14/+36; TuningConfigTests pins the new defaults. 390/390 EditMode green. Investigation: wf_c6c87dc5-9c3 (turret lane). Operator fork: 40 Ore + cap 6 (stricter). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -50,10 +50,14 @@ 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 source of truth).
|
||||
// Derive occupancy from the live structure set (authoritative); also count turrets for the per-base cap.
|
||||
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);
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -63,6 +63,10 @@ namespace ProjectM.Simulation
|
||||
/// 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 keeps
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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<CharacterControl>(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<CharacterComponent>(e).GroundedMovementSharpness, 1e-3f,
|
||||
"i-frame window raises GroundedMovementSharpness to ~200 (the blink).");
|
||||
}
|
||||
@@ -133,9 +133,9 @@ namespace ProjectM.Tests
|
||||
|
||||
var ds = em.GetComponentData<DashState>(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<DashCooldown>(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<DashCooldown>(e).NextTick, "Cooldown = now + 36.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user