// 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));
}
}
}