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>
67 lines
3.6 KiB
C#
67 lines
3.6 KiB
C#
// Assets/_Project/Scripts/Gameplay/EnemyDefinition.cs
|
|
using UnityEngine;
|
|
|
|
namespace TD.Gameplay
|
|
{
|
|
/// <summary>
|
|
/// Data definition for a single enemy type. One asset per type; shared across all
|
|
/// instances spawned in a match. Consumed by <see cref="WaveManager"/> at spawn time.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Follows the same ScriptableObject pattern as <c>TowerDefinition</c>: data lives
|
|
/// in project assets, only the asset reference (or its fields) crosses runtime code.
|
|
/// Replace <see cref="EnemyPrefab"/> with a real mesh/animator when art is ready —
|
|
/// no code changes required.
|
|
///
|
|
/// <para><b>An enemy type is flavour, not a difficulty tier.</b> There is deliberately no
|
|
/// absolute health field: health comes from <see cref="EnemyScalingConfig"/> based on how far
|
|
/// into the run the encounter sits, and this asset only says how the type deviates from that
|
|
/// (<see cref="HpMultiplier"/>). Because waves are drawn at random from a phase pool, any
|
|
/// difficulty baked into the roster would be difficulty handed out by the luck of the draw.
|
|
/// Every type should be a viable occupant of any slot.</para>
|
|
/// </remarks>
|
|
[CreateAssetMenu(fileName = "EnemyDefinition", menuName = "TD/Enemy Definition", order = 3)]
|
|
public class EnemyDefinition : ScriptableObject
|
|
{
|
|
[Header("Identity")]
|
|
[Tooltip("Human-readable name shown in debug logs and future enemy-info UI.")]
|
|
public string DisplayName;
|
|
|
|
[Header("Stats")]
|
|
[Tooltip("How tough this enemy is RELATIVE to its encounter's budget — not an absolute " +
|
|
"hit-point value. 1.0 = exactly its share of the budget; 1.25 = a quarter " +
|
|
"tougher than the slot calls for. Keep it near 1.0 and let the wave's enemy " +
|
|
"count express tankiness (fewer enemies each get a bigger share).")]
|
|
[Min(0.01f)]
|
|
public float HpMultiplier = 1f;
|
|
|
|
[Tooltip("Movement speed in world units per second along the A* path. ABSOLUTE and " +
|
|
"unscaled — speed is a fixed character trait, not a progression axis. See " +
|
|
"EnemyScalingConfig for why.")]
|
|
public float MoveSpeed = 3f;
|
|
|
|
[Tooltip("When true this enemy flies: it paths on the baked terrain grid, ignoring " +
|
|
"towers, so it soars directly over the maze instead of following it. " +
|
|
"Towers with GroundedOnly=true will not target it.")]
|
|
public bool IsFlying;
|
|
|
|
[Tooltip("Height (world units) above the ground a flying enemy hovers, so it " +
|
|
"visually clears towers. Only used when IsFlying is true. Keep modest — " +
|
|
"tower targeting uses 3D range, so large values eat into effective range.")]
|
|
public float FlightHeight = 3f;
|
|
|
|
[Header("Costs")]
|
|
[Tooltip("Number of lives deducted from the shared pool when this enemy " +
|
|
"reaches the Goal. Boss enemies might cost 2 or more lives.")]
|
|
public int LivesCost = 1;
|
|
// GoldReward removed in favor of per-wave GoldPerEnemy in GoldConfig — every
|
|
// enemy in a given wave grants the same kill reward regardless of type. If
|
|
// per-type variation is needed later, add an optional override here.
|
|
|
|
[Header("Visuals")]
|
|
[Tooltip("Prefab spawned in the world for this enemy. Must have NetworkObject, " +
|
|
"NetworkTransform, EnemyHealth, EnemyStatus, and EnemyMovement at its root. " +
|
|
"Place it on the Enemy physics layer.")]
|
|
public GameObject EnemyPrefab;
|
|
}
|
|
}
|