Add settings menu to control audio and return to main menu
This commit is contained in:
parent
c88d3fc625
commit
0b7bc936ef
13 changed files with 697 additions and 22 deletions
|
|
@ -43,6 +43,10 @@ namespace TD.UI
|
|||
|
||||
[Header("Settings")] [SerializeField] private float rejectionMessageDuration = 2.5f;
|
||||
|
||||
[Tooltip("Optional icon for the top-right menu button. Leave empty to use the " +
|
||||
"procedurally-drawn gear (no art asset required).")]
|
||||
[SerializeField] private Sprite menuButtonIcon;
|
||||
|
||||
[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).")]
|
||||
|
|
@ -159,6 +163,21 @@ namespace TD.UI
|
|||
// instead of every gameplay script needing to know about every input widget.
|
||||
public static bool IsTextInputActive { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// True while a modal UI surface (currently the gear menu) owns the screen. Gameplay
|
||||
/// systems should treat this exactly like <see cref="IsTextInputActive"/> — see
|
||||
/// <see cref="IsUiCapturingInput"/>, which is the flag they actually gate on.
|
||||
/// </summary>
|
||||
public static bool IsModalUiOpen { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// True when any UI surface is consuming keyboard/mouse input this frame: a focused
|
||||
/// text field OR an open modal menu. Gameplay input handlers (camera, selection,
|
||||
/// hotkeys, Escape-to-cancel) gate on this so typing or a menu never doubles as a
|
||||
/// gameplay command.
|
||||
/// </summary>
|
||||
public static bool IsUiCapturingInput => IsTextInputActive || IsModalUiOpen;
|
||||
|
||||
// Frame until which chat's "Enter opens chat" behavior is suppressed. Other
|
||||
// standalone text-input surfaces (e.g. TD.Dev.DebugConsole) set this when their
|
||||
// own Enter-driven submit/close already consumed the keypress, so the same
|
||||
|
|
@ -183,6 +202,7 @@ namespace TD.UI
|
|||
private bool deckSubscribed; // true once we've hooked the local PlayerTowerDeck.OnDeckChanged
|
||||
private PlayerTowerDeck subscribedDeck; // the deck we hooked, so we can unsubscribe the same instance
|
||||
private MinimapView minimapView;
|
||||
private GameMenuView gameMenu; // gear button + modal menu overlay
|
||||
private IPanel myPanel; // tracked separately so OnDestroy only clears the static if it still points at us
|
||||
|
||||
// ----- Hotkeys ----------------------------------------------------
|
||||
|
|
@ -703,6 +723,12 @@ namespace TD.UI
|
|||
// ChatService.PostLocalSystem on every peer.
|
||||
BuildChatPanel(root);
|
||||
|
||||
// Gear menu (top-right of the top bar). Built last so its overlay z-orders
|
||||
// above every other HUD surface. "Return to Title Screen" reuses the same
|
||||
// disconnect path as the match-end overlay's button.
|
||||
gameMenu = new GameMenuView(root, Require<Button>(root, "menu-button"),
|
||||
menuButtonIcon, OnReturnToMainMenuClicked);
|
||||
|
||||
// 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).
|
||||
|
|
@ -799,12 +825,21 @@ namespace TD.UI
|
|||
UpdateEnemyInfoIfShown();
|
||||
UpdateDraftVisibility();
|
||||
UpdateSpellCooldowns();
|
||||
HandleChatInput();
|
||||
|
||||
// Skip gameplay hotkeys while the chat input is focused — letters
|
||||
// typed into chat should not also fire Q/W/E/R tower builds.
|
||||
if (!IsTextInputActive)
|
||||
HandleHotkeys();
|
||||
HandleGameMenuInput();
|
||||
|
||||
// Chat and gameplay hotkeys are both off-limits while the menu is up —
|
||||
// Enter shouldn't pop chat open behind the overlay, and Q/W/E/R shouldn't
|
||||
// start a tower placement the player can't see.
|
||||
if (!IsModalUiOpen)
|
||||
{
|
||||
HandleChatInput();
|
||||
|
||||
// Skip gameplay hotkeys while the chat input is focused — letters
|
||||
// typed into chat should not also fire Q/W/E/R tower builds.
|
||||
if (!IsTextInputActive)
|
||||
HandleHotkeys();
|
||||
}
|
||||
|
||||
minimapView?.Tick();
|
||||
}
|
||||
|
|
@ -842,6 +877,59 @@ namespace TD.UI
|
|||
}
|
||||
}
|
||||
|
||||
// ----- Gear menu ---------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Escape handling for the gear menu, plus publishing <see cref="IsModalUiOpen"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>Escape is shared.</b> Five other systems already consume Escape
|
||||
/// (cancel placement, cancel paint, cancel spell aim, close chat, clear selection),
|
||||
/// and Unity gives no ordering guarantee between their Update calls. Rather than
|
||||
/// introduce a cross-component arbiter, the menu claims Escape only when nothing
|
||||
/// else has anything to cancel — a predicate over state that doesn't change during
|
||||
/// the frame, so the outcome is the same regardless of which Update ran first.</para>
|
||||
///
|
||||
/// <para>The player-visible result is the familiar RTS stack: Escape backs out of
|
||||
/// placement / paint / aiming / typing first, then clears the selection, then opens
|
||||
/// the menu. While the menu is open Escape backs out one page at a time and
|
||||
/// <see cref="IsModalUiOpen"/> keeps the gameplay handlers from seeing the key at
|
||||
/// all — so the transition frame is the only one where ordering could matter, and
|
||||
/// on that frame the other systems are by definition idle.</para>
|
||||
/// </remarks>
|
||||
private void HandleGameMenuInput()
|
||||
{
|
||||
if (gameMenu == null) return;
|
||||
|
||||
IsModalUiOpen = gameMenu.IsOpen;
|
||||
|
||||
var kb = Keyboard.current;
|
||||
if (kb == null || !kb.escapeKey.wasPressedThisFrame) return;
|
||||
|
||||
if (gameMenu.IsOpen)
|
||||
{
|
||||
gameMenu.Back();
|
||||
}
|
||||
else if (!EscapeClaimedByGameplay())
|
||||
{
|
||||
gameMenu.Open();
|
||||
}
|
||||
|
||||
IsModalUiOpen = gameMenu.IsOpen;
|
||||
}
|
||||
|
||||
// True when some other system will act on this frame's Escape press. Spell aiming
|
||||
// isn't checked directly (BuilderSpellCastController isn't referenced here) because
|
||||
// it requires the local builder to be selected, which the selection check already
|
||||
// covers.
|
||||
private bool EscapeClaimedByGameplay()
|
||||
{
|
||||
if (IsTextInputActive || chatInputOpen) return true;
|
||||
if (placementController != null && placementController.IsPlacing) return true;
|
||||
if (paintController != null && paintController.IsPainting) return true;
|
||||
return SelectionState.Instance?.SelectedObject != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads raw keyboard state via the New Input System and fires the matching
|
||||
/// action for any bound hotkey pressed this frame. Mirrors the disabled-button
|
||||
|
|
@ -867,6 +955,11 @@ namespace TD.UI
|
|||
{
|
||||
if (Instance == this) Instance = null;
|
||||
|
||||
// The menu dies with the HUD (scene change / disconnect). Clear the static so a
|
||||
// stale "modal open" can't gate input in the next scene.
|
||||
IsModalUiOpen = false;
|
||||
gameMenu = null;
|
||||
|
||||
minimapView?.Dispose();
|
||||
minimapView = null;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue