UnityTowerDefense/Assets/_Project/Scripts/Gameplay/EnemyAbilities/EnemySpawnContext.cs
Matt F 16706a1ecf Scale difficulty by run position; wipe towers at phase end
Follow-up pass on the 2.0 refactor, closing the gap between "waves are drawn
at random" and "difficulty still lives in the enemy assets".

- Enemy health is no longer authored per enemy type. New EnemyScalingConfig
  scales a per-zone HP *budget* by encounter number; per-enemy health is that
  budget divided by the wave's enemy count. Scaling the total rather than the
  per-enemy value keeps enemy count a texture knob (few tanks vs many swarmers)
  instead of a second, uncontrolled difficulty axis. EnemyDefinition.MaxHp
  becomes HpMultiplier, a deviation around 1.0. Speed stays archetype-only and
  unscaled -- escalating it compounds with health and invalidates tower balance
  mid-run.
- GoldConfig entries no longer reference a WaveDefinition. Payout is a property
  of run position, not of which wave got drawn: WaveGoldEntry ->
  EncounterGoldEntry, Waves -> Encounters, keyed by global encounter number.
  Its inspector labels elements "Encounter N" and projects cumulative earnings.
- The wave's voted buffs now show as icon badges in the top bar, read from the
  same RunState slot the spawn path uses so the display can't drift from what
  the enemies actually carry. Hover names the buff; unillustrated cards fall
  back to a lettered badge rather than vanishing.
- Clearing a phase's boss destroys every built tower, unrefunded. Queued build
  jobs still refund -- those towers were never delivered. Runs with the field
  empty, so the walkability churn doesn't hit the re-path scheduler.
- Fixed an RPC codegen break: [ClientRpc] requires a ClientRpc suffix, unlike
  the newer [Rpc(SendTo...)] style this file doesn't use.
- Setup checklist reordered into dependency order; it previously asked for a
  RunDefinition two sections before creating one.

Also carries the editor-side asset reorganisation into Definitions/RunDefinitions
and the sprite move into Enemy/Player draft icon folders.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 00:32:33 -07:00

52 lines
2.5 KiB
C#

// Assets/_Project/Scripts/Gameplay/EnemyAbilities/EnemySpawnContext.cs
namespace TD.Gameplay.EnemyAbilities
{
/// <summary>
/// Mutable per-spawn copy of an enemy's stats, handed to each of its abilities before the
/// enemy is built so they can alter what it spawns as.
/// </summary>
/// <remarks>
/// <b>Why a context rather than more parameters.</b> Wave buffs like "grounded enemies become
/// flying" or "enemies spawn at half health" have to land <i>before</i>
/// <c>EnemyHealth.InitializeServer</c> and before the spawn position is computed (flyers are
/// raised by their flight height). Threading each new stat through <c>SpawnEnemy</c> as another
/// argument was already unwieldy at two; this keeps the signature flat and gives abilities one
/// obvious place to write.
///
/// <para>Seeded from the <see cref="EnemyDefinition"/>, never written back to it — the
/// definition is a shared asset and a buff that mutated it would leak across waves, matches,
/// and (in the editor) sessions.</para>
/// </remarks>
public struct EnemySpawnContext
{
public float MaxHp;
public float MoveSpeed;
public bool IsFlying;
public float FlightHeight;
public int LivesCost;
/// <summary>Uniform transform scale relative to the prefab's authored scale.</summary>
public float VisualScale;
/// <summary>
/// Seeds a context from an enemy definition's authored stats, with health supplied by the
/// caller.
/// </summary>
/// <param name="def">The type being spawned. Supplies everything except health.</param>
/// <param name="resolvedMaxHp">Health this enemy spawns with, already resolved from the
/// encounter's budget and the type's <see cref="EnemyDefinition.HpMultiplier"/> — see
/// <see cref="EnemyScalingConfig.ResolvePerEnemyHp"/>. Passed in rather than read off the
/// definition because it depends on run position and the wave's enemy count, neither of
/// which the asset knows about.</param>
public static EnemySpawnContext FromDefinition(EnemyDefinition def, float resolvedMaxHp)
=> new EnemySpawnContext
{
MaxHp = resolvedMaxHp,
MoveSpeed = def.MoveSpeed,
IsFlying = def.IsFlying,
FlightHeight = def.FlightHeight,
LivesCost = def.LivesCost,
VisualScale = 1f,
};
}
}