Updating HUD, Gold Config, and finishing off Play flow for 9player map.
This commit is contained in:
parent
a7be12fa9b
commit
3dcc0e7edd
28 changed files with 2272 additions and 9601 deletions
|
|
@ -58,13 +58,40 @@ namespace TD.Gameplay
|
|||
[Tooltip("Shared lives pool at the start of a match.")]
|
||||
[SerializeField] private int startingLives = 20;
|
||||
|
||||
[Tooltip("Single source of truth for every gold tunable: starting gold, per-wave " +
|
||||
"kill rewards, completion bonus, no-leak bonus. Required for a real match; " +
|
||||
"if unset the game falls back to per-player startingGold defaults and grants " +
|
||||
"no kill/wave rewards (designer-error indicator, not a supported runtime mode).")]
|
||||
[SerializeField] private GoldConfig goldConfig;
|
||||
|
||||
// ----- Networked state --------------------------------------------
|
||||
|
||||
// Per-slot zone-leak counters. Index = (int)PlayerSlot; size = 10 (0-9).
|
||||
// Index 0 (PlayerSlot.None) is allocated but never written.
|
||||
// Replicated so the HUD can show per-player leak scores on all peers.
|
||||
// Per-slot total leak counters across the whole match. Index = (int)PlayerSlot;
|
||||
// size = 10 (0-9). Index 0 (PlayerSlot.None) is allocated but never written.
|
||||
// Replicated so the HUD scoreboard can show total leaks per player.
|
||||
//
|
||||
// Semantics: zoneLeakCounts[P] is incremented exactly once per enemy that
|
||||
// ORIGINATED in player P's spawn AND escaped P's zone (crossed P's leak
|
||||
// volume). Transit through other zones (e.g. a P1 enemy passing through
|
||||
// P4 on its way to the goal) does NOT increment any counter — this is the
|
||||
// "enemies I failed to stop in my own maze" metric, not a transit count.
|
||||
private readonly NetworkList<int> zoneLeakCounts = new NetworkList<int>();
|
||||
|
||||
// Per-slot leak counter for the CURRENT wave only. Same shape as zoneLeakCounts;
|
||||
// server resets every entry to 0 at the start of each wave. Used to determine
|
||||
// who earns the NoLeaksBonus on wave completion. Not currently surfaced in UI
|
||||
// separately from zoneLeakCounts — could be exposed if a "this wave: 0 leaks"
|
||||
// indicator becomes desirable.
|
||||
private readonly NetworkList<int> waveLeakCounts = new NetworkList<int>();
|
||||
|
||||
// Networked prep-phase countdown. Counts down from WaveDefinition.PrepTime to
|
||||
// zero during prep; 0 while the wave is active or being mopped up. Read by the
|
||||
// HUD (next-wave-label) to render "next: 0:12". Server is the only writer.
|
||||
private readonly NetworkVariable<float> prepCountdown = new NetworkVariable<float>(
|
||||
value: 0f,
|
||||
readPerm: NetworkVariableReadPermission.Everyone,
|
||||
writePerm: NetworkVariableWritePermission.Server);
|
||||
|
||||
// ----- Server-local runtime state ---------------------------------
|
||||
|
||||
private int remainingLives;
|
||||
|
|
@ -87,9 +114,12 @@ namespace TD.Gameplay
|
|||
|
||||
if (!IsServer) return;
|
||||
|
||||
// Populate the NetworkList with 10 zeros (indices 0-9 for PlayerSlot.None..Player9).
|
||||
// Populate the NetworkLists with 10 zeros (indices 0-9 for PlayerSlot.None..Player9).
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
zoneLeakCounts.Add(0);
|
||||
waveLeakCounts.Add(0);
|
||||
}
|
||||
|
||||
remainingLives = startingLives;
|
||||
|
||||
|
|
@ -115,6 +145,21 @@ namespace TD.Gameplay
|
|||
ms.SetLives(remainingLives);
|
||||
ms.OnPhaseChanged += HandlePhaseChanged;
|
||||
|
||||
// Apply StartingGold from the config to every connected player, overwriting
|
||||
// whatever fallback the PlayerGoldManager set during its own OnNetworkSpawn.
|
||||
// PlayerGoldManager spawns once per client connection (in MainMenu/Lobby),
|
||||
// before the Match scene exists — so this is where the config-driven init
|
||||
// actually lands. Skipped silently if no goldConfig is assigned; the per-
|
||||
// player fallback startingGold stays.
|
||||
if (goldConfig != null)
|
||||
{
|
||||
foreach (var pms in PlayerMatchState.AllPlayers)
|
||||
{
|
||||
var gm = PlayerGoldManager.GetForClient(pms.OwnerClientId);
|
||||
if (gm != null) gm.ServerSetGold(goldConfig.StartingGold);
|
||||
}
|
||||
}
|
||||
|
||||
if (ms.Phase == MatchPhase.Playing)
|
||||
StartNextWave();
|
||||
}
|
||||
|
|
@ -137,8 +182,8 @@ namespace TD.Gameplay
|
|||
public int TotalWaves => waveDefinitions?.Length ?? 0;
|
||||
|
||||
/// <summary>
|
||||
/// Number of times enemies have leaked out of the given player's zone.
|
||||
/// Replicated — safe to call on any peer.
|
||||
/// Number of times enemies have leaked out of the given player's zone over the
|
||||
/// entire match. Replicated — safe to call on any peer.
|
||||
/// </summary>
|
||||
public int GetZoneLeakCount(PlayerSlot slot)
|
||||
{
|
||||
|
|
@ -146,6 +191,20 @@ namespace TD.Gameplay
|
|||
return (idx >= 0 && idx < zoneLeakCounts.Count) ? zoneLeakCounts[idx] : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="GoldConfig"/> assigned to this wave manager, or null if unset.
|
||||
/// Exposed so other systems (HUD, future income panels) can read its values.
|
||||
/// </summary>
|
||||
public GoldConfig GoldConfig => goldConfig;
|
||||
|
||||
/// <summary>
|
||||
/// Seconds remaining in the prep phase before the next wave starts spawning.
|
||||
/// Zero outside of the prep phase. Replicated; safe to call on any peer.
|
||||
/// HUD reads this each frame to render the countdown label.
|
||||
/// </summary>
|
||||
public float PrepCountdown => prepCountdown.Value;
|
||||
|
||||
|
||||
// ----- Phase handling ---------------------------------------------
|
||||
|
||||
private void HandlePhaseChanged(MatchPhase previous, MatchPhase next)
|
||||
|
|
@ -172,6 +231,18 @@ namespace TD.Gameplay
|
|||
// shows the upcoming wave number during the countdown.
|
||||
MatchState.Instance?.SetCurrentWave(currentWaveIndex + 1); // 1-based
|
||||
|
||||
// Reset per-wave bookkeeping for the wave that's about to begin:
|
||||
// - waveLeakCounts: per-slot leaks this wave, used for the no-leak bonus.
|
||||
// - PlayerGoldManager.goldEarnedThisWave: HUD top-bar resets so the
|
||||
// "+N g/wave" counter starts at 0.
|
||||
for (int i = 0; i < waveLeakCounts.Count; i++)
|
||||
waveLeakCounts[i] = 0;
|
||||
foreach (var pms in PlayerMatchState.AllPlayers)
|
||||
{
|
||||
var gm = PlayerGoldManager.GetForClient(pms.OwnerClientId);
|
||||
if (gm != null) gm.ServerResetWaveEarnings();
|
||||
}
|
||||
|
||||
activeEnemyCount = 0;
|
||||
spawningComplete = false;
|
||||
|
||||
|
|
@ -229,9 +300,34 @@ namespace TD.Gameplay
|
|||
private IEnumerator RunWave(WaveDefinition def, bool skipPrep = false)
|
||||
{
|
||||
// Prep phase — players build while the countdown ticks. Skipped when
|
||||
// a dev cheat forces the wave to start immediately.
|
||||
// a dev cheat forces the wave to start immediately. We tick the
|
||||
// replicated prepCountdown each frame so every HUD can render the
|
||||
// remaining time consistently. Server is the only writer; clients
|
||||
// observe the value via NetworkVariable replication.
|
||||
if (!skipPrep)
|
||||
yield return new WaitForSeconds(def.PrepTime);
|
||||
{
|
||||
prepCountdown.Value = def.PrepTime;
|
||||
float remaining = def.PrepTime;
|
||||
// Throttle network sync to ~10 Hz. NetworkVariable replicates on every
|
||||
// mutation; at 60 fps we'd send ~600 deltas per 10s prep purely to
|
||||
// animate text that only changes once per second on the HUD. 0.1s
|
||||
// gives a smooth-enough fall while keeping bandwidth minimal.
|
||||
const float NetworkSyncInterval = 0.1f;
|
||||
float nextSync = def.PrepTime - NetworkSyncInterval;
|
||||
while (remaining > 0f)
|
||||
{
|
||||
yield return null;
|
||||
remaining = Mathf.Max(0f, remaining - Time.deltaTime);
|
||||
if (remaining <= nextSync || remaining <= 0f)
|
||||
{
|
||||
prepCountdown.Value = remaining;
|
||||
nextSync = remaining - NetworkSyncInterval;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ensure the countdown reads zero entering the spawn phase, regardless of
|
||||
// whether prep was skipped or just expired.
|
||||
prepCountdown.Value = 0f;
|
||||
|
||||
// Spawn phase.
|
||||
if (def.Entries != null)
|
||||
|
|
@ -276,11 +372,15 @@ namespace TD.Gameplay
|
|||
if (PlayerMatchState.GetForSlot(zone.Owner) == null) continue;
|
||||
|
||||
// Use the first spawner in the zone. Future: round-robin through Spawners.
|
||||
SpawnEnemy(def, zone.Spawners[0].TilePosition);
|
||||
// Pass zone.Owner explicitly so EnemyMovement knows which player owns
|
||||
// this enemy for leak attribution — can't be derived from the spawner
|
||||
// tile's owner-grid entry because SpawnerVolume sits outside
|
||||
// PlayerZoneVolume (so OwnerGrid[spawnerTile] = None).
|
||||
SpawnEnemy(def, zone.Spawners[0].TilePosition, zone.Owner);
|
||||
}
|
||||
}
|
||||
|
||||
private void SpawnEnemy(EnemyDefinition def, Vector2Int spawnerTile)
|
||||
private void SpawnEnemy(EnemyDefinition def, Vector2Int spawnerTile, PlayerSlot ownerSlot)
|
||||
{
|
||||
if (def.EnemyPrefab == null)
|
||||
{
|
||||
|
|
@ -304,8 +404,8 @@ namespace TD.Gameplay
|
|||
return;
|
||||
}
|
||||
|
||||
health.InitializeServer(def.MaxHp, def.GoldReward, def.LivesCost, def.IsFlying);
|
||||
movement.InitializeServer(def.MoveSpeed, spawnerTile);
|
||||
health.InitializeServer(def.MaxHp, def.LivesCost, def.IsFlying);
|
||||
movement.InitializeServer(def.MoveSpeed, spawnerTile, ownerSlot);
|
||||
|
||||
health.OnDied += HandleEnemyKilled;
|
||||
movement.OnZoneLeaked += HandleZoneLeak;
|
||||
|
|
@ -320,22 +420,29 @@ namespace TD.Gameplay
|
|||
|
||||
private void HandleEnemyKilled(EnemyHealth health)
|
||||
{
|
||||
// Kill reward comes from GoldConfig for the current wave — same value for
|
||||
// every enemy in the wave regardless of EnemyDefinition type. Missing config
|
||||
// or out-of-range wave → 0 reward (gold flow disabled, designer-error mode).
|
||||
int killReward = 0;
|
||||
var goldEntry = goldConfig?.GetWaveEntry(currentWaveIndex + 1);
|
||||
if (goldEntry != null) killReward = goldEntry.GoldPerEnemy;
|
||||
|
||||
// Award kill gold to the tower owner that landed the killing blow.
|
||||
PlayerSlot killerSlot = health.LastHitOwner;
|
||||
if (killerSlot != PlayerSlot.None)
|
||||
if (killerSlot != PlayerSlot.None && killReward > 0)
|
||||
{
|
||||
var pms = PlayerMatchState.GetForSlot(killerSlot);
|
||||
if (pms != null)
|
||||
PlayerGoldManager.GetForClient(pms.OwnerClientId)
|
||||
?.AwardGold(health.GoldReward);
|
||||
?.AwardGold(killReward);
|
||||
}
|
||||
|
||||
// Show a "+N" gold popup above the corpse on every peer. Capture the
|
||||
// position here on the server — by the time the RPC fires on clients
|
||||
// the death sequence will be moving the corpse, but the spawn point
|
||||
// is good enough and we want the popup to anchor where the kill happened.
|
||||
if (health.GoldReward > 0)
|
||||
ShowGoldRewardClientRpc(health.transform.position, health.GoldReward);
|
||||
if (killReward > 0)
|
||||
ShowGoldRewardClientRpc(health.transform.position, killReward);
|
||||
|
||||
UnsubscribeEnemy(health);
|
||||
DecrementAndCheckComplete();
|
||||
|
|
@ -343,10 +450,15 @@ namespace TD.Gameplay
|
|||
|
||||
private void HandleZoneLeak(PlayerSlot leavingZone)
|
||||
{
|
||||
// Increment the per-slot leak counter for the zone the enemy is leaving.
|
||||
// EnemyMovement fires this exactly once per enemy, when it escapes its
|
||||
// origin zone. We increment both the match-total and the per-wave counter
|
||||
// for the originating player. Per-wave count drives the no-leak bonus
|
||||
// eligibility check at wave completion.
|
||||
int idx = (int)leavingZone;
|
||||
if (idx >= 0 && idx < zoneLeakCounts.Count)
|
||||
zoneLeakCounts[idx]++;
|
||||
if (idx >= 0 && idx < waveLeakCounts.Count)
|
||||
waveLeakCounts[idx]++;
|
||||
}
|
||||
|
||||
private void HandleEnemyReachedGoal(EnemyMovement movement, int livesCost)
|
||||
|
|
@ -432,8 +544,55 @@ namespace TD.Gameplay
|
|||
if (ms == null || ms.Phase == MatchPhase.Defeat || ms.Phase == MatchPhase.Victory)
|
||||
return;
|
||||
|
||||
// Award per-wave bonuses BEFORE advancing the wave (so waveLeakCounts still
|
||||
// reflects this wave's leaks, and goldEarnedThisWave still accumulates this
|
||||
// wave's bonus on top of kill gold). Completion bonus is unconditional;
|
||||
// no-leak bonus only if the player's waveLeakCounts entry is exactly 0.
|
||||
AwardWaveCompletionBonuses();
|
||||
|
||||
Debug.Log($"[WaveManager] Wave {currentWaveIndex + 1} complete. Starting next wave.");
|
||||
StartNextWave();
|
||||
}
|
||||
|
||||
// Server-only. Iterates active players, awards CompletionBonus to each, plus
|
||||
// NoLeaksBonus to those whose per-wave leak counter is zero. Floating-text popups
|
||||
// are spawned at each player's builder position so the reward is visible in-world.
|
||||
// Skipped silently if no goldConfig or no entry for this wave.
|
||||
private void AwardWaveCompletionBonuses()
|
||||
{
|
||||
var entry = goldConfig?.GetWaveEntry(currentWaveIndex + 1);
|
||||
if (entry == null) return;
|
||||
|
||||
int completionBonus = entry.CompletionBonus;
|
||||
int noLeaksBonus = entry.NoLeaksBonus;
|
||||
if (completionBonus <= 0 && noLeaksBonus <= 0) return;
|
||||
|
||||
foreach (var pms in PlayerMatchState.AllPlayers)
|
||||
{
|
||||
var gm = PlayerGoldManager.GetForClient(pms.OwnerClientId);
|
||||
if (gm == null) continue;
|
||||
|
||||
int award = 0;
|
||||
if (completionBonus > 0) award += completionBonus;
|
||||
|
||||
int slotIdx = (int)pms.Slot;
|
||||
bool zeroLeaks = slotIdx >= 0 && slotIdx < waveLeakCounts.Count
|
||||
&& waveLeakCounts[slotIdx] == 0;
|
||||
if (zeroLeaks && noLeaksBonus > 0) award += noLeaksBonus;
|
||||
|
||||
if (award > 0)
|
||||
{
|
||||
gm.AwardGold(award);
|
||||
|
||||
// Surface the bonus in-world so players see it land. Position the
|
||||
// popup at the player's builder if we can find one; otherwise the
|
||||
// origin (popups still spawn, just centered on world origin).
|
||||
Vector3 popupPos = Vector3.zero;
|
||||
var builder = Builder.GetForClient(pms.OwnerClientId);
|
||||
if (builder != null) popupPos = builder.CurrentPosition;
|
||||
ShowGoldRewardClientRpc(popupPos, award);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue