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
|
|
@ -1,4 +1,5 @@
|
|||
// Assets/_Project/Scripts/Gameplay/EnemyAbility.cs
|
||||
using System.Collections.Generic;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using TD.Core;
|
||||
|
|
@ -7,57 +8,101 @@ using TD.Gameplay.EnemyAbilities;
|
|||
namespace TD.Gameplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Per-enemy ability slot. Holds the <see cref="EnemyAbilityDefinition"/> rolled for this
|
||||
/// instance (if any) and drives its server-only hooks.
|
||||
/// Per-enemy ability set. Holds every <see cref="EnemyAbilityDefinition"/> its wave has
|
||||
/// accumulated and drives their server-only hooks.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Initialization:</b> Call <see cref="InitializeServer"/> on the server immediately after
|
||||
/// <c>Instantiate</c> and before <c>NetworkObject.Spawn()</c>, following the same pattern as
|
||||
/// <c>EnemyHealth.InitializeServer</c> / <c>EnemyMovement.InitializeServer</c>.
|
||||
///
|
||||
/// <b>A set, not a slot.</b> This used to hold one ability rolled at random per enemy. Wave
|
||||
/// buffs stack across a phase's cycles, so it now holds a list and fans every hook out over
|
||||
/// all of them. <see cref="kinds"/> replicates the set so clients can show players what
|
||||
/// they're facing.
|
||||
///
|
||||
/// <b>Optional component:</b> Not required by <c>EnemyHealth</c> or <c>EnemyMovement</c> —
|
||||
/// <c>WaveManager.SpawnEnemy</c> only rolls and assigns an ability when this component is
|
||||
/// present on the prefab, so existing enemy prefabs are unaffected until it's added to them.
|
||||
/// <c>WaveManager.SpawnEnemy</c> only assigns abilities when this component is present on the
|
||||
/// prefab, so enemy prefabs without it simply never carry wave buffs.
|
||||
///
|
||||
/// <b>On-death hook is not event-driven:</b> <c>WaveManager.HandleEnemyKilled</c> calls
|
||||
/// <see cref="EnemyAbilityDefinition.ServerOnDeath"/> directly (via <see cref="Definition"/>)
|
||||
/// rather than this component subscribing to <c>EnemyHealth.OnDied</c> itself — that keeps a
|
||||
/// split-spawn's <c>activeEnemyCount</c> increment ordered before the triggering kill's
|
||||
/// decrement, regardless of NetworkBehaviour subscription order.
|
||||
/// <see cref="ServerInvokeOnDeath"/> directly rather than this component subscribing to
|
||||
/// <c>EnemyHealth.OnDied</c> itself — that keeps a split-spawn's <c>activeEnemyCount</c>
|
||||
/// increment ordered before the triggering kill's decrement, regardless of NetworkBehaviour
|
||||
/// subscription order.
|
||||
/// </remarks>
|
||||
[RequireComponent(typeof(NetworkObject))]
|
||||
public class EnemyAbility : NetworkBehaviour
|
||||
{
|
||||
private readonly NetworkVariable<byte> kind = new NetworkVariable<byte>(
|
||||
(byte)EnemyAbilityKind.None,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server);
|
||||
// Replicated ability kinds carried by this enemy. Empty means no wave buffs. Exists so
|
||||
// clients can render "this wave splits and flies" in the enemy-info panel without
|
||||
// re-deriving it from run state.
|
||||
private NetworkList<byte> kinds;
|
||||
|
||||
// ----- Pre-spawn init (server-local) ----------------------------------
|
||||
|
||||
private EnemyAbilityDefinition pendingDefinition;
|
||||
private bool hasPendingInit;
|
||||
private readonly List<EnemyAbilityDefinition> pendingDefinitions = new List<EnemyAbilityDefinition>();
|
||||
private bool hasPendingInit;
|
||||
|
||||
// ----- Public state -----------------------------------------------------
|
||||
|
||||
/// <summary>The ability rolled for this enemy, or null if none was rolled.</summary>
|
||||
public EnemyAbilityDefinition Definition { get; private set; }
|
||||
private readonly List<EnemyAbilityDefinition> definitions = new List<EnemyAbilityDefinition>();
|
||||
|
||||
/// <summary>Replicated kind of the rolled ability. <see cref="EnemyAbilityKind.None"/> if
|
||||
/// none was rolled.</summary>
|
||||
public EnemyAbilityKind Kind => (EnemyAbilityKind)kind.Value;
|
||||
/// <summary>The abilities this enemy carries. Server-side; empty on clients.</summary>
|
||||
public IReadOnlyList<EnemyAbilityDefinition> Definitions => definitions;
|
||||
|
||||
/// <summary>True if this enemy carries no abilities at all.</summary>
|
||||
public bool IsPlain => definitions.Count == 0;
|
||||
|
||||
/// <summary>
|
||||
/// The stats this enemy actually spawned with, after every ability's spawn modification.
|
||||
/// Server-side.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Kept so on-death abilities can derive from what the enemy really was rather than from
|
||||
/// its <see cref="EnemyDefinition"/>. Split-on-death needs this: its minions are a
|
||||
/// percentage of "the enemy that died", which is not the authored asset once other cards
|
||||
/// on the same wave have modified it.
|
||||
/// </remarks>
|
||||
public EnemySpawnContext SpawnContext { get; private set; }
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
kinds = new NetworkList<byte>();
|
||||
}
|
||||
|
||||
/// <summary>Replicated check for a specific ability. Safe on any peer.</summary>
|
||||
public bool HasKind(EnemyAbilityKind kind)
|
||||
{
|
||||
byte target = (byte)kind;
|
||||
for (int i = 0; i < kinds.Count; i++)
|
||||
if (kinds[i] == target) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// ----- Server-only pre-spawn init -------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Called by <c>WaveManager</c> on the server after <c>Instantiate</c> and before
|
||||
/// <c>NetworkObject.Spawn()</c>. <paramref name="definition"/> may be null, meaning no
|
||||
/// ability was rolled for this enemy.
|
||||
/// <c>NetworkObject.Spawn()</c>. <paramref name="abilities"/> may be null or empty,
|
||||
/// meaning this enemy carries no wave buffs.
|
||||
/// </summary>
|
||||
public void InitializeServer(EnemyAbilityDefinition definition)
|
||||
/// <remarks>
|
||||
/// Copies rather than retains the caller's list — <c>WaveManager</c> reuses one scratch
|
||||
/// buffer across every enemy in a wave, so holding the reference would alias every enemy
|
||||
/// to the same (later mutated) contents.
|
||||
/// </remarks>
|
||||
public void InitializeServer(IReadOnlyList<EnemyAbilityDefinition> abilities,
|
||||
EnemySpawnContext spawnContext)
|
||||
{
|
||||
pendingDefinition = definition;
|
||||
hasPendingInit = true;
|
||||
pendingDefinitions.Clear();
|
||||
if (abilities != null)
|
||||
{
|
||||
for (int i = 0; i < abilities.Count; i++)
|
||||
if (abilities[i] != null) pendingDefinitions.Add(abilities[i]);
|
||||
}
|
||||
SpawnContext = spawnContext;
|
||||
hasPendingInit = true;
|
||||
}
|
||||
|
||||
// ----- NGO lifecycle --------------------------------------------------
|
||||
|
|
@ -66,22 +111,89 @@ namespace TD.Gameplay
|
|||
{
|
||||
if (!IsServer || !hasPendingInit) return;
|
||||
|
||||
Definition = pendingDefinition;
|
||||
kind.Value = (byte)(Definition != null ? Definition.Kind : EnemyAbilityKind.None);
|
||||
definitions.Clear();
|
||||
definitions.AddRange(pendingDefinitions);
|
||||
hasPendingInit = false;
|
||||
|
||||
if (Definition != null)
|
||||
Debug.Log($"[EnemyAbility] {name} rolled {Definition.Kind}.");
|
||||
timers = definitions.Count > 0
|
||||
? new float[definitions.Count]
|
||||
: System.Array.Empty<float>();
|
||||
|
||||
Definition?.ServerOnSpawn(this);
|
||||
kinds.Clear();
|
||||
for (int i = 0; i < definitions.Count; i++)
|
||||
kinds.Add((byte)definitions[i].Kind);
|
||||
|
||||
for (int i = 0; i < definitions.Count; i++)
|
||||
definitions[i].ServerOnSpawn(this, i);
|
||||
}
|
||||
|
||||
// ----- Server tick ------------------------------------------------
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!IsServer || Definition == null) return;
|
||||
Definition.ServerTick(this, Time.deltaTime);
|
||||
if (!IsServer || definitions.Count == 0) return;
|
||||
|
||||
float dt = Time.deltaTime;
|
||||
for (int i = 0; i < definitions.Count; i++)
|
||||
definitions[i].ServerTick(this, i, dt);
|
||||
}
|
||||
|
||||
// ----- Per-enemy ability scratch ----------------------------------
|
||||
|
||||
// One float per carried ability, owned by that ability. Exists because the definitions
|
||||
// are shared ScriptableObjects: a cooldown stored on the asset would be shared by every
|
||||
// enemy in the wave, making them all fire in lockstep. Allocated once at init, sized to
|
||||
// the ability count — never resized, since the set is fixed at spawn.
|
||||
private float[] timers = System.Array.Empty<float>();
|
||||
|
||||
/// <summary>
|
||||
/// A per-enemy float owned by the ability at <paramref name="abilityIndex"/>, returned by
|
||||
/// reference so it can be read and written in place. Typically a cooldown accumulator.
|
||||
/// </summary>
|
||||
public ref float TimerFor(int abilityIndex)
|
||||
{
|
||||
if (abilityIndex < 0 || abilityIndex >= timers.Length)
|
||||
{
|
||||
// Out-of-range means a caller passed an index that doesn't match its slot. Hand
|
||||
// back a scratch cell rather than throwing mid-tick; the ability just won't
|
||||
// accumulate, which is visible in play but not fatal.
|
||||
Debug.LogError($"[EnemyAbility] TimerFor({abilityIndex}) is out of range on " +
|
||||
$"{name}. The ability will not keep time.");
|
||||
return ref timerFallback;
|
||||
}
|
||||
return ref timers[abilityIndex];
|
||||
}
|
||||
|
||||
private float timerFallback;
|
||||
|
||||
// ----- Server hook fan-out ----------------------------------------
|
||||
|
||||
/// <summary>Server-only: run every carried ability's on-death hook.</summary>
|
||||
public void ServerInvokeOnDeath(EnemyHealth health)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
for (int i = 0; i < definitions.Count; i++)
|
||||
definitions[i].ServerOnDeath(this, health);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-only: chain every carried ability's kill-reward modifier. Result is clamped at
|
||||
/// zero so a stack of reward-suppressing cards can't hand out negative gold.
|
||||
/// </summary>
|
||||
public int ServerModifyKillReward(int reward)
|
||||
{
|
||||
if (!IsServer) return reward;
|
||||
for (int i = 0; i < definitions.Count; i++)
|
||||
reward = definitions[i].ServerModifyKillReward(this, reward);
|
||||
return Mathf.Max(0, reward);
|
||||
}
|
||||
|
||||
/// <summary>Server-only: run every carried ability's reached-goal hook.</summary>
|
||||
public void ServerInvokeReachedGoal(PlayerSlot originZone)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
for (int i = 0; i < definitions.Count; i++)
|
||||
definitions[i].ServerOnReachedGoal(this, originZone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue