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
|
|
@ -7,32 +7,60 @@ namespace TD.Gameplay.EnemyAbilities
|
|||
/// <summary>
|
||||
/// On death, spawns <see cref="SplitCount"/> smaller copies of the dying enemy's own type at
|
||||
/// the corpse's position, continuing on toward the goal instead of restarting from the wave's
|
||||
/// spawner. There's no separate "minion" EnemyDefinition to author — the mini copies reuse
|
||||
/// whichever EnemyDefinition/prefab the dying enemy itself was spawned from, scaled down by
|
||||
/// <see cref="HpMultiplier"/>/<see cref="ScaleMultiplier"/>.
|
||||
/// spawner.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>No separate minion asset.</b> The copies reuse whichever <see cref="EnemyDefinition"/>
|
||||
/// and prefab the dying enemy was spawned from; every stat is expressed here as a percentage
|
||||
/// of what the parent actually spawned with, so one card covers any enemy it lands on.
|
||||
///
|
||||
/// <para><b>Percentages are of the PARENT, not the asset.</b> If other cards on the same wave
|
||||
/// have already modified the enemy, the minions scale off the modified values — a split of a
|
||||
/// buffed enemy produces buffed minions, which is what players will expect from watching it.</para>
|
||||
///
|
||||
/// <para><b>Minions inherit no abilities at all</b> — not even this one. That is what makes the
|
||||
/// recursion terminate: a minion of a splitting enemy is a plain enemy, so a wave carrying
|
||||
/// Split produces exactly one extra generation regardless of how many other cards it has
|
||||
/// collected. Enforced at the spawn site, not here.</para>
|
||||
/// </remarks>
|
||||
[CreateAssetMenu(fileName = "SplitOnDeathAbility", menuName = "TD/Enemy Abilities/Split On Death")]
|
||||
public class SplitOnDeathAbilityDefinition : EnemyAbilityDefinition
|
||||
{
|
||||
public override EnemyAbilityKind Kind => EnemyAbilityKind.SplitOnDeath;
|
||||
|
||||
[Header("Split")]
|
||||
[Tooltip("How many mini copies spawn on death.")]
|
||||
[Tooltip("How many copies spawn when the parent dies.")]
|
||||
[Min(1)]
|
||||
public int SplitCount = 2;
|
||||
|
||||
[Tooltip("Each mini copy's MaxHp = the dying enemy's own MaxHp * this multiplier.")]
|
||||
[Range(0.01f, 1f)]
|
||||
public float HpMultiplier = 0.5f;
|
||||
[Header("Minion stats (percent of the parent's spawned values)")]
|
||||
[Tooltip("Minion max HP as a fraction of the parent's max HP.")]
|
||||
[Range(0.01f, 2f)]
|
||||
public float HpPercent = 0.5f;
|
||||
|
||||
[Tooltip("Uniform transform scale applied to each mini copy, relative to the normal " +
|
||||
"prefab scale. Requires the enemy prefab's NetworkTransform to sync scale, " +
|
||||
"or the mini will render full-size on remote peers.")]
|
||||
[Range(0.1f, 1f)]
|
||||
public float ScaleMultiplier = 0.6f;
|
||||
[Tooltip("Minion move speed as a fraction of the parent's speed. Above 1 makes the split " +
|
||||
"faster than what died — the classic 'smaller and quicker' read.")]
|
||||
[Range(0.01f, 3f)]
|
||||
public float SpeedPercent = 1.25f;
|
||||
|
||||
[Tooltip("Random scatter radius (world units) applied to each split spawn so they don't " +
|
||||
"spawn exactly on top of each other.")]
|
||||
[Tooltip("Minion transform scale as a fraction of the parent's scale. Requires the enemy " +
|
||||
"prefab's NetworkTransform to sync scale, or minions render full-size on remote peers.")]
|
||||
[Range(0.1f, 2f)]
|
||||
public float ScalePercent = 0.6f;
|
||||
|
||||
[Tooltip("Minion lives cost as a fraction of the parent's, rounded down but never below 1 " +
|
||||
"— a leak always has to hurt, or splits become a way to farm free ground.")]
|
||||
[Range(0f, 2f)]
|
||||
public float LivesCostPercent = 1f;
|
||||
|
||||
[Tooltip("Minion flight height as a fraction of the parent's. Only meaningful when the " +
|
||||
"parent is flying; ground splits ignore it.")]
|
||||
[Range(0.1f, 2f)]
|
||||
public float FlightHeightPercent = 1f;
|
||||
|
||||
[Header("Placement")]
|
||||
[Tooltip("Random scatter radius (world units) applied to each spawn so minions don't " +
|
||||
"stack exactly on top of each other.")]
|
||||
[Min(0f)]
|
||||
public float ScatterRadius = 0.5f;
|
||||
|
||||
|
|
@ -41,12 +69,24 @@ namespace TD.Gameplay.EnemyAbilities
|
|||
var def = health.Definition;
|
||||
if (def == null) return;
|
||||
|
||||
var movement = instance.GetComponent<EnemyMovement>();
|
||||
var atTile = GridCoordinates.WorldToGrid(instance.transform.position);
|
||||
var movement = instance.GetComponent<EnemyMovement>();
|
||||
var atTile = GridCoordinates.WorldToGrid(instance.transform.position);
|
||||
var ownerSlot = movement != null ? movement.OriginZone : PlayerSlot.None;
|
||||
|
||||
// Derive the minion's stats from what the parent actually spawned as.
|
||||
var parent = instance.SpawnContext;
|
||||
var minion = new EnemySpawnContext
|
||||
{
|
||||
MaxHp = Mathf.Max(1f, parent.MaxHp * HpPercent),
|
||||
MoveSpeed = Mathf.Max(0.01f, parent.MoveSpeed * SpeedPercent),
|
||||
IsFlying = parent.IsFlying,
|
||||
FlightHeight = parent.FlightHeight * FlightHeightPercent,
|
||||
LivesCost = Mathf.Max(1, Mathf.FloorToInt(parent.LivesCost * LivesCostPercent)),
|
||||
VisualScale = parent.VisualScale * ScalePercent,
|
||||
};
|
||||
|
||||
WaveManager.Instance?.ServerSpawnSplitEnemies(
|
||||
def, SplitCount, atTile, ownerSlot, ScatterRadius, HpMultiplier, ScaleMultiplier);
|
||||
def, SplitCount, atTile, ownerSlot, ScatterRadius, minion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue