UnityTowerDefense/Assets/_Project/Scripts/Gameplay/WaveDefinition.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

110 lines
4.4 KiB
C#

// Assets/_Project/Scripts/Gameplay/WaveDefinition.cs
using System;
using UnityEngine;
namespace TD.Gameplay
{
/// <summary>
/// A single spawn group within a wave: one enemy type and how many of them.
/// </summary>
[Serializable]
public struct WaveEntry
{
[Tooltip("The enemy type to spawn for this group.")]
public EnemyDefinition EnemyType;
[Tooltip("How many enemies of this type to spawn.")]
public int Count;
}
/// <summary>
/// Defines the composition of a single wave. One asset per wave; referenced in
/// order by <see cref="WaveManager.waveDefinitions"/>.
/// </summary>
/// <remarks>
/// Entries are processed in array order. Multiple entries let designers mix enemy
/// types within one wave (e.g. 10 fast scouts followed by 3 armoured brutes).
/// All enemies spawn held at wave start; they are released in 10% chunks separated
/// by <see cref="ReleaseInterval"/> seconds. The wave is not complete until all
/// enemies are dead or have leaked.
/// </remarks>
[CreateAssetMenu(fileName = "WaveDefinition", menuName = "TD/Wave Definition", order = 4)]
public class WaveDefinition : ScriptableObject
{
[Tooltip("Seconds between the wave-number advancing (start of prep) and the " +
"first enemies becoming visible. Gives players time to build before the horde appears.")]
public float PrepTime = 10f;
[Tooltip("Seconds between each chunk release. All enemies spawn held at wave " +
"start and are released 10% at a time on this interval.")]
public float ReleaseInterval = 2f;
[Tooltip("Enemy groups that make up this wave. Processed in order.")]
public WaveEntry[] Entries;
/// <summary>
/// The enemy type this wave is "about" — the first assigned entry's type. Null if the
/// wave has no usable entries.
/// </summary>
/// <remarks>
/// The 2.0 design gives each wave a single enemy type, and enemy upgrades are keyed to a
/// wave slot, so this is the identity the phase draw uses to keep slots distinct and the
/// HUD uses to label an upcoming wave. <see cref="Entries"/> still supports multiple
/// groups (useful for staggering counts of the same type), but mixing <i>types</i> within
/// one wave makes this ambiguous — see <see cref="HasSingleEnemyType"/>.
/// </remarks>
public EnemyDefinition PrimaryEnemyType
{
get
{
if (Entries == null) return null;
foreach (var e in Entries)
{
if (e.EnemyType != null && e.Count > 0) return e.EnemyType;
}
return null;
}
}
/// <summary>
/// True if every usable entry in this wave spawns the same enemy type. False means
/// <see cref="PrimaryEnemyType"/> is only telling part of the story — the run validator
/// warns on these rather than rejecting them, since a mixed wave still plays fine, it
/// just labels and upgrades oddly.
/// </summary>
public bool HasSingleEnemyType
{
get
{
var first = PrimaryEnemyType;
if (first == null) return false;
foreach (var e in Entries)
{
if (e.EnemyType != null && e.Count > 0 && e.EnemyType != first) return false;
}
return true;
}
}
/// <summary>Total enemies spawned per zone by this wave, across all entries.</summary>
/// <remarks>
/// Also the divisor for the encounter's health budget — see
/// <see cref="EnemyScalingConfig.ResolvePerEnemyHp"/>. That makes count a <i>texture</i>
/// knob rather than a difficulty one: a wave of 8 spawns eight tanks, a wave of 150 spawns
/// a fragile swarm, and both present the same total health for their slot in the run.
/// </remarks>
public int TotalEnemyCount
{
get
{
if (Entries == null) return 0;
int total = 0;
foreach (var e in Entries)
{
if (e.EnemyType != null && e.Count > 0) total += e.Count;
}
return total;
}
}
}
}