feat: Implement powerup inventory UI, settings menu, and foundational managers for input and game settings.

This commit is contained in:
Yogi Wiguna
2026-03-10 12:52:53 +08:00
parent da7192ac07
commit e1e928389c
12 changed files with 723 additions and 98 deletions
+43 -41
View File
@@ -3,6 +3,7 @@ extends Node
var player: Node3D
var movement_manager: Node
var race_manager: Node
@onready var SettingsManager = get_node_or_null("/root/SettingsManager")
func initialize(p_player: Node3D, p_movement_manager: Node, p_race_manager: Node):
player = p_player
@@ -87,48 +88,49 @@ func handle_unhandled_input(event):
var mode = LobbyManager.get_game_mode()
var is_sng = mode == GameMode.Mode.STOP_N_GO
match event.keycode:
KEY_KP_1, KEY_1, KEY_KP_2, KEY_2, KEY_KP_3, KEY_3, KEY_KP_4, KEY_4:
if is_sng:
# Stop n Go Mapping: 1-Speed, 2-Ghost
match event.keycode:
KEY_KP_1, KEY_1:
player.activate_powerup(0) # FASTER_SPEED
KEY_KP_2, KEY_2:
player.activate_powerup(3) # INVISIBLE_MODE
else:
# Unified Mapping for all modes: 1-Speed, 2-Wall, 3-Freeze, 4-Ghost
match event.keycode:
KEY_KP_1, KEY_1:
player.activate_powerup(0) # FASTER_SPEED
KEY_KP_2, KEY_2:
player.activate_powerup(2) # BLOCK_FLOOR
KEY_KP_3, KEY_3:
player.activate_powerup(1) # AREA_FREEZE
KEY_KP_4, KEY_4:
player.activate_powerup(3) # INVISIBLE_MODE
# KEY_R:
# player.auto_put_item()
KEY_Q:
if player.powerup_manager:
# Attack Mode (formerly Special)
# Now we want "Straight to Attack Mode" style
player.powerup_manager.use_special_effect()
# Force visual update / mutual exclusivity manually if powerup manager doesn't do it yet
if player.is_attack_mode and player.has_method("enter_attack_mode"):
# Re-triggering enter_attack_mode might be redundant but safely ensures visuals/Knock=False
player.enter_attack_mode()
# Get dynamic keybinds
var key_p1 = SettingsManager.get_control_keycode("powerup_1")
var key_p2 = SettingsManager.get_control_keycode("powerup_2")
var key_p3 = SettingsManager.get_control_keycode("powerup_3")
var key_p4 = SettingsManager.get_control_keycode("powerup_4")
var ek = event.keycode
# Unified check for PowerUp keys
if ek == key_p1:
player.activate_powerup(0) # FASTER_SPEED
elif ek == key_p2:
if is_sng:
player.activate_powerup(3) # Ghost for SNG P2
else:
player.activate_powerup(2) # Wall for Normal P2
elif ek == key_p3 and not is_sng:
player.activate_powerup(1) # AREA_FREEZE
elif ek == key_p4 and not is_sng:
player.activate_powerup(3) # INVISIBLE_MODE
# Non-Remappable variants (Numpad) - Optional fallback
elif ek == KEY_KP_1:
player.activate_powerup(0)
elif ek == KEY_KP_2:
player.activate_powerup(3 if is_sng else 2)
# Action Buttons (Remappable)
elif ek == SettingsManager.get_control_keycode("attack_mode"):
if player.powerup_manager:
player.powerup_manager.use_special_effect()
if player.is_attack_mode and player.has_method("enter_attack_mode"):
player.enter_attack_mode()
KEY_E:
if player.is_carrying_tekton and player.powerup_manager:
# Spawn Boost (Now uses Tekton)
if player.powerup_manager.has_method("spawn_boost_reward"):
player.powerup_manager.spawn_boost_reward()
KEY_G:
if not player.is_carrying_tekton and player.powerup_manager:
if player.powerup_manager.can_use_special():
player.grab_tekton()
elif ek == SettingsManager.get_control_keycode("spawn_boost"):
if player.is_carrying_tekton and player.powerup_manager:
if player.powerup_manager.has_method("spawn_boost_reward"):
player.powerup_manager.spawn_boost_reward()
elif ek == SettingsManager.get_control_keycode("tekton_grab"):
if not player.is_carrying_tekton and player.powerup_manager:
if player.powerup_manager.can_use_special():
player.grab_tekton()
# Handle spawn point selection if not yet selected
+100
View File
@@ -0,0 +1,100 @@
extends Node
# SettingsManager - Global singleton for handling game settings persistence
# Autoloaded as "SettingsManager"
const SETTINGS_FILE = "user://game_settings.cfg"
# Default values
var settings = {
"video": {
"fullscreen": true,
"vsync": true,
"resolution_idx": 1
},
"audio": {
"master_volume": 0.8,
"music_volume": 0.7,
"sfx_volume": 0.9
},
"controls": {
"powerup_1": KEY_1,
"powerup_2": KEY_2,
"powerup_3": KEY_3,
"powerup_4": KEY_4,
"grab": KEY_SPACE,
"put": KEY_R,
"attack_mode": KEY_Q,
"spawn_boost": KEY_E,
"tekton_grab": KEY_G
}
}
signal settings_applied
signal control_remapped(action: String, key: int)
func _ready():
load_settings()
apply_all_settings()
func load_settings():
var config = ConfigFile.new()
var err = config.load(SETTINGS_FILE)
if err == OK:
for section in settings.keys():
for key in settings[section].keys():
settings[section][key] = config.get_value(section, key, settings[section][key])
print("[Settings] Loaded.")
else:
print("[Settings] Using defaults.")
func save_settings():
var config = ConfigFile.new()
for section in settings.keys():
for key in settings[section].keys():
config.set_value(section, key, settings[section][key])
config.save(SETTINGS_FILE)
func apply_all_settings():
apply_video_settings()
apply_audio_settings()
emit_signal("settings_applied")
func apply_video_settings():
var video = settings.video
if video.fullscreen:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
else:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
if video.vsync:
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ENABLED)
else:
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
func apply_audio_settings():
var audio = settings.audio
set_bus_volume("Master", audio.master_volume)
set_bus_volume("Music", audio.music_volume)
set_bus_volume("SFX", audio.sfx_volume)
func set_bus_volume(bus_name: String, volume_linear: float):
var bus_idx = AudioServer.get_bus_index(bus_name)
if bus_idx != -1:
AudioServer.set_bus_volume_db(bus_idx, linear_to_db(volume_linear))
AudioServer.set_bus_mute(bus_idx, volume_linear <= 0.001)
func set_control(action_name: String, keycode: int):
if settings.controls.has(action_name):
settings.controls[action_name] = keycode
emit_signal("control_remapped", action_name, keycode)
save_settings()
func get_control_keycode(action_name: String) -> int:
return settings.controls.get(action_name, -1)
func get_control_text(action_name: String) -> String:
var code = get_control_keycode(action_name)
if code == -1: return "Unbound"
return OS.get_keycode_string(code)
+1
View File
@@ -0,0 +1 @@
uid://c1ouaaqnn0lrc
+56 -31
View File
@@ -142,11 +142,26 @@ func _create_touch_ui():
tekton_grab_button = _find_or_create_action_button(actions_container, "TektonGrab", "👋", Vector2(-280, -80))
# Order: AttackMode, SpawnBoost, Grab
if attack_mode_button: actions_container.move_child(attack_mode_button, 0)
if spawn_boost_button: actions_container.move_child(spawn_boost_button, 1)
if grab_button: actions_container.move_child(grab_button, 2)
if tekton_grab_button: actions_container.move_child(tekton_grab_button, 3)
# Order: AttackMode, SpawnBoost, Grab, TektonGrab
if attack_mode_button:
actions_container.move_child(attack_mode_button, 0)
attack_mode_button.icon = load("res://assets/graphics/touch_control/attack_mode.png")
attack_mode_button.expand_icon = true
if spawn_boost_button:
actions_container.move_child(spawn_boost_button, 1)
spawn_boost_button.icon = load("res://assets/graphics/touch_control/spawn_tile.png")
spawn_boost_button.expand_icon = true
if grab_button:
actions_container.move_child(grab_button, 2)
grab_button.icon = load("res://assets/graphics/touch_control/take_tile.png")
grab_button.expand_icon = true
if tekton_grab_button:
actions_container.move_child(tekton_grab_button, 3)
tekton_grab_button.icon = load("res://assets/graphics/touch_control/grab_tekton.png")
tekton_grab_button.expand_icon = true
# Hide Put Button
if put_button:
@@ -391,6 +406,7 @@ func _load_settings():
var err = config.load(CONFIG_PATH)
if err != OK:
print("[TouchControls] No saved settings found, using defaults")
_apply_settings() # Apply default settings immediately
return
# Load settings values
@@ -439,46 +455,55 @@ func _apply_settings():
# Apply joystick visibility
if virtual_joystick:
virtual_joystick.visible = joystick_enabled
# Apply touch buttons visibility - FORCED ON per request to "just show them"
if actions_container:
actions_container.visible = true
actions_container.set_anchors_preset(Control.PRESET_FULL_RECT)
actions_container.mouse_filter = Control.MOUSE_FILTER_PASS
# Apply touch buttons visibility - dependent on master joystick_enabled switch
# If joystick is disabled, ALL touch controls are hidden
# Note: We ignore touch_buttons_enabled here to ensure "Enable Virtual Joystick" shows EVERYTHING as requested
var buttons_visible = joystick_enabled
var buttons_visible = true
print("[TouchControls] Applying settings: JoystickEnabled=", joystick_enabled, " ButtonsVisible=", buttons_visible)
print("[TouchControls] Applying settings: ButtonsVisible=", buttons_visible)
if grab_button:
grab_button.visible = buttons_visible
grab_button.visible = false
grab_button.scale = Vector2(button_scale, button_scale)
# Use offsets for anchored controls, not position
# grab_button.offset_left = button_positions.grab.x
# grab_button.offset_top = button_positions.grab.y
# grab_button.offset_right = button_positions.grab.x + button_size
# grab_button.offset_bottom = button_positions.grab.y + button_size
grab_button.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
grab_button.offset_left = button_positions.grab.x
grab_button.offset_top = button_positions.grab.y
grab_button.offset_right = button_positions.grab.x + button_size
grab_button.offset_bottom = button_positions.grab.y + button_size
if put_button:
put_button.visible = false # Always INTENTIONALLY HIDDEN per request
# put_button.scale = Vector2(button_scale, button_scale)
# put_button.offset_left = button_positions.put.x
# put_button.offset_top = button_positions.put.y
# put_button.offset_right = button_positions.put.x + button_size
# put_button.offset_bottom = button_positions.put.y + button_size
if attack_mode_button:
attack_mode_button.visible = buttons_visible
attack_mode_button.visible = true
attack_mode_button.scale = Vector2(button_scale, button_scale)
# attack_mode_button.offset_left = button_positions.attack_mode.x
# attack_mode_button.offset_top = button_positions.attack_mode.y
# attack_mode_button.offset_right = button_positions.attack_mode.x + button_size
# attack_mode_button.offset_bottom = button_positions.attack_mode.y + button_size
attack_mode_button.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
attack_mode_button.offset_left = button_positions.attack_mode.x
attack_mode_button.offset_top = button_positions.attack_mode.y
attack_mode_button.offset_right = button_positions.attack_mode.x + button_size
attack_mode_button.offset_bottom = button_positions.attack_mode.y + button_size
if spawn_boost_button:
spawn_boost_button.visible = buttons_visible
spawn_boost_button.visible = true
spawn_boost_button.scale = Vector2(button_scale, button_scale)
# spawn_boost_button.offset_left = button_positions.spawn_boost.x
# spawn_boost_button.offset_top = button_positions.spawn_boost.y
# spawn_boost_button.offset_right = button_positions.spawn_boost.x + button_size
# spawn_boost_button.offset_bottom = button_positions.spawn_boost.y + button_size
spawn_boost_button.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
spawn_boost_button.offset_left = button_positions.spawn_boost.x
spawn_boost_button.offset_top = button_positions.spawn_boost.y
spawn_boost_button.offset_right = button_positions.spawn_boost.x + button_size
spawn_boost_button.offset_bottom = button_positions.spawn_boost.y + button_size
if tekton_grab_button:
tekton_grab_button.visible = true
tekton_grab_button.scale = Vector2(button_scale, button_scale)
tekton_grab_button.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
tekton_grab_button.offset_left = -280
tekton_grab_button.offset_top = -80
tekton_grab_button.offset_right = -280 + button_size
tekton_grab_button.offset_bottom = -80 + button_size
# Force layer update
visible = true
+17 -7
View File
@@ -8,11 +8,16 @@ var icon_containers: Dictionary = {} # { EffectEnum: Button }
# Local State
var selected_effect: int = -1
var special_manager_ref: Node = null # Reference to SpecialTilesManager
@onready var SettingsManager = get_node_or_null("/root/SettingsManager")
signal effect_selected(effect: int)
func _ready():
print("[PowerUpUI] _ready called")
# Connect to SettingsManager to update labels
if SettingsManager and not SettingsManager.control_remapped.is_connected(_on_control_remapped):
SettingsManager.control_remapped.connect(_on_control_remapped)
# Map Effect Enum to UI Nodes (Assumes specific names in main.tscn)
# We try to get them immediately. If they are children, they should be accessible.
var container = get_node_or_null("Container")
@@ -54,8 +59,13 @@ func _ready():
if freeze_btn: freeze_btn.visible = true
_update_shortcuts_for_mode(false)
print("[PowerUpUI] UI Initialization Complete. Mapped %d buttons." % icon_containers.size())
func _on_control_remapped(_action: String, _key: int):
# Refresh all labels
_update_shortcuts_for_mode(LobbyManager.is_game_mode(GameMode.Mode.STOP_N_GO))
func _setup_btn(effect_id: int, btn: Button):
if not btn:
print("[PowerUpUI] Warning: Button for effect %d is null" % effect_id)
@@ -138,15 +148,15 @@ func _update_btn_shortcut(effect_id: int, btn: Button):
var key_text = ""
if is_sng:
match effect_id:
0: key_text = "1" # Speed
3: key_text = "2" # Ghost
_: key_text = "" # Others hidden/disabled
0: key_text = SettingsManager.get_control_text("powerup_1")
3: key_text = SettingsManager.get_control_text("powerup_2") # Map P2 to Slot 3 (Ghost) in SNG
_: key_text = ""
else:
match effect_id:
0: key_text = "1" # Speed
2: key_text = "2" # Wall
1: key_text = "3" # Freeze
3: key_text = "4" # Ghost
0: key_text = SettingsManager.get_control_text("powerup_1")
2: key_text = SettingsManager.get_control_text("powerup_2")
1: key_text = SettingsManager.get_control_text("powerup_3")
3: key_text = SettingsManager.get_control_text("powerup_4")
sc_lbl.text = key_text
+110
View File
@@ -0,0 +1,110 @@
extends CanvasLayer
# SettingsMenu - Handles UI logic for the premium settings panel
@onready var tab_container = $PanelContainer/VBoxContainer/ContentSection/TabContainer
@onready var close_button = $PanelContainer/VBoxContainer/Header/CloseButton
# Video Controls
@onready var fullscreen_toggle = %FullscreenToggle
@onready var vsync_toggle = %VSyncToggle
# Audio Controls
@onready var master_slider = %MasterSlider
@onready var music_slider = %MusicSlider
@onready var sfx_slider = %SFXSlider
# Controls (Keybinds)
@onready var SettingsManager = get_node_or_null("/root/SettingsManager")
var listening_action: String = "" # Set when waiting for a keypress
func _ready():
# Theme inheritance is broken by CanvasLayer root, no need for theme = null
_load_ui_values()
_connect_signals()
# Initial visibility
visible = false
func _load_ui_values():
if not SettingsManager:
print("[SettingsMenu] ERROR: SettingsManager not found")
return
var s = SettingsManager.settings
# Video
fullscreen_toggle.button_pressed = s.video.fullscreen
vsync_toggle.button_pressed = s.video.vsync
# Audio
master_slider.value = s.audio.master_volume
music_slider.value = s.audio.music_volume
sfx_slider.value = s.audio.sfx_volume
# Controls
_update_all_key_labels()
func _connect_signals():
# Video
fullscreen_toggle.toggled.connect(_on_video_setting_changed)
vsync_toggle.toggled.connect(_on_video_setting_changed)
# Audio
master_slider.value_changed.connect(_on_audio_setting_changed.bind("master_volume"))
music_slider.value_changed.connect(_on_audio_setting_changed.bind("music_volume"))
sfx_slider.value_changed.connect(_on_audio_setting_changed.bind("sfx_volume"))
# Close
close_button.pressed.connect(func(): visible = false)
# Connect remapping buttons
for action_name in SettingsManager.settings.controls.keys():
var btn = get_node_or_null("%" + action_name.to_pascal_case() + "Btn")
if btn:
btn.pressed.connect(_on_remap_button_pressed.bind(action_name))
func _on_video_setting_changed(_unused = false):
SettingsManager.settings.video.fullscreen = fullscreen_toggle.button_pressed
SettingsManager.settings.video.vsync = vsync_toggle.button_pressed
SettingsManager.save_settings()
SettingsManager.apply_video_settings()
func _on_audio_setting_changed(key: String, value: float):
SettingsManager.settings.audio[key] = value
SettingsManager.save_settings()
SettingsManager.apply_audio_settings()
func _on_remap_button_pressed(action_name: String):
listening_action = action_name
var btn = get_node_or_null("%" + action_name.to_pascal_case() + "Btn")
if btn:
btn.text = "..."
btn.modulate = Color(0.5, 1.0, 0.5) # Signal "Listening"
func _input(event):
if listening_action != "" and event is InputEventKey and event.pressed:
# Capture the key
var keycode = event.keycode
SettingsManager.set_control(listening_action, keycode)
# Feedback and Reset
_update_key_label(listening_action)
listening_action = ""
# Consume event to prevent triggers
get_viewport().set_input_as_handled()
func _update_all_key_labels():
for action_name in SettingsManager.settings.controls.keys():
_update_key_label(action_name)
func _update_key_label(action_name: String):
var btn = get_node_or_null("%" + action_name.to_pascal_case() + "Btn")
if btn:
btn.text = SettingsManager.get_control_text(action_name)
btn.modulate = Color.WHITE
func open():
_load_ui_values()
visible = true
+1
View File
@@ -0,0 +1 @@
uid://cdege6m8u5cp