B button launches upgrade menu
This commit is contained in:
parent
79cd331141
commit
3ada934e41
1 changed files with 89 additions and 56 deletions
|
|
@ -25,53 +25,56 @@ namespace TD.UI
|
|||
|
||||
// ----- Inspector --------------------------------------------------
|
||||
|
||||
[Header("Scene References")]
|
||||
[Tooltip("The local client's TowerPlacementController.")]
|
||||
[SerializeField] private TowerPlacementController placementController;
|
||||
[Header("Scene References")] [Tooltip("The local client's TowerPlacementController.")] [SerializeField]
|
||||
private TowerPlacementController placementController;
|
||||
|
||||
[Tooltip("The local client's TowerPaintController (drives the Paint tab + paint cursor).")]
|
||||
[SerializeField] private TowerPaintController paintController;
|
||||
|
||||
[Tooltip("The TowerPlacementManager NetworkObject in the scene.")]
|
||||
[SerializeField] private TowerPlacementManager placementManager;
|
||||
[Tooltip("The TowerPlacementManager NetworkObject in the scene.")] [SerializeField]
|
||||
private TowerPlacementManager placementManager;
|
||||
|
||||
[Tooltip("The local client's CameraController. Used by the minimap for click-to-jump " +
|
||||
"and drag-to-pan.")]
|
||||
[SerializeField] private CameraController cameraController;
|
||||
[SerializeField]
|
||||
private CameraController cameraController;
|
||||
|
||||
[Header("Settings")]
|
||||
[SerializeField] private float rejectionMessageDuration = 2.5f;
|
||||
[Header("Settings")] [SerializeField] private float rejectionMessageDuration = 2.5f;
|
||||
|
||||
[Tooltip("Maximum visible height of the chat feed in pixels. Content past this " +
|
||||
"height is clipped — older messages scroll off the top of the visible area " +
|
||||
"but stay in history (scroll up while chat is open to view).")]
|
||||
[SerializeField] private float chatMaxHeight = 280f;
|
||||
[SerializeField]
|
||||
private float chatMaxHeight = 280f;
|
||||
|
||||
[Tooltip("Maximum messages kept in chat history. Defaults to effectively unlimited " +
|
||||
"(int.MaxValue) — every message sent during a match stays scrollable. " +
|
||||
"Lower the value if a long match ever shows DOM perf issues; this field " +
|
||||
"is the safety valve, not a normal-play limit.")]
|
||||
[SerializeField] private int chatMaxMessages = int.MaxValue;
|
||||
[SerializeField]
|
||||
private int chatMaxMessages = int.MaxValue;
|
||||
|
||||
[Tooltip("Color used for SYSTEM chat messages (e.g. 'Life Lost', income changes).")]
|
||||
[SerializeField] private Color chatSystemColor = new Color(1f, 0.7f, 0.2f);
|
||||
[Tooltip("Color used for SYSTEM chat messages (e.g. 'Life Lost', income changes).")] [SerializeField]
|
||||
private Color chatSystemColor = new Color(1f, 0.7f, 0.2f);
|
||||
|
||||
[Tooltip("Color used for PLAYER chat message bodies. Sender prefix uses the player's slot color.")]
|
||||
[SerializeField] private Color chatPlayerColor = new Color(0.92f, 0.92f, 0.92f);
|
||||
[SerializeField]
|
||||
private Color chatPlayerColor = new Color(0.92f, 0.92f, 0.92f);
|
||||
|
||||
// ----- Cached UI element references -------------------------------
|
||||
|
||||
private Label goldLabel;
|
||||
private Label waveLabel;
|
||||
private Label livesLabel;
|
||||
private Label nextWaveLabel; // prep countdown ("next: 0:12")
|
||||
private Label leakedLabel; // local player's origin-leak count ("leaked: 3")
|
||||
private Label incomeLabel; // top-bar per-wave gold-earned counter ("+150 g/wave")
|
||||
private Label nextWaveLabel; // prep countdown ("next: 0:12")
|
||||
private Label leakedLabel; // local player's origin-leak count ("leaked: 3")
|
||||
private Label incomeLabel; // top-bar per-wave gold-earned counter ("+150 g/wave")
|
||||
private VisualElement playerListContainer; // right-panel scoreboard rows
|
||||
private Label portraitName;
|
||||
private Label levelLabel;
|
||||
private VisualElement statLines;
|
||||
private VisualElement commandGrid;
|
||||
|
||||
private VisualElement actionFrame; // hidden via display:none when no actions are available
|
||||
private VisualElement commandTabs; // Build/Paint tab row — shown only for a Builder selection
|
||||
private Button tabBuild;
|
||||
|
|
@ -79,6 +82,7 @@ namespace TD.UI
|
|||
private VisualElement buildProgressContainer; // info-panel sub-view, shown for BuildSiteVisual selections
|
||||
private VisualElement buildProgressFill; // width driven each frame from progress
|
||||
private Label buildProgressPercent;
|
||||
|
||||
private Label ttTitle;
|
||||
private Label ttDesc;
|
||||
private Label ttStats;
|
||||
|
|
@ -91,7 +95,7 @@ namespace TD.UI
|
|||
// without rebuilding the elements.
|
||||
private VisualElement enemyHealthBar;
|
||||
private VisualElement enemyHealthFill;
|
||||
private Label enemyHealthText;
|
||||
private Label enemyHealthText;
|
||||
|
||||
// Match-end overlay — built once on Start and toggled on Phase changes.
|
||||
private VisualElement matchEndOverlay;
|
||||
|
|
@ -99,15 +103,15 @@ namespace TD.UI
|
|||
// Buff menu overlay — toggled by the B key via ToggleBuffMenu().
|
||||
private VisualElement buffMenuOverlay;
|
||||
private VisualElement buffMenuContent;
|
||||
private Label matchEndTitle;
|
||||
private Label matchEndTitle;
|
||||
|
||||
// Chat panel (bottom-left, above portrait) — programmatic. The container
|
||||
// holds both the scrollable feed and the input. Highlight + scroll
|
||||
// interactivity are toggled on the container when typing.
|
||||
private VisualElement chatContainer;
|
||||
private ScrollView chatFeed;
|
||||
private TextField chatInput;
|
||||
private bool chatInputOpen;
|
||||
private ScrollView chatFeed;
|
||||
private TextField chatInput;
|
||||
private bool chatInputOpen;
|
||||
|
||||
// Frame on which the chat input was opened or closed. Enter on that frame
|
||||
// and the next one is ignored to prevent the open/close-triggering keypress
|
||||
|
|
@ -129,12 +133,12 @@ namespace TD.UI
|
|||
private CommandTab activeTab = CommandTab.Build;
|
||||
|
||||
private Coroutine rejectionFadeCoroutine;
|
||||
private bool placementManagerReady; // true once TowerPlacementManager.Instance is non-null
|
||||
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 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
|
||||
private IPanel myPanel; // tracked separately so OnDestroy only clears the static if it still points at us
|
||||
|
||||
// ----- Hotkeys ----------------------------------------------------
|
||||
//
|
||||
|
|
@ -156,10 +160,15 @@ namespace TD.UI
|
|||
private readonly struct HotkeyBinding
|
||||
{
|
||||
public readonly Key Key;
|
||||
public readonly VisualElement Button; // for enabledSelf gating
|
||||
public readonly VisualElement Button; // for enabledSelf gating
|
||||
public readonly System.Action Action;
|
||||
|
||||
public HotkeyBinding(Key k, VisualElement b, System.Action a)
|
||||
{ Key = k; Button = b; Action = a; }
|
||||
{
|
||||
Key = k;
|
||||
Button = b;
|
||||
Action = a;
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Static hit-test probe --------------------------------------
|
||||
|
|
@ -255,6 +264,7 @@ namespace TD.UI
|
|||
|
||||
// Cache element references — log a warning for any that are missing
|
||||
// so UXML/USS mismatches surface immediately.
|
||||
|
||||
goldLabel = Require<Label>(root, "gold-label");
|
||||
waveLabel = Require<Label>(root, "wave-label");
|
||||
livesLabel = Require<Label>(root, "lives-label");
|
||||
|
|
@ -273,14 +283,15 @@ namespace TD.UI
|
|||
|
||||
if (tabBuild != null) tabBuild.clicked += () => SwitchTab(CommandTab.Build);
|
||||
if (tabPaint != null) tabPaint.clicked += () => SwitchTab(CommandTab.Paint);
|
||||
|
||||
buildProgressContainer = Require<VisualElement>(root, "build-progress");
|
||||
buildProgressFill = Require<VisualElement>(root, "build-progress-fill");
|
||||
buildProgressPercent = Require<Label>(root, "build-progress-percent");
|
||||
ttTitle = Require<Label>(root, "tt-title");
|
||||
ttDesc = Require<Label>(root, "tt-desc");
|
||||
ttStats = Require<Label>(root, "tt-stats");
|
||||
ttCost = Require<Label>(root, "tt-cost");
|
||||
rejectionLabel = Require<Label>(root, "rejection-label");
|
||||
buildProgressFill = Require<VisualElement>(root, "build-progress-fill");
|
||||
buildProgressPercent = Require<Label>(root, "build-progress-percent");
|
||||
ttTitle = Require<Label>(root, "tt-title");
|
||||
ttDesc = Require<Label>(root, "tt-desc");
|
||||
ttStats = Require<Label>(root, "tt-stats");
|
||||
ttCost = Require<Label>(root, "tt-cost");
|
||||
rejectionLabel = Require<Label>(root, "rejection-label");
|
||||
|
||||
// Map area and its transparent ancestors must not consume pointer
|
||||
// events so clicks reach the 3D scene underneath. The bottom-ui is now
|
||||
|
|
@ -340,8 +351,8 @@ namespace TD.UI
|
|||
private void OnEnable()
|
||||
{
|
||||
TowerPlacementController.OnRejectionMessageReady += ShowRejectionMessage;
|
||||
WaveManager.OnLifeLost += HandleLifeLost;
|
||||
ChatService.OnMessageReceived += HandleChatMessage;
|
||||
WaveManager.OnLifeLost += HandleLifeLost;
|
||||
ChatService.OnMessageReceived += HandleChatMessage;
|
||||
// Try to subscribe now; if SelectionState.Awake hasn't run yet (Unity does
|
||||
// not guarantee Awake/OnEnable ordering across objects), Start will retry.
|
||||
TrySubscribeSelection();
|
||||
|
|
@ -350,13 +361,14 @@ namespace TD.UI
|
|||
private void OnDisable()
|
||||
{
|
||||
TowerPlacementController.OnRejectionMessageReady -= ShowRejectionMessage;
|
||||
WaveManager.OnLifeLost -= HandleLifeLost;
|
||||
ChatService.OnMessageReceived -= HandleChatMessage;
|
||||
WaveManager.OnLifeLost -= HandleLifeLost;
|
||||
ChatService.OnMessageReceived -= HandleChatMessage;
|
||||
if (selectionSubscribed && SelectionState.Instance != null)
|
||||
{
|
||||
SelectionState.Instance.OnSelectionChanged -= HandleSelectionChanged;
|
||||
selectionSubscribed = false;
|
||||
}
|
||||
|
||||
if (matchStateSubscribed && MatchState.Instance != null)
|
||||
{
|
||||
MatchState.Instance.OnPhaseChanged -= HandlePhaseChanged;
|
||||
|
|
@ -421,6 +433,7 @@ namespace TD.UI
|
|||
buildProgressFill.style.width =
|
||||
new StyleLength(new Length(progress * 100f, LengthUnit.Percent));
|
||||
}
|
||||
|
||||
if (buildProgressPercent != null)
|
||||
{
|
||||
buildProgressPercent.text = $"{Mathf.RoundToInt(progress * 100f)}%";
|
||||
|
|
@ -608,8 +621,8 @@ namespace TD.UI
|
|||
nameLabel.style.color = PlayerColors.Get(pms.Slot);
|
||||
nameLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
nameLabel.style.fontSize = 10;
|
||||
nameLabel.style.whiteSpace = WhiteSpace.NoWrap; // keep name on one line
|
||||
nameLabel.style.flexGrow = 1; // takes all leftover width
|
||||
nameLabel.style.whiteSpace = WhiteSpace.NoWrap; // keep name on one line
|
||||
nameLabel.style.flexGrow = 1; // takes all leftover width
|
||||
nameLabel.style.flexShrink = 1;
|
||||
nameLabel.style.overflow = Overflow.Hidden;
|
||||
nameLabel.style.textOverflow = TextOverflow.Ellipsis;
|
||||
|
|
@ -653,6 +666,7 @@ namespace TD.UI
|
|||
// gated by the lobby), but the values do.
|
||||
private static readonly System.Text.StringBuilder s_scoreboardSigBuf =
|
||||
new System.Text.StringBuilder(64);
|
||||
|
||||
private string ComputeScoreboardSignature(System.Collections.Generic.List<PlayerMatchState> players)
|
||||
{
|
||||
s_scoreboardSigBuf.Clear();
|
||||
|
|
@ -662,12 +676,14 @@ namespace TD.UI
|
|||
var gm = PlayerGoldManager.GetForClient(pms.OwnerClientId);
|
||||
int gold = gm != null ? gm.CurrentGold : 0;
|
||||
int leaks = (wm != null && pms.Slot != PlayerSlot.None)
|
||||
? wm.GetZoneLeakCount(pms.Slot) : 0;
|
||||
? wm.GetZoneLeakCount(pms.Slot)
|
||||
: 0;
|
||||
s_scoreboardSigBuf.Append((int)pms.Slot).Append(':')
|
||||
.Append(pms.DisplayName ?? string.Empty).Append(':')
|
||||
.Append(gold).Append(':')
|
||||
.Append(leaks).Append(';');
|
||||
.Append(pms.DisplayName ?? string.Empty).Append(':')
|
||||
.Append(gold).Append(':')
|
||||
.Append(leaks).Append(';');
|
||||
}
|
||||
|
||||
return s_scoreboardSigBuf.ToString();
|
||||
}
|
||||
|
||||
|
|
@ -675,7 +691,7 @@ namespace TD.UI
|
|||
|
||||
private const int GRID_COLS = 5;
|
||||
private const int GRID_ROWS = 3;
|
||||
private const int GRID_MAX = GRID_COLS * GRID_ROWS;
|
||||
private const int GRID_MAX = GRID_COLS * GRID_ROWS;
|
||||
|
||||
// First-time check that TowerPlacementManager exists. Once ready, populates the
|
||||
// grid for the current selection. Subsequent populates flow through
|
||||
|
|
@ -685,7 +701,7 @@ namespace TD.UI
|
|||
if (placementManager == null)
|
||||
placementManager = TowerPlacementManager.Instance;
|
||||
|
||||
if (placementManager == null) return; // not spawned yet — retry next frame
|
||||
if (placementManager == null) return; // not spawned yet — retry next frame
|
||||
|
||||
placementManagerReady = true;
|
||||
PopulateGridForSelection(SelectionState.Instance?.SelectedObject);
|
||||
|
|
@ -700,7 +716,7 @@ namespace TD.UI
|
|||
private void PopulateGridForSelection(ISelectable selection)
|
||||
{
|
||||
if (commandGrid == null) return;
|
||||
if (!placementManagerReady) return; // deferred to TryReadyPlacementManager
|
||||
if (!placementManagerReady) return; // deferred to TryReadyPlacementManager
|
||||
|
||||
// Selection changed — invalidate the previous frame's hotkey bindings
|
||||
// before creating new ones. Without this, stale buttons from a previous
|
||||
|
|
@ -710,8 +726,8 @@ namespace TD.UI
|
|||
// Decide whether any actions exist for this selection. The action frame
|
||||
// is hidden entirely when there are none — matches the WC3-style UX.
|
||||
bool hasActions = selection is Builder
|
||||
|| selection is TowerInstance
|
||||
|| selection is BuildSiteVisual;
|
||||
|| selection is TowerInstance
|
||||
|| selection is BuildSiteVisual;
|
||||
if (actionFrame != null)
|
||||
actionFrame.style.display = hasActions ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
|
||||
|
|
@ -729,7 +745,7 @@ namespace TD.UI
|
|||
RefreshTabActiveState();
|
||||
|
||||
commandGrid.Clear();
|
||||
if (!hasActions) return; // grid stays empty; frame is hidden anyway
|
||||
if (!hasActions) return; // grid stays empty; frame is hidden anyway
|
||||
|
||||
// Build the 15-cell action layout for the active selection kind.
|
||||
var cells = new VisualElement[GRID_MAX];
|
||||
|
|
@ -754,12 +770,14 @@ namespace TD.UI
|
|||
cells[i] = CreateTowerButton(def, typeId, HotkeyLayout[i]);
|
||||
i++;
|
||||
}
|
||||
|
||||
}
|
||||
cells[GRID_MAX - 1] = CreateBuffMenuButton(HotkeyLayout[GRID_MAX - 1]);
|
||||
}
|
||||
else if (selection is TowerInstance tower)
|
||||
{
|
||||
// WC3 layout convention: primary action top-left (Q), sell bottom-right (B).
|
||||
cells[0] = CreateUpgradeButton(tower, HotkeyLayout[0]);
|
||||
cells[0] = CreateUpgradeButton(tower, HotkeyLayout[0]);
|
||||
cells[GRID_MAX - 1] = CreateSellButton(tower, HotkeyLayout[GRID_MAX - 1]);
|
||||
}
|
||||
else if (selection is BuildSiteVisual bsv)
|
||||
|
|
@ -908,6 +926,7 @@ namespace TD.UI
|
|||
costLabel.pickingMode = PickingMode.Ignore;
|
||||
btn.Add(costLabel);
|
||||
}
|
||||
|
||||
return btn;
|
||||
}
|
||||
|
||||
|
|
@ -917,9 +936,12 @@ namespace TD.UI
|
|||
private VisualElement CreateUpgradeButton(TowerInstance tower, Key hotkey)
|
||||
{
|
||||
var btn = CreateActionButton(
|
||||
costText: "", // tier cost unknown until upgrade system lands
|
||||
hotkey: hotkey,
|
||||
onClick: () => { /* TODO: upgrade flow */ });
|
||||
costText: "", // tier cost unknown until upgrade system lands
|
||||
hotkey: hotkey,
|
||||
onClick: () =>
|
||||
{
|
||||
/* TODO: upgrade flow */
|
||||
});
|
||||
btn.SetEnabled(false);
|
||||
return btn;
|
||||
}
|
||||
|
|
@ -931,12 +953,23 @@ namespace TD.UI
|
|||
: 0;
|
||||
var btn = CreateActionButton(
|
||||
costText: sellValue > 0 ? $"+{sellValue}g" : "",
|
||||
hotkey: hotkey,
|
||||
onClick: () => { /* TODO: sell flow */ });
|
||||
hotkey: hotkey,
|
||||
onClick: () =>
|
||||
{
|
||||
/* TODO: sell flow */
|
||||
});
|
||||
btn.SetEnabled(false);
|
||||
return btn;
|
||||
}
|
||||
|
||||
private VisualElement CreateBuffMenuButton(Key hotkey)
|
||||
{
|
||||
return CreateActionButton(
|
||||
costText: "Buffs",
|
||||
hotkey: hotkey,
|
||||
onClick: () => ToggleBuffMenu());
|
||||
}
|
||||
|
||||
// Cancel action for an in-progress build. Fires the owner-only RPC; the
|
||||
// server cancels the matching job (or, for shelved sites, refunds + despawns
|
||||
// directly), full gold is refunded, the BuildSiteVisual is despawned, and
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue