feat: Implement core game systems including settings management, player input, and initial gameplay mechanics with associated UI.

This commit is contained in:
Yogi Wiguna
2026-03-12 10:35:26 +08:00
parent 4f6783b468
commit 93eda69ad6
10 changed files with 218 additions and 155 deletions
+17 -46
View File
@@ -82,7 +82,7 @@ func _process(delta):
player.highlight_cells_if_authorized(area, highlight_id)
func handle_unhandled_input(event):
# Early return if not authorized human playersa
# Early return if not authorized human player
if not player.is_multiplayer_authority() or player.is_bot or player.is_in_group("Bots"):
player.set_process_unhandled_input(false)
return
@@ -97,66 +97,37 @@ func handle_unhandled_input(event):
# --- Keyboard Shortcuts (Event-based) ---
if event is InputEventKey and event.pressed and not event.echo:
var mode = LobbyManager.get_game_mode()
var is_sng = mode == GameMode.Mode.STOP_N_GO
# Safety check for SettingsManager
if not SettingsManager:
return
# 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:
# Single Slot Logic: Find whatever powerup is currently set to 'true' and activate it
var active_effect = -1
if player.special_tiles_manager:
for effect_key in player.special_tiles_manager.inventory:
if player.special_tiles_manager.inventory[effect_key]:
active_effect = effect_key
break
if active_effect != -1:
player.activate_powerup(active_effect)
else:
print("No powerup in slot 1 to activate.")
# KP Fallback
elif ek == KEY_KP_1:
var active_effect = -1
if player.special_tiles_manager:
for effect_key in player.special_tiles_manager.inventory:
if player.special_tiles_manager.inventory[effect_key]:
active_effect = effect_key
break
if active_effect != -1:
player.activate_powerup(active_effect)
# Action Buttons (Remappable)
elif ek == SettingsManager.get_control_keycode("attack_mode"):
# 1. Unified check for POWER-UP Activation
if event.is_action_pressed("use_powerup"):
player.activate_held_powerup()
get_viewport().set_input_as_handled()
return
# 3. Action Buttons (Remappable via InputMap)
if event.is_action_pressed("action_knock_tekton"):
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()
get_viewport().set_input_as_handled()
elif ek == SettingsManager.get_control_keycode("spawn_boost"):
elif event.is_action_pressed("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()
get_viewport().set_input_as_handled()
elif ek == SettingsManager.get_control_keycode("tekton_grab"):
elif event.is_action_pressed("action_grab_tekton"):
if not player.is_carrying_tekton and player.powerup_manager:
if player.powerup_manager.can_use_special():
if player.powerup_manager.has_method("can_use_special"): # Corrected method name
player.grab_tekton()
# Handle spawn point selection if not yet selected
get_viewport().set_input_as_handled()
# Handle spawn point selection if not yet selected
if not player.spawn_point_selected and player.highlighted_spawn_points.size() > 0:
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
+70 -6
View File
@@ -31,23 +31,33 @@ var settings = {
},
"controls": {
"use_controller": false,
# Movement (Keybinds are typically fixed to WASD/Arrows in Input Map, but we show them)
# Movement
"move_up": KEY_W,
"move_up_alt": KEY_UP,
"move_down": KEY_S,
"move_down_alt": KEY_DOWN,
"move_left": KEY_A,
"move_left_alt": KEY_LEFT,
"move_right": KEY_D,
"move_right_alt": KEY_RIGHT,
# Actions
"grab": KEY_SPACE,
"grab_alt": KEY_J,
"put": KEY_R,
"put_alt": KEY_K,
"tekton_grab": KEY_G,
"tekton_grab_alt": KEY_L,
# Power-Up Controls
"powerup_1": KEY_1,
"powerup_2": KEY_2,
"powerup_3": KEY_3,
"powerup_4": KEY_4,
"use_powerup": KEY_F,
"use_powerup_alt": KEY_SHIFT,
# Power Bar Controls / Special
"attack_mode": KEY_Q,
"spawn_boost": KEY_E
"attack_mode_alt": KEY_U,
"spawn_boost": KEY_E,
"spawn_boost_alt": KEY_I
}
}
@@ -80,6 +90,7 @@ func save_settings():
func apply_all_settings():
apply_video_settings()
apply_audio_settings()
apply_control_settings()
emit_signal("settings_applied")
func apply_video_settings():
@@ -149,9 +160,62 @@ func set_bus_volume(bus_name: String, volume_linear: float):
AudioServer.set_bus_volume_db(bus_idx, linear_to_db(volume_linear))
AudioServer.set_bus_mute(bus_idx, volume_linear <= 0.001)
func apply_control_settings():
# Sync custom settings with InputMap
var mapping = {
"move_up": "move_north",
"move_down": "move_south",
"move_left": "move_west",
"move_right": "move_east",
"grab": "action_grab",
"put": "action_put",
"use_powerup": "use_powerup",
"tekton_grab": "action_grab_tekton",
"attack_mode": "action_knock_tekton",
"spawn_boost": "spawn_boost"
}
for setting_key in mapping.keys():
var action_name = mapping[setting_key]
if not InputMap.has_action(action_name):
InputMap.add_action(action_name)
InputMap.action_erase_events(action_name)
# Add Primary
var primary_key = settings.controls.get(setting_key)
if primary_key:
var event = InputEventKey.new()
event.keycode = primary_key
InputMap.action_add_event(action_name, event)
# Add Secondary
var secondary_key = settings.controls.get(setting_key + "_alt")
if secondary_key:
var event = InputEventKey.new()
event.keycode = secondary_key
InputMap.action_add_event(action_name, event)
# Add Joypad defaults for movement
if action_name.begins_with("move_"):
var joy_axis = -1
var axis_val = 0.0
match action_name:
"move_north": joy_axis = JOY_AXIS_LEFT_Y; axis_val = -1.0
"move_south": joy_axis = JOY_AXIS_LEFT_Y; axis_val = 1.0
"move_west": joy_axis = JOY_AXIS_LEFT_X; axis_val = -1.0
"move_east": joy_axis = JOY_AXIS_LEFT_X; axis_val = 1.0
if joy_axis != -1:
var joy_event = InputEventJoypadMotion.new()
joy_event.axis = joy_axis
joy_event.axis_value = axis_val
InputMap.action_add_event(action_name, joy_event)
func set_control(action_name: String, keycode: int):
if settings.controls.has(action_name):
settings.controls[action_name] = keycode
apply_control_settings() # Apply immediately
emit_signal("control_remapped", action_name, keycode)
save_settings()
+1 -27
View File
@@ -206,11 +206,6 @@ func activate_effect(effect: int, target_player: Node3D = null):
print("PowerUp %s not found in inventory or false. Inventory: %s" % [effect, inventory])
return
# Check Cooldown
if powerup_cooldowns.get(effect, 0.0) > 0:
print("PowerUp %s on cooldown." % SpecialEffect.keys()[effect])
return
# Check Attack Mode Restriction
if player.get("is_attack_mode") and effect == SpecialEffect.INVISIBLE_MODE:
NotificationManager.send_message(player, "Cannot enter Ghost mode while in Attack Mode!", NotificationManager.MessageType.WARNING)
@@ -221,16 +216,8 @@ func activate_effect(effect: int, target_player: Node3D = null):
NotificationManager.send_message(player, "Cannot use this power while carrying a Tekton!", NotificationManager.MessageType.WARNING)
return
# Calculate Cooldown based on Level
var level = powerup_levels.get(effect, 1)
# Linear Interp: Lvl 1 = 15s, Lvl 8 = 5s
# Slope = (5 - 15) / (8 - 1) = -10 / 7 = -1.428...
var cooldown_time = COOLDOWN_L1 + ((level - 1) * (COOLDOWN_L8 - COOLDOWN_L1) / 7.0)
powerup_cooldowns[effect] = cooldown_time
emit_signal("cooldown_updated", effect, cooldown_time, cooldown_time)
print("[SpecialTiles] Player %s activated %s (Lvl %d). Cooldown: %.1fs" % [player.name, SpecialEffect.keys()[effect], level, cooldown_time])
print("[SpecialTiles] Player %s activated %s (Lvl %d). No cooldown applied." % [player.name, SpecialEffect.keys()[effect], level])
match effect:
SpecialEffect.FASTER_SPEED:
@@ -555,19 +542,6 @@ func _check_for_icy_floor():
pass
func _process(delta):
# Update Cooldowns
for effect in powerup_cooldowns.keys():
if powerup_cooldowns[effect] > 0:
powerup_cooldowns[effect] -= delta
# Emit signal occasionally or only on change? Every frame might be too much for UI?
# Optimization: Emit every 0.1s or if diff is significant?
# For snappy UI text, frame sync is okay for local player.
emit_signal("cooldown_updated", effect, powerup_cooldowns[effect], 0.0) # max unused for tick
if powerup_cooldowns[effect] <= 0:
powerup_cooldowns[effect] = 0
emit_signal("cooldown_updated", effect, 0, 0)
# Update Active Buffs (Speed)
if active_buffs.has(SpecialEffect.FASTER_SPEED):
active_buffs[SpecialEffect.FASTER_SPEED] -= delta
+15 -1
View File
@@ -15,6 +15,7 @@ var attack_mode_button: Button # Renamed from special_button
var spawn_boost_button: Button
var settings_button: Button
var tekton_grab_button: Button
@onready var SettingsManager = get_node_or_null("/root/SettingsManager")
# Settings - persisted to config file
@@ -30,6 +31,7 @@ var button_positions: Dictionary = {
"attack_mode": Vector2(-200, -80), # Renamed
"spawn_boost": Vector2(-120, -80)
}
var button_scale: float = 1.0
# Reference to main scene and player
@@ -163,6 +165,7 @@ func _create_touch_ui():
put_button = _find_or_create_action_button(actions_container, "Put", "📦", button_positions.put)
tekton_grab_button = _find_or_create_action_button(actions_container, "TektonGrab", "👋", Vector2(-280, -80))
# Order: AttackMode, SpawnBoost, Grab, TektonGrab
if attack_mode_button:
@@ -184,6 +187,7 @@ func _create_touch_ui():
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:
@@ -306,6 +310,7 @@ func _ensure_shortcut_label(btn: Button, button_name: String):
"AttackMode": existing_lbl.text = SettingsManager.get_control_text("attack_mode")
"SpawnBoost": existing_lbl.text = SettingsManager.get_control_text("spawn_boost")
"TektonGrab": existing_lbl.text = SettingsManager.get_control_text("tekton_grab")
print("[TouchControls] Updated %s shortcut label to: %s" % [button_name, existing_lbl.text])
@@ -338,6 +343,7 @@ func _ensure_shortcut_label(btn: Button, button_name: String):
"AttackMode": shortcut_lbl.text = SettingsManager.get_control_text("attack_mode") if SettingsManager else "Q"
"SpawnBoost": shortcut_lbl.text = SettingsManager.get_control_text("spawn_boost") if SettingsManager else "E"
"TektonGrab": shortcut_lbl.text = SettingsManager.get_control_text("tekton_grab") if SettingsManager else "G"
btn.add_child(shortcut_lbl)
@@ -358,6 +364,7 @@ func _on_button_pressed(button_name: String):
"AttackMode": btn = attack_mode_button
"SpawnBoost": btn = spawn_boost_button
"TektonGrab": btn = tekton_grab_button
if btn:
var tween = create_tween()
@@ -396,6 +403,7 @@ func _on_button_pressed(button_name: String):
if local_player.has_method("grab_tekton"):
local_player.grab_tekton()
func _on_button_released(button_name: String):
var btn: Button
match button_name:
@@ -404,6 +412,7 @@ func _on_button_released(button_name: String):
"AttackMode": btn = attack_mode_button
"SpawnBoost": btn = spawn_boost_button
"TektonGrab": btn = tekton_grab_button
if btn:
var tween = create_tween()
@@ -445,7 +454,12 @@ func _load_settings():
attack_mode_pos = config.get_value("touch_controls", "special_position", Vector2(-200, -80))
var spawn_boost_pos = config.get_value("touch_controls", "spawn_boost_position", Vector2(-120, -80))
button_positions = {"grab": grab_pos, "put": put_pos, "attack_mode": attack_mode_pos, "spawn_boost": spawn_boost_pos}
button_positions = {
"grab": grab_pos,
"put": put_pos,
"attack_mode": attack_mode_pos,
"spawn_boost": spawn_boost_pos
}
# Apply loaded settings
_apply_settings()
+1 -16
View File
@@ -146,7 +146,7 @@ func _update_btn_shortcut(_effect_id: int, btn: Button):
return
# Show only Shortcut 1 as per single-slot request
sc_lbl.text = "[%s]" % SettingsManager.get_control_text("powerup_1")
sc_lbl.text = "[%s]" % SettingsManager.get_control_text("use_powerup")
func _on_btn_pressed(effect_id: int):
print("[PowerUpUI] Clicked Button %d" % effect_id)
@@ -228,21 +228,6 @@ func _on_powerup_unlocked(effect: int, level: int):
else:
print("[PowerUpUI] ERROR: Unlocked Effect %d but no UI button found! Keys: %s" % [effect, icon_containers.keys()])
func _on_cooldown_updated(effect: int, time_left: float, max_time: float):
if icon_containers.has(effect):
var btn = icon_containers[effect]
var cd_lbl = btn.get_node_or_null("CooldownLabel")
if cd_lbl:
if time_left > 0:
cd_lbl.text = "%d" % int(time_left)
btn.disabled = true
btn.modulate = Color(0.7, 0.7, 0.7, 0.8)
else:
cd_lbl.text = ""
# Re-enable if we own it
if special_manager_ref and special_manager_ref.inventory.get(effect, false):
btn.disabled = false
btn.modulate = Color.WHITE
func _on_inventory_updated(inventory: Dictionary):
# Update UI icons (Only show ONE active slot as per user request)
+16 -6
View File
@@ -105,16 +105,26 @@ func _connect_signals():
# Close
close_button.pressed.connect(func(): visible = false)
# Connect remapping buttons (exclude non-keybinds like use_controller)
# Connect remapping buttons
for action_name in SettingsManager.settings.controls.keys():
if action_name == "use_controller":
continue
var btn = get_node_or_null("%" + action_name.to_pascal_case() + "Btn")
if btn:
if btn.pressed.is_connected(_on_remap_button_pressed):
btn.pressed.disconnect(_on_remap_button_pressed)
btn.pressed.connect(_on_remap_button_pressed.bind(action_name))
# Check Primary Button
var primary_btn = get_node_or_null("%" + action_name.to_pascal_case() + "Btn")
if primary_btn:
if primary_btn.pressed.is_connected(_on_remap_button_pressed):
primary_btn.pressed.disconnect(_on_remap_button_pressed)
primary_btn.pressed.connect(_on_remap_button_pressed.bind(action_name))
# Check Alt Button
if not action_name.ends_with("_alt"):
var alt_action = action_name + "_alt"
var alt_btn = get_node_or_null("%" + action_name.to_pascal_case() + "AltBtn")
if alt_btn:
if alt_btn.pressed.is_connected(_on_remap_button_pressed):
alt_btn.pressed.disconnect(_on_remap_button_pressed)
alt_btn.pressed.connect(_on_remap_button_pressed.bind(alt_action))
func _on_video_setting_changed(_unused = false):
SettingsManager.settings.video.fullscreen = fullscreen_toggle.button_pressed