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
|
|
@ -303,6 +303,10 @@ namespace TD.Gameplay
|
|||
ApplyTint();
|
||||
paintColor.OnValueChanged += HandlePaintColorChanged;
|
||||
|
||||
// An upgrade swaps the replicated TypeId out from under every peer; re-resolve so
|
||||
// clients pick up the new stats and tint instead of holding the pre-upgrade asset.
|
||||
definitionTypeId.OnValueChanged += HandleDefinitionTypeChanged;
|
||||
|
||||
// Register for minimap rendering.
|
||||
MinimapEntityRegistry.Register(this);
|
||||
|
||||
|
|
@ -332,7 +336,8 @@ namespace TD.Gameplay
|
|||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
paintColor.OnValueChanged -= HandlePaintColorChanged;
|
||||
paintColor.OnValueChanged -= HandlePaintColorChanged;
|
||||
definitionTypeId.OnValueChanged -= HandleDefinitionTypeChanged;
|
||||
|
||||
// Un-stamp the footprint when the tower is destroyed (sold, wave end, etc.)
|
||||
// so the tiles become walkable and buildable again.
|
||||
|
|
@ -412,6 +417,152 @@ namespace TD.Gameplay
|
|||
upgradeCount.Value += 1;
|
||||
}
|
||||
|
||||
// ----- Upgrading ------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Fired on every peer when this tower's definition changes (i.e. it was upgraded).
|
||||
/// The HUD subscribes to relabel a selected tower's action grid.
|
||||
/// </summary>
|
||||
public event System.Action OnDefinitionChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gold cost to convert this tower into <paramref name="target"/>. The target's own
|
||||
/// <see cref="TowerDefinition.GoldCost"/> is the price — upgrade nodes are never placed
|
||||
/// directly, so their cost field is free to mean "what this upgrade costs".
|
||||
/// </summary>
|
||||
public static int GetUpgradeCost(TowerDefinition target) => target != null ? target.GoldCost : 0;
|
||||
|
||||
/// <summary>
|
||||
/// True if <paramref name="target"/> is a legal upgrade of this tower's current type: a
|
||||
/// direct child in the upgrade tree, with a matching footprint.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>The footprint check is not cosmetic.</b> A tower's occupied/unwalkable tiles are
|
||||
/// stamped into the grid at its current size; converting to a differently-sized node would
|
||||
/// leave the stamp describing a shape the tower no longer has, corrupting both pathfinding
|
||||
/// and future placement checks. Growing a tower's footprint needs a stamp-swap (and a
|
||||
/// re-validation that the new tiles are even free), which the tree doesn't currently need —
|
||||
/// so it's rejected loudly rather than half-supported.
|
||||
/// </remarks>
|
||||
public bool CanUpgradeTo(TowerDefinition target)
|
||||
{
|
||||
if (target == null || resolvedDefinition == null) return false;
|
||||
if (resolvedDefinition.UpgradePaths == null) return false;
|
||||
|
||||
bool isChild = false;
|
||||
foreach (var path in resolvedDefinition.UpgradePaths)
|
||||
{
|
||||
if (path == target) { isChild = true; break; }
|
||||
}
|
||||
if (!isChild) return false;
|
||||
|
||||
return target.FootprintSize == resolvedDefinition.FootprintSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collects the upgrades <paramref name="clientId"/> can currently apply to this tower:
|
||||
/// direct children of its type that the player has unlocked. Does not filter on gold —
|
||||
/// the HUD shows unaffordable upgrades disabled rather than hiding them, so players can
|
||||
/// see what they're saving for.
|
||||
/// </summary>
|
||||
public void CollectAvailableUpgrades(ulong clientId, List<(TowerDefinition Def, int TypeId)> into)
|
||||
{
|
||||
into.Clear();
|
||||
|
||||
var unlocked = PlayerTowerUpgrades.GetForClient(clientId);
|
||||
var pm = TowerPlacementManager.Instance;
|
||||
if (unlocked == null || pm == null || resolvedDefinition?.UpgradePaths == null) return;
|
||||
|
||||
foreach (var path in resolvedDefinition.UpgradePaths)
|
||||
{
|
||||
if (path == null) continue;
|
||||
if (!pm.TryGetTypeId(path, out int typeId)) continue;
|
||||
if (!unlocked.Contains(typeId)) continue;
|
||||
if (!CanUpgradeTo(path)) continue;
|
||||
|
||||
into.Add((path, typeId));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client → server request to convert this tower into <paramref name="targetTypeId"/>.
|
||||
/// Accepted only from the owner, only for a node they've unlocked, only along a real tree
|
||||
/// edge, and only if they can pay.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Every check is repeated here even though the HUD already filters — the HUD is a
|
||||
/// convenience, this is the authority. Same posture as placement, paint, and sell.
|
||||
/// </remarks>
|
||||
[Rpc(SendTo.Server)]
|
||||
public void RequestUpgradeServerRpc(int targetTypeId, RpcParams rpcParams = default)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
if (serverSold) return; // mid-sell; nothing to upgrade
|
||||
|
||||
ulong senderClientId = rpcParams.Receive.SenderClientId;
|
||||
PlayerSlot senderSlot = PlayerMatchState.SlotForClient(senderClientId);
|
||||
if (senderSlot == PlayerSlot.None || senderSlot != ownerSlot.Value)
|
||||
{
|
||||
Debug.Log($"[TowerInstance] Upgrade rejected: client {senderClientId} " +
|
||||
$"({senderSlot}) does not own tower owned by {ownerSlot.Value}.");
|
||||
return;
|
||||
}
|
||||
|
||||
var target = TowerPlacementManager.GetDefinition(targetTypeId);
|
||||
if (target == null)
|
||||
{
|
||||
Debug.Log($"[TowerInstance] Upgrade rejected: TypeId {targetTypeId} is not in the catalog.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CanUpgradeTo(target))
|
||||
{
|
||||
Debug.Log($"[TowerInstance] Upgrade rejected: '{target.name}' is not a " +
|
||||
$"same-footprint child of '{resolvedDefinition?.name}'.");
|
||||
return;
|
||||
}
|
||||
|
||||
var unlocked = PlayerTowerUpgrades.GetForClient(senderClientId);
|
||||
if (unlocked == null || !unlocked.Contains(targetTypeId))
|
||||
{
|
||||
Debug.Log($"[TowerInstance] Upgrade rejected: client {senderClientId} has not " +
|
||||
$"unlocked '{target.name}'.");
|
||||
return;
|
||||
}
|
||||
|
||||
int cost = GetUpgradeCost(target);
|
||||
var gold = PlayerGoldManager.GetForClient(senderClientId);
|
||||
if (gold == null || gold.CurrentGold < cost)
|
||||
{
|
||||
Debug.Log($"[TowerInstance] Upgrade rejected: client {senderClientId} cannot " +
|
||||
$"afford '{target.name}' ({cost}g).");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cost > 0) gold.DeductGold(cost);
|
||||
|
||||
// Record the spend before switching type, so the refund reflects everything sunk in
|
||||
// and the tower loses any full-refund-while-unupgraded status.
|
||||
ServerAddUpgradeInvestment(cost);
|
||||
|
||||
// Switching the replicated TypeId is the upgrade: TowerCombat re-reads Definition
|
||||
// every tick, so the new stats take effect on the next shot with nothing to notify.
|
||||
definitionTypeId.Value = targetTypeId;
|
||||
resolvedDefinition = target;
|
||||
|
||||
OnDefinitionChanged?.Invoke();
|
||||
}
|
||||
|
||||
// Re-resolve and re-tint on clients when the type changes under them (an upgrade landed).
|
||||
private void HandleDefinitionTypeChanged(int previous, int current)
|
||||
{
|
||||
if (previous == current) return;
|
||||
|
||||
resolvedDefinition = TowerPlacementManager.GetDefinition(current);
|
||||
ApplyTint();
|
||||
OnDefinitionChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client → server request to sell this tower. Accepted only from the tower's owner
|
||||
/// (same ownership rule as placement and paint). The server refunds gold, broadcasts
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue