Restructures the game around the cyclical run loop from Game Design Doc V2: 5 waves = a cycle, 3 cycles = a phase, each phase ends in a boss. - New TD.Gameplay.Waves: WaveGroup / PhaseDefinition / RunDefinition author the run as draggable weighted pools; RunState owns phase/cycle position, the drawn wave slots, and the per-slot enemy buff sets. WaveManager's flat wave array is gone -- it now only runs the encounter RunState points at. - New TD.Gameplay.EnemyUpgrades: the post-wave enemy-buff vote, with public live-replicated ballots so the HUD can show who voted for what. - Inter-wave flow is now strictly sequential: draft -> vote -> build, each stage ending early once every player has acted. - Enemy abilities inverted from per-instance random rolls to deterministic, stacking per-wave-slot sets. Six cards ship: Split (reworked), Flight, Blink, No Bounty, Gold Theft, Double Up. - Tower upgrades are a two-step tree: a draft pick unlocks a node, gold converts an already-placed tower in place. - Boss encounters flag their enemies and drive a boss HP bar. - Player cap reduced to 3 via MatchRules.MaxPlayers. - GoldConfig is now keyed by global encounter number rather than wave index. Compiles clean; NOT yet verified in-engine. Editor wiring still required -- see Docs/2.0_Setup_Checklist.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
107 lines
5.9 KiB
C#
107 lines
5.9 KiB
C#
// Assets/_Project/Scripts/Gameplay/EnemyAbilities/EnemyAbilityDefinition.cs
|
|
using UnityEngine;
|
|
using TD.Core;
|
|
|
|
namespace TD.Gameplay.EnemyAbilities
|
|
{
|
|
/// <summary>
|
|
/// Base class for one enemy ability (e.g. "split into smaller enemies on death"). Abilities
|
|
/// are attached to a <b>wave slot</b> by the post-wave player vote, and every enemy that wave
|
|
/// spawns from then on carries the whole accumulated set.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para><b>Assignment is deterministic, not random.</b> This system originally rolled an
|
|
/// ability per individual enemy against a no-ability weight. Under the 2.0 design the players
|
|
/// choose it, it applies to every enemy in the wave, and it persists for the rest of the phase
|
|
/// — so the roll is gone and <c>WaveManager</c> reads the slot's set instead. What survived is
|
|
/// the shape: one asset per kind, server-only hooks.</para>
|
|
///
|
|
/// <para><b>Abilities stack.</b> A wave can collect several across a phase's cycles, so an
|
|
/// enemy holds a list and every hook below runs once per ability. Implementations must not
|
|
/// assume they are the only ability on the enemy.</para>
|
|
///
|
|
/// <para><b>Server-only hooks.</b> All hooks run on the server only — <see cref="EnemyAbility"/>
|
|
/// gates every call on <c>IsServer</c>. They default to no-ops so a new card only overrides the
|
|
/// one or two it actually needs.</para>
|
|
///
|
|
/// <para><b>No per-enemy state on the asset.</b> A single asset is shared by every enemy
|
|
/// carrying the ability, across every wave and match. Anything per-instance belongs on the
|
|
/// <see cref="EnemyAbility"/> component or the enemy itself.</para>
|
|
/// </remarks>
|
|
public abstract class EnemyAbilityDefinition : ScriptableObject
|
|
{
|
|
/// <summary>Which enemy ability this asset's data belongs to.</summary>
|
|
public abstract EnemyAbilityKind Kind { get; }
|
|
|
|
[Header("Presentation")]
|
|
[Tooltip("Name shown in debug logs and the enemy-info panel.")]
|
|
public string DisplayName;
|
|
|
|
[Tooltip("Short description shown in the enemy-info panel.")]
|
|
[TextArea(2, 4)]
|
|
public string Description;
|
|
|
|
// ----- Spawn-time stat modification --------------------------------
|
|
|
|
/// <summary>
|
|
/// Server-only: alter what this enemy spawns as, before it is built. Runs for every enemy
|
|
/// of the buffed wave. Default no-op.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This is the hook for cards that change what an enemy fundamentally <i>is</i> — flight,
|
|
/// health, speed, size. It must run before the enemy exists, because spawn position
|
|
/// (flyers are raised) and <c>EnemyHealth</c>/<c>EnemyMovement</c> initialization all read
|
|
/// these values once and keep them.
|
|
///
|
|
/// <para>When several abilities stack, they see each other's edits in application order,
|
|
/// so multiplicative modifiers compose naturally. Prefer multiplying over assigning for
|
|
/// anything numeric, or the last card to run silently wins.</para>
|
|
/// </remarks>
|
|
public virtual void ServerModifySpawn(ref EnemySpawnContext context) { }
|
|
|
|
// ----- Lifetime hooks ----------------------------------------------
|
|
|
|
/// <summary>Server-only: called once, right after the ability set is assigned (before the
|
|
/// enemy's NetworkObject is spawned to clients). Default no-op.</summary>
|
|
/// <param name="abilityIndex">This ability's slot on <paramref name="instance"/> — the
|
|
/// same index <see cref="ServerTick"/> receives, so per-enemy timers can be seeded here.</param>
|
|
public virtual void ServerOnSpawn(EnemyAbility instance, int abilityIndex) { }
|
|
|
|
/// <summary>
|
|
/// Server-only: called every frame this ability is active on a live enemy. Default no-op.
|
|
/// </summary>
|
|
/// <param name="abilityIndex">This ability's slot on <paramref name="instance"/>. Pass it
|
|
/// to <see cref="EnemyAbility.TimerFor"/> to reach a per-enemy float this ability owns.</param>
|
|
/// <remarks>
|
|
/// <b>The asset holds no per-enemy state.</b> One <see cref="EnemyAbilityDefinition"/>
|
|
/// instance is shared by every enemy carrying the ability, so a cooldown stored in a field
|
|
/// here would be shared by the entire wave — every enemy would blink in lockstep, or worse,
|
|
/// race each other's writes. <see cref="EnemyAbility.TimerFor"/> gives each enemy its own
|
|
/// slot by reference, which is enough for the timer-driven abilities and costs no
|
|
/// allocation.
|
|
/// </remarks>
|
|
public virtual void ServerTick(EnemyAbility instance, int abilityIndex, float dt) { }
|
|
|
|
/// <summary>Server-only: called the instant the enemy's HP reaches zero, before the
|
|
/// death animation/despawn sequence plays. Default no-op.</summary>
|
|
public virtual void ServerOnDeath(EnemyAbility instance, EnemyHealth health) { }
|
|
|
|
// ----- Economy hooks -----------------------------------------------
|
|
|
|
/// <summary>
|
|
/// Server-only: adjust the gold a player earns for killing this enemy. Returns the reward
|
|
/// to pass on; default is unchanged.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Chained across a stacked ability set, each ability receiving the previous one's result.
|
|
/// Callers clamp the final value at zero, so returning a negative is safe but pointless.
|
|
/// </remarks>
|
|
public virtual int ServerModifyKillReward(EnemyAbility instance, int reward) => reward;
|
|
|
|
/// <summary>
|
|
/// Server-only: called when this enemy reaches the defense point, after lives have been
|
|
/// deducted. The hook for leak-punishing cards that cost more than lives. Default no-op.
|
|
/// </summary>
|
|
public virtual void ServerOnReachedGoal(EnemyAbility instance, PlayerSlot originZone) { }
|
|
}
|
|
}
|