// Assets/_Project/Scripts/Gameplay/EnemyScalingConfig.cs using UnityEngine; namespace TD.Gameplay { /// /// How enemy health scales across a run. Single source of truth for difficulty progression, /// the health counterpart to . /// /// /// The problem this solves. Waves are drawn at random from a phase pool, so difficulty /// can no longer live in the assets — an enemy authored at /// 1000 HP is brutal at encounter 1 and trivial at encounter 40, and the draw decides which /// you get. Progression has to be a property of how far the players have come, exactly /// as gold payouts are. /// /// Budget, not per-enemy HP. This config scales the total health an /// encounter presents; per-enemy health is that budget divided by the wave's enemy count. If /// the curve set per-enemy health instead, enemy count would silently become a second /// difficulty axis and a 150-strong swarm would hit three times harder than a 50-strong wave /// in the same slot. Dividing a fixed budget turns count into a texture knob: eight /// giants and a hundred rats threaten equally, but demand different mazes. /// /// Budget is per zone. Every player zone spawns the full wave, so this is the /// health one player faces, not the lobby total. It does not change with player count. /// /// Speed is deliberately absent. Speed stays on the /// , unscaled. Escalating speed alongside health compounds into /// a difficulty cliff and quietly invalidates tower balance as a run goes — projectile lead, /// slow-effect value and time-under-fire all shift. Fast-versus-slow reads best as a fixed /// character trait; health and the voted wave buffs carry escalation. /// [CreateAssetMenu(fileName = "EnemyScalingConfig", menuName = "TD/Enemy Scaling Config", order = 3)] public class EnemyScalingConfig : ScriptableObject { [Header("Health curve")] [Tooltip("Total enemy health the run's FIRST encounter presents, per player zone. " + "Per-enemy health is this divided by the wave's enemy count.")] [Min(1f)] public float BaseHpBudget = 5000f; [Tooltip("Multiplier applied per encounter. 1.18 ≈ +18% health each encounter, so the " + "budget roughly doubles every four. Encounters are counted continuously across " + "the whole run, so cycle 2 is strictly harder than cycle 1.")] [Min(1f)] public float GrowthPerEncounter = 1.18f; [Header("Boss")] [Tooltip("Extra multiplier on a boss encounter's budget, on top of the curve. Bosses " + "spawn few enemies, so nearly all of this lands on the single boss body.")] [Min(1f)] public float BossHpMultiplier = 6f; /// /// Total health budget for the given 1-based encounter number /// (RunState.GlobalEncounterNumber), per player zone. /// public float GetEncounterHpBudget(int encounterNumber, bool isBoss) { int steps = Mathf.Max(0, encounterNumber - 1); float budget = BaseHpBudget * Mathf.Pow(GrowthPerEncounter, steps); if (isBoss) budget *= BossHpMultiplier; return budget; } /// /// Health for one enemy in the given encounter: the encounter's budget shared across /// bodies, then skewed by the enemy type's /// . /// /// /// The archetype multiplier is a deliberate deviation from budget, not a /// redistribution of it — a wave of 1.25× enemies really is 25% tougher than its slot /// calls for. Keep multipliers near 1.0 and let the wave's enemy count express tankiness; /// that is the knob the budget is designed to divide. /// public float ResolvePerEnemyHp(int encounterNumber, bool isBoss, int waveEnemyCount, float archetypeMultiplier) { int bodies = Mathf.Max(1, waveEnemyCount); float share = GetEncounterHpBudget(encounterNumber, isBoss) / bodies; return Mathf.Max(1f, share * Mathf.Max(0.01f, archetypeMultiplier)); } } }