From 3d01fcce860e8960cce8435d29e24164380f875d Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Fri, 12 Jun 2026 20:26:00 -0500 Subject: [PATCH] Lots of little tweaks and fixes --- .claude/docs/architecture.md | 10 +- .claude/docs/dialogue.md | 20 +-- .claude/docs/overworld.md | 2 + .claude/docs/stubs.md | 2 +- .claude/docs/ui.md | 166 +++++++++++++++------- cutscene/dialogue/DialogueAction.gd | 3 + overworld/camera/OverworldCamera.gd | 26 +++- overworld/entity/EntityInteractingArea.gd | 25 ++++ project.godot | 4 + scene/Pause.gd | 2 + scene/Settings.gd | 1 + ui/RootUI.gd | 8 ++ ui/RootUI.tscn | 44 +++++- ui/UISingleton.gd | 22 +++ ui/component/ConfirmDialog.gd | 30 ++++ ui/component/DialogueTextbox.gd | 26 +--- ui/component/DialogueTextbox.tscn | 2 +- ui/component/InteractIndicator.gd | 56 ++++++++ ui/component/InteractIndicator.gd.uid | 1 + ui/component/InteractIndicator.tscn | 13 ++ ui/component/MainMenuConfirmDialog.tscn | 38 +++++ ui/component/ModalBackdrop.gd | 48 +++++++ ui/component/QuitConfirmDialog.gd | 5 + ui/component/QuitConfirmDialog.gd.uid | 1 + ui/component/QuitConfirmDialog.tscn | 38 +++++ ui/mainmenu/MainMenu.gd | 9 ++ ui/mainmenu/MainMenu.tscn | 7 +- ui/pause/PauseMain.gd | 37 ++--- ui/pause/PauseMain.tscn | 23 +-- ui/pause/PauseMenu.gd | 27 ++-- ui/settings/SettingsMenu.gd | 11 ++ ui/settings/SettingsMenu.tscn | 23 ++- 32 files changed, 570 insertions(+), 160 deletions(-) create mode 100644 ui/component/ConfirmDialog.gd create mode 100644 ui/component/InteractIndicator.gd create mode 100644 ui/component/InteractIndicator.gd.uid create mode 100644 ui/component/InteractIndicator.tscn create mode 100644 ui/component/MainMenuConfirmDialog.tscn create mode 100644 ui/component/ModalBackdrop.gd create mode 100644 ui/component/QuitConfirmDialog.gd create mode 100644 ui/component/QuitConfirmDialog.gd.uid create mode 100644 ui/component/QuitConfirmDialog.tscn diff --git a/.claude/docs/architecture.md b/.claude/docs/architecture.md index 2c236df..81e8b02 100644 --- a/.claude/docs/architecture.md +++ b/.claude/docs/architecture.md @@ -17,7 +17,7 @@ Never instantiate these — access only via the global handle. | `QUEST` | Quest management (stub) | | `CUTSCENE` | Cutscene global (stub) | | `DialogueManager` | godot_dialogue_manager v3.10.4 — parses and steps through `.dialogue` files | -| `UI` | Root UI accessor — `UI.TEXTBOX`, `UI.DEBUG_MENU`, `UI.GAME_MENU` | +| `UI` | Root UI accessor — `UI.PAUSE_MENU`, `UI.QUIT_DIALOG`, `UI.MAIN_MENU_DIALOG`, `UI.BACKDROP`, `UI.DEBUG_MENU`, `UI.GAME_MENU` | ## Scene Graph @@ -25,7 +25,13 @@ Never instantiate these — access only via the global handle. RootScene (Node3D) └─ overworld / battle / cooking / initial ← one shown at a time RootUI (Control, always visible) - └─ VNTextbox, DebugMenu, PauseMenu, GameMenu + ├─ DebugMenu + ├─ GameMenu + ├─ ChatBoxContainer (world-space dialogue textboxes) + ├─ ModalBackdrop (repositions dynamically — see ui.md) + ├─ PauseMenu + ├─ QuitConfirmDialog + └─ MainMenuConfirmDialog ``` `RootScene` listens to `SCENE.sceneChanged` and shows/hides the correct sub-tree. diff --git a/.claude/docs/dialogue.md b/.claude/docs/dialogue.md index a0692c6..874bb34 100644 --- a/.claude/docs/dialogue.md +++ b/.claude/docs/dialogue.md @@ -118,14 +118,14 @@ Whether a line of dialogue is triggered by: ### Dialogue and movement control -Dialogue does **not** automatically block movement. Each dialogue sequence declares whether it pauses movement: +Dialogue does **not** automatically block movement or camera. Each dialogue sequence declares whether it pauses them: -- **Blocking** (most NPC conversations triggered by player): sets `UI.dialogueActive = true` for the duration; `EntityMovement._canMove()` returns false. -- **Non-blocking** (ambient chatter, background NPC conversations, timed popups): dialogue runs without setting `dialogueActive`; the player can move freely. +- **Blocking** (most NPC conversations triggered by player): sets `UI.activeConversation = true` for the duration; `EntityMovement._canMove()` returns false and `OverworldCamera._canOrbit()` returns false. +- **Non-blocking** (ambient chatter, background NPC conversations, timed popups): dialogue runs without setting `activeConversation`; the player can move and orbit the camera freely. This is configured per `DialogueAction` call, not per line. -> **Implemented:** `DialogueMode.CONVERSATION` sets `UI.activeConversation = true` (blocks movement). `NARRATION` and `AMBIENT` are non-blocking. `UI.dialogueActive` is driven by `DialogueManager.dialogue_started/ended` signals and is true for any running dialogue regardless of mode. +> **Implemented:** `DialogueMode.CONVERSATION` sets `UI.activeConversation = true` (blocks movement and camera orbit). `NARRATION` and `AMBIENT` are non-blocking. `UI.dialogueActive` is driven by `DialogueManager.dialogue_started/ended` signals (emitted by `DialogueAction`) and is true for any running dialogue regardless of mode. ### Text reveal (scrolling) @@ -185,13 +185,13 @@ The choice textbox follows the same world-space anchor and screen-edge clamping Every dialogue sequence has a `DialogueMode` that controls movement blocking and advancement behaviour. Set it per `DialogueAction` call — not per line. -| Mode | Movement | Advancement | Typical use | -|---|---|---|---| -| `CONVERSATION` | Blocked | Player (Interact) | NPC interactions, cutscene dialogue | -| `NARRATION` | Non-blocking | Player (Interact) | Item pickups, announcements the player can dismiss when ready | -| `AMBIENT` | Non-blocking | Timed (auto) | Background NPC-to-NPC chatter, timed popups | +| Mode | Movement | Camera orbit | Advancement | Typical use | +|---|---|---|---|---| +| `CONVERSATION` | Blocked | Locked | Player (Interact) | NPC interactions, cutscene dialogue | +| `NARRATION` | Non-blocking | Free | Player (Interact) | Item pickups, announcements the player can dismiss when ready | +| `AMBIENT` | Non-blocking | Free | Timed (auto) | Background NPC-to-NPC chatter, timed popups | -`UI.dialogueActive` is driven by `DialogueManager.dialogue_started` / `dialogue_ended` signals and is true whenever any dialogue is running, regardless of mode. Movement blocking is checked separately: `EntityMovement._canMove()` is false only when an active `CONVERSATION` sequence is in progress. +`UI.dialogueActive` is driven by `DialogueManager.dialogue_started` / `dialogue_ended` signals (emitted by `DialogueAction`) and is true whenever any dialogue is running, regardless of mode. Movement and camera blocking are checked separately via `UI.activeConversation`: `EntityMovement._canMove()` and `OverworldCamera._canOrbit()` both return false only when an active `CONVERSATION` sequence is in progress. More modes can be added to this enum as new use cases arise. diff --git a/.claude/docs/overworld.md b/.claude/docs/overworld.md index ae28f88..911b85b 100644 --- a/.claude/docs/overworld.md +++ b/.claude/docs/overworld.md @@ -94,6 +94,8 @@ Camera-relative direction is derived from the active `Camera3D`'s basis — the | `centeredPitch:float` | `30.0` | Target pitch in CENTERED mode | | `collisionMask:int` | `1` | Physics layers the camera avoids; entities are on layer 2 and excluded by default | +**Input lock:** `_canOrbit()` returns false when `UI.activeConversation` is true. All manual orbit input (controller stick, right-click drag, `center_camera`) is suppressed. If right-click was held when a conversation starts, the mouse is released automatically. The camera stays at its current position and the positioning math still runs, so it remains correctly placed relative to the (non-moving) player. + **Mode transitions:** - FREE → CENTERED: after `centeredDelay` seconds of player movement with no camera input, or immediately via `center_camera` (G / LB) - CENTERED → FREE: any manual camera input (controller stick or right-click drag) diff --git a/.claude/docs/stubs.md b/.claude/docs/stubs.md index 8fa5757..4374eef 100644 --- a/.claude/docs/stubs.md +++ b/.claude/docs/stubs.md @@ -12,4 +12,4 @@ These exist in the codebase but have no real implementation yet. Don't assume th | `BattleItem.perform()` | Stub | | `CookingScene.tscn` | Placeholder UI only | | Response branching in `DialogueAction` | Auto-selects first allowed response; no response UI exists yet | -| `Pause.gd` | Logic commented out | +| `Pause.gd` | Wired — opens/closes `UI.PAUSE_MENU`; blocked on `INITIAL` scene | diff --git a/.claude/docs/ui.md b/.claude/docs/ui.md index aeda31b..1c07b75 100644 --- a/.claude/docs/ui.md +++ b/.claude/docs/ui.md @@ -5,68 +5,44 @@ ``` RootUI (Control, fullscreen, always visible) ├── DebugMenu -├── PauseMenu -│ ├── PauseSettings -│ └── PauseMain ├── GameMenu │ ├── GameMenuPartyTab │ └── GameMenuItemsTab -└── VNTextbox +├── ChatBoxContainer +│ └── InteractIndicator +├── ModalBackdrop ← shared backdrop; repositions dynamically +├── PauseMenu +│ ├── PauseMain +│ └── PauseSettings +├── QuitConfirmDialog +└── MainMenuConfirmDialog ``` -`RootUI` is a permanent child of `RootScene` and registers itself with the `UI` singleton on `_enter_tree`. Access its children via `UI.TEXTBOX`, `UI.DEBUG_MENU`, and `UI.GAME_MENU`. +Child order matters — later siblings render on top. `ModalBackdrop` shifts its own position in the tree at runtime to sit just below whichever modal overlay is currently topmost (see [ModalBackdrop](#modalbackdrop)). + +`RootUI` is a permanent child of `RootScene` and registers itself with the `UI` singleton on `_enter_tree`. Access its children through the `UI` singleton. ## UI singleton `UI` (autoload) is the global access point. -| Accessor | Returns | Notes | +| Accessor | Type | Notes | |---|---|---| -| `UI.TEXTBOX` | `VNTextbox` | Bottom-screen dialogue box | | `UI.DEBUG_MENU` | `DebugMenu` | Dev scene-jump overlay | | `UI.GAME_MENU` | `GameMenu` | JRPG-style in-game menu | -| `UI.dialogueActive` | `bool` | `true` for the entire duration of a `DialogueAction`, including line transitions | +| `UI.PAUSE_MENU` | `PauseMenu` | Pause overlay; also pauses the scene tree | +| `UI.QUIT_DIALOG` | `QuitConfirmDialog` | "Quit to desktop?" confirm; call `.open()` to show | +| `UI.MAIN_MENU_DIALOG` | `ConfirmDialog` | "Return to main menu?" confirm; call `.open()` to show | +| `UI.BACKDROP` | `ModalBackdrop` | Shared semi-transparent backdrop; managed automatically | +| `UI.dialogueActive` | `bool` | `true` for the entire duration of a `DialogueAction` | +| `UI.activeConversation` | `bool` | `true` only during a `CONVERSATION`-mode dialogue | +| `UI.chatBoxContainer` | `Control` | Parent node for world-space dialogue textboxes | -`dialogueActive` is set by `DialogueAction` — it is broader than just "textbox visible." Movement blocks on both flags: `UI.dialogueActive` prevents movement even while the textbox is briefly hidden between lines. - -## VNTextbox - -Bottom-anchored `PanelContainer` that reveals text character-by-character and paginates when content overflows 4 lines. - -**Showing text from code:** - -```gdscript -# Fire-and-forget -UI.TEXTBOX.setText("Hello world.") - -# Await player dismiss — use this in cutscene callables -await UI.TEXTBOX.setTextAndWait("Hello world.") -``` - -**Flow:** -1. `setText` resets reveal state and sets new text; textbox becomes visible automatically -2. Player holds `interact` to speed up reveal; press again after reveal completes to advance page or close -3. `textboxClosing` signal fires when the last page is dismissed -4. `setTextAndWait` awaits that signal before returning - -**Input guard:** `EntityMovement._canMove()` returns `false` while `!UI.TEXTBOX.isClosed`. Don't set text without also expecting movement to be blocked. - -**Signal:** `textboxClosing` — emitted once per `setTextAndWait` call when player dismisses. - -## AdvancedRichText - -`RichTextLabel` subclass (`@tool`) used inside `VNTextbox`. Handles: - -- Smart word-wrap (`TextServer.AUTOWRAP_WORD_SMART`) -- Pagination via `maxLines` / `startLine` exports -- Inline input icons: `[input action=interact]text[/input]` → replaced with the icon image from `res://ui/input/{action}.tres` -- Optional auto-translation via Godot's `tr()` - -Supported icon actions: `interact`, `pause`, `debug`, `up`, `down`, `left`, `right`. +`dialogueActive` is set by `DialogueManager` signals — it is broader than any single textbox being visible. Movement blocks on `dialogueActive` and on `UI.GAME_MENU.isOpen()`. ## ClosableMenu -Base class for any togglable panel. Extends `Control`; `isOpen` drives `visible`. +Base class for any togglable panel. Extends `Control`; the `isOpen:bool` export drives `visible`. ```gdscript menu.open() # shows, emits opened @@ -78,6 +54,67 @@ Signals: `opened`, `closed`. All new menus that need standard show/hide behaviour should extend `ClosableMenu`. +## ConfirmDialog + +Reusable "Yes / No" confirmation overlay at `res://ui/component/ConfirmDialog.gd`. Extends `ClosableMenu`. + +```gdscript +dialog.open() # shows, focuses No (safe default), emits opened +dialog.confirmed # signal — fires after Yes is pressed, before close +``` + +**Focus locking:** `focus_neighbor_top/bottom` on both buttons is wired so controller navigation cannot escape the dialog to elements behind it. + +**`ui_cancel`** closes the dialog the same as pressing No. + +**Subclassing:** + +```gdscript +class_name MyConfirmDialog extends ConfirmDialog + +func _ready() -> void: + super._ready() + confirmed.connect(func(): do_the_thing()) +``` + +`QuitConfirmDialog` uses this pattern — it extends `ConfirmDialog` and connects `confirmed → get_tree().quit()` in its own `_ready()`. + +**Adding a new confirm dialog:** +1. Create a `.tscn` with `ConfirmDialog.gd` as script; set label text in the scene +2. Add `btnYes`/`btnNo` node path exports pointing to your two buttons +3. Add the instance to `RootUI.tscn` after `PauseMenu` (so it renders on top) +4. Register it with the backdrop in `RootUI._ready()`: `modalBackdrop.register(myDialog)` +5. Expose via `RootUI.gd` export + `UISingleton.gd` accessor if other systems need it +6. Connect `myDialog.confirmed` wherever the action should fire + +## ModalBackdrop + +`res://ui/component/ModalBackdrop.gd` — a single fullscreen semi-transparent `ColorRect` shared across all modal overlays. + +**How it works:** each registered overlay's `opened`/`closed` signals are connected. When any overlay opens, the backdrop becomes visible and `move_child()`s itself in the scene tree to sit immediately before the highest-index open overlay. When all overlays close, it hides. + +**Result:** only one backdrop is ever visible, and it always sits between the game world (or lower overlays) and the frontmost open overlay — even when overlays are stacked (e.g. `QuitConfirmDialog` over `PauseMenu`). + +**Registering a new overlay:** + +```gdscript +# In RootUI._ready() — overlay must be a direct child of RootUI and have opened/closed signals +modalBackdrop.register(myOverlay) +``` + +`PauseMenu`, `QuitConfirmDialog`, and `MainMenuConfirmDialog` are all registered. + +## AdvancedRichText + +`RichTextLabel` subclass (`@tool`) used inside world-space dialogue textboxes. Handles: + +- Smart word-wrap (`TextServer.AUTOWRAP_WORD_SMART`) +- Pagination via `maxLines` / `startLine` exports +- Inline input icons: `[input action=interact]text[/input]` → replaced with the icon image from `res://ui/input/{action}.tres` +- Optional auto-translation via Godot's `tr()` + +Supported icon actions: `interact`, `pause`, `debug`, `up`, `down`, `left`, `right`. + ## Game menu JRPG-style in-game menu at `res://ui/gamemenu/`. Open with the `menu` input (**Tab** on keyboard, **Y** on controller). Blocks player movement while open via `EntityMovement._canMove()`. @@ -107,17 +144,39 @@ To add a new tab: add a value to `GameMenu.Tab`, create a tab scene/script under ## Pause menu -`PauseMenu` wraps `PauseMain` (item list) and `PauseSettings` (settings tabs). +`PauseMenu` wraps `PauseMain` (button list) and `PauseSettings` (settings tabs). Opening it calls `get_tree().paused = true`; closing restores it. | Method | Effect | |---|---| -| `PauseMenu.open()` | Shows container, opens PauseMain | -| `PauseMenu.close()` | Hides everything | +| `PauseMenu.open()` | Pauses tree, shows container, opens PauseMain, emits `opened` | +| `PauseMenu.close()` | Unpauses tree, hides everything, emits `closed` | | `PauseMenu.isOpen() -> bool` | Visibility state | -`ui_cancel` inside the pause menu: if PauseSettings is open, closes it and reopens PauseMain; otherwise closes the whole menu. +**`ui_cancel` behaviour inside PauseMenu:** +- If `QuitConfirmDialog` or `MainMenuConfirmDialog` is open → ignored (the dialog handles it) +- If `PauseSettings` is open → closes settings, reopens PauseMain +- Otherwise → closes PauseMenu -> **Stub:** `Pause.gd` (the singleton) has its logic commented out. Pause menu is not yet wired to actual game-pause state. +**PauseMain buttons:** + +| Button | Behaviour | +|---|---| +| Resume | Closes PauseMenu | +| Settings | Opens PauseSettings | +| Main Menu | Opens `MainMenuConfirmDialog`; on confirm → `SCENE.setScene(INITIAL)` | +| Quit Game | Opens `QuitConfirmDialog`; on confirm → `get_tree().quit()` | + +When either confirm dialog cancels, focus returns to the button that opened it. + +**Cannot open on main menu:** `Pause.gd` checks `SCENE.currentScene == INITIAL` and skips opening when already closed. + +## Main menu + +`res://ui/mainmenu/MainMenu.tscn`. Buttons: **New Game**, **Settings**, **Quit Game**. + +- New Game → `SCENE.setScene(OVERWORLD)` + `OVERWORLD.mapChange(...)` +- Settings → opens the `MainMenuSettings` overlay +- Quit Game → opens `UI.QUIT_DIALOG`; on cancel, focus returns to the Quit button ## Settings menu @@ -134,7 +193,7 @@ To add a new tab: add a value to `GameMenu.Tab`, create a tab scene/script under | Cooking | `SCENE.setScene(COOKING)` | | Initial | `SCENE.setScene(INITIAL)` | -Access via `UI.DEBUG_MENU`. Starts hidden; `isClosed` getter/setter controls visibility. +Access via `UI.DEBUG_MENU`. Starts hidden. ## Theme & assets @@ -145,6 +204,7 @@ Access via `UI.DEBUG_MENU`. Starts hidden; `isClosed` getter/setter controls vis ## Adding a new menu 1. Create a scene whose root extends `ClosableMenu` (or `Control` if open/close isn't needed) -2. Add it as a child of `RootUI.tscn` +2. Add it as a child of `RootUI.tscn` — position after `PauseMenu` if it should render above it 3. Export a typed reference on `RootUI.gd` and wire it in the Inspector -4. Expose via a getter on `UISingleton.gd` if other systems need access (follow the `TEXTBOX` / `DEBUG_MENU` pattern) +4. Expose via a getter on `UISingleton.gd` if other systems need access (follow the `PAUSE_MENU` / `GAME_MENU` pattern) +5. If it needs a backdrop, register it: `modalBackdrop.register(myMenu)` in `RootUI._ready()` diff --git a/cutscene/dialogue/DialogueAction.gd b/cutscene/dialogue/DialogueAction.gd index cc967d3..4e6e759 100644 --- a/cutscene/dialogue/DialogueAction.gd +++ b/cutscene/dialogue/DialogueAction.gd @@ -28,6 +28,7 @@ static func dialogueCallable(params:Dictionary) -> int: else _TextboxGd.AdvancementMode.PLAYER ) + DialogueManager.dialogue_started.emit(resource) var line:DialogueLine = await DialogueManager.get_next_dialogue_line(resource, title, extraStates) while line != null: var entity:Entity = OVERWORLD.getEntityByDialogueName(line.character) @@ -48,6 +49,8 @@ static func dialogueCallable(params:Dictionary) -> int: else: line = await DialogueManager.get_next_dialogue_line(resource, line.next_id, extraStates) + DialogueManager.dialogue_ended.emit(resource) + if mode == DialogueMode.CONVERSATION: UI.activeConversation = false diff --git a/overworld/camera/OverworldCamera.gd b/overworld/camera/OverworldCamera.gd index 7a4ae16..f0ea3e3 100644 --- a/overworld/camera/OverworldCamera.gd +++ b/overworld/camera/OverworldCamera.gd @@ -42,7 +42,15 @@ var _freeTimer:float = 0.0 var _mouseDelta:Vector2 = Vector2.ZERO var _rightMouseHeld:bool = false +func _canOrbit() -> bool: + return not UI.activeConversation + func _input(event:InputEvent) -> void: + if not _canOrbit(): + if _rightMouseHeld: + _rightMouseHeld = false + Input.mouse_mode = Input.MOUSE_MODE_VISIBLE + return if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT: _rightMouseHeld = event.pressed Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if _rightMouseHeld else Input.MOUSE_MODE_VISIBLE @@ -56,13 +64,17 @@ func _process(delta:float) -> void: var xMult:float = -1.0 if SETTINGS.invertCameraX else 1.0 var yMult:float = 1.0 if SETTINGS.invertCameraY else -1.0 - var orbitInput:Vector2 = Input.get_vector( - "camera_orbit_left", "camera_orbit_right", - "camera_orbit_up", "camera_orbit_down" - ) + var orbitInput:Vector2 = Vector2.ZERO + var mouseActive:bool = false + + if _canOrbit(): + orbitInput = Input.get_vector( + "camera_orbit_left", "camera_orbit_right", + "camera_orbit_up", "camera_orbit_down" + ) + mouseActive = _mouseDelta.length_squared() > 0.0 var controllerActive:bool = orbitInput.length() > 0.01 - var mouseActive:bool = _mouseDelta.length_squared() > 0.0 # Any manual camera input returns to FREE and resets the centering timer if controllerActive or mouseActive: @@ -83,10 +95,10 @@ func _process(delta:float) -> void: if mouseActive: _yaw += _mouseDelta.x * mouseSensitivity * SETTINGS.cameraSpeedMouse * xMult _pitch += _mouseDelta.y * mouseSensitivity * SETTINGS.cameraSpeedMouse * yMult - _mouseDelta = Vector2.ZERO + _mouseDelta = Vector2.ZERO # center_camera input → switch to MANUAL_CENTER immediately - if Input.is_action_just_pressed("center_camera"): + if _canOrbit() and Input.is_action_just_pressed("center_camera"): _mode = CameraMode.MANUAL_CENTER # In FREE mode, accumulate time toward auto-centering while the player is moving diff --git a/overworld/entity/EntityInteractingArea.gd b/overworld/entity/EntityInteractingArea.gd index d8d8191..9690794 100644 --- a/overworld/entity/EntityInteractingArea.gd +++ b/overworld/entity/EntityInteractingArea.gd @@ -22,11 +22,36 @@ func _exit_tree() -> void: self.area_entered.disconnect(_onAreaEntered) self.area_exited.disconnect(_onAreaExited) +func _process(_delta:float) -> void: + if entity.movementType != Entity.MovementType.PLAYER: + return + if UI.INTERACT_INDICATOR and UI.INTERACT_INDICATOR.visible: + UI.INTERACT_INDICATOR.updateWorldPosition() + +func _getBestInteractable() -> Entity: + for area in interactableAreas: + if area.isInteractable(): + return area.entity + return null + +func _updateIndicator() -> void: + if entity.movementType != Entity.MovementType.PLAYER: + return + if UI.INTERACT_INDICATOR == null: + return + var best:Entity = _getBestInteractable() + if best: + UI.INTERACT_INDICATOR.setEntity(best) + else: + UI.INTERACT_INDICATOR.clear() + func _onAreaEntered(area:Area3D) -> void: if area is EntityInteractableArea: if area.entity == entity: return interactableAreas.append(area) + _updateIndicator() func _onAreaExited(area:Area3D) -> void: interactableAreas.erase(area) + _updateIndicator() diff --git a/project.godot b/project.godot index 69a38fd..2707e31 100644 --- a/project.godot +++ b/project.godot @@ -81,6 +81,7 @@ ui_cancel={ ui_left={ "deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194319,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null) , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":13,"pressure":0.0,"pressed":false,"script":null) , Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":0,"axis_value":-1.0,"script":null) ] @@ -88,6 +89,7 @@ ui_left={ ui_right={ "deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194321,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null) , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":14,"pressure":0.0,"pressed":false,"script":null) , Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":0,"axis_value":1.0,"script":null) ] @@ -95,6 +97,7 @@ ui_right={ ui_up={ "deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194320,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null) , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":11,"pressure":0.0,"pressed":false,"script":null) , Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":1,"axis_value":-1.0,"script":null) ] @@ -102,6 +105,7 @@ ui_up={ ui_down={ "deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194322,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null) , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":12,"pressure":0.0,"pressed":false,"script":null) , Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":1,"axis_value":1.0,"script":null) ] diff --git a/scene/Pause.gd b/scene/Pause.gd index 370b898..491f3d3 100644 --- a/scene/Pause.gd +++ b/scene/Pause.gd @@ -9,6 +9,8 @@ func _unhandled_input(event:InputEvent) -> void: var menu:PauseMenu = UI.PAUSE_MENU if menu == null: return + if SCENE.currentScene == SceneSingleton.SceneType.INITIAL and !menu.isOpen(): + return if menu.isOpen(): menu.close() else: diff --git a/scene/Settings.gd b/scene/Settings.gd index 6a4548c..c9fabca 100644 --- a/scene/Settings.gd +++ b/scene/Settings.gd @@ -4,3 +4,4 @@ var invertCameraX:bool = false var invertCameraY:bool = false var cameraSpeedController:float = 1.0 var cameraSpeedMouse:float = 1.0 +var textSpeed:float = 1.0 diff --git a/ui/RootUI.gd b/ui/RootUI.gd index ec9d027..813390a 100644 --- a/ui/RootUI.gd +++ b/ui/RootUI.gd @@ -3,6 +3,9 @@ class_name RootUI extends Control @export var debugMenu:DebugMenu @export var gameMenu:GameMenu @export var pauseMenu:PauseMenu +@export var quitConfirmDialog:QuitConfirmDialog +@export var mainMenuConfirmDialog:ConfirmDialog +@export var modalBackdrop:ModalBackdrop @export var chatBoxContainer:Control func _enter_tree() -> void: @@ -11,3 +14,8 @@ func _enter_tree() -> void: func _exit_tree() -> void: if UI.rootUi == self: UI.rootUi = null + +func _ready() -> void: + modalBackdrop.register(pauseMenu) + modalBackdrop.register(quitConfirmDialog) + modalBackdrop.register(mainMenuConfirmDialog) diff --git a/ui/RootUI.tscn b/ui/RootUI.tscn index 37b5603..1c4420b 100644 --- a/ui/RootUI.tscn +++ b/ui/RootUI.tscn @@ -1,11 +1,15 @@ -[gd_scene load_steps=5 format=3 uid="uid://baos0arpiskbp"] +[gd_scene load_steps=9 format=3 uid="uid://baos0arpiskbp"] [ext_resource type="Script" uid="uid://dq3qyyayugt5l" path="res://ui/RootUI.gd" id="1_son71"] [ext_resource type="PackedScene" uid="uid://c0i5e2dj11d8c" path="res://ui/pause/PauseMenu.tscn" id="2_atyu8"] [ext_resource type="PackedScene" uid="uid://b38dr0wkix76t" path="res://ui/debugmenu/DebugMenu.tscn" id="4_u132g"] [ext_resource type="PackedScene" uid="uid://bv5r2x9m4k7n1" path="res://ui/gamemenu/GameMenu.tscn" id="5_gmenu"] +[ext_resource type="PackedScene" path="res://ui/component/InteractIndicator.tscn" id="6_iind"] +[ext_resource type="Script" path="res://ui/component/ModalBackdrop.gd" id="7_mbdp"] +[ext_resource type="PackedScene" uid="uid://cqdf1x7m2canp" path="res://ui/component/QuitConfirmDialog.tscn" id="8_qcd"] +[ext_resource type="PackedScene" uid="uid://bmmc3x8n1d7qp" path="res://ui/component/MainMenuConfirmDialog.tscn" id="9_mmcd"] -[node name="RootUI" type="Control" node_paths=PackedStringArray("debugMenu", "gameMenu", "pauseMenu", "chatBoxContainer")] +[node name="RootUI" type="Control" node_paths=PackedStringArray("debugMenu", "gameMenu", "pauseMenu", "quitConfirmDialog", "mainMenuConfirmDialog", "modalBackdrop", "chatBoxContainer")] layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 @@ -17,6 +21,9 @@ script = ExtResource("1_son71") debugMenu = NodePath("DebugMenu") gameMenu = NodePath("GameMenu") pauseMenu = NodePath("PauseMenu") +quitConfirmDialog = NodePath("QuitConfirmDialog") +mainMenuConfirmDialog = NodePath("MainMenuConfirmDialog") +modalBackdrop = NodePath("ModalBackdrop") chatBoxContainer = NodePath("ChatBoxContainer") metadata/_custom_type_script = "uid://dq3qyyayugt5l" @@ -24,11 +31,6 @@ metadata/_custom_type_script = "uid://dq3qyyayugt5l" visible = false layout_mode = 1 -[node name="PauseMenu" parent="." instance=ExtResource("2_atyu8")] -visible = false -layout_mode = 1 -process_mode = 3 - [node name="GameMenu" parent="." instance=ExtResource("5_gmenu")] visible = false layout_mode = 1 @@ -41,3 +43,31 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 mouse_filter = 2 + +[node name="InteractIndicator" parent="ChatBoxContainer" instance=ExtResource("6_iind")] + +[node name="ModalBackdrop" type="ColorRect" parent="."] +visible = false +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 1 +process_mode = 3 +color = Color(0, 0, 0, 0.5) +script = ExtResource("7_mbdp") + +[node name="PauseMenu" parent="." instance=ExtResource("2_atyu8")] +visible = false +layout_mode = 1 +process_mode = 3 + +[node name="QuitConfirmDialog" parent="." instance=ExtResource("8_qcd")] +visible = false +layout_mode = 1 + +[node name="MainMenuConfirmDialog" parent="." instance=ExtResource("9_mmcd")] +visible = false +layout_mode = 1 diff --git a/ui/UISingleton.gd b/ui/UISingleton.gd index 04421f0..27efea4 100644 --- a/ui/UISingleton.gd +++ b/ui/UISingleton.gd @@ -1,6 +1,7 @@ extends Node var rootUi:RootUI = null +var interactIndicator:InteractIndicator = null # True whenever any dialogue resource is being processed by DialogueManager. # Driven by DialogueManager.dialogue_started / dialogue_ended signals. @@ -19,6 +20,9 @@ func _onDialogueStarted(_resource:DialogueResource) -> void: func _onDialogueEnded(_resource:DialogueResource) -> void: dialogueActive = false +var INTERACT_INDICATOR:InteractIndicator: + get(): return interactIndicator + var chatBoxContainer:Control: get(): if rootUi: @@ -42,3 +46,21 @@ var PAUSE_MENU:PauseMenu: if rootUi: return rootUi.pauseMenu return null + +var QUIT_DIALOG:QuitConfirmDialog: + get(): + if rootUi: + return rootUi.quitConfirmDialog + return null + +var MAIN_MENU_DIALOG:ConfirmDialog: + get(): + if rootUi: + return rootUi.mainMenuConfirmDialog + return null + +var BACKDROP:ModalBackdrop: + get(): + if rootUi: + return rootUi.modalBackdrop + return null diff --git a/ui/component/ConfirmDialog.gd b/ui/component/ConfirmDialog.gd new file mode 100644 index 0000000..8c9a8fd --- /dev/null +++ b/ui/component/ConfirmDialog.gd @@ -0,0 +1,30 @@ +class_name ConfirmDialog extends ClosableMenu + +signal confirmed + +@export var btnYes:Button +@export var btnNo:Button + +func _ready() -> void: + close() + btnYes.pressed.connect(_onYes) + btnNo.pressed.connect(close) + btnYes.focus_neighbor_top = btnNo.get_path() + btnYes.focus_neighbor_bottom = btnNo.get_path() + btnNo.focus_neighbor_top = btnYes.get_path() + btnNo.focus_neighbor_bottom = btnYes.get_path() + +func _onYes() -> void: + close() + confirmed.emit() + +func open() -> void: + super.open() + btnNo.grab_focus() + +func _unhandled_input(event:InputEvent) -> void: + if !isOpen: + return + if event.is_action_pressed("ui_cancel"): + close() + get_viewport().set_input_as_handled() diff --git a/ui/component/DialogueTextbox.gd b/ui/component/DialogueTextbox.gd index a201604..e14c9d3 100644 --- a/ui/component/DialogueTextbox.gd +++ b/ui/component/DialogueTextbox.gd @@ -5,8 +5,7 @@ const SCENE:PackedScene = preload("res://ui/component/DialogueTextbox.tscn") enum AdvancementMode { PLAYER, TIMED } const LINES_PER_PAGE:int = 4 -const CHARS_PER_SECOND:float = 20.0 -const SPEEDUP_MULTIPLIER:float = 4.0 +const CHARS_PER_SECOND:float = 24.0 const PAUSE_COMMA:float = 0.15 const PAUSE_SENTENCE:float = 0.4 const PAUSE_ELLIPSIS_DOT:float = 0.3 @@ -24,7 +23,6 @@ var _pauseTimer:float = 0.0 var _autoAdvanceTimer:float = 0.0 var _isRevealing:bool = false var _isWaitingForInput:bool = false -var _hasLetGoOfInteract:bool = true var _advancementMode:AdvancementMode = AdvancementMode.PLAYER @onready var _speakerLabel:Label = $VBoxContainer/SpeakerLabel @@ -68,7 +66,6 @@ func setup(line:DialogueLine, entity:Entity, mode:AdvancementMode = AdvancementM _autoAdvanceTimer = 0.0 _isRevealing = true _isWaitingForInput = false - _hasLetGoOfInteract = !Input.is_action_pressed("interact") _updateWorldPosition() visible = true @@ -85,9 +82,10 @@ func _process(delta:float) -> void: return _updateWorldPosition() - _advanceIndicator.visible = _isWaitingForInput if _isWaitingForInput: _advanceIndicator.modulate.a = 0.5 + 0.5 * sin(Time.get_ticks_msec() / 300.0) + else: + _advanceIndicator.modulate.a = 0.0 if _isRevealing: _processReveal(delta) @@ -119,16 +117,13 @@ func _buildPreWrappedText(parsed:String) -> String: return result func _processReveal(delta:float) -> void: - if Input.is_action_just_released("interact"): - _hasLetGoOfInteract = true - - var speedMult:float = SPEEDUP_MULTIPLIER if (_hasLetGoOfInteract and Input.is_action_pressed("interact")) else 1.0 + var scaledDelta:float = delta * SETTINGS.textSpeed if _pauseTimer > 0.0: - _pauseTimer -= delta * speedMult + _pauseTimer -= scaledDelta return - _revealTimer += delta * speedMult + _revealTimer += scaledDelta while _revealTimer >= 1.0 / CHARS_PER_SECOND: _revealTimer -= 1.0 / CHARS_PER_SECOND @@ -185,12 +180,6 @@ func _onRevealComplete() -> void: _isWaitingForInput = true func _processAdvanceInput() -> void: - if Input.is_action_just_released("interact"): - _hasLetGoOfInteract = true - - if not _hasLetGoOfInteract: - return - if Input.is_action_just_pressed("interact"): _advance() @@ -200,8 +189,7 @@ func _processAutoAdvance(delta:float) -> void: _advance() func _advance() -> void: - _advanceIndicator.visible = false - _advanceIndicator.modulate.a = 1.0 + _advanceIndicator.modulate.a = 0.0 var totalLines:int = _parsedText.count("\n") + 1 var hasMorePages:bool = _startLine + _linesPerPage < totalLines if hasMorePages: diff --git a/ui/component/DialogueTextbox.tscn b/ui/component/DialogueTextbox.tscn index 6049448..6af2a80 100644 --- a/ui/component/DialogueTextbox.tscn +++ b/ui/component/DialogueTextbox.tscn @@ -28,6 +28,6 @@ autowrap_mode = 3 [node name="AdvanceIndicator" type="Label" parent="VBoxContainer"] layout_mode = 2 +modulate = Color(1, 1, 1, 0) text = "▼" horizontal_alignment = 2 -visible = false diff --git a/ui/component/InteractIndicator.gd b/ui/component/InteractIndicator.gd new file mode 100644 index 0000000..afef06b --- /dev/null +++ b/ui/component/InteractIndicator.gd @@ -0,0 +1,56 @@ +class_name InteractIndicator extends PanelContainer + +var _entity:Entity = null + +func _enter_tree() -> void: + UI.interactIndicator = self + +func _exit_tree() -> void: + if UI.interactIndicator == self: + UI.interactIndicator = null + +func _ready() -> void: + visible = false + DialogueManager.dialogue_started.connect(_onDialogueStarted) + DialogueManager.dialogue_ended.connect(_onDialogueEnded) + +func setEntity(entity:Entity) -> void: + if is_instance_valid(_entity): + _entity.tree_exiting.disconnect(_onEntityExiting) + _entity = entity + _entity.tree_exiting.connect(_onEntityExiting) + visible = _canShow() + if visible: + updateWorldPosition() + +func clear() -> void: + if is_instance_valid(_entity): + _entity.tree_exiting.disconnect(_onEntityExiting) + _entity = null + visible = false + +func _canShow() -> bool: + return _entity != null and not UI.dialogueActive + +func _onEntityExiting() -> void: + _entity = null + visible = false + +func _onDialogueStarted(_resource:DialogueResource) -> void: + visible = false + +func _onDialogueEnded(_resource:DialogueResource) -> void: + visible = _canShow() + if visible: + updateWorldPosition() + +func updateWorldPosition() -> void: + var camera:Camera3D = get_viewport().get_camera_3d() + if camera == null: + return + var worldPos:Vector3 = _entity.global_position + Vector3(0, 2.5, 0) + var screenPos:Vector2 = camera.unproject_position(worldPos) + var viewportSize:Vector2 = get_viewport().get_visible_rect().size + position = screenPos - size * 0.5 + position.x = clamp(position.x, 0.0, viewportSize.x - size.x) + position.y = clamp(position.y, 0.0, viewportSize.y - size.y) diff --git a/ui/component/InteractIndicator.gd.uid b/ui/component/InteractIndicator.gd.uid new file mode 100644 index 0000000..1dc481f --- /dev/null +++ b/ui/component/InteractIndicator.gd.uid @@ -0,0 +1 @@ +uid://xrcb2e7jwlm0 diff --git a/ui/component/InteractIndicator.tscn b/ui/component/InteractIndicator.tscn new file mode 100644 index 0000000..f39f952 --- /dev/null +++ b/ui/component/InteractIndicator.tscn @@ -0,0 +1,13 @@ +[gd_scene load_steps=3 format=3] + +[ext_resource type="Theme" path="res://ui/UI Theme.tres" id="1"] +[ext_resource type="Script" path="res://ui/component/InteractIndicator.gd" id="2"] + +[node name="InteractIndicator" type="PanelContainer"] +mouse_filter = 2 +theme = ExtResource("1") +script = ExtResource("2") + +[node name="Label" type="Label" parent="."] +layout_mode = 2 +text = "INTERACT" diff --git a/ui/component/MainMenuConfirmDialog.tscn b/ui/component/MainMenuConfirmDialog.tscn new file mode 100644 index 0000000..1c23c76 --- /dev/null +++ b/ui/component/MainMenuConfirmDialog.tscn @@ -0,0 +1,38 @@ +[gd_scene load_steps=2 format=3 uid="uid://bmmc3x8n1d7qp"] + +[ext_resource type="Script" path="res://ui/component/ConfirmDialog.gd" id="1_mmcd"] + +[node name="MainMenuConfirmDialog" type="Control" node_paths=PackedStringArray("btnYes", "btnNo")] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +process_mode = 3 +script = ExtResource("1_mmcd") +btnYes = NodePath("VBoxContainer/Yes") +btnNo = NodePath("VBoxContainer/No") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="Label" type="Label" parent="VBoxContainer"] +layout_mode = 2 +text = "Return to main menu?" +horizontal_alignment = 1 + +[node name="Yes" type="Button" parent="VBoxContainer"] +layout_mode = 2 +text = "Yes" + +[node name="No" type="Button" parent="VBoxContainer"] +layout_mode = 2 +text = "No" diff --git a/ui/component/ModalBackdrop.gd b/ui/component/ModalBackdrop.gd new file mode 100644 index 0000000..22fcc9e --- /dev/null +++ b/ui/component/ModalBackdrop.gd @@ -0,0 +1,48 @@ +class_name ModalBackdrop extends ColorRect + +# Tracks which overlays are currently open. Each entry must be a direct sibling +# (child of the same parent). The backdrop repositions itself in the scene tree +# to sit immediately below whichever open overlay has the highest tree index, +# so only one backdrop is ever visible regardless of how many overlays are open. +var _openOverlays:Array[Control] = [] + +func _ready() -> void: + visible = false + +func register(overlay:Control) -> void: + assert(overlay.get_parent() == get_parent(), "ModalBackdrop: overlay must be a sibling") + assert(overlay.has_signal("opened") and overlay.has_signal("closed"), + "ModalBackdrop: overlay must have opened/closed signals") + overlay.connect("opened", func(): _onOpened(overlay)) + overlay.connect("closed", func(): _onClosed(overlay)) + +func _onOpened(overlay:Control) -> void: + if overlay not in _openOverlays: + _openOverlays.append(overlay) + _reposition() + +func _onClosed(overlay:Control) -> void: + _openOverlays.erase(overlay) + _reposition() + +func _reposition() -> void: + if _openOverlays.is_empty(): + visible = false + return + visible = true + var top := _topOverlay() + var topIdx := top.get_index() + var myIdx := get_index() + if myIdx == topIdx - 1: + return + if myIdx < topIdx: + get_parent().move_child(self, topIdx - 1) + else: + get_parent().move_child(self, topIdx) + +func _topOverlay() -> Control: + var top:Control = _openOverlays[0] + for overlay in _openOverlays: + if overlay.get_index() > top.get_index(): + top = overlay + return top diff --git a/ui/component/QuitConfirmDialog.gd b/ui/component/QuitConfirmDialog.gd new file mode 100644 index 0000000..f977e7d --- /dev/null +++ b/ui/component/QuitConfirmDialog.gd @@ -0,0 +1,5 @@ +class_name QuitConfirmDialog extends ConfirmDialog + +func _ready() -> void: + super._ready() + confirmed.connect(func(): get_tree().quit()) diff --git a/ui/component/QuitConfirmDialog.gd.uid b/ui/component/QuitConfirmDialog.gd.uid new file mode 100644 index 0000000..70ef646 --- /dev/null +++ b/ui/component/QuitConfirmDialog.gd.uid @@ -0,0 +1 @@ +uid://deov3ob0lojyo diff --git a/ui/component/QuitConfirmDialog.tscn b/ui/component/QuitConfirmDialog.tscn new file mode 100644 index 0000000..d27dce1 --- /dev/null +++ b/ui/component/QuitConfirmDialog.tscn @@ -0,0 +1,38 @@ +[gd_scene load_steps=2 format=3 uid="uid://cqdf1x7m2canp"] + +[ext_resource type="Script" path="res://ui/component/QuitConfirmDialog.gd" id="1_qcd"] + +[node name="QuitConfirmDialog" type="Control" node_paths=PackedStringArray("btnYes", "btnNo")] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +process_mode = 3 +script = ExtResource("1_qcd") +btnYes = NodePath("VBoxContainer/Yes") +btnNo = NodePath("VBoxContainer/No") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="Label" type="Label" parent="VBoxContainer"] +layout_mode = 2 +text = "Quit to desktop?" +horizontal_alignment = 1 + +[node name="Yes" type="Button" parent="VBoxContainer"] +layout_mode = 2 +text = "Yes" + +[node name="No" type="Button" parent="VBoxContainer"] +layout_mode = 2 +text = "No" diff --git a/ui/mainmenu/MainMenu.gd b/ui/mainmenu/MainMenu.gd index 15548ba..612dc62 100644 --- a/ui/mainmenu/MainMenu.gd +++ b/ui/mainmenu/MainMenu.gd @@ -2,12 +2,14 @@ class_name MainMenu extends Control @export var btnNewGame:Button @export var btnSettings:Button +@export var btnQuit:Button @export var settingsMenu:ClosableMenu @export_file("*.tscn") var newGameScene:String func _ready() -> void: btnNewGame.pressed.connect(onNewGamePressed) btnSettings.pressed.connect(onSettingsPressed) + btnQuit.pressed.connect(_onQuitPressed) settingsMenu.opened.connect(_onSettingsOpened) settingsMenu.closed.connect(_onSettingsClosed) btnNewGame.grab_focus() @@ -26,6 +28,13 @@ func _unhandled_input(event:InputEvent) -> void: settingsMenu.close() get_viewport().set_input_as_handled() +func _onQuitPressed() -> void: + UI.QUIT_DIALOG.closed.connect(_onQuitDialogClosed, CONNECT_ONE_SHOT) + UI.QUIT_DIALOG.open() + +func _onQuitDialogClosed() -> void: + btnQuit.grab_focus() + func onNewGamePressed() -> void: SCENE.setScene(SceneSingleton.SceneType.OVERWORLD) OVERWORLD.mapChange(newGameScene, "PlayerSpawnPoint") diff --git a/ui/mainmenu/MainMenu.tscn b/ui/mainmenu/MainMenu.tscn index d57bda1..d71831c 100644 --- a/ui/mainmenu/MainMenu.tscn +++ b/ui/mainmenu/MainMenu.tscn @@ -4,7 +4,7 @@ [ext_resource type="Script" uid="uid://bcjfv6dw0ugvo" path="res://ui/component/ClosableMenu.gd" id="2_f3vro"] [ext_resource type="PackedScene" uid="uid://d3f31lli1ahts" path="res://ui/settings/SettingsMenu.tscn" id="3_44i87"] -[node name="Main Menu" type="Control" node_paths=PackedStringArray("btnNewGame", "btnSettings", "settingsMenu")] +[node name="Main Menu" type="Control" node_paths=PackedStringArray("btnNewGame", "btnSettings", "btnQuit", "settingsMenu")] layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 @@ -14,6 +14,7 @@ grow_vertical = 2 script = ExtResource("1_vp3lc") btnNewGame = NodePath("VBoxContainer/NewGame") btnSettings = NodePath("VBoxContainer/Settings") +btnQuit = NodePath("VBoxContainer/Quit") settingsMenu = NodePath("MainMenuSettings") newGameScene = "uid://d0ywgijpuqy0r" metadata/_custom_type_script = "uid://btfeuku41py2b" @@ -35,6 +36,10 @@ text = "New Game" layout_mode = 2 text = "Settings" +[node name="Quit" type="Button" parent="VBoxContainer"] +layout_mode = 2 +text = "Quit Game" + [node name="MainMenuSettings" type="Control" parent="."] visible = false layout_mode = 1 diff --git a/ui/pause/PauseMain.gd b/ui/pause/PauseMain.gd index 0e8b666..baef3cc 100644 --- a/ui/pause/PauseMain.gd +++ b/ui/pause/PauseMain.gd @@ -2,44 +2,37 @@ class_name PauseMain extends VBoxContainer signal resumeRequested signal settingsRequested -signal mainMenuRequested -signal quitRequested @export var btnResume:Button @export var btnSettings:Button @export var btnMainMenu:Button @export var btnQuit:Button -@export var mainButtons:VBoxContainer -@export var confirmQuit:VBoxContainer -@export var btnQuitConfirm:Button -@export var btnQuitCancel:Button func _ready() -> void: visible = false btnResume.pressed.connect(resumeRequested.emit) btnSettings.pressed.connect(settingsRequested.emit) - btnMainMenu.pressed.connect(mainMenuRequested.emit) - btnQuit.pressed.connect(_showConfirm) - btnQuitConfirm.pressed.connect(quitRequested.emit) - btnQuitCancel.pressed.connect(cancelConfirm) + btnMainMenu.pressed.connect(_showMainMenuConfirm) + btnQuit.pressed.connect(_showQuitConfirm) + UI.QUIT_DIALOG.closed.connect(_onQuitDialogClosed) + UI.MAIN_MENU_DIALOG.closed.connect(_onMainMenuDialogClosed) -func _showConfirm() -> void: - mainButtons.visible = false - confirmQuit.visible = true - btnQuitCancel.grab_focus() +func _showQuitConfirm() -> void: + UI.QUIT_DIALOG.open() -func cancelConfirm() -> void: - mainButtons.visible = true - confirmQuit.visible = false - btnQuit.grab_focus() +func _showMainMenuConfirm() -> void: + UI.MAIN_MENU_DIALOG.open() -func isConfirming() -> bool: - return confirmQuit.visible +func _onQuitDialogClosed() -> void: + if isOpen(): + btnQuit.grab_focus() + +func _onMainMenuDialogClosed() -> void: + if isOpen(): + btnMainMenu.grab_focus() func open() -> void: visible = true - if isConfirming(): - cancelConfirm() btnResume.grab_focus() func close() -> void: diff --git a/ui/pause/PauseMain.tscn b/ui/pause/PauseMain.tscn index 9ec3e8b..043f883 100644 --- a/ui/pause/PauseMain.tscn +++ b/ui/pause/PauseMain.tscn @@ -2,7 +2,7 @@ [ext_resource type="Script" uid="uid://c7kvg0jw6w340" path="res://ui/pause/PauseMain.gd" id="1_b5xfl"] -[node name="PauseMain" type="VBoxContainer" node_paths=PackedStringArray("btnResume", "btnSettings", "btnMainMenu", "btnQuit", "mainButtons", "confirmQuit", "btnQuitConfirm", "btnQuitCancel")] +[node name="PauseMain" type="VBoxContainer" node_paths=PackedStringArray("btnResume", "btnSettings", "btnMainMenu", "btnQuit")] anchors_preset = 8 anchor_left = 0.5 anchor_top = 0.5 @@ -16,10 +16,6 @@ btnResume = NodePath("MainButtons/Resume") btnSettings = NodePath("MainButtons/Settings") btnMainMenu = NodePath("MainButtons/MainMenu") btnQuit = NodePath("MainButtons/Quit") -mainButtons = NodePath("MainButtons") -confirmQuit = NodePath("ConfirmQuit") -btnQuitConfirm = NodePath("ConfirmQuit/Yes") -btnQuitCancel = NodePath("ConfirmQuit/No") [node name="Title" type="Label" parent="."] layout_mode = 2 @@ -44,20 +40,3 @@ text = "Main Menu" [node name="Quit" type="Button" parent="MainButtons"] layout_mode = 2 text = "Quit Game" - -[node name="ConfirmQuit" type="VBoxContainer" parent="."] -layout_mode = 2 -visible = false - -[node name="Label" type="Label" parent="ConfirmQuit"] -layout_mode = 2 -text = "Quit to desktop?" -horizontal_alignment = 1 - -[node name="Yes" type="Button" parent="ConfirmQuit"] -layout_mode = 2 -text = "Yes" - -[node name="No" type="Button" parent="ConfirmQuit"] -layout_mode = 2 -text = "No" diff --git a/ui/pause/PauseMenu.gd b/ui/pause/PauseMenu.gd index 329a123..8ad7791 100644 --- a/ui/pause/PauseMenu.gd +++ b/ui/pause/PauseMenu.gd @@ -1,5 +1,8 @@ class_name PauseMenu extends Control +signal opened +signal closed + @export var MAIN:PauseMain @export var settingsPanel:PauseSettings @@ -7,8 +10,7 @@ func _ready() -> void: close() MAIN.resumeRequested.connect(close) MAIN.settingsRequested.connect(_openSettings) - MAIN.mainMenuRequested.connect(_goToMainMenu) - MAIN.quitRequested.connect(func(): get_tree().quit()) + UI.MAIN_MENU_DIALOG.confirmed.connect(_goToMainMenu) func isOpen() -> bool: return visible @@ -17,12 +19,14 @@ func open() -> void: visible = true get_tree().paused = true MAIN.open() + opened.emit() func close() -> void: get_tree().paused = false visible = false MAIN.close() settingsPanel.close() + closed.emit() func _openSettings() -> void: MAIN.close() @@ -35,12 +39,13 @@ func _goToMainMenu() -> void: func _unhandled_input(event:InputEvent) -> void: if !visible: return - if event.is_action_pressed("ui_cancel"): - if MAIN.isConfirming(): - MAIN.cancelConfirm() - elif settingsPanel.isOpen(): - settingsPanel.close() - MAIN.open() - else: - close() - get_viewport().set_input_as_handled() + if !event.is_action_pressed("ui_cancel"): + return + if (UI.QUIT_DIALOG != null and UI.QUIT_DIALOG.isOpen) or (UI.MAIN_MENU_DIALOG != null and UI.MAIN_MENU_DIALOG.isOpen): + return + if settingsPanel.isOpen(): + settingsPanel.close() + MAIN.open() + else: + close() + get_viewport().set_input_as_handled() diff --git a/ui/settings/SettingsMenu.gd b/ui/settings/SettingsMenu.gd index 0917248..aaa35ec 100644 --- a/ui/settings/SettingsMenu.gd +++ b/ui/settings/SettingsMenu.gd @@ -1,11 +1,14 @@ class_name SettingsMenu extends Control +const TEXT_SPEED_VALUES:Array[float] = [0.2, 1.0, 2.0] + @export var tabs:TabBar @export var tabControls:Array[Control] @export var checkInvertX:CheckBox @export var checkInvertY:CheckBox @export var sliderControllerSpeed:HSlider @export var sliderMouseSpeed:HSlider +@export var optionTextSpeed:OptionButton func _ready() -> void: tabs.tab_changed.connect(onTabChanged) @@ -17,8 +20,16 @@ func _ready() -> void: sliderMouseSpeed.value = SETTINGS.cameraSpeedMouse sliderControllerSpeed.value_changed.connect(func(v:float): SETTINGS.cameraSpeedController = v) sliderMouseSpeed.value_changed.connect(func(v:float): SETTINGS.cameraSpeedMouse = v) + optionTextSpeed.select(_textSpeedToIndex(SETTINGS.textSpeed)) + optionTextSpeed.item_selected.connect(func(idx:int): SETTINGS.textSpeed = TEXT_SPEED_VALUES[idx]) onTabChanged(tabs.current_tab) +func _textSpeedToIndex(speed:float) -> int: + match speed: + 0.2: return 0 + 2.0: return 2 + _: return 1 + func _notification(what:int) -> void: if what == NOTIFICATION_VISIBILITY_CHANGED and visible: tabs.grab_focus() diff --git a/ui/settings/SettingsMenu.tscn b/ui/settings/SettingsMenu.tscn index 401bfe9..f5aba5c 100644 --- a/ui/settings/SettingsMenu.tscn +++ b/ui/settings/SettingsMenu.tscn @@ -2,7 +2,7 @@ [ext_resource type="Script" uid="uid://efmr0xkbw1py" path="res://ui/settings/SettingsMenu.gd" id="1_4lnig"] -[node name="SettingsMenu" type="Control" node_paths=PackedStringArray("tabs", "tabControls", "checkInvertX", "checkInvertY", "sliderControllerSpeed", "sliderMouseSpeed")] +[node name="SettingsMenu" type="Control" node_paths=PackedStringArray("tabs", "tabControls", "checkInvertX", "checkInvertY", "sliderControllerSpeed", "sliderMouseSpeed", "optionTextSpeed")] layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 @@ -11,11 +11,12 @@ grow_horizontal = 2 grow_vertical = 2 script = ExtResource("1_4lnig") tabs = NodePath("VBoxContainer/TabBar") -tabControls = [NodePath("VBoxContainer/ScrollContainer/LabelGameplay"), NodePath("VBoxContainer/ScrollContainer/LabelSound"), NodePath("VBoxContainer/ScrollContainer/LabelGraphics"), NodePath("VBoxContainer/ScrollContainer/PanelControls")] +tabControls = [NodePath("VBoxContainer/ScrollContainer/PanelGameplay"), NodePath("VBoxContainer/ScrollContainer/LabelSound"), NodePath("VBoxContainer/ScrollContainer/LabelGraphics"), NodePath("VBoxContainer/ScrollContainer/PanelControls")] checkInvertX = NodePath("VBoxContainer/ScrollContainer/PanelControls/CheckInvertX") checkInvertY = NodePath("VBoxContainer/ScrollContainer/PanelControls/CheckInvertY") sliderControllerSpeed = NodePath("VBoxContainer/ScrollContainer/PanelControls/SliderControllerSpeed") sliderMouseSpeed = NodePath("VBoxContainer/ScrollContainer/PanelControls/SliderMouseSpeed") +optionTextSpeed = NodePath("VBoxContainer/ScrollContainer/PanelGameplay/OptionTextSpeed") [node name="VBoxContainer" type="VBoxContainer" parent="."] layout_mode = 1 @@ -48,10 +49,24 @@ visible = false layout_mode = 2 text = "Sound" -[node name="LabelGameplay" type="Label" parent="VBoxContainer/ScrollContainer"] +[node name="PanelGameplay" type="VBoxContainer" parent="VBoxContainer/ScrollContainer"] visible = false layout_mode = 2 -text = "Gameplay" + +[node name="LabelTextSpeed" type="Label" parent="VBoxContainer/ScrollContainer/PanelGameplay"] +layout_mode = 2 +text = "Text Speed" + +[node name="OptionTextSpeed" type="OptionButton" parent="VBoxContainer/ScrollContainer/PanelGameplay"] +layout_mode = 2 +focus_mode = 2 +item_count = 3 +item_0/text = "Slow" +item_0/id = 0 +item_1/text = "Normal" +item_1/id = 1 +item_2/text = "Fast" +item_2/id = 2 [node name="PanelControls" type="VBoxContainer" parent="VBoxContainer/ScrollContainer"] visible = false