First pass at full refactor to 2.0 design
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>
This commit is contained in:
parent
7e5c3a8279
commit
4892d7253d
64 changed files with 4023 additions and 344 deletions
|
|
@ -5,21 +5,28 @@ using TD.Core;
|
|||
namespace TD.Gameplay.EnemyAbilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for one enemy ability (e.g. "split into smaller enemies on death"). Rolled
|
||||
/// at random for each spawned enemy by <see cref="EnemyAbilityPool.RollRandom"/> and applied
|
||||
/// via <see cref="EnemyAbility.InitializeServer"/>.
|
||||
/// 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>One asset per kind.</b> <see cref="Kind"/> is fixed per subclass, same as
|
||||
/// <see cref="BuilderSpells.BuilderSpellDefinition"/>. <see cref="EnemyAbilityPool"/> uses
|
||||
/// it to build a fixed-size, enum-indexed lookup table.</para>
|
||||
/// <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>Server-only hooks.</b> All three hooks below only ever run on the server —
|
||||
/// <see cref="EnemyAbility"/> only calls them when <c>IsServer</c> is true. <see
|
||||
/// cref="ServerOnSpawn"/> and <see cref="ServerTick"/> are no-ops for abilities that don't
|
||||
/// need spawn-time setup or a per-frame timer (e.g. Split on Death uses neither); they exist
|
||||
/// so a future cooldown-driven ability (disable towers, teleport) doesn't need a base-class
|
||||
/// change.</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
|
||||
{
|
||||
|
|
@ -27,29 +34,74 @@ namespace TD.Gameplay.EnemyAbilities
|
|||
public abstract EnemyAbilityKind Kind { get; }
|
||||
|
||||
[Header("Presentation")]
|
||||
[Tooltip("Name shown in debug logs and future enemy-info UI.")]
|
||||
[Tooltip("Name shown in debug logs and the enemy-info panel.")]
|
||||
public string DisplayName;
|
||||
|
||||
[Tooltip("Short description shown in future enemy-info UI.")]
|
||||
[Tooltip("Short description shown in the enemy-info panel.")]
|
||||
[TextArea(2, 4)]
|
||||
public string Description;
|
||||
|
||||
[Header("Selection")]
|
||||
[Tooltip("Relative weight of this ability being rolled, vs. the pool's other abilities " +
|
||||
"and its no-ability chance. Same semantics as DraftOption.Weight.")]
|
||||
[Min(0f)]
|
||||
public float Weight = 1f;
|
||||
// ----- Spawn-time stat modification --------------------------------
|
||||
|
||||
/// <summary>Server-only: called once, right after this ability is assigned (before the
|
||||
/// <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>
|
||||
public virtual void ServerOnSpawn(EnemyAbility instance) { }
|
||||
/// <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>
|
||||
public virtual void ServerTick(EnemyAbility instance, float dt) { }
|
||||
/// <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) { }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue