Files
Project-M/Assets/_Project/Scripts/Simulation/Tuning.cs
T
kronic 9f61f7c6fe LANTERN kit into the shipping scene + frame rename (audit H2/M4)
H2 was the audit's sharpest finding: in Game.unity every player spawned
with four ability sockets pointing at SparkIds the baked AbilityDatabase
did not contain, so all four resolved to Damage=0 Range=0 Cooldown=0.
Melee and dash were the only working combat verbs in the built game.
Cause: the 5 LANTERN Sparks were added to GymSub.unity and never to
Gameplay.unity.

- Gameplay.unity's AbilityDatabaseAuthoring now carries all 9 defs (the
  4 legacy ids keep their numbers; Sparks are 5-9) with their effect
  prefabs. Live-verified in Play: sockets now read
  Vortex 8dmg/6range/420cd, Blink 20range/1cd, Hook & Pull
  15dmg/25range/120cd, Light Zone 6dmg/5range/480cd.
- Removed 6 orphaned authoring GameObjects the purge left behind in the
  subscene (StorageSpawner, StructureCatalog, ItemDatabase,
  SpitterProjectileConfig, BoonCatalog, MetaCatalog) and the RoomDressing
  object in Game.unity — the latter is what scattered 47 Synty desert
  props into the seabed murk.
- Deleted 4 now-unreferenced prefabs: EnemySpit, Pylon, Storage, Wall.
- Frame rename (M4): FrameKind.Warrior/Ranger -> Bathynaut/Harpooner,
  93 identifier sites. Byte values pinned (2/3), so no ghost-hash or save
  impact. The menu said "Warrior"/"Ranger" to players three weeks after
  the frames were renamed in design.
- Menu now reads LANTERN / "Light is territory — co-op descent" instead
  of "PROJECT M" / "Frontier colony — co-op" (the Awakening-Engine
  tagline, two directions stale).
- Removed the "Replay Tutorial" button and the Settings ONBOARDING
  section: both drove coach-marks DR-051 deleted. The settings fields
  still round-trip so existing settings files load unchanged.

All four project scenes + all prefabs verified free of missing scripts.
Server measured at 59.6 ticks/s against a 60 Hz target. 295/295 green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 13:11:58 -07:00

