Files
Project-M/Assets/_Project/Scripts/Simulation/Tuning.cs
T
kronic 09183cc139 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>
2026-06-25 23:07:57 -07:00

100 lines
6.2 KiB
C#

namespace ProjectM.Simulation
{
/// <summary>
/// Central home for gameplay-balance constants that were previously buried as <c>private const</c>s
/// inside individual systems, so designers have one searchable place to tune them. Burst-safe (compile-time
/// <c>const</c>s only — they inline into the consuming systems with no runtime cost or managed reference).
/// <para>
/// Systems reference these via <c>Tuning.*</c> (wired in the 2026-06-04 polish pass, Stage C). When adding a
/// new tunable value, prefer adding it here over a local private const UNLESS it already has an obvious,
/// well-named public home (see the cross-references below) — duplicating a literal creates two sources of truth.
/// </para>
/// <para>
/// <b>Values that already live in a clear, public, semantically-named home (NOT duplicated here):</b>
/// <list type="bullet">
/// <item><see cref="CyclePhase.ExpeditionTicks"/> / <see cref="CyclePhase.BuildTicks"/> — cycle phase durations.</item>
/// <item><see cref="RegionMath.ExpeditionOffsetX"/> — base→expedition world-space offset.</item>
/// <item>Per-ability/character stats — authored in ScriptableObjects, baked to the AbilityDatabase blob (M3).</item>
/// </list>
/// </para>
/// </summary>
public static class Tuning
{
// ---- Ability damage upgrade (AbilityUpgradeSystem) ----
/// <summary>Distinct sentinel SourceId so the upgrade <c>StatModifier</c> is found + grown in place
/// (replace-by-SourceId keeps the bounded modifier buffer from growing a row per upgrade).</summary>
public const uint AbilityUpgradeSourceId = 0x00A0E711u;
/// <summary>Damage bonus added per upgrade tier (PercentAdd op): +25% per tier.</summary>
public const float AbilityUpgradeTierStep = 0.25f;
/// <summary>Aether cost charged to the shared ledger per upgrade tier.</summary>
public const int AbilityUpgradeCostAmount = 20;
// ---- Resource harvest (ResourceHarvestSystem) ----
/// <summary>Effective projectile radius used by the swept-segment node-hit test (added to the node's
/// <c>HitRadius</c>). Tunnel-safe because the segment is reconstructed from <c>Projectile.LastStep</c>.</summary>
public const float HarvestProjectileRadius = 0.2f;
// ---- Enemy knockback (ProjectileDamageSystem stamps on hit; EnemyAISystem applies + suppresses seek/strike) ----
/// <summary>Knockback speed (world units/sec) a Husk recoils at when shot; 0 disables knockback globally.</summary>
public const float KnockbackSpeed = 8f;
/// <summary>Server ticks the knockback lasts (~60 ticks/sec).</summary>
public const int KnockbackDurationTicks = 8;
// ---- Husk attack telegraph (EnemyAISystem 2-phase strike; client cue in CombatFeedbackSystem) ----
/// <summary>Wind-up ticks before a Husk strike lands (~0.37s @ 60 ticks/sec) — sized for a fair tell
/// under interp lag (>= ~250ms reaction + interp buffer; Slice 1 readability). 0/1 = near-instant (legacy).</summary>
public const int AttackWindupTicks = 22;
// ---- Production / automation (M7: Harvester/Conveyor/Fabricator) ----
/// <summary>Max production cycles a single machine awards in one process (bounds within-session
/// catch-up after any skipped ticks; restore re-seats the baseline so this never reflects wall-clock).</summary>
public const int MaxProductionCatchup = 600;
/// <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 keeps
/// its persisted ledger). Bootstraps the Fabricator(30)->Charge->Turret(10) chain so a turret placed before any
/// mining isn't a silent cold deadlock. Ore-only so the 'build a Fabricator to arm turrets' lesson survives.</summary>
public const int StartingOre = 50;
// ---- Inventory (per-player bag; InventoryMath / ResourceHarvestSystem / InventoryDepositSystem) ----
/// <summary>Max stacks a player can carry; InventoryMath rejects deposits past this and the harvest remainder spills to the global ledger.</summary>
public const int InventoryMaxSlots = 24;
/// <summary>Default per-slot stack cap when an item has no ItemDatabase entry (the catalog is optional at runtime).</summary>
public const int DefaultStackMax = 999;
// ---- Equipment stat-mod SourceIds (EquipSystem) ----
// One DISTINCT sentinel per slot: slot i tags its mods with EquipSourceIdBase + i. All of a slot's
// inline mods share that one id and are stripped target-agnostically via
// TimedModifierUtil.RemoveBySourceId on unequip/swap. Full StatModifier SourceId map (keep DISJOINT):
// 0u = pickups + debug-injection; 0x00A0E711 = ability-damage upgrade; 0x00DEB061 = debug stat command;
// 0x00E91000.. = equipment (4 slots); 0x00C1A550.. = class traits (Slice 2, permanent).
/// <summary>Base for per-slot equipment SourceIds; slot i tags its mods with <c>EquipSourceIdBase + i</c>.</summary>
public const uint EquipSourceIdBase = 0x00E91000u;
/// <summary>Slice 2: base SourceId for a class's permanent trait StatModifiers (Warrior/Ranger seeds).
/// DISJOINT from equipment/upgrade/debug ranges; class traits are NEVER stripped (permanent per session).</summary>
public const uint ClassSourceId = 0x00C1A550u;
}
}