-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathoptions_menu.gd
More file actions
432 lines (368 loc) · 14.2 KB
/
options_menu.gd
File metadata and controls
432 lines (368 loc) · 14.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
## Copyright (C) 2025 Egor Kostan
## SPDX-License-Identifier: GPL-3.0-or-later
## Options Menu Script
##
## Handles log level selection, difficulty adjustment, and back navigation
## in the options menu.
##
## Supports web overlays for UI interactions and ignores pause mode.
##
## :vartype log_level_display_to_enum: Dictionary
## :vartype log_lvl_option: OptionButton
## :vartype back_button: Button
## :vartype difficulty_slider: HSlider
## :vartype difficulty_label: Label
class_name OptionsMenu
extends CanvasLayer
## The wrappers (like JavaScriptBridgeWrapper and presumably OSWrapper)
## are designed to abstract away direct singleton calls, making the code
## easier to unit test by allowing mocks/stubs without relying on the
## actual engine singletons.
var js_bridge_wrapper: JavaScriptBridgeWrapper = JavaScriptBridgeWrapper.new()
var os_wrapper: OSWrapper = OSWrapper.new() # Assuming OSWrapper is defined similarly
var audio_scene: PackedScene = preload("res://scenes/audio_settings.tscn")
var controls_scene: PackedScene = preload("res://scenes/key_mapping_menu.tscn")
var advanced_scene: PackedScene = preload("res://scenes/advanced_settings.tscn")
var gameplay_settings_scene: PackedScene = preload("res://scenes/gameplay_settings.tscn")
var _options_back_button_pressed_cb: JavaScriptObject
var _controls_pressed_cb: JavaScriptObject
var _audio_pressed_cb: JavaScriptObject
var _advanced_pressed_cb: JavaScriptObject
var _gameplay_settings_pressed_cb: JavaScriptObject
var _torn_down: bool = false # Guard against multiple teardown calls
@onready var options_back_button: Button = $Panel/OptionsVBoxContainer/OptionsBackButton
@onready var audio_settings_button: Button = $Panel/OptionsVBoxContainer/AudioSettingsButton
@onready var key_mapping_button: Button = $Panel/OptionsVBoxContainer/KeyMappingButton
@onready var gameplay_settings_button: Button = $Panel/OptionsVBoxContainer/GameplaySettingsButton
@onready var advanced_settings_button: Button = $Panel/OptionsVBoxContainer/AdvancedSettingsButton
@onready var version_label: Label = $Panel/OptionsVBoxContainer/VersionLabel
@onready var options_vbox: VBoxContainer = $Panel/OptionsVBoxContainer
func _ready() -> void:
## Initializes the options menu when the node enters the scene tree.
##
## Populates the log level options, sets initial values, connects signals,
## and configures process mode.
##
## Toggles web overlays to visible layout (but still invisible visually) if on web.
## Exposes functions to JS for web overlays if on web.
##
## :rtype: void
# Game version
version_label.text = "Version: " + Globals.get_game_version()
Globals.log_message("Updated label to: " + version_label.text, Globals.LogLevel.DEBUG)
if not options_back_button.pressed.is_connected(_on_options_back_button_pressed):
options_back_button.pressed.connect(_on_options_back_button_pressed)
if not key_mapping_button.pressed.is_connected(_on_key_mapping_button_pressed):
key_mapping_button.pressed.connect(_on_key_mapping_button_pressed)
if not audio_settings_button.pressed.is_connected(_on_audio_settings_button_pressed):
audio_settings_button.pressed.connect(_on_audio_settings_button_pressed)
if not gameplay_settings_button.pressed.is_connected(_on_gameplay_settings_button_pressed):
gameplay_settings_button.pressed.connect(_on_gameplay_settings_button_pressed)
if not advanced_settings_button.pressed.is_connected(_on_advanced_settings_button_pressed):
advanced_settings_button.pressed.connect(_on_advanced_settings_button_pressed)
# Configure for web overlays (invisible but positioned)
process_mode = Node.PROCESS_MODE_ALWAYS # Ignore pause
Globals.log_message("Options menu loaded.", Globals.LogLevel.DEBUG)
if os_wrapper.has_feature("web"):
# Toggle overlays...
(
js_bridge_wrapper
. eval(
"""
document.getElementById('controls-button').style.display = 'block';
document.getElementById('audio-button').style.display = 'block';
document.getElementById('advanced-button').style.display = 'block';
document.getElementById('gameplay-button').style.display = 'block';
document.getElementById('options-back-button').style.display = 'block';
""",
true
)
)
# Expose callbacks to JS (store refs to prevent GC)
var js_window: JavaScriptObject = js_bridge_wrapper.get_interface("window")
if js_window:
_options_back_button_pressed_cb = js_bridge_wrapper.create_callback(
Callable(self, "_on_options_back_button_pressed_js")
)
js_window.optionsBackPressed = _options_back_button_pressed_cb
_controls_pressed_cb = js_bridge_wrapper.create_callback(
Callable(self, "_on_controls_pressed_js")
)
js_window.controlsPressed = _controls_pressed_cb
_audio_pressed_cb = js_bridge_wrapper.create_callback(
Callable(self, "_on_audio_pressed_js")
)
js_window.audioPressed = _audio_pressed_cb
_advanced_pressed_cb = js_bridge_wrapper.create_callback(
Callable(self, "_on_advanced_pressed_js")
)
js_window.advancedPressed = _advanced_pressed_cb
_gameplay_settings_pressed_cb = js_bridge_wrapper.create_callback(
Callable(self, "_on_gameplay_settings_pressed_js")
)
js_window.gameplayPressed = _gameplay_settings_pressed_cb
Globals.log_message(
"Exposed options menu callbacks to JS for web overlays.", Globals.LogLevel.DEBUG
)
_torn_down = false # Reset guard on ready
_grab_first_button_focus() # Dynamically grab focus on the first button
## Called when returning from the Key Mapping menu.
## Focuses the Key Mapping button using the same safe helper as everywhere else.
func grab_focus_on_key_mapping_button() -> void:
Globals.ensure_initial_focus(
key_mapping_button,
[
advanced_settings_button,
audio_settings_button,
key_mapping_button,
gameplay_settings_button,
options_back_button
],
"Options Menu (returned from Key Mapping)"
)
## Called when returning from the Gameplay menu.
## Focuses the Gameplay Settings button using the same safe helper as everywhere else.
func grab_focus_on_gameplay_settings_button() -> void:
Globals.ensure_initial_focus(
gameplay_settings_button,
[
advanced_settings_button,
audio_settings_button,
key_mapping_button,
gameplay_settings_button,
options_back_button
],
"Options Menu (returned from Gameplay)"
)
## Called when returning from the Audio Settings menu.
## Focuses the Audio Settings button using the same safe helper as everywhere else.
func grab_focus_on_audio_settings_button() -> void:
Globals.ensure_initial_focus(
audio_settings_button,
[
advanced_settings_button,
audio_settings_button,
key_mapping_button,
gameplay_settings_button,
options_back_button
],
"Options Menu (returned from Audio Options)"
)
func _grab_first_button_focus() -> void:
## Finds the first visible/enabled Button in the container and hands it
## to the centralized focus helper. The helper will decide whether to
## actually grab focus or skip (and log accordingly).
var candidate: Button = null
for child in options_vbox.get_children():
if child is Button and child.visible and not child.disabled:
candidate = child
break
if candidate:
Globals.ensure_initial_focus(
candidate,
[
advanced_settings_button,
audio_settings_button,
key_mapping_button,
gameplay_settings_button,
options_back_button
],
"Options Menu"
)
else:
Globals.log_message(
"No Button found in OptionsVBoxContainer for initial focus!", Globals.LogLevel.WARNING
)
func _input(event: InputEvent) -> void:
## Handles input events for the options menu.
##
## Logs mouse click positions for debugging.
##
## :param event: The input event to process.
## :type event: InputEvent
## :rtype: void
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
var pos: Vector2 = event.position # Explicitly type as Vector2
Globals.log_message("Clicked at: (%s, %s)" % [pos.x, pos.y], Globals.LogLevel.DEBUG)
func _teardown() -> void:
## Cleans up on options close.
##
## Shows previous menu from stack, resets globals,
## and restores focus to the Options button if it's a menu with the group.
##
## :rtype: void
if _torn_down:
return
_torn_down = true
if not Globals.hidden_menus.is_empty():
var prev_menu: Node = Globals.hidden_menus.pop_back()
if is_instance_valid(prev_menu):
prev_menu.visible = true
Globals.log_message("Showing menu: " + prev_menu.name, Globals.LogLevel.DEBUG)
# Unified check using the "MenuWithOptions" group you added in Editor
if prev_menu.is_in_group("MenuWithOptions"):
# Log context for debug (pause vs main, using node name)
var log_context: String = (
"from " + ("PAUSE menu" if prev_menu.name == "PauseMenu" else "MAIN menu")
)
_grab_options_focus(prev_menu, log_context)
else:
Globals.log_message(
"No MenuWithOptions group on " + prev_menu.name + " - skipping focus grab",
Globals.LogLevel.DEBUG
)
Globals.options_open = false
Globals.options_instance = null
Globals.log_message("Options menu exited.", Globals.LogLevel.DEBUG)
func _grab_options_focus(menu_node: Node, log_context: String) -> void:
## Helper to grab focus on OptionsButton.
##
## Validates and logs for easy debugging.
##
## :param menu_node: Menu node with the button.
## :type menu_node: Node
## :param log_context: Debug context (e.g., "from MAIN menu").
## :type log_context: String
## :rtype: void
var options_btn: Button = menu_node.get_node_or_null("VBoxContainer/OptionsButton")
if is_instance_valid(options_btn):
options_btn.call_deferred("grab_focus") # Deferred to ensure after visibility change
Globals.log_message(
"Grabbed focus on OPTIONS button " + log_context + "...", Globals.LogLevel.DEBUG
)
else:
Globals.log_message(
"OptionsButton not found in " + menu_node.name + " - check path or scene structure!",
Globals.LogLevel.ERROR
)
func _exit_tree() -> void:
## Handles node exit from scene tree.
##
## Restores hidden menu, clears flags/refs, logs exit.
##
## :rtype: void
_teardown() # Centralized cleanup
Globals.log_message("Options menu exited.", Globals.LogLevel.DEBUG)
# New: JS callback for audio button
# warning-ignore:unused_argument
func _on_audio_pressed_js(_args: Array) -> void:
## JS callback for audio button press.
##
## Routes to signal handler.
##
## :param _args: Unused array from JS.
## :type _args: Array
## :rtype: void
_on_audio_settings_button_pressed()
# New: JS callback for advanced button
# warning-ignore:unused_argument
func _on_advanced_pressed_js(_args: Array) -> void:
## JS callback for advanced button press.
##
## Routes to signal handler.
##
## :param _args: Unused array from JS.
## :type _args: Array
## :rtype: void
_on_advanced_settings_button_pressed()
# New: JS callback for gameplay settings button
# warning-ignore:unused_argument
func _on_gameplay_settings_pressed_js(_args: Array) -> void:
## JS callback for gameplay settings button press.
##
## Routes to signal handler.
##
## :param _args: Unused array from JS.
## :type _args: Array
## :rtype: void
_on_gameplay_settings_button_pressed()
# New: JS callback for controls button
# warning-ignore:unused_argument
func _on_controls_pressed_js(_args: Array) -> void:
## JS callback for controls button press.
##
## Routes to signal handler.
##
## :param _args: Unused array from JS.
## :type _args: Array
## :rtype: void
_on_key_mapping_button_pressed()
# Change: Signal handler (no arg)
func _on_options_back_button_pressed() -> void:
## Handles the Back button press from the signal.
##
## Shows hidden menu if valid, logs the action, removes the options menu,
## and hides web overlays if on web.
##
## :rtype: void
Globals.log_message("Options Back button pressed.", Globals.LogLevel.DEBUG)
_teardown() # Centralized cleanup
# Hide options overlays after closing menu
_hide_web_overlays()
Globals.options_open = false # Reset flag first
Globals.options_instance = null # Optional: Clear ref
queue_free()
# New: JS-specific callback (exactly one Array arg, no default)
func _on_options_back_button_pressed_js(args: Array) -> void:
## JS callback for back button press.
##
## Routes to the signal handler (ignores args).
##
## :param args: Array (unused, from JS).
## :type args: Array
## :rtype: void
Globals.log_message(
"JS back_pressed callback called with args: " + str(args), Globals.LogLevel.DEBUG
)
_on_options_back_button_pressed()
func _hide_web_overlays() -> void:
## Hides web overlays for options menu buttons.
##
## Executes JS to set display none for specific elements.
##
## :rtype: void
if os_wrapper.has_feature("web") and js_bridge_wrapper:
(
js_bridge_wrapper
. eval(
"""
document.getElementById('audio-button').style.display = 'none';
document.getElementById('advanced-button').style.display = 'none';
document.getElementById('controls-button').style.display = 'none';
document.getElementById('gameplay-button').style.display = 'none';
document.getElementById('options-back-button').style.display = 'none';
""",
true
)
)
func _open_sub_menu(scene: PackedScene, log_msg: String) -> void:
## Opens a sub-menu by instantiating and adding the scene.
##
## Logs the action, adds instance to root, hides current menu,
## and hides web overlays if applicable.
##
## :param scene: The PackedScene to instantiate.
## :type scene: PackedScene
## :param log_msg: The message to log.
## :type log_msg: String
## :rtype: void
Globals.log_message(log_msg, Globals.LogLevel.DEBUG)
var instance: Node = scene.instantiate()
get_tree().root.add_child(instance)
Globals.hidden_menus.push_back(self)
self.visible = false
_hide_web_overlays()
## Handles Audio button press.
## Hides options menu, loads audio settings.
## :rtype: void
func _on_audio_settings_button_pressed() -> void:
_open_sub_menu(audio_scene, "Audio button pressed.")
## Handles Controls button press.
## Hides options menu, loads Key Mappings settings.
## :rtype: void
func _on_key_mapping_button_pressed() -> void:
_open_sub_menu(controls_scene, "Controls button pressed.")
func _on_advanced_settings_button_pressed() -> void:
_open_sub_menu(advanced_scene, "Advanced Settings button pressed.")
func _on_gameplay_settings_button_pressed() -> void:
_open_sub_menu(gameplay_settings_scene, "Gameplay Settings button pressed.")