245 lines
15 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="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;
/// <summary>Charger lunge telegraph windup (ticks). Single source shared by EnemyBaker (the baked client
/// danger-ramp denominator) and TuningConfig.Defaults, mirroring the Grunt AttackWindupTicks pattern so the
/// baked telegraph can't drift from the server windup.</summary>
public const int ChargerWindupTicks = 30;
// ---- 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) — seed capital for the build loop.</summary>
public const int StartingOre = 90;
// ---- Expedition run economy (RoomFieldSystem / RunDirectorSystem) ----
/// <summary>Run-wide resource-node allotment: RunDirectorSystem stages it into RunRuntime.NodeBudgetRemaining
/// at the launch edge; RoomFieldSystem floors each room's scatter to what remains and spends it down. THE
/// scarcity knob — ~1.5 nodes/room over an 8-room run (dense Reward rooms drain it fastest; late rooms can
/// run dry, which is the intended tension).</summary>
public const int ExpeditionNodeBudget = 12;
/// <summary>Boss-room health multiplier over the base enemy prefab (RoomEnemyDirectorSystem spawns ONE
/// boss per Boss room; v1 = a beefed Charger — a real boss kit is a later rung).</summary>
public const float BossHealthMultiplier = 8f;
/// <summary>Boss-room visual scale multiplier (LocalTransform.Scale is a replicated [GhostField] — set once
/// at spawn on the baked value, legible at a glance).</summary>
public const float BossScaleMultiplier = 1.6f;
/// <summary>Room-landing grace before the FIRST enemy slot spawns (~1.7 s @ 60): the party just teleported
/// in and needs a beat to orient — without it the wave engages while the camera is still settling (demo
/// polish; an idle player died in ~2 s of landing). Subsequent slots keep the normal drip cadence.</summary>
public const uint RoomEntryGraceTicks = 100;
/// <summary>Phase 1 (B3) corpse window (~0.9 s @60): a dead enemy lingers as a non-acting, non-counting,
/// non-targetable corpse so the client death animation has time to read before the despawn burst.</summary>
public const uint EnemyDeathWindowTicks = 54;
/// <summary>Phase 1 (B5): boss HP grows by this fraction of its base per LIVING expedition player beyond the
/// first (counted at spawn; Health.Max replicates so the bar stays truthful).</summary>
public const float BossHealthPerExtraPlayer = 0.75f;
// ---- Exploding barrels (Phase 1.5 hazard; server-only detonation consts — see Exploding_Barrels_Build_Spec) ----
/// <summary>Fuse ticks between the pop (Remaining hits 0) and detonation (~1.5 s @60; operator feel call
/// 07-10 — the 0.6 s fuse popped before the danger could be read): long enough to READ the blast-radius
/// disc + walk out, and for the replicated Remaining=0 to straddle snapshots (the client's fuse cue).</summary>
public const uint BarrelFuseTicks = 90;
/// <summary>Detonation radius (world units, XZ). Above melee reach so point-blank pops are punished, but a
/// fuse-length walk clears it comfortably.</summary>
public const float BarrelExplodeRadius = 3.25f;
/// <summary>Detonation damage — hits LIVING enemies AND players alike (friendly fire = the bait mechanic).</summary>
public const float BarrelExplodeDamage = 26f;
// ---- Blight geyser (Phase 1.5b bundle-3 hazard; periodic BOTH-SIDES AoE — see Geyser_Build_Spec) ----
/// <summary>Ticks between eruptions (~5 s @60). Server reschedules NextEruptTick = now + this each erupt
/// (never +=, per the review — anchored += catch-up-storms after a skip).</summary>
public const uint GeyserPeriodTicks = 300;
/// <summary>Telegraph window (ticks, ~1.3 s @60): the client grows the warning disc over the last this-many
/// ticks before an eruption (int so the client lead comparison stays signed).</summary>
public const int GeyserTelegraphTicks = 78;
/// <summary>Eruption AoE radius (world units, XZ) — hits LIVING players AND enemies (friendly fire = lure bait).</summary>
public const float GeyserEruptRadius = 3.0f;
/// <summary>Eruption damage per hit.</summary>
public const float GeyserEruptDamage = 22f;
// ---- Expedition BOSS (a scaled Charger given a real kit by BossAISystem; server-only feel consts) ----
/// <summary>Radial SLAM AoE radius (world units): a player inside this ring at wind-up elapse eats the hit
/// unless dashing (i-frames). Sized well above melee reach so the tell reads as a boss-scale ring.</summary>
public const float BossSlamRadius = 5.5f;
/// <summary>Radial SLAM damage. Heavy but survivable given generous i-frames + respawn (readable-but-fair).</summary>
public const float BossSlamDamage = 34f;
/// <summary>SLAM telegraph lead (~0.75 s @60): a fair dodge window under interp lag before the ring lands.</summary>
public const uint BossSlamWindupTicks = 45;
/// <summary>Ticks between SLAMs in phase one (~4 s). Phase two multiplies this by BossPhase2SlamCooldownMult.</summary>
public const uint BossSlamCooldownTicks = 240;
// Phase 1 (B4): the boss's SECOND attack - a telegraphed gap-closing LUNGE (repositioning; the slam stays
// the damage beat). Windup is readable (~0.6 s); travel reuses the Charger contract (committed, wall-stopped).
public const uint BossLungeWindupTicks = 35;
public const float BossLungeSpeed = 20f;
public const uint BossLungeDurationTicks = 22;
public const uint BossLungeCooldownTicks = 300;
/// <summary>Lunge only when the target is beyond this (else slam) and within LungeMaxRange.</summary>
public const float BossLungeMinRange = 7f;
public const float BossLungeMaxRange = 20f;
/// <summary>Health fraction at/below which the boss enters phase two (faster + summons adds).</summary>
public const float BossPhase2HealthFraction = 0.5f;
/// <summary>Phase-two move-speed multiplier over the boss's base EnemyStats.MoveSpeed (a visible gear shift).</summary>
public const float BossPhase2SpeedMult = 1.45f;
/// <summary>Phase-two SLAM cooldown multiplier (&lt;1 = slams more often when enraged).</summary>
public const float BossPhase2SlamCooldownMult = 0.6f;
/// <summary>Swarmers summoned per phase-two summon (spawned around the boss, tagged as room zone enemies).</summary>
public const int BossSummonCount = 3;
/// <summary>Ticks between phase-two summons (~7 s).</summary>
public const uint BossSummonCooldownTicks = 420;
/// <summary>Cap on live zone enemies the boss will summon toward — it holds fire above this so a boss room
/// never floods past readable (also bounded by the director's MaxAlive count).</summary>
public const int BossSummonLiveCap = 8;
// ---- DR-046 room-exit PORTAL / loot window ----
/// <summary>Client-side portal position offset from the room origin (where the party landed): a short step
/// back toward the entrance, so 'leave the way you came'. Client VFX + proximity prompt only.</summary>
public const float PortalOffsetZ = -5f;
/// <summary>How close the local player must be to the portal to show 'E to LEAVE' + send the interact.</summary>
public const float PortalInteractRange = 3.5f;
/// <summary>RoomExplore soft-timeout (~30 s @60): auto-advance if nobody interacts the portal (no softlock).</summary>
public const uint ExploreGraceTicks = 1800;
// ---- 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;
// 0x00B00000..0x00B10000 = run-scoped BOONS (stripped on return; top slot 0x00B0FFFF = Frenzy timed buff); 0x00C1A550.. = class traits (permanent);
// 0x00E7A000..0x00E7A100 = permanent META upgrades (Step 12a); 0x00E91000.. = equipment (4 slots).
/// <summary>Base of the run-scoped BOON SourceId band: each applied pick draws BoonSourceIdBase +
/// (BoonPickCounter++ % BoonSourceIdSpan), so boons stack as distinct rows and ONE range-strip on the
/// Returning edge clears the whole run's boons (the two-channel model — boons never persist).</summary>
public const uint BoonSourceIdBase = 0x00B00000u;
/// <summary>Width of the boon band [Base, Base+Span) — far above any realistic per-run pick count.</summary>
public const uint BoonSourceIdSpan = 0x10000u;
/// <summary>Phase 1.7: the single Frenzy on-kill timed buff's SourceId, pinned to the TOP of the boon band
/// (Base + Span - 1 = 0x00B0FFFF). The per-pick counter allocates from the BOTTOM (Base + counter % Span) and
/// cannot reach Span-1 within a run, so it never aliases a pick; the Returning whole-band RemoveBySourceIdRange
/// still clears it for free. Refreshed (never stacked) via TimedModifierUtil.Upsert. Do NOT copy the sibling-band
/// Base+smallIndex idiom into the boon band — its low offsets are counter-consumed.</summary>
public const uint FrenzySourceId = BoonSourceIdBase + BoonSourceIdSpan - 1u; // 0x00B0FFFF
/// <summary>Phase 1.7: Frenzy surge duration (ticks, ~60/s) re-stamped on each kill.</summary>
public const int FrenzyDurationTicks = 240;
/// <summary>Phase 1.7: Frenzy cooldown modifier (PercentMult on CooldownTicks; -0.30 = 30% faster abilities).</summary>
public const float FrenzyCooldownMult = -0.30f;
/// <summary>DR-046: base PREP-LOADOUT run-scoped SourceId band [Base, Base+Span). DISJOINT from boon
/// (0x00B00000), class (0x00C1A550), meta (0x00E7A000), equip (0x00E91000); one prep option's live
/// StatModifier is keyed PrepSourceIdBase + optionId. Stripped on the Returning edge like boons.</summary>
public const uint PrepSourceIdBase = 0x00D00000u;
/// <summary>Width of the prep band (far above the tiny option count).</summary>
public const uint PrepSourceIdSpan = 0x10000u;
/// <summary>Base of the PERMANENT meta-upgrade SourceId band: a purchased tier's live StatModifier is
/// keyed MetaSourceIdBase + UpgradeId (absolute-value UPSERT — one row per owned upgrade, set to
/// ValuePerTier*tier). Persisted via MetaTierState (SaveData v6) and re-applied born-correct at spawn.
/// DISJOINT from every other band; NEVER stripped (the permanent channel of the two-channel model).</summary>
public const uint MetaSourceIdBase = 0x00E7A000u;
/// <summary>Width of the meta band [Base, Base+Span) — bounds UpgradeId to a byte-sized catalog.</summary>
public const uint MetaSourceIdSpan = 0x100u;
/// <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 (Bathynaut/Harpooner seeds).
/// DISJOINT from equipment/upgrade/debug ranges; class traits are NEVER stripped (permanent per session).</summary>
public const uint ClassSourceId = 0x00C1A550u;
}
}