Adding Text Mesh Pro, new enemies, new enemy waves, new HUD functionality, New animators, a retry function, and floating text for gold earned and lives lost.
This commit is contained in:
parent
3287e8ea43
commit
f6cc6a7102
110 changed files with 62003 additions and 251 deletions
75
Assets/_Project/Scripts/UI/FloatingText.cs
Normal file
75
Assets/_Project/Scripts/UI/FloatingText.cs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
// Assets/_Project/Scripts/UI/FloatingText.cs
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace TD.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// One-shot world-space text that floats upward and fades out, then destroys itself.
|
||||
/// Spawned by <see cref="FloatingTextSpawner"/> for kill-reward and life-loss feedback.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Prefab setup:</b> a GameObject with a <c>TextMeshPro</c> (3D, NOT TextMeshProUGUI)
|
||||
/// component as a child or root, this script, and no Canvas required — TMP renders in
|
||||
/// world space natively. The text auto-billboards toward the main camera each frame.
|
||||
///
|
||||
/// Lifetime, float speed, and fade are tuned on the prefab. The spawner only sets
|
||||
/// content + color.
|
||||
/// </remarks>
|
||||
public class FloatingText : MonoBehaviour
|
||||
{
|
||||
[Tooltip("The TextMeshPro component that renders the text. Auto-located in children " +
|
||||
"if left empty.")]
|
||||
[SerializeField] private TMP_Text text;
|
||||
|
||||
[Tooltip("World units per second the text drifts upward.")]
|
||||
[SerializeField] private float floatSpeed = 1.5f;
|
||||
|
||||
[Tooltip("Seconds the text remains visible before the GameObject is destroyed.")]
|
||||
[SerializeField] private float lifetime = 1.2f;
|
||||
|
||||
private float elapsed;
|
||||
private Color baseColor;
|
||||
|
||||
/// <summary>Sets content and color. Call once immediately after Instantiate.</summary>
|
||||
public void Init(string content, Color color)
|
||||
{
|
||||
if (text == null) text = GetComponentInChildren<TMP_Text>();
|
||||
if (text != null)
|
||||
{
|
||||
text.text = content;
|
||||
text.color = color;
|
||||
}
|
||||
baseColor = color;
|
||||
elapsed = 0f;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
elapsed += Time.deltaTime;
|
||||
if (elapsed >= lifetime)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
// Drift upward.
|
||||
transform.position += Vector3.up * floatSpeed * Time.deltaTime;
|
||||
|
||||
// Fade alpha linearly from 1 → 0 over the lifetime.
|
||||
if (text != null)
|
||||
{
|
||||
float alpha = 1f - (elapsed / lifetime);
|
||||
var c = baseColor;
|
||||
c.a = alpha;
|
||||
text.color = c;
|
||||
}
|
||||
|
||||
// Billboard to the main camera. Using the camera's forward (rather than
|
||||
// position-difference) keeps the text upright even when the camera tilts.
|
||||
var cam = Camera.main;
|
||||
if (cam != null)
|
||||
transform.rotation = Quaternion.LookRotation(cam.transform.forward);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Project/Scripts/UI/FloatingText.cs.meta
Normal file
2
Assets/_Project/Scripts/UI/FloatingText.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 4e505af421a8fd24e9b8f3d8786beba8
|
||||
107
Assets/_Project/Scripts/UI/FloatingTextSpawner.cs
Normal file
107
Assets/_Project/Scripts/UI/FloatingTextSpawner.cs
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
// Assets/_Project/Scripts/UI/FloatingTextSpawner.cs
|
||||
using UnityEngine;
|
||||
|
||||
namespace TD.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Scene singleton that spawns <see cref="FloatingText"/> instances above
|
||||
/// world positions for gold-reward and life-loss feedback.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Why a singleton:</b> the spawner is shared by every caller (kills, leaks,
|
||||
/// future status pop-ups) and holds inspector-tunable colors and a prefab
|
||||
/// reference. Plain <c>MonoBehaviour</c> — visual-only, no networking required.
|
||||
///
|
||||
/// <b>Who calls this:</b> <see cref="TD.Gameplay.WaveManager"/> invokes
|
||||
/// <see cref="SpawnGoldReward"/> and <see cref="SpawnLifeLoss"/> via ClientRpc
|
||||
/// so every peer shows the popup locally.
|
||||
///
|
||||
/// <b>Inspector setup:</b> drop this on any scene GameObject (e.g. a
|
||||
/// <c>FloatingTextSpawner</c> empty), assign the floating-text prefab, tune
|
||||
/// colors to match the HUD.
|
||||
/// </remarks>
|
||||
public class FloatingTextSpawner : MonoBehaviour
|
||||
{
|
||||
// ----- Singleton --------------------------------------------------
|
||||
|
||||
public static FloatingTextSpawner Instance { get; private set; }
|
||||
|
||||
// ----- Inspector --------------------------------------------------
|
||||
|
||||
[Tooltip("Prefab to instantiate for each pop-up. Must have a FloatingText " +
|
||||
"component and a TextMeshPro renderer.")]
|
||||
[SerializeField] private FloatingText prefab;
|
||||
|
||||
[Tooltip("Color used for kill-reward gold pop-ups. Tune to match the HUD's " +
|
||||
"gold label color.")]
|
||||
[SerializeField] private Color goldColor = new Color(1f, 0.84f, 0.2f);
|
||||
|
||||
[Tooltip("Color used for life-loss pop-ups (enemies reaching the goal).")]
|
||||
[SerializeField] private Color livesColor = new Color(0.95f, 0.25f, 0.25f);
|
||||
|
||||
[Tooltip("Default vertical offset above the source position. Tunes how high " +
|
||||
"above the enemy the text starts.")]
|
||||
[SerializeField] private float verticalOffset = 2.0f;
|
||||
|
||||
[Tooltip("Maximum random horizontal jitter applied to the spawn position so " +
|
||||
"consecutive pop-ups don't stack visually.")]
|
||||
[SerializeField] private float positionJitter = 0.4f;
|
||||
|
||||
[Tooltip("Maximum random vertical jitter applied on top of verticalOffset.")]
|
||||
[SerializeField] private float verticalJitter = 0.2f;
|
||||
|
||||
// ----- Lifecycle --------------------------------------------------
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Debug.LogWarning("[FloatingTextSpawner] Duplicate instance detected. " +
|
||||
"Only one should exist per scene.");
|
||||
return;
|
||||
}
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (Instance == this) Instance = null;
|
||||
}
|
||||
|
||||
// ----- Public API -------------------------------------------------
|
||||
|
||||
/// <summary>Spawns a gold-reward popup (e.g. "+10") at the given world position.</summary>
|
||||
public void SpawnGoldReward(Vector3 worldPos, int amount)
|
||||
=> SpawnInternal(worldPos, $"+{amount}", goldColor);
|
||||
|
||||
/// <summary>Spawns a life-loss popup (e.g. "-1") at the given world position.</summary>
|
||||
public void SpawnLifeLoss(Vector3 worldPos, int amount)
|
||||
=> SpawnInternal(worldPos, $"-{amount}", livesColor);
|
||||
|
||||
/// <summary>Lower-level overload: arbitrary content and color.</summary>
|
||||
public void SpawnCustom(Vector3 worldPos, string text, Color color)
|
||||
=> SpawnInternal(worldPos, text, color);
|
||||
|
||||
// ----- Internal ---------------------------------------------------
|
||||
|
||||
private void SpawnInternal(Vector3 worldPos, string text, Color color)
|
||||
{
|
||||
if (prefab == null)
|
||||
{
|
||||
Debug.LogWarning("[FloatingTextSpawner] No prefab assigned. " +
|
||||
"Pop-up will not be shown.");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 jitter = new Vector3(
|
||||
Random.Range(-positionJitter, positionJitter),
|
||||
Random.Range(0f, verticalJitter),
|
||||
Random.Range(-positionJitter, positionJitter));
|
||||
|
||||
Vector3 spawnPos = worldPos + Vector3.up * verticalOffset + jitter;
|
||||
|
||||
var instance = Instantiate(prefab, spawnPos, Quaternion.identity);
|
||||
instance.Init(text, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Project/Scripts/UI/FloatingTextSpawner.cs.meta
Normal file
2
Assets/_Project/Scripts/UI/FloatingTextSpawner.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a31401869def6d64c87e645f1da1aa2a
|
||||
|
|
@ -1,9 +1,12 @@
|
|||
// Assets/_Project/Scripts/UI/HUDController.cs
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.UIElements;
|
||||
using TD.Core;
|
||||
using TD.Gameplay;
|
||||
using TD.Towers;
|
||||
using TD.UI.Minimap;
|
||||
|
|
@ -38,6 +41,7 @@ namespace TD.UI
|
|||
|
||||
private Label goldLabel;
|
||||
private Label waveLabel;
|
||||
private Label livesLabel;
|
||||
private Label portraitName;
|
||||
private Label levelLabel;
|
||||
private VisualElement statLines;
|
||||
|
|
@ -51,6 +55,18 @@ namespace TD.UI
|
|||
private Label ttStats;
|
||||
private Label ttCost;
|
||||
private Label rejectionLabel;
|
||||
private VisualElement portraitFrame;
|
||||
|
||||
// Enemy-info sub-panel — built programmatically and inserted into stat-lines
|
||||
// whenever an Enemy is selected. Cached so we can update HP each frame
|
||||
// without rebuilding the elements.
|
||||
private VisualElement enemyHealthBar;
|
||||
private VisualElement enemyHealthFill;
|
||||
private Label enemyHealthText;
|
||||
|
||||
// Match-end overlay — built once on Start and toggled on Phase changes.
|
||||
private VisualElement matchEndOverlay;
|
||||
private Label matchEndTitle;
|
||||
|
||||
// ----- State ------------------------------------------------------
|
||||
|
||||
|
|
@ -58,6 +74,7 @@ namespace TD.UI
|
|||
private bool placementManagerReady; // true once TowerPlacementManager.Instance is non-null
|
||||
private bool uiInitialized;
|
||||
private bool selectionSubscribed; // true once we've successfully hooked SelectionState.OnSelectionChanged
|
||||
private bool matchStateSubscribed; // true once OnPhaseChanged is hooked
|
||||
private MinimapView minimapView;
|
||||
private IPanel myPanel; // tracked separately so OnDestroy only clears the static if it still points at us
|
||||
|
||||
|
|
@ -149,6 +166,16 @@ namespace TD.UI
|
|||
|
||||
// Seed portrait/grid in case the builder already auto-selected before Start.
|
||||
HandleSelectionChanged(SelectionState.Instance?.SelectedObject);
|
||||
|
||||
TrySubscribeMatchState();
|
||||
}
|
||||
|
||||
private void TrySubscribeMatchState()
|
||||
{
|
||||
if (matchStateSubscribed) return;
|
||||
if (MatchState.Instance == null) return;
|
||||
MatchState.Instance.OnPhaseChanged += HandlePhaseChanged;
|
||||
matchStateSubscribed = true;
|
||||
}
|
||||
|
||||
private void InitializeUI()
|
||||
|
|
@ -172,6 +199,7 @@ namespace TD.UI
|
|||
// so UXML/USS mismatches surface immediately.
|
||||
goldLabel = Require<Label>(root, "gold-label");
|
||||
waveLabel = Require<Label>(root, "wave-label");
|
||||
livesLabel = Require<Label>(root, "lives-label");
|
||||
portraitName = Require<Label>(root, "portrait-name");
|
||||
levelLabel = Require<Label>(root, "level-label");
|
||||
statLines = Require<VisualElement>(root, "stat-lines");
|
||||
|
|
@ -210,6 +238,15 @@ namespace TD.UI
|
|||
minimapView = new MinimapView(minimapContainer, cameraController);
|
||||
}
|
||||
|
||||
// Portrait click → center camera on current selection (WC3 convention).
|
||||
portraitFrame = root.Q<VisualElement>("portrait-frame");
|
||||
if (portraitFrame != null)
|
||||
portraitFrame.RegisterCallback<ClickEvent>(OnPortraitClicked);
|
||||
|
||||
// Build the match-end overlay (Victory / Defeat + Retry). Hidden until
|
||||
// MatchState.OnPhaseChanged fires Victory or Defeat.
|
||||
BuildMatchEndOverlay(root);
|
||||
|
||||
// Publish the panel so non-UI systems can query "is pointer over the HUD".
|
||||
// Stored on `myPanel` too so OnDestroy only clears the static if it still
|
||||
// points at this instance (defensive against re-creation overlap).
|
||||
|
|
@ -235,6 +272,11 @@ namespace TD.UI
|
|||
SelectionState.Instance.OnSelectionChanged -= HandleSelectionChanged;
|
||||
selectionSubscribed = false;
|
||||
}
|
||||
if (matchStateSubscribed && MatchState.Instance != null)
|
||||
{
|
||||
MatchState.Instance.OnPhaseChanged -= HandlePhaseChanged;
|
||||
matchStateSubscribed = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void TrySubscribeSelection()
|
||||
|
|
@ -252,8 +294,13 @@ namespace TD.UI
|
|||
if (!placementManagerReady)
|
||||
TryReadyPlacementManager();
|
||||
|
||||
if (!matchStateSubscribed)
|
||||
TrySubscribeMatchState();
|
||||
|
||||
RefreshGoldDisplay();
|
||||
RefreshMatchStateDisplays();
|
||||
UpdateBuildProgressIfShown();
|
||||
UpdateEnemyInfoIfShown();
|
||||
HandleHotkeys();
|
||||
minimapView?.Tick();
|
||||
}
|
||||
|
|
@ -330,6 +377,29 @@ namespace TD.UI
|
|||
goldLabel.text = gm != null ? $"{gm.CurrentGold:N0} g" : "-- g";
|
||||
}
|
||||
|
||||
// ----- Match-state display ----------------------------------------
|
||||
|
||||
// Polled each frame from Update. MatchState exposes Lives and CurrentWave
|
||||
// as plain int properties backed by NetworkVariables; polling avoids
|
||||
// having to add OnLivesChanged / OnCurrentWaveChanged events to MatchState
|
||||
// for every consumer that wants to display them. The cost is one int read
|
||||
// and a string allocation per frame — negligible at HUD scale.
|
||||
private void RefreshMatchStateDisplays()
|
||||
{
|
||||
var ms = MatchState.Instance;
|
||||
|
||||
if (livesLabel != null)
|
||||
livesLabel.text = ms != null ? $"lives: {ms.Lives}" : "lives: --";
|
||||
|
||||
if (waveLabel != null)
|
||||
{
|
||||
int total = WaveManager.Instance?.TotalWaves ?? 0;
|
||||
waveLabel.text = ms != null && ms.CurrentWave > 0 && total > 0
|
||||
? $"Wave {ms.CurrentWave} / {total}"
|
||||
: "Wave --";
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Command grid -----------------------------------------------
|
||||
|
||||
private const int GRID_COLS = 5;
|
||||
|
|
@ -579,6 +649,12 @@ namespace TD.UI
|
|||
isBuildSite ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
|
||||
// Stat lines — clear and rebuild based on selection kind.
|
||||
// The enemy-info cached references (health bar + label) are invalidated
|
||||
// by Clear() and re-cached if a new Enemy gets selected below.
|
||||
enemyHealthBar = null;
|
||||
enemyHealthFill = null;
|
||||
enemyHealthText = null;
|
||||
|
||||
if (statLines != null)
|
||||
{
|
||||
statLines.Clear();
|
||||
|
|
@ -598,10 +674,113 @@ namespace TD.UI
|
|||
AddStatLine($"Slow: {(1f - def.SlowFactor) * 100f:0}%");
|
||||
}
|
||||
}
|
||||
else if (selection is EnemyHealth enemy)
|
||||
{
|
||||
BuildEnemyInfo(enemy);
|
||||
}
|
||||
// BuildSiteVisual: no stat lines — progress bar conveys the state.
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Enemy info -------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Builds the enemy info stat block (HP bar + speed + bounty) inside the
|
||||
/// stat-lines container. The bar and label references are cached so
|
||||
/// <see cref="UpdateEnemyInfoIfShown"/> can drive them every frame as the
|
||||
/// enemy takes damage.
|
||||
/// </summary>
|
||||
private void BuildEnemyInfo(EnemyHealth enemy)
|
||||
{
|
||||
var def = enemy.Definition;
|
||||
|
||||
// ----- Health bar (X / Y) -----
|
||||
// A thin horizontal bar with a green fill and a centered "X / Y" label
|
||||
// on top. Styled with inline-style so we don't need to add USS classes
|
||||
// — keeps this self-contained.
|
||||
var hpRow = new VisualElement();
|
||||
hpRow.style.flexDirection = FlexDirection.Row;
|
||||
hpRow.style.alignItems = Align.Center;
|
||||
hpRow.style.marginTop = 4;
|
||||
hpRow.style.marginBottom = 4;
|
||||
|
||||
var hpLabel = new Label("HP");
|
||||
hpLabel.style.minWidth = 28;
|
||||
hpLabel.style.color = new Color(0.85f, 0.85f, 0.85f);
|
||||
hpRow.Add(hpLabel);
|
||||
|
||||
// Background + fill share a parent so the fill can be styled as a
|
||||
// percentage width inside the background.
|
||||
var hpBackground = new VisualElement();
|
||||
hpBackground.style.flexGrow = 1;
|
||||
hpBackground.style.height = 14;
|
||||
hpBackground.style.backgroundColor = new Color(0.1f, 0.1f, 0.1f, 0.85f);
|
||||
hpBackground.style.borderTopWidth = hpBackground.style.borderBottomWidth =
|
||||
hpBackground.style.borderLeftWidth = hpBackground.style.borderRightWidth = 1;
|
||||
var borderColor = new Color(0.3f, 0.3f, 0.3f);
|
||||
hpBackground.style.borderTopColor = hpBackground.style.borderBottomColor =
|
||||
hpBackground.style.borderLeftColor = hpBackground.style.borderRightColor = borderColor;
|
||||
|
||||
var hpFill = new VisualElement();
|
||||
hpFill.style.height = Length.Percent(100);
|
||||
hpFill.style.width = Length.Percent(100);
|
||||
hpFill.style.backgroundColor = new Color(0.3f, 0.85f, 0.3f);
|
||||
hpFill.pickingMode = PickingMode.Ignore;
|
||||
hpBackground.Add(hpFill);
|
||||
|
||||
// Overlay "X / Y" label, centered above the bar.
|
||||
var hpText = new Label();
|
||||
hpText.pickingMode = PickingMode.Ignore;
|
||||
hpText.style.position = Position.Absolute;
|
||||
hpText.style.left = 0;
|
||||
hpText.style.right = 0;
|
||||
hpText.style.top = 0;
|
||||
hpText.style.bottom = 0;
|
||||
hpText.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||||
hpText.style.color = Color.white;
|
||||
hpText.style.fontSize = 11;
|
||||
hpBackground.Add(hpText);
|
||||
|
||||
hpRow.Add(hpBackground);
|
||||
statLines.Add(hpRow);
|
||||
|
||||
// Cache references for per-frame updates.
|
||||
enemyHealthBar = hpBackground;
|
||||
enemyHealthFill = hpFill;
|
||||
enemyHealthText = hpText;
|
||||
|
||||
// Initial value (will be refreshed every frame in UpdateEnemyInfoIfShown).
|
||||
UpdateEnemyHpDisplay(enemy);
|
||||
|
||||
// ----- Speed + Bounty stat lines -----
|
||||
if (def != null)
|
||||
{
|
||||
AddStatLine($"Speed: {def.MoveSpeed:0.0}");
|
||||
AddStatLine($"Bounty: {def.GoldReward} g");
|
||||
// (Weaknesses/resistances will go here once the resistance system lands.)
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateEnemyInfoIfShown()
|
||||
{
|
||||
if (enemyHealthFill == null) return; // no Enemy selected
|
||||
var sel = SelectionState.Instance?.SelectedObject;
|
||||
if (sel is EnemyHealth enemy && (UnityEngine.Object)enemy != null)
|
||||
UpdateEnemyHpDisplay(enemy);
|
||||
}
|
||||
|
||||
private void UpdateEnemyHpDisplay(EnemyHealth enemy)
|
||||
{
|
||||
float cur = Mathf.Max(0f, enemy.CurrentHp);
|
||||
float max = Mathf.Max(1f, enemy.MaxHp);
|
||||
float pct = Mathf.Clamp01(cur / max);
|
||||
|
||||
if (enemyHealthFill != null)
|
||||
enemyHealthFill.style.width = Length.Percent(pct * 100f);
|
||||
if (enemyHealthText != null)
|
||||
enemyHealthText.text = $"{Mathf.CeilToInt(cur)} / {Mathf.CeilToInt(max)}";
|
||||
}
|
||||
|
||||
private void AddStatLine(string text)
|
||||
{
|
||||
var label = new Label(text);
|
||||
|
|
@ -667,6 +846,145 @@ namespace TD.UI
|
|||
rejectionFadeCoroutine = null;
|
||||
}
|
||||
|
||||
// ----- Portrait click: center camera on selection -----------------
|
||||
|
||||
// WC3 / SC2 convention: clicking the portrait recenters the camera on
|
||||
// whoever is selected. Zoom is preserved (CameraController.JumpTo only
|
||||
// moves the pivot). No-op when nothing is selected or the camera isn't wired.
|
||||
private void OnPortraitClicked(ClickEvent evt)
|
||||
{
|
||||
var sel = SelectionState.Instance?.SelectedObject;
|
||||
if (sel == null) return;
|
||||
if (cameraController == null) return;
|
||||
|
||||
var t = sel.SelectionTransform;
|
||||
if (t == null) return;
|
||||
|
||||
cameraController.JumpTo(t.position);
|
||||
}
|
||||
|
||||
// ----- Match-end overlay (Victory / Defeat + Retry) ---------------
|
||||
|
||||
private void BuildMatchEndOverlay(VisualElement root)
|
||||
{
|
||||
matchEndOverlay = new VisualElement();
|
||||
matchEndOverlay.style.position = Position.Absolute;
|
||||
matchEndOverlay.style.left = 0;
|
||||
matchEndOverlay.style.right = 0;
|
||||
matchEndOverlay.style.top = 0;
|
||||
matchEndOverlay.style.bottom = 0;
|
||||
matchEndOverlay.style.alignItems = Align.Center;
|
||||
matchEndOverlay.style.justifyContent = Justify.Center;
|
||||
matchEndOverlay.style.backgroundColor = new Color(0f, 0f, 0f, 0.6f);
|
||||
matchEndOverlay.style.display = DisplayStyle.None;
|
||||
// The overlay swallows pointer events so the player can't click towers
|
||||
// or builders through it while it's visible.
|
||||
matchEndOverlay.pickingMode = PickingMode.Position;
|
||||
|
||||
var panel = new VisualElement();
|
||||
panel.style.minWidth = 320;
|
||||
panel.style.paddingTop = 24;
|
||||
panel.style.paddingBottom = 24;
|
||||
panel.style.paddingLeft = 32;
|
||||
panel.style.paddingRight = 32;
|
||||
panel.style.backgroundColor = new Color(0.08f, 0.08f, 0.10f, 0.95f);
|
||||
panel.style.borderTopWidth = panel.style.borderBottomWidth =
|
||||
panel.style.borderLeftWidth = panel.style.borderRightWidth = 2;
|
||||
var border = new Color(0.4f, 0.4f, 0.45f);
|
||||
panel.style.borderTopColor = panel.style.borderBottomColor =
|
||||
panel.style.borderLeftColor = panel.style.borderRightColor = border;
|
||||
panel.style.alignItems = Align.Center;
|
||||
|
||||
matchEndTitle = new Label("Victory");
|
||||
matchEndTitle.style.fontSize = 32;
|
||||
matchEndTitle.style.color = Color.white;
|
||||
matchEndTitle.style.marginBottom = 16;
|
||||
matchEndTitle.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
panel.Add(matchEndTitle);
|
||||
|
||||
var retryBtn = new Button(OnRetryClicked) { text = "Retry" };
|
||||
retryBtn.style.minWidth = 120;
|
||||
retryBtn.style.height = 36;
|
||||
retryBtn.style.fontSize = 16;
|
||||
retryBtn.style.marginTop = 8;
|
||||
panel.Add(retryBtn);
|
||||
|
||||
matchEndOverlay.Add(panel);
|
||||
root.Add(matchEndOverlay);
|
||||
}
|
||||
|
||||
private void HandlePhaseChanged(MatchPhase previous, MatchPhase next)
|
||||
{
|
||||
if (matchEndOverlay == null) return;
|
||||
|
||||
switch (next)
|
||||
{
|
||||
case MatchPhase.Victory:
|
||||
if (matchEndTitle != null)
|
||||
{
|
||||
matchEndTitle.text = "Victory";
|
||||
matchEndTitle.style.color = new Color(1f, 0.84f, 0.2f);
|
||||
}
|
||||
matchEndOverlay.style.display = DisplayStyle.Flex;
|
||||
break;
|
||||
|
||||
case MatchPhase.Defeat:
|
||||
if (matchEndTitle != null)
|
||||
{
|
||||
matchEndTitle.text = "Defeat";
|
||||
matchEndTitle.style.color = new Color(0.95f, 0.25f, 0.25f);
|
||||
}
|
||||
matchEndOverlay.style.display = DisplayStyle.Flex;
|
||||
break;
|
||||
|
||||
default:
|
||||
matchEndOverlay.style.display = DisplayStyle.None;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Reloads the active scene and restarts the host. Host-only — testing
|
||||
// convenience until a real lobby/restart flow exists. The static
|
||||
// sceneLoaded callback survives the scene reload (HUDController dies),
|
||||
// re-arms StartHost once the fresh scene has finished loading, and
|
||||
// unsubscribes itself.
|
||||
private void OnRetryClicked()
|
||||
{
|
||||
var nm = NetworkManager.Singleton;
|
||||
if (nm == null)
|
||||
{
|
||||
Debug.LogWarning("[HUDController] Retry clicked but NetworkManager is null.");
|
||||
return;
|
||||
}
|
||||
if (!nm.IsServer)
|
||||
{
|
||||
Debug.LogWarning("[HUDController] Retry only works on the host. " +
|
||||
"Clients should ask the host to retry.");
|
||||
return;
|
||||
}
|
||||
|
||||
Scene active = SceneManager.GetActiveScene();
|
||||
s_pendingHostRestartBuildIndex = active.buildIndex;
|
||||
SceneManager.sceneLoaded += OnSceneLoadedForRetry;
|
||||
|
||||
nm.Shutdown();
|
||||
SceneManager.LoadScene(active.buildIndex);
|
||||
}
|
||||
|
||||
private static int s_pendingHostRestartBuildIndex = -1;
|
||||
|
||||
private static void OnSceneLoadedForRetry(Scene loaded, LoadSceneMode mode)
|
||||
{
|
||||
if (loaded.buildIndex != s_pendingHostRestartBuildIndex) return;
|
||||
|
||||
SceneManager.sceneLoaded -= OnSceneLoadedForRetry;
|
||||
s_pendingHostRestartBuildIndex = -1;
|
||||
|
||||
var nm = NetworkManager.Singleton;
|
||||
if (nm != null) nm.StartHost();
|
||||
else Debug.LogWarning("[HUDController] Retry: no NetworkManager in reloaded scene.");
|
||||
}
|
||||
|
||||
// ----- Helpers ----------------------------------------------------
|
||||
|
||||
private static T Require<T>(VisualElement root, string name) where T : VisualElement
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue