diff --git a/.claude/docs/architecture.md b/.claude/docs/architecture.md index 0042ecd..2c236df 100644 --- a/.claude/docs/architecture.md +++ b/.claude/docs/architecture.md @@ -13,10 +13,11 @@ Never instantiate these — access only via the global handle. | `OVERWORLD` | Map switching with threaded loading | | `COOKING` | Cooking mini-game lifecycle | | `SAVE` | Persistence (stub) | +| `SETTINGS` | Runtime settings — `invertCameraX:bool`, `invertCameraY:bool` | | `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` | Root UI accessor — `UI.TEXTBOX`, `UI.DEBUG_MENU`, `UI.GAME_MENU` | ## Scene Graph @@ -24,7 +25,7 @@ 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 + └─ VNTextbox, DebugMenu, PauseMenu, GameMenu ``` `RootScene` listens to `SCENE.sceneChanged` and shows/hides the correct sub-tree. diff --git a/.claude/docs/overworld.md b/.claude/docs/overworld.md new file mode 100644 index 0000000..cbe1289 --- /dev/null +++ b/.claude/docs/overworld.md @@ -0,0 +1,96 @@ +# Overworld System + +## Scene structure + +``` +OverworldScene (Node3D) ← always resident, managed by SCENE singleton + └─ Map (Node3D) ← emptied and repopulated on each map change + +TestMap (Node3D) ← typical map root, extends Node3D + ├─ Player (Entity) ← movementType = PLAYER, entityId = "player" + ├─ NPC/object (Entity) ... ← interactType drives what happens on interact + ├─ TestMapBase (StaticBody3D) ← reusable terrain plane (200×200, collision layer 1) + └─ Camera3D (OverworldCamera) ← targetNode → Player +``` + +## Map transitions + +Use `OVERWORLD.mapChange(path, destinationNodeName)` to switch maps. + +```gdscript +OVERWORLD.mapChange("res://overworld/map/SomeMap.tscn", "SpawnPoint") +``` + +Flow: fade-out begins → map loads on a background thread → when both complete, `OVERWORLD.mapChanged` fires → `OverworldScene` clears `Map` children and instances the new map → fade-in begins. The `destinationNodeName` is passed with the signal for the new map to use as a spawn point (not yet wired to player placement — see [stubs](stubs.md)). + +## Entity + +All overworld objects (player, NPCs, items, triggers) are instances of [entity/Entity.tscn](../../overworld/entity/Entity.tscn) with different export values. + +| Export | Purpose | +|---|---| +| `entityId:String` | UUID; use the Inspector button to regenerate | +| `movementType:MovementType` | `NONE` (static), `DISABLED`, or `PLAYER` (input-driven) | +| `interactType:InteractType` | What happens when the player presses Interact nearby | +| `dialogueResource:DialogueResource` | `.dialogue` file — required for `CONVERSATION` | +| `dialogueTitle:String` | Dialogue section to start from (default `"start"`) | +| `oneTimeItem:ItemResource` | Item granted on interact — required for `ONE_TIME_ITEM` | +| `cutscene:CutsceneResource` | Cutscene to run — required for `CUTSCENE` | + +### Interaction types + +| `InteractType` | Behaviour | +|---|---| +| `NONE` | Not interactable | +| `CONVERSATION` | Runs `dialogueResource` from `dialogueTitle` via `DialogueAction` | +| `ONE_TIME_ITEM` | Grants `oneTimeItem`, then frees the entity | +| `CUTSCENE` | Queues and starts `cutscene` | +| `BATTLE_TEST` | Starts a test battle (hardcoded enemy, for dev use) | + +To add a new interaction type: add a value to `Entity.InteractType`, then add the matching `match` branch in `EntityInteractableArea.onInteract()` ([entity/EntityInteractableArea.gd](../../overworld/entity/EntityInteractableArea.gd)). + +### Collision layers + +| Area | Layer | Mask | Purpose | +|---|---|---|---| +| `EntityInteractingArea` | 0 | 2 | Player's reach — detects nearby interactables | +| `EntityInteractableArea` | 2 | 0 | Entity's surface — detected by other reaches | + +The asymmetric setup means entities never trigger themselves. + +## Movement + +`EntityMovement` (a child `Node` under `Components`) handles all physics each frame: + +1. Apply gravity if airborne +2. Apply friction (`velocity.x/z *= delta * FRICTION`) +3. If `_canMove()` and `movementType == PLAYER`: read input, compute camera-relative direction, set velocity +4. `move_and_slide()` + +Movement is blocked (`_canMove() → false`) when `UI.dialogueActive`, `UI.TEXTBOX` is open, or `UI.GAME_MENU.isOpen()`. + +Camera-relative direction is derived from the active `Camera3D`'s basis — the camera's Y-zeroed and renormalized X/Z axes map input axes to world axes. The entity faces (`look_at`) the movement direction each frame. + +## Camera + +`OverworldCamera` orbits around `targetNode` using yaw/pitch angles driven by `camera_orbit_*` inputs. + +| Export | Default | Purpose | +|---|---|---| +| `targetNode:Node3D` | — | Node to orbit (assign Player in scene) | +| `pivotOffset:Vector3` | `(0, 1.2, 0)` | Orbit point above entity origin | +| `distance:float` | `10.0` | Orbit radius | +| `pitchMin/Max:float` | `-10° / 70°` | Vertical clamp | +| `orbitSensitivity:float` | `120.0` | Degrees/sec at full input | +| `collisionMask:int` | — | Layers the camera avoids (terrain = layer 1) | + +If the ray from pivot to desired camera position hits `collisionMask`, the camera is pulled in to just in front of the hit point (with `COLLISION_MARGIN = 0.3`), clamped to `minDistance`. + +## Adding a new map + +1. Create a new scene (`Node3D` root) in `overworld/map/` +2. Add `Entity` instances, set `interactType` and relevant exports in the Inspector +3. Instance `TestMapBase` (or your own terrain) as a child +4. Add a `Camera3D` with `OverworldCamera` script; set `targetNode` to the Player entity +5. Add a `Player` entity with `movementType = PLAYER` and `entityId = "player"` +6. Switch to it with `OVERWORLD.mapChange("res://overworld/map/YourMap.tscn", "")` diff --git a/.claude/docs/systems.md b/.claude/docs/systems.md index cb11a71..90e1c55 100644 --- a/.claude/docs/systems.md +++ b/.claude/docs/systems.md @@ -9,6 +9,8 @@ ## Entities (Overworld) +See [Overworld](overworld.md) for the full reference (scene structure, interaction types, camera, map transitions). + - All interactable world objects extend `Entity` (CharacterBody3D) - Interaction type set via `@export var interactType:InteractType` - Interaction routing lives in `EntityInteractableArea.onInteract()` — add new `InteractType` values there @@ -21,6 +23,8 @@ ## UI +See [UI](ui.md) for the full reference (VNTextbox, ClosableMenu, pause/debug/settings menus, AdvancedRichText, adding new menus). + - `UI.TEXTBOX.setTextAndWait(text)` — show dialogue and await player dismiss (use `await`) - Movement is blocked automatically when `UI.TEXTBOX` is visible (`EntityMovement._canMove()` checks this) - Menus extend `ClosableMenu` for open/close/toggle + `closed`/`opened` signals diff --git a/.claude/docs/ui.md b/.claude/docs/ui.md new file mode 100644 index 0000000..aeda31b --- /dev/null +++ b/.claude/docs/ui.md @@ -0,0 +1,150 @@ +# UI System + +## Scene structure + +``` +RootUI (Control, fullscreen, always visible) +├── DebugMenu +├── PauseMenu +│ ├── PauseSettings +│ └── PauseMain +├── GameMenu +│ ├── GameMenuPartyTab +│ └── GameMenuItemsTab +└── VNTextbox +``` + +`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`. + +## UI singleton + +`UI` (autoload) is the global access point. + +| Accessor | Returns | 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 | + +`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`. + +## ClosableMenu + +Base class for any togglable panel. Extends `Control`; `isOpen` drives `visible`. + +```gdscript +menu.open() # shows, emits opened +menu.close() # hides, emits closed +menu.toggle() +``` + +Signals: `opened`, `closed`. + +All new menus that need standard show/hide behaviour should extend `ClosableMenu`. + +## 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()`. + +**Structure:** +- Left sidebar: `ItemList` tab selector (Party, Items) +- Right panel: active tab content + +| Tab | Content | +|---|---| +| Party | One card per `PartyMember` — name, status, HP/MP, ATK/DEF/SPD/MAG/LCK | +| Items | One row per `ItemStack` in `PARTY.BACKPACK` — item name + quantity | + +Both tabs are populated dynamically on open; call `refresh()` on the active tab directly if data changes while the menu is already open. + +**Key methods on `GameMenu`:** + +| Method | Effect | +|---|---| +| `open()` | Shows menu, refreshes active tab, grabs sidebar focus | +| `close()` | Hides menu | +| `isOpen() -> bool` | Visibility state | + +`ui_cancel` or `menu` closes the menu. The `menu` input opens it only when `UI.dialogueActive` is false and the textbox is closed. + +To add a new tab: add a value to `GameMenu.Tab`, create a tab scene/script under `ui/gamemenu/`, instance it in `GameMenu.tscn` as a sibling of the other tabs, add an `@export` for it in `GameMenu.gd`, and add the `match` branch in `_selectTab()`. + +## Pause menu + +`PauseMenu` wraps `PauseMain` (item list) and `PauseSettings` (settings tabs). + +| Method | Effect | +|---|---| +| `PauseMenu.open()` | Shows container, opens PauseMain | +| `PauseMenu.close()` | Hides everything | +| `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. + +> **Stub:** `Pause.gd` (the singleton) has its logic commented out. Pause menu is not yet wired to actual game-pause state. + +## Settings menu + +`SettingsMenu` is a shared tabbed component instanced in both `MainMenuSettings` and `PauseSettings`. Three tabs: **Gameplay**, **Sound**, **Graphics** — currently contain placeholder labels only. + +## Debug menu + +`DebugMenu` (toggle via `debug` input — **F1**) provides four scene-jump buttons: + +| Button | Action | +|---|---| +| Overworld | `SCENE.setScene(OVERWORLD)` | +| Battle | `SCENE.setScene(BATTLE)` | +| Cooking | `SCENE.setScene(COOKING)` | +| Initial | `SCENE.setScene(INITIAL)` | + +Access via `UI.DEBUG_MENU`. Starts hidden; `isClosed` getter/setter controls visibility. + +## Theme & assets + +- Global theme: `res://ui/UI Theme.tres` — applied to all UI nodes +- Input icons: `res://ui/input/{action}.tres` — one file per action name +- Font: configured globally in project settings + +## 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` +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) diff --git a/CLAUDE.md b/CLAUDE.md index 9588236..7dd5b38 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,9 +11,13 @@ Detailed reference lives in [.claude/docs/](.claude/docs/): - [Systems](.claude/docs/systems.md) — battle, entities, items, UI conventions - [Dialogue](.claude/docs/dialogue.md) — DialogueManager integration, writing .dialogue files, DialogueAction - [Stubs](.claude/docs/stubs.md) — incomplete / placeholder systems to avoid relying on +- [Overworld](.claude/docs/overworld.md) — map transitions, Entity exports, interaction types, camera, movement +- [UI](.claude/docs/ui.md) — UI singleton, VNTextbox, ClosableMenu, pause/debug/settings menus, AdvancedRichText @.claude/docs/code-style.md @.claude/docs/architecture.md @.claude/docs/systems.md @.claude/docs/dialogue.md @.claude/docs/stubs.md +@.claude/docs/overworld.md +@.claude/docs/ui.md diff --git a/overworld/camera/OverworldCamera.gd b/overworld/camera/OverworldCamera.gd index 03e22cd..f8b6575 100644 --- a/overworld/camera/OverworldCamera.gd +++ b/overworld/camera/OverworldCamera.gd @@ -27,9 +27,10 @@ func _process(delta:float) -> void: "camera_orbit_left", "camera_orbit_right", "camera_orbit_up", "camera_orbit_down" ) - _yaw += orbitInput.x * orbitSensitivity * delta - # Invert Y so stick-up = camera rises (bird's-eye) - _pitch -= orbitInput.y * orbitSensitivity * delta + var xMult:float = -1.0 if SETTINGS.invertCameraX else 1.0 + var yMult:float = 1.0 if SETTINGS.invertCameraY else -1.0 + _yaw += orbitInput.x * orbitSensitivity * delta * xMult + _pitch += orbitInput.y * orbitSensitivity * delta * yMult _pitch = clamp(_pitch, pitchMin, pitchMax) var pivot:Vector3 = targetNode.global_transform.origin + pivotOffset diff --git a/overworld/entity/EntityMovement.gd b/overworld/entity/EntityMovement.gd index 11ee775..c1623aa 100644 --- a/overworld/entity/EntityMovement.gd +++ b/overworld/entity/EntityMovement.gd @@ -70,6 +70,8 @@ func _canMove() -> bool: return false if !UI.TEXTBOX.isClosed: return false + if UI.GAME_MENU && UI.GAME_MENU.isOpen(): + return false return true # diff --git a/project.godot b/project.godot index fed116f..20f4680 100644 --- a/project.godot +++ b/project.godot @@ -26,6 +26,7 @@ BATTLE="*res://battle/Battle.gd" PARTY="*res://party/Party.gd" COOKING="*res://cooking/Cooking.gd" SAVE="*res://save/Save.gd" +SETTINGS="*res://scene/Settings.gd" CUTSCENE="*res://cutscene/CutsceneSingleton.gd" UI="*res://ui/UISingleton.gd" ControllerIcons="*res://addons/controller_icons/ControllerIcons.gd" @@ -106,11 +107,12 @@ interact={ pause={ "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":0,"physical_keycode":4194305,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":6,"pressure":0.0,"pressed":false,"script":null) ] } debug={ "deadzone": 0.5, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194306,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194332,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } run={ @@ -118,6 +120,12 @@ run={ "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194325,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } +menu={ +"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":0,"physical_keycode":4194306,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":3,"pressure":0.0,"pressed":false,"script":null) +] +} camera_orbit_left={ "deadzone": 0.15, "events": [Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":2,"axis_value":-1.0,"script":null) diff --git a/scene/Pause.gd b/scene/Pause.gd index 1606d53..370b898 100644 --- a/scene/Pause.gd +++ b/scene/Pause.gd @@ -1,31 +1,16 @@ class_name PauseSingleton extends Node -# var cutscenePaused:bool = false +func _ready() -> void: + process_mode = Node.PROCESS_MODE_ALWAYS -# func cutscenePause() -> void: -# cutscenePaused = true - -# func cutsceneResume() -> void: -# cutscenePaused = false - -# func isMovementPaused() -> bool: -# if cutscenePaused: -# return true - -# if !UI.TEXTBOX.isClosed: -# return true - -# if UI.PAUSE.isOpen(): -# return true - -# if OVERWORLD.isMapChanging(): -# return true - -# return false - -# func menuPause() -> void: -# # if UI.PAUSE.isOpen(): -# # UI.PAUSE.close() -# # else: -# # UI.PAUSE.open() -# pass +func _unhandled_input(event:InputEvent) -> void: + if !event.is_action_pressed("pause"): + return + var menu:PauseMenu = UI.PAUSE_MENU + if menu == null: + return + if menu.isOpen(): + menu.close() + else: + menu.open() + get_viewport().set_input_as_handled() diff --git a/scene/Settings.gd b/scene/Settings.gd new file mode 100644 index 0000000..e99eb3c --- /dev/null +++ b/scene/Settings.gd @@ -0,0 +1,4 @@ +extends Node + +var invertCameraX:bool = false +var invertCameraY:bool = false diff --git a/scene/Settings.gd.uid b/scene/Settings.gd.uid new file mode 100644 index 0000000..8dc70aa --- /dev/null +++ b/scene/Settings.gd.uid @@ -0,0 +1 @@ +uid://cvrjh5g0gy883 diff --git a/ui/RootUI.gd b/ui/RootUI.gd index 976064e..9b65add 100644 --- a/ui/RootUI.gd +++ b/ui/RootUI.gd @@ -2,6 +2,8 @@ class_name RootUI extends Control @export var debugMenu:DebugMenu @export var textBox:VNTextbox +@export var gameMenu:GameMenu +@export var pauseMenu:PauseMenu func _enter_tree() -> void: UI.rootUi = self diff --git a/ui/RootUI.tscn b/ui/RootUI.tscn index 307176f..ff1dc5c 100644 --- a/ui/RootUI.tscn +++ b/ui/RootUI.tscn @@ -1,11 +1,12 @@ -[gd_scene load_steps=5 format=3 uid="uid://baos0arpiskbp"] +[gd_scene load_steps=6 format=3 uid="uid://baos0arpiskbp"] [ext_resource type="PackedScene" uid="uid://bkx3l0kckf4a8" path="res://ui/component/VNTextbox.tscn" id="1_1mtk3"] [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"] -[node name="RootUI" type="Control" node_paths=PackedStringArray("debugMenu", "textBox")] +[node name="RootUI" type="Control" node_paths=PackedStringArray("debugMenu", "textBox", "gameMenu", "pauseMenu")] layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 @@ -16,6 +17,8 @@ mouse_filter = 2 script = ExtResource("1_son71") debugMenu = NodePath("DebugMenu") textBox = NodePath("VNTextbox") +gameMenu = NodePath("GameMenu") +pauseMenu = NodePath("PauseMenu") metadata/_custom_type_script = "uid://dq3qyyayugt5l" [node name="DebugMenu" parent="." instance=ExtResource("4_u132g")] @@ -25,6 +28,11 @@ 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 [node name="VNTextbox" parent="." instance=ExtResource("1_1mtk3")] visible = false diff --git a/ui/UISingleton.gd b/ui/UISingleton.gd index 1d0d3d1..6a0a1b0 100644 --- a/ui/UISingleton.gd +++ b/ui/UISingleton.gd @@ -16,4 +16,16 @@ var TEXTBOX: get(): if rootUi && rootUi.textBox: return rootUi.textBox - return null \ No newline at end of file + return null + +var GAME_MENU: + get(): + if rootUi && rootUi.gameMenu: + return rootUi.gameMenu + return null + +var PAUSE_MENU: + get(): + if rootUi && rootUi.pauseMenu: + return rootUi.pauseMenu + return null diff --git a/ui/gamemenu/GameMenu.gd b/ui/gamemenu/GameMenu.gd new file mode 100644 index 0000000..ef0ad98 --- /dev/null +++ b/ui/gamemenu/GameMenu.gd @@ -0,0 +1,52 @@ +class_name GameMenu extends Control + +enum Tab { PARTY, ITEMS } + +@export var SIDEBAR:ItemList +@export var PARTY_TAB:GameMenuPartyTab +@export var ITEMS_TAB:GameMenuItemsTab + +var _currentTab:Tab = Tab.PARTY + +func _ready() -> void: + visible = false + SIDEBAR.item_selected.connect(_onTabSelected) + +func open() -> void: + visible = true + _selectTab(_currentTab) + SIDEBAR.select(_currentTab) + SIDEBAR.grab_focus() + +func close() -> void: + visible = false + +func isOpen() -> bool: + return visible + +func _onTabSelected(index:int) -> void: + _selectTab(index as Tab) + +func _selectTab(tab:Tab) -> void: + _currentTab = tab + PARTY_TAB.visible = (tab == Tab.PARTY) + ITEMS_TAB.visible = (tab == Tab.ITEMS) + match tab: + Tab.PARTY: + PARTY_TAB.refresh() + Tab.ITEMS: + ITEMS_TAB.refresh() + +func _unhandled_input(event:InputEvent) -> void: + if event.is_action_pressed("menu"): + if visible: + close() + elif !UI.dialogueActive && UI.TEXTBOX.isClosed: + open() + get_viewport().set_input_as_handled() + return + if !visible: + return + if event.is_action_pressed("ui_cancel"): + close() + get_viewport().set_input_as_handled() diff --git a/ui/gamemenu/GameMenu.gd.uid b/ui/gamemenu/GameMenu.gd.uid new file mode 100644 index 0000000..ae6175f --- /dev/null +++ b/ui/gamemenu/GameMenu.gd.uid @@ -0,0 +1 @@ +uid://dvqu7spul754e diff --git a/ui/gamemenu/GameMenu.tscn b/ui/gamemenu/GameMenu.tscn new file mode 100644 index 0000000..36f4060 --- /dev/null +++ b/ui/gamemenu/GameMenu.tscn @@ -0,0 +1,76 @@ +[gd_scene load_steps=5 format=3 uid="uid://bv5r2x9m4k7n1"] + +[ext_resource type="Script" uid="uid://dn8p3y6a1s5t2" path="res://ui/gamemenu/GameMenu.gd" id="1_gmgd"] +[ext_resource type="PackedScene" uid="uid://br9c7x4t1n6q2" path="res://ui/gamemenu/GameMenuPartyTab.tscn" id="2_ptab"] +[ext_resource type="PackedScene" uid="uid://bq4m7v2k9d3c1" path="res://ui/gamemenu/GameMenuItemsTab.tscn" id="3_itab"] + +[node name="GameMenu" type="Control" node_paths=PackedStringArray("SIDEBAR", "PARTY_TAB", "ITEMS_TAB")] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 0 +script = ExtResource("1_gmgd") +SIDEBAR = NodePath("MarginContainer/HBoxContainer/SidebarPanel/VBoxContainer/Sidebar") +PARTY_TAB = NodePath("MarginContainer/HBoxContainer/ContentPanel/GameMenuPartyTab") +ITEMS_TAB = NodePath("MarginContainer/HBoxContainer/ContentPanel/GameMenuItemsTab") + +[node name="ColorRect" type="ColorRect" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +color = Color(0, 0, 0, 0.75) + +[node name="MarginContainer" type="MarginContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 16 +theme_override_constants/margin_right = 16 +theme_override_constants/margin_top = 12 +theme_override_constants/margin_bottom = 12 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer"] +layout_mode = 2 +theme_override_constants/separation = 8 + +[node name="SidebarPanel" type="PanelContainer" parent="MarginContainer/HBoxContainer"] +layout_mode = 2 +custom_minimum_size = Vector2(120, 0) + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/HBoxContainer/SidebarPanel"] +layout_mode = 2 +theme_override_constants/separation = 4 + +[node name="MenuTitle" type="Label" parent="MarginContainer/HBoxContainer/SidebarPanel/VBoxContainer"] +layout_mode = 2 +horizontal_alignment = 1 +text = "MENU" + +[node name="Sidebar" type="ItemList" parent="MarginContainer/HBoxContainer/SidebarPanel/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 +auto_width = true +item_count = 2 +item_0/text = "Party" +item_1/text = "Items" + +[node name="ContentPanel" type="PanelContainer" parent="MarginContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="GameMenuPartyTab" parent="MarginContainer/HBoxContainer/ContentPanel" instance=ExtResource("2_ptab")] +layout_mode = 2 + +[node name="GameMenuItemsTab" parent="MarginContainer/HBoxContainer/ContentPanel" instance=ExtResource("3_itab")] +layout_mode = 2 +visible = false diff --git a/ui/gamemenu/GameMenuItemsTab.gd b/ui/gamemenu/GameMenuItemsTab.gd new file mode 100644 index 0000000..33cc9d0 --- /dev/null +++ b/ui/gamemenu/GameMenuItemsTab.gd @@ -0,0 +1,32 @@ +class_name GameMenuItemsTab extends Control + +func refresh() -> void: + var container = $ScrollContainer/ItemContainer + for child in container.get_children(): + child.queue_free() + + var items = PARTY.BACKPACK.items + if items.is_empty(): + var empty = Label.new() + empty.text = "No items." + container.add_child(empty) + return + + for stack in items: + container.add_child(_makeItemRow(stack)) + +func _makeItemRow(stack:ItemStack) -> Control: + var panel = PanelContainer.new() + var hbox = HBoxContainer.new() + panel.add_child(hbox) + + var nameLabel = Label.new() + nameLabel.text = Item.getItemHandle(stack.item).capitalize() + nameLabel.size_flags_horizontal = Control.SIZE_EXPAND_FILL + hbox.add_child(nameLabel) + + var qtyLabel = Label.new() + qtyLabel.text = "x%d" % stack.quantity + hbox.add_child(qtyLabel) + + return panel diff --git a/ui/gamemenu/GameMenuItemsTab.gd.uid b/ui/gamemenu/GameMenuItemsTab.gd.uid new file mode 100644 index 0000000..5752b3d --- /dev/null +++ b/ui/gamemenu/GameMenuItemsTab.gd.uid @@ -0,0 +1 @@ +uid://bmhnyuhkwti7i diff --git a/ui/gamemenu/GameMenuItemsTab.tscn b/ui/gamemenu/GameMenuItemsTab.tscn new file mode 100644 index 0000000..721dd99 --- /dev/null +++ b/ui/gamemenu/GameMenuItemsTab.tscn @@ -0,0 +1,25 @@ +[gd_scene load_steps=2 format=3 uid="uid://bq4m7v2k9d3c1"] + +[ext_resource type="Script" uid="uid://cx6h2j9r4b8w5" path="res://ui/gamemenu/GameMenuItemsTab.gd" id="1_itabgd"] + +[node name="GameMenuItemsTab" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_itabgd") + +[node name="ScrollContainer" type="ScrollContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="ItemContainer" type="VBoxContainer" parent="ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/separation = 4 diff --git a/ui/gamemenu/GameMenuPartyTab.gd b/ui/gamemenu/GameMenuPartyTab.gd new file mode 100644 index 0000000..143f2b4 --- /dev/null +++ b/ui/gamemenu/GameMenuPartyTab.gd @@ -0,0 +1,68 @@ +class_name GameMenuPartyTab extends Control + +func refresh() -> void: + var list = $ScrollContainer/MemberList + for child in list.get_children(): + child.queue_free() + for member in PARTY.getFullParty(): + list.add_child(_makeMemberCard(member)) + +func _makeMemberCard(member:PartyMember) -> Control: + var panel = PanelContainer.new() + var margin = MarginContainer.new() + panel.add_child(margin) + margin.add_theme_constant_override("margin_left", 8) + margin.add_theme_constant_override("margin_right", 8) + margin.add_theme_constant_override("margin_top", 6) + margin.add_theme_constant_override("margin_bottom", 6) + + var vbox = VBoxContainer.new() + margin.add_child(vbox) + + # Name + status row + var headerRow = HBoxContainer.new() + vbox.add_child(headerRow) + + var nameLabel = Label.new() + nameLabel.text = member.name + nameLabel.size_flags_horizontal = Control.SIZE_EXPAND_FILL + headerRow.add_child(nameLabel) + + var statusLabel = Label.new() + statusLabel.text = "DEAD" if member.status == BattleFighter.Status.DEAD else "OK" + headerRow.add_child(statusLabel) + + # HP row + var hpRow = HBoxContainer.new() + hpRow.add_theme_constant_override("separation", 6) + vbox.add_child(hpRow) + var hpKey = Label.new() + hpKey.text = "HP" + hpKey.custom_minimum_size = Vector2(30, 0) + hpRow.add_child(hpKey) + var hpVal = Label.new() + hpVal.text = "%d / %d" % [member.health, member.maxHealth] + hpRow.add_child(hpVal) + + # MP row + var mpRow = HBoxContainer.new() + mpRow.add_theme_constant_override("separation", 6) + vbox.add_child(mpRow) + var mpKey = Label.new() + mpKey.text = "MP" + mpKey.custom_minimum_size = Vector2(30, 0) + mpRow.add_child(mpKey) + var mpVal = Label.new() + mpVal.text = "%d / %d" % [member.mp, member.maxMp] + mpRow.add_child(mpVal) + + # Stats row + var statsRow = HBoxContainer.new() + statsRow.add_theme_constant_override("separation", 14) + vbox.add_child(statsRow) + for pair in [["ATK", member.attack], ["DEF", member.defense], ["SPD", member.speed], ["MAG", member.magic], ["LCK", member.luck]]: + var label = Label.new() + label.text = "%s %d" % [pair[0], pair[1]] + statsRow.add_child(label) + + return panel diff --git a/ui/gamemenu/GameMenuPartyTab.gd.uid b/ui/gamemenu/GameMenuPartyTab.gd.uid new file mode 100644 index 0000000..b534b73 --- /dev/null +++ b/ui/gamemenu/GameMenuPartyTab.gd.uid @@ -0,0 +1 @@ +uid://bfklpf0grikg5 diff --git a/ui/gamemenu/GameMenuPartyTab.tscn b/ui/gamemenu/GameMenuPartyTab.tscn new file mode 100644 index 0000000..6a7eae0 --- /dev/null +++ b/ui/gamemenu/GameMenuPartyTab.tscn @@ -0,0 +1,25 @@ +[gd_scene load_steps=2 format=3 uid="uid://br9c7x4t1n6q2"] + +[ext_resource type="Script" uid="uid://dw3n5k8m2p1a7" path="res://ui/gamemenu/GameMenuPartyTab.gd" id="1_ptabgd"] + +[node name="GameMenuPartyTab" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_ptabgd") + +[node name="ScrollContainer" type="ScrollContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="MemberList" type="VBoxContainer" parent="ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/separation = 6 diff --git a/ui/pause/PauseMain.gd b/ui/pause/PauseMain.gd index eb568d5..d360092 100644 --- a/ui/pause/PauseMain.gd +++ b/ui/pause/PauseMain.gd @@ -1,19 +1,25 @@ class_name PauseMain extends VBoxContainer +signal resumeRequested +signal settingsRequested +signal quitRequested + +@export var btnResume:Button +@export var btnSettings:Button +@export var btnQuit:Button + func _ready() -> void: visible = false - $HBoxContainer/ItemList.item_selected.connect(onItemSelected) + btnResume.pressed.connect(resumeRequested.emit) + btnSettings.pressed.connect(settingsRequested.emit) + btnQuit.pressed.connect(quitRequested.emit) func open() -> void: visible = true - $HBoxContainer/ItemList.clear() - $HBoxContainer/ItemList.grab_focus() + btnResume.grab_focus() func close() -> void: visible = false func isOpen() -> bool: return visible - -func onItemSelected(index:int) -> void: - print("Selected item index: ", index) diff --git a/ui/pause/PauseMain.tscn b/ui/pause/PauseMain.tscn index 9663100..a7c046b 100644 --- a/ui/pause/PauseMain.tscn +++ b/ui/pause/PauseMain.tscn @@ -2,51 +2,33 @@ [ext_resource type="Script" uid="uid://c7kvg0jw6w340" path="res://ui/pause/PauseMain.gd" id="1_b5xfl"] -[node name="PauseMain" type="VBoxContainer"] -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 +[node name="PauseMain" type="VBoxContainer" node_paths=PackedStringArray("btnResume", "btnSettings", "btnQuit")] +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 script = ExtResource("1_b5xfl") metadata/_custom_type_script = "uid://c7kvg0jw6w340" +btnResume = NodePath("Resume") +btnSettings = NodePath("Settings") +btnQuit = NodePath("Quit") -[node name="HBoxContainer" type="HBoxContainer" parent="."] +[node name="Title" type="Label" parent="."] layout_mode = 2 -size_flags_vertical = 3 +text = "Paused" +horizontal_alignment = 1 -[node name="PanelContainer" type="PanelContainer" parent="HBoxContainer"] +[node name="Resume" type="Button" parent="."] layout_mode = 2 -size_flags_horizontal = 3 +text = "Resume" -[node name="ItemList" type="ItemList" parent="HBoxContainer"] +[node name="Settings" type="Button" parent="."] layout_mode = 2 -auto_width = true -item_count = 6 -item_0/text = "Equpiment" -item_1/text = "Abilities" -item_2/text = "Items" -item_3/text = "Quests" -item_4/text = "Settings" -item_5/text = "Save" +text = "Settings" -[node name="HBoxContainer2" type="HBoxContainer" parent="."] +[node name="Quit" type="Button" parent="."] layout_mode = 2 - -[node name="PanelContainer" type="PanelContainer" parent="HBoxContainer2"] -layout_mode = 2 -size_flags_horizontal = 3 - -[node name="HBoxContainer" type="GridContainer" parent="HBoxContainer2/PanelContainer"] -layout_mode = 2 -size_flags_horizontal = 8 -theme_override_constants/h_separation = 8 -columns = 2 - -[node name="Money" type="Label" parent="HBoxContainer2/PanelContainer/HBoxContainer"] -layout_mode = 2 -text = "%MONEY%" - -[node name="Playtime" type="Label" parent="HBoxContainer2/PanelContainer/HBoxContainer"] -layout_mode = 2 -text = "%PLAYTIME%" +text = "Quit Game" diff --git a/ui/pause/PauseMenu.gd b/ui/pause/PauseMenu.gd index e06e97c..db6406f 100644 --- a/ui/pause/PauseMenu.gd +++ b/ui/pause/PauseMenu.gd @@ -1,29 +1,38 @@ class_name PauseMenu extends Control @export var MAIN:PauseMain -@export var SETTINGS:PauseSettings +@export var settingsPanel:PauseSettings func _ready() -> void: close() + MAIN.resumeRequested.connect(close) + MAIN.settingsRequested.connect(_openSettings) + MAIN.quitRequested.connect(func(): get_tree().quit()) func isOpen() -> bool: return visible func open() -> void: visible = true + get_tree().paused = true MAIN.open() func close() -> void: + get_tree().paused = false visible = false MAIN.close() - SETTINGS.close() + settingsPanel.close() + +func _openSettings() -> void: + MAIN.close() + settingsPanel.open() func _unhandled_input(event:InputEvent) -> void: if !visible: return if event.is_action_pressed("ui_cancel"): - if SETTINGS.isOpen(): - SETTINGS.close() + if settingsPanel.isOpen(): + settingsPanel.close() MAIN.open() else: close() diff --git a/ui/pause/PauseMenu.tscn b/ui/pause/PauseMenu.tscn index 4e8131d..c071ed8 100644 --- a/ui/pause/PauseMenu.tscn +++ b/ui/pause/PauseMenu.tscn @@ -4,7 +4,7 @@ [ext_resource type="Script" uid="uid://cgvf34t5qgwbm" path="res://ui/pause/PauseMenu.gd" id="1_82qxy"] [ext_resource type="PackedScene" uid="uid://qgk5trrh6dfd" path="res://ui/pause/PauseSettings.tscn" id="2_3djnw"] -[node name="PauseMenu" type="Control" node_paths=PackedStringArray("MAIN", "SETTINGS")] +[node name="PauseMenu" type="Control" node_paths=PackedStringArray("MAIN", "settingsPanel")] layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 @@ -13,7 +13,7 @@ grow_horizontal = 2 grow_vertical = 2 script = ExtResource("1_82qxy") MAIN = NodePath("PauseMain") -SETTINGS = NodePath("PauseSettings") +settingsPanel = NodePath("PauseSettings") [node name="PauseSettings" parent="." instance=ExtResource("2_3djnw")] visible = false diff --git a/ui/settings/SettingsMenu.gd b/ui/settings/SettingsMenu.gd index fb10da3..0fb7096 100644 --- a/ui/settings/SettingsMenu.gd +++ b/ui/settings/SettingsMenu.gd @@ -2,9 +2,15 @@ class_name SettingsMenu extends Control @export var tabs:TabBar @export var tabControls:Array[Control] +@export var checkInvertX:CheckBox +@export var checkInvertY:CheckBox func _ready() -> void: tabs.tab_changed.connect(onTabChanged) + checkInvertX.button_pressed = SETTINGS.invertCameraX + checkInvertY.button_pressed = SETTINGS.invertCameraY + checkInvertX.toggled.connect(func(v:bool): SETTINGS.invertCameraX = v) + checkInvertY.toggled.connect(func(v:bool): SETTINGS.invertCameraY = v) onTabChanged(tabs.current_tab) func _notification(what:int) -> void: @@ -16,3 +22,10 @@ func onTabChanged(tabIndex:int) -> void: control.visible = false if tabIndex >= 0 and tabIndex < tabControls.size(): tabControls[tabIndex].visible = true + _focusFirstIn(tabControls[tabIndex]) + +func _focusFirstIn(container:Control) -> void: + for child in container.get_children(): + if child is Control and child.focus_mode != Control.FOCUS_NONE: + child.grab_focus() + return diff --git a/ui/settings/SettingsMenu.tscn b/ui/settings/SettingsMenu.tscn index 5ca34da..a9b2566 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")] +[node name="SettingsMenu" type="Control" node_paths=PackedStringArray("tabs", "tabControls", "checkInvertX", "checkInvertY")] layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 @@ -11,7 +11,9 @@ 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")] +tabControls = [NodePath("VBoxContainer/ScrollContainer/LabelGameplay"), NodePath("VBoxContainer/ScrollContainer/LabelSound"), NodePath("VBoxContainer/ScrollContainer/LabelGraphics"), NodePath("VBoxContainer/ScrollContainer/PanelControls")] +checkInvertX = NodePath("VBoxContainer/ScrollContainer/PanelControls/CheckInvertX") +checkInvertY = NodePath("VBoxContainer/ScrollContainer/PanelControls/CheckInvertY") [node name="VBoxContainer" type="VBoxContainer" parent="."] layout_mode = 1 @@ -24,10 +26,11 @@ grow_vertical = 2 [node name="TabBar" type="TabBar" parent="VBoxContainer"] layout_mode = 2 current_tab = 0 -tab_count = 3 +tab_count = 4 tab_0/title = "Gameplay" tab_1/title = "Sound" tab_2/title = "Graphics" +tab_3/title = "Controls" [node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer"] layout_mode = 2 @@ -47,3 +50,21 @@ text = "Sound" visible = false layout_mode = 2 text = "Gameplay" + +[node name="PanelControls" type="VBoxContainer" parent="VBoxContainer/ScrollContainer"] +visible = false +layout_mode = 2 + +[node name="LabelController" type="Label" parent="VBoxContainer/ScrollContainer/PanelControls"] +layout_mode = 2 +text = "Controller" + +[node name="CheckInvertX" type="CheckBox" parent="VBoxContainer/ScrollContainer/PanelControls"] +layout_mode = 2 +focus_mode = 2 +text = "Invert Camera X" + +[node name="CheckInvertY" type="CheckBox" parent="VBoxContainer/ScrollContainer/PanelControls"] +layout_mode = 2 +focus_mode = 2 +text = "Invert Camera Y"