feat: Implement core game managers, player movement logic, and initial UI scenes.
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
[ ADT's Daily Report - 2025-12-21 ]
|
||||
|
||||
Updated the `tekton-enet` ( Armageddon Multiplayer ) on branch `launcher`
|
||||
|
||||
you can test also, if theres any bug, please report it to me, also try build the project on android to see if theres any bug on touch controls
|
||||
|
||||
**General Fixes**
|
||||
|
||||
✅ **Fixed "Invalid packet received" Errors** - The error `Failed to get path from RPC: Main/1` was caused by `player.rpc()` calls trying to find the same node path on clients. Fixed by routing all sync calls through `main.gd` RPCs which look up players by ID.
|
||||
|
||||
✅ **sync_playerboard RPC Fix** - Updated `main.gd` `sync_playerboard(player_id, new_playerboard)` to actually update `player.playerboard` data, not just UI.
|
||||
|
||||
✅ **sync_player_goals RPC Fix** - Changed `goals_cycle_manager.gd` `regenerate_goals_for_player()` to use `main_scene.rpc("sync_player_goals", peer_id, int_goals)` instead of `player.rpc("sync_goals")`.
|
||||
|
||||
**Randomized Spawn System Fixes**
|
||||
|
||||
✅ **Spawn Rollback Fix** - Restructured `_setup_host_game()` in `main.gd` to add all players synchronously first, then call `_assign_random_spawn_positions()` BEFORE the 0.3s await. This ensures spawn positions are set before player `_ready()` runs position initialization.
|
||||
|
||||
✅ **Client-Side Spawn Init** - Modified `player.gd` (lines 200, 212) to check `LobbyManager.get_randomize_spawn()` before running position initialization. When random spawn is enabled, clients skip position init entirely and wait for host RPC.
|
||||
|
||||
**RPC Node Path Fixes**
|
||||
|
||||
✅ **Fixed "Invalid packet received" Errors** - The error `Failed to get path from RPC: Main/1` was caused by `player.rpc()` calls trying to find the same node path on clients. Fixed by routing all sync calls through `main.gd` RPCs which look up players by ID.
|
||||
|
||||
✅ **sync_playerboard RPC Fix** - Updated `main.gd` `sync_playerboard(player_id, new_playerboard)` to actually update `player.playerboard` data, not just UI.
|
||||
|
||||
✅ **sync_player_goals RPC Fix** - Changed `goals_cycle_manager.gd` `regenerate_goals_for_player()` to use `main_scene.rpc("sync_player_goals", peer_id, int_goals)` instead of `player.rpc("sync_goals")`.
|
||||
|
||||
**Goal Completion Sync**
|
||||
|
||||
✅ **Client Goal Completion Detection** - Added `_check_goal_completion()` call to server-side `_execute_grab()` in `playerboard_manager.gd`. This was the missing piece - client grabs were validated by server but goal check was never called server-side, so client goal completions weren't triggering regeneration.
|
||||
|
||||
**Special Ability Cooldown**
|
||||
|
||||
✅ **4-Second Cooldown for F Key** - Added `SPECIAL_COOLDOWN = 4.0` constant, `special_cooldown_timer` variable, and `_process()` to tick down cooldown in `powerup_manager.gd`. Shows "Special on cooldown! (X.Xs)" message if trying to use too soon.
|
||||
|
||||
**Animation**
|
||||
|
||||
✅ **AnimationTimeline** - added animation for player movement
|
||||
✅ **AnimationTimeline** - added animation for player special ability
|
||||
✅ **AnimationTimeline** - added animation for pickup, put
|
||||
|
||||
**Character selection**
|
||||
✅ **Character selection** - added character selection for player
|
||||
|
||||
**Message Display Improvements**
|
||||
|
||||
✅ **Player Names in Messages** - Updated `add_message_to_bar()` in `main.gd` to display messages in format: `[PlayerName] message` (e.g., "⚡ [Player1] Power-up bar filled!").
|
||||
|
||||
**Touch Controls**
|
||||
|
||||
✅ **Settings Button Functionality** - Implemented `_on_settings_pressed()` in `touch_controls.gd` to open `SettingsPanel` from main.tscn when pressed.
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
[ ADT's Daily Report - 2025-12-23 ]
|
||||
|
||||
Updated the `tekton-enet` ( Armageddon Multiplayer ) on branch `launcher`
|
||||
|
||||
**Touch Controls Enhancement**
|
||||
|
||||
✅ **Touch Button Settings** - Added `touch_buttons_enabled` and `joystick_enabled` toggles to `touch_controls.gd`. Settings now persist to `user://touch_controls_settings.cfg` using ConfigFile.
|
||||
|
||||
✅ **Position Mapping API** - Added public methods `set_touch_buttons_enabled()`, `set_joystick_enabled()`, `set_button_position()`, and `get_settings()` for controlling touch UI programmatically.
|
||||
|
||||
**Player Collision Fix**
|
||||
|
||||
✅ **Prevent Player-to-Player Collision** - Modified `player.tscn` CharacterBody3D to use `collision_layer = 2`. Players now only collide with world objects, not each other, preventing desync issues when walking into same space.
|
||||
|
||||
**Profile Access Improvement**
|
||||
|
||||
✅ **Profile Button on All Screens** - Added PROFILE button to MainMenuPanel and RoomListPanel in `lobby.tscn`. Connected buttons to `_on_profile_btn_pressed()` in `lobby.gd`.
|
||||
|
||||
**Multiplayer Disconnect Fix**
|
||||
|
||||
✅ **Proper Nakama Match Cleanup** - Added `_cleanup_multiplayer()` function in `main.gd` that calls `NakamaManager.bridge.leave()`, clears `current_match_id`, and resets multiplayer peer. Called from both `_on_quit_match_pressed()` and `_on_back_to_menu_pressed()`.
|
||||
|
||||
**Bug Fixes**
|
||||
|
||||
✅ **Game Over Panel Fix** - Fixed duplicate parent error in `_show_game_over_panel()` where BackToMenuBtn was being added to two parents. Button now correctly added only to centered HBoxContainer.
|
||||
@@ -0,0 +1,27 @@
|
||||
[ ADT's Daily Report - 2025-12-27 ]
|
||||
|
||||
Updated the `tekton-enet` ( Armageddon Multiplayer ) on branch `launcher`
|
||||
|
||||
**Screen Shake Implementation**
|
||||
|
||||
✅ **Dependency Injection** - Refactored `screen_shake.gd` to accept camera instance via `initialize()` method instead of searching scene tree. Updated `main.gd` to inject `$Camera3D` on creation.
|
||||
|
||||
**Touch Controls Visibility Fix**
|
||||
|
||||
✅ **Unified Toggle Logic** - Fixed `main.gd` `_on_joystick_toggled()` to call `set_joystick_enabled()` instead of directly setting visibility, ensuring all touch controls hide/show together.
|
||||
|
||||
✅ **Anchor Offset Fix** - Fixed touch buttons being invisible by switching from `.position` to proper `.offset_left/top/right/bottom` for anchored controls. Buttons now correctly appear in bottom-right corner.
|
||||
|
||||
✅ **Layer Z-Order** - Reduced `CanvasLayer.layer` from 100 to 10 so pause menu renders above touch controls.
|
||||
|
||||
✅ **Settings Panel Sync** - Updated `_on_settings_pressed()` in `main.gd` to sync JoystickToggle and sliders with current `TouchControlsManager` state when settings panel opens.
|
||||
|
||||
**Special Tile Effects**
|
||||
|
||||
✅ **Pickup Animation** - Added pulse and flash effect to playerboard slots when special tiles (Heart, Diamond, Star, Coin) are collected. Implemented in `ui_manager.gd` via `_pulse_slot_effect()`.
|
||||
|
||||
✅ **State Tracking** - Added `_previous_playerboard_state` to detect newly placed tiles and trigger effects only on changes.
|
||||
|
||||
**Playerboard Display Fix**
|
||||
|
||||
✅ **Modulate Logic** - Fixed `update_playerboard_ui()` in `ui_manager.gd` where broken if/else structure caused non-center slots to inherit wrong modulate values. Now center slots show goals dimmed, collected tiles bright, and non-center slots always at full brightness.
|
||||
@@ -39,6 +39,10 @@ window/size/window_width_override=1280
|
||||
window/size/window_height_override=720
|
||||
window/stretch/mode="viewport"
|
||||
|
||||
[editor]
|
||||
|
||||
run/main_run_args="--touch"
|
||||
|
||||
[editor_plugins]
|
||||
|
||||
enabled=PackedStringArray("res://addons/enhanced_gridmap/plugin.cfg", "res://addons/com.heroiclabs.nakama/plugin.cfg")
|
||||
|
||||
@@ -5,6 +5,7 @@ extends Control
|
||||
@onready var player_name_input = $MainMenuPanel/VBoxContainer/InputSection/PlayerNameInput
|
||||
@onready var create_room_btn = $MainMenuPanel/VBoxContainer/ButtonSection/CreateRoomBtn
|
||||
@onready var browse_rooms_btn = $MainMenuPanel/VBoxContainer/ButtonSection/BrowseRoomsBtn
|
||||
@onready var main_menu_profile_btn = $MainMenuPanel/VBoxContainer/ButtonSection/ProfileBtn
|
||||
|
||||
# UI References - Room List
|
||||
@onready var room_list_panel = $RoomListPanel
|
||||
@@ -13,6 +14,7 @@ extends Control
|
||||
@onready var refresh_btn = $RoomListPanel/VBoxContainer/ButtonContainer/RefreshBtn
|
||||
@onready var join_btn = $RoomListPanel/VBoxContainer/ButtonContainer/JoinBtn
|
||||
@onready var back_btn = $RoomListPanel/VBoxContainer/ButtonContainer/BackBtn
|
||||
@onready var room_list_profile_btn = $RoomListPanel/VBoxContainer/ButtonContainer/ProfileBtn
|
||||
|
||||
# UI References - Lobby Panel
|
||||
@onready var lobby_panel = $LobbyPanel
|
||||
@@ -28,6 +30,8 @@ extends Control
|
||||
@onready var copy_id_btn = $LobbyPanel/TopBar/MatchIdContainer/CopyIdBtn
|
||||
@onready var duration_option = $LobbyPanel/TopBar/SettingsSection/DurationOption
|
||||
@onready var duration_text_label = $LobbyPanel/TopBar/SettingsSection/DurationTextLabel
|
||||
@onready var random_spawn_check = $LobbyPanel/TopBar/SettingsSection/RandomSpawnCheck
|
||||
@onready var random_spawn_label = $LobbyPanel/TopBar/SettingsSection/RandomSpawnLabel
|
||||
|
||||
# UI References - Player Slots
|
||||
@onready var players_container = $LobbyPanel/PlayersContainer
|
||||
@@ -77,17 +81,22 @@ func _ready():
|
||||
# Connect button signals - Main Menu
|
||||
create_room_btn.pressed.connect(_on_create_room_pressed)
|
||||
browse_rooms_btn.pressed.connect(_on_browse_rooms_pressed)
|
||||
if main_menu_profile_btn:
|
||||
main_menu_profile_btn.pressed.connect(_on_profile_btn_pressed)
|
||||
|
||||
# Connect button signals - Room List
|
||||
refresh_btn.pressed.connect(_on_refresh_pressed)
|
||||
join_btn.pressed.connect(_on_join_pressed)
|
||||
back_btn.pressed.connect(_on_back_pressed)
|
||||
if room_list_profile_btn:
|
||||
room_list_profile_btn.pressed.connect(_on_profile_btn_pressed)
|
||||
|
||||
# Connect button signals - Lobby
|
||||
profile_btn.pressed.connect(_on_profile_btn_pressed)
|
||||
logout_btn.pressed.connect(_on_logout_pressed)
|
||||
copy_id_btn.pressed.connect(_on_copy_id_pressed)
|
||||
duration_option.item_selected.connect(_on_duration_selected)
|
||||
random_spawn_check.toggled.connect(_on_random_spawn_toggled)
|
||||
area_left_btn.pressed.connect(func(): LobbyManager.cycle_area(-1))
|
||||
area_right_btn.pressed.connect(func(): LobbyManager.cycle_area(1))
|
||||
leave_btn.pressed.connect(_on_leave_pressed)
|
||||
@@ -104,6 +113,7 @@ func _ready():
|
||||
LobbyManager.all_players_ready.connect(_on_all_players_ready)
|
||||
LobbyManager.game_starting.connect(_on_game_starting)
|
||||
LobbyManager.match_duration_changed.connect(_on_match_duration_changed)
|
||||
LobbyManager.randomize_spawn_changed.connect(_on_randomize_spawn_changed)
|
||||
LobbyManager.character_changed.connect(_on_character_changed)
|
||||
LobbyManager.area_changed.connect(_on_area_changed)
|
||||
LobbyManager.player_list_changed.connect(_update_player_slots)
|
||||
@@ -236,6 +246,15 @@ func _on_duration_selected(index: int) -> void:
|
||||
if index >= 0 and index < durations.size():
|
||||
LobbyManager.set_match_duration(durations[index])
|
||||
|
||||
func _on_random_spawn_toggled(enabled: bool) -> void:
|
||||
if not LobbyManager.is_host:
|
||||
return
|
||||
LobbyManager.set_randomize_spawn(enabled)
|
||||
|
||||
func _update_random_spawn_label(enabled: bool) -> void:
|
||||
if random_spawn_label:
|
||||
random_spawn_label.text = "Random ✓" if enabled else "Random ✗"
|
||||
|
||||
func _on_profile_btn_pressed() -> void:
|
||||
if not profile_panel_instance:
|
||||
var profile_panel_scene := load("res://scenes/ui/profile_panel.tscn")
|
||||
@@ -289,6 +308,12 @@ func _on_room_joined(room_data: Dictionary) -> void:
|
||||
if not is_host:
|
||||
_update_duration_text_label(LobbyManager.get_match_duration())
|
||||
|
||||
# Random spawn: host sees checkbox, clients see label
|
||||
random_spawn_check.visible = is_host
|
||||
random_spawn_label.visible = not is_host
|
||||
random_spawn_check.button_pressed = LobbyManager.get_randomize_spawn()
|
||||
_update_random_spawn_label(LobbyManager.get_randomize_spawn())
|
||||
|
||||
# Area selector: only host can interact
|
||||
area_left_btn.disabled = not is_host
|
||||
area_right_btn.disabled = not is_host
|
||||
@@ -329,6 +354,10 @@ func _on_match_duration_changed(duration_seconds: int) -> void:
|
||||
if not LobbyManager.is_host:
|
||||
_update_duration_text_label(duration_seconds)
|
||||
|
||||
func _on_randomize_spawn_changed(enabled: bool) -> void:
|
||||
if not LobbyManager.is_host:
|
||||
_update_random_spawn_label(enabled)
|
||||
|
||||
func _on_character_changed(_player_id: int, _character_name: String) -> void:
|
||||
_update_player_slots()
|
||||
|
||||
|
||||
@@ -94,6 +94,12 @@ layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 16
|
||||
text = "BROWSE ROOMS"
|
||||
|
||||
[node name="ProfileBtn" type="Button" parent="MainMenuPanel/VBoxContainer/ButtonSection"]
|
||||
custom_minimum_size = Vector2(0, 36)
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 14
|
||||
text = "PROFILE"
|
||||
|
||||
[node name="RoomListPanel" type="PanelContainer" parent="."]
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
@@ -165,6 +171,11 @@ custom_minimum_size = Vector2(110, 44)
|
||||
layout_mode = 2
|
||||
text = "BACK"
|
||||
|
||||
[node name="ProfileBtn" type="Button" parent="RoomListPanel/VBoxContainer/ButtonContainer"]
|
||||
custom_minimum_size = Vector2(80, 44)
|
||||
layout_mode = 2
|
||||
text = "PROFILE"
|
||||
|
||||
[node name="LobbyPanel" type="Control" parent="."]
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
@@ -268,6 +279,23 @@ theme_override_colors/font_color = Color(0.647, 0.996, 0.224, 1)
|
||||
theme_override_font_sizes/font_size = 11
|
||||
text = "3 min"
|
||||
|
||||
[node name="SpawnSpacer" type="Control" parent="LobbyPanel/TopBar/SettingsSection"]
|
||||
custom_minimum_size = Vector2(15, 0)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="RandomSpawnCheck" type="CheckButton" parent="LobbyPanel/TopBar/SettingsSection"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 11
|
||||
button_pressed = true
|
||||
text = "Random Spawn"
|
||||
|
||||
[node name="RandomSpawnLabel" type="Label" parent="LobbyPanel/TopBar/SettingsSection"]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.647, 0.996, 0.224, 1)
|
||||
theme_override_font_sizes/font_size = 11
|
||||
text = "Random ✓"
|
||||
|
||||
[node name="HostBanner" type="PanelContainer" parent="LobbyPanel"]
|
||||
layout_mode = 1
|
||||
anchors_preset = 5
|
||||
|
||||
+199
-32
@@ -8,6 +8,8 @@ extends Node3D
|
||||
var ui_manager
|
||||
var obstacle_manager
|
||||
var goals_cycle_manager
|
||||
var screen_shake_manager
|
||||
var touch_controls
|
||||
|
||||
# Minimal local state
|
||||
var _connection_check_timer: float = 0.0
|
||||
@@ -30,7 +32,8 @@ func _ready():
|
||||
ui_manager.setup_leaderboard_ui(self)
|
||||
ui_manager.setup_powerup_bar_ui(self)
|
||||
_setup_obstacle_ui()
|
||||
_setup_global_match_timer_ui()
|
||||
# GlobalMatchTimer is now static in main.tscn - no setup needed
|
||||
# NetworkPanel is visible during gameplay
|
||||
|
||||
# Auto-start game if coming from lobby (already connected to match)
|
||||
if NakamaManager.is_connected_to_nakama() and multiplayer.get_unique_id() != 0:
|
||||
@@ -56,6 +59,18 @@ func _init_managers():
|
||||
add_child(goals_cycle_manager)
|
||||
goals_cycle_manager.initialize(self)
|
||||
|
||||
# Screen shake manager for impact feedback
|
||||
screen_shake_manager = load("res://scripts/managers/screen_shake.gd").new()
|
||||
screen_shake_manager.name = "ScreenShakeManager"
|
||||
add_child(screen_shake_manager)
|
||||
screen_shake_manager.initialize($Camera3D)
|
||||
|
||||
# Touch controls for mobile
|
||||
touch_controls = load("res://scripts/managers/touch_controls.gd").new()
|
||||
touch_controls.name = "TouchControls"
|
||||
add_child(touch_controls)
|
||||
touch_controls.initialize(self)
|
||||
|
||||
# Connect signals for UI updates
|
||||
goals_cycle_manager.timer_updated.connect(_on_timer_updated)
|
||||
goals_cycle_manager.score_updated.connect(_on_score_updated)
|
||||
@@ -103,7 +118,11 @@ func add_message_to_bar(player_name: String, message: String, type: int = Messag
|
||||
icon = "💬 "
|
||||
color = Color(0.9, 0.9, 0.9) # Light gray
|
||||
|
||||
label.text = "%s%s" % [icon, message]
|
||||
# Include player name in message if provided
|
||||
if player_name and player_name != "":
|
||||
label.text = "%s[%s] %s" % [icon, player_name, message]
|
||||
else:
|
||||
label.text = "%s%s" % [icon, message]
|
||||
label.add_theme_color_override("font_color", color)
|
||||
|
||||
# Add shadow for better visibility
|
||||
@@ -248,14 +267,20 @@ func _process(delta):
|
||||
# =============================================================================
|
||||
|
||||
func _on_match_joined(match_id: String):
|
||||
$NetworkPanel/NetworkInfo/UniquePeerID.text = str(multiplayer.get_unique_id())
|
||||
|
||||
if multiplayer.is_server():
|
||||
$NetworkPanel/NetworkInfo/NetworkSideDisplay.text = "Server (Match: %s)" % match_id
|
||||
_setup_host_game()
|
||||
var network_panel = get_node_or_null("PauseMenu/Panel/NetworkPanel")
|
||||
if network_panel:
|
||||
network_panel.get_node("NetworkInfo/UniquePeerID").text = str(multiplayer.get_unique_id())
|
||||
if multiplayer.is_server():
|
||||
network_panel.get_node("NetworkInfo/NetworkSideDisplay").text = "Server (Match: %s)" % match_id
|
||||
_setup_host_game()
|
||||
else:
|
||||
network_panel.get_node("NetworkInfo/NetworkSideDisplay").text = "Client"
|
||||
_setup_client_game()
|
||||
else:
|
||||
$NetworkPanel/NetworkInfo/NetworkSideDisplay.text = "Client"
|
||||
_setup_client_game()
|
||||
if multiplayer.is_server():
|
||||
_setup_host_game()
|
||||
else:
|
||||
_setup_client_game()
|
||||
|
||||
# =============================================================================
|
||||
# Game Setup
|
||||
@@ -273,9 +298,24 @@ func _setup_host_game():
|
||||
GameStateManager.add_player(player_id)
|
||||
GameStateManager.local_player_character = player_character
|
||||
ui_manager.set_local_player(player_character)
|
||||
if touch_controls:
|
||||
touch_controls.set_player(player_character)
|
||||
|
||||
# Wait for player to be fully ready (player.gd has 0.1s await in _ready before managers init)
|
||||
await get_tree().create_timer(0.2).timeout
|
||||
# Spawn client players that joined via lobby (need to add them first)
|
||||
var lobby_players = LobbyManager.get_players()
|
||||
for lobby_player in lobby_players:
|
||||
var peer_id = lobby_player.get("id", 0)
|
||||
if peer_id != 1 and peer_id != 0: # Skip host (1) and invalid (0)
|
||||
print("Spawning lobby player: ", peer_id)
|
||||
_spawn_lobby_client_sync(peer_id)
|
||||
|
||||
# IMMEDIATELY assign random spawn positions before any player _ready() completes
|
||||
# Player _ready() has 0.1s await, so we assign before that completes
|
||||
if LobbyManager.get_randomize_spawn():
|
||||
_assign_random_spawn_positions()
|
||||
|
||||
# Wait for players to be fully ready (player.gd has 0.1s await in _ready before managers init)
|
||||
await get_tree().create_timer(0.3).timeout
|
||||
|
||||
# Set host goals - get goals directly from GoalManager
|
||||
var host_goals = GoalManager.get_goals_for_player(0)
|
||||
@@ -290,14 +330,17 @@ func _setup_host_game():
|
||||
_update_player_goals_ui(0, host_goals)
|
||||
ui_manager.update_playerboard_ui()
|
||||
|
||||
# Spawn client players that joined via lobby
|
||||
var lobby_players = LobbyManager.get_players()
|
||||
# Set goals for lobby client players
|
||||
var player_index = 1
|
||||
for lobby_player in lobby_players:
|
||||
var peer_id = lobby_player.get("id", 0)
|
||||
if peer_id != 1 and peer_id != 0: # Skip host (1) and invalid (0)
|
||||
print("Spawning lobby player: ", peer_id)
|
||||
await get_tree().create_timer(0.3).timeout
|
||||
_spawn_lobby_client(peer_id)
|
||||
if peer_id != 1 and peer_id != 0:
|
||||
var client_player = get_node_or_null(str(peer_id))
|
||||
if client_player and player_index < GoalManager.preset_goals.size():
|
||||
var client_goals = GoalManager.preset_goals[player_index].duplicate()
|
||||
client_player.goals = client_goals
|
||||
call_deferred("_deferred_set_player_goals", peer_id, client_goals)
|
||||
player_index += 1
|
||||
|
||||
# Add bots (only if no lobby players connected)
|
||||
if GameStateManager.enable_bots and lobby_players.size() <= 1:
|
||||
@@ -306,8 +349,8 @@ func _setup_host_game():
|
||||
|
||||
_start_game()
|
||||
|
||||
func _spawn_lobby_client(peer_id: int):
|
||||
"""Spawn a client player that was in the lobby."""
|
||||
func _spawn_lobby_client_sync(peer_id: int):
|
||||
"""Spawn a client player synchronously (no await)."""
|
||||
if has_node(str(peer_id)):
|
||||
return
|
||||
|
||||
@@ -319,13 +362,7 @@ func _spawn_lobby_client(peer_id: int):
|
||||
# Tell all clients to create this player
|
||||
rpc("add_newly_connected_player_character", peer_id)
|
||||
|
||||
# Wait for player to be ready then assign goals
|
||||
await get_tree().create_timer(0.3).timeout
|
||||
var player_index = GameStateManager.players.find(peer_id)
|
||||
if player_index >= 0 and player_index < GoalManager.preset_goals.size():
|
||||
var player_goals = GoalManager.preset_goals[player_index].duplicate()
|
||||
player_character.goals = player_goals
|
||||
call_deferred("_deferred_set_player_goals", peer_id, player_goals)
|
||||
# Goals will be assigned after players are ready in _setup_host_game
|
||||
|
||||
func _setup_client_game():
|
||||
"""Setup client when transitioning from lobby."""
|
||||
@@ -340,6 +377,8 @@ func _setup_client_game():
|
||||
GameStateManager.add_player(my_id)
|
||||
GameStateManager.local_player_character = player_character
|
||||
ui_manager.set_local_player(player_character)
|
||||
if touch_controls:
|
||||
touch_controls.set_player(player_character)
|
||||
ui_manager.update_button_states()
|
||||
print("Created local player for client: ", my_id)
|
||||
|
||||
@@ -349,19 +388,24 @@ func _setup_client_game():
|
||||
|
||||
func _auto_start_from_lobby():
|
||||
"""Called when main.tscn is loaded from lobby - game is already connected."""
|
||||
$NetworkPanel/NetworkInfo/UniquePeerID.text = str(multiplayer.get_unique_id())
|
||||
|
||||
# Get match ID from LobbyManager
|
||||
var match_id = LobbyManager.current_room.get("match_id", "")
|
||||
var short_id = match_id.substr(0, 8) if match_id.length() > 8 else match_id
|
||||
|
||||
# Update NetworkPanel in PauseMenu (if exists)
|
||||
var network_panel = get_node_or_null("PauseMenu/Panel/NetworkPanel")
|
||||
if network_panel:
|
||||
network_panel.get_node("NetworkInfo/UniquePeerID").text = str(multiplayer.get_unique_id())
|
||||
if multiplayer.is_server():
|
||||
network_panel.get_node("NetworkInfo/NetworkSideDisplay").text = "Host (Match: %s)" % short_id
|
||||
else:
|
||||
network_panel.get_node("NetworkInfo/NetworkSideDisplay").text = "Client (Match: %s)" % short_id
|
||||
|
||||
if multiplayer.is_server():
|
||||
print("Auto-starting as HOST - Match: ", short_id)
|
||||
$NetworkPanel/NetworkInfo/NetworkSideDisplay.text = "Host (Match: %s)" % short_id
|
||||
_setup_host_game()
|
||||
else:
|
||||
print("Auto-starting as CLIENT - Match: ", short_id)
|
||||
$NetworkPanel/NetworkInfo/NetworkSideDisplay.text = "Client (Match: %s)" % short_id
|
||||
_setup_client_game()
|
||||
|
||||
func _start_game():
|
||||
@@ -383,6 +427,34 @@ func _start_game():
|
||||
var all_players = get_tree().get_nodes_in_group("Players")
|
||||
ui_manager.initialize_leaderboard_with_players(all_players)
|
||||
|
||||
func _assign_random_spawn_positions():
|
||||
"""Assign random unique spawn positions to all players."""
|
||||
var spawn_locations = [
|
||||
Vector2i(0, 0), Vector2i(0, 1), Vector2i(0, 2), Vector2i(0, 3),
|
||||
Vector2i(0, 4), Vector2i(0, 5), Vector2i(0, 6), Vector2i(0, 7),
|
||||
Vector2i(0, 8), Vector2i(0, 9), Vector2i(0, 10), Vector2i(0, 11)
|
||||
]
|
||||
|
||||
# Shuffle spawn locations
|
||||
var shuffled_spawns = spawn_locations.duplicate()
|
||||
shuffled_spawns.shuffle()
|
||||
|
||||
# Get all players
|
||||
var all_players = get_tree().get_nodes_in_group("Players")
|
||||
|
||||
# Assign positions
|
||||
var spawn_index = 0
|
||||
for player in all_players:
|
||||
if spawn_index >= shuffled_spawns.size():
|
||||
break
|
||||
var spawn_pos = shuffled_spawns[spawn_index]
|
||||
# Set position and sync to all clients
|
||||
player.current_position = spawn_pos
|
||||
player.position = player.grid_to_world(spawn_pos)
|
||||
player.spawn_point_selected = true
|
||||
player.rpc("set_spawn_position", spawn_pos)
|
||||
spawn_index += 1
|
||||
|
||||
# =============================================================================
|
||||
# Player Management
|
||||
# =============================================================================
|
||||
@@ -428,6 +500,8 @@ func add_player_character(peer_id: int):
|
||||
if peer_id == multiplayer.get_unique_id():
|
||||
GameStateManager.local_player_character = player_character
|
||||
ui_manager.set_local_player(player_character)
|
||||
if touch_controls:
|
||||
touch_controls.set_player(player_character)
|
||||
ui_manager.update_button_states()
|
||||
ui_manager.update_playerboard_ui()
|
||||
|
||||
@@ -617,6 +691,12 @@ func _update_goals_ui_for_player(player_id: int, goals: Array):
|
||||
|
||||
@rpc("any_peer", "call_local")
|
||||
func sync_playerboard(player_id: int, new_playerboard: Array):
|
||||
# Find the player and update their playerboard
|
||||
var player = get_node_or_null(str(player_id))
|
||||
if player:
|
||||
player.playerboard = new_playerboard.duplicate()
|
||||
|
||||
# Update UI for local player
|
||||
if player_id == multiplayer.get_unique_id() and GameStateManager.local_player_character:
|
||||
ui_manager.update_playerboard_ui()
|
||||
update_all_players_boards()
|
||||
@@ -1009,9 +1089,8 @@ func _show_game_over_panel():
|
||||
back_btn.custom_minimum_size = Vector2(300, 60)
|
||||
back_btn.add_theme_font_size_override("font_size", 20)
|
||||
back_btn.pressed.connect(_on_back_to_menu_pressed)
|
||||
inner_vbox.add_child(back_btn)
|
||||
|
||||
# Center the button
|
||||
# Center the button in a container
|
||||
var btn_container = HBoxContainer.new()
|
||||
btn_container.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
btn_container.add_child(back_btn)
|
||||
@@ -1026,11 +1105,28 @@ func _on_back_to_menu_pressed():
|
||||
# Clean up game state
|
||||
GameStateManager.end_game()
|
||||
LobbyManager.reset()
|
||||
# Properly disconnect from Nakama match
|
||||
_cleanup_multiplayer()
|
||||
|
||||
# Go back to lobby
|
||||
if get_tree():
|
||||
get_tree().change_scene_to_file("res://scenes/lobby.tscn")
|
||||
|
||||
func _cleanup_multiplayer():
|
||||
"""Properly leave Nakama match and cleanup multiplayer state."""
|
||||
print("[Main] Cleaning up multiplayer connection...")
|
||||
|
||||
# Leave the Nakama match through the bridge
|
||||
if NakamaManager.bridge:
|
||||
NakamaManager.bridge.leave()
|
||||
|
||||
# Clear the current match ID
|
||||
NakamaManager.current_match_id = ""
|
||||
|
||||
# Reset multiplayer peer to disconnect cleanly
|
||||
if multiplayer.get_multiplayer_peer():
|
||||
multiplayer.set_multiplayer_peer(null)
|
||||
|
||||
func _deferred_init_leaderboard():
|
||||
"""Initialize leaderboard after a delay to ensure all players are loaded."""
|
||||
# Longer delay ensures players are synced
|
||||
@@ -1151,3 +1247,74 @@ func _get_ordinal(n: int) -> String:
|
||||
3: return "3rd"
|
||||
4: return "4th"
|
||||
_: return str(n) + "th"
|
||||
|
||||
# =============================================================================
|
||||
# Pause Menu & Settings
|
||||
# =============================================================================
|
||||
|
||||
func _input(event):
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
_toggle_pause_menu()
|
||||
|
||||
func _toggle_pause_menu():
|
||||
var pause_menu = get_node_or_null("PauseMenu")
|
||||
if pause_menu:
|
||||
pause_menu.visible = not pause_menu.visible
|
||||
get_tree().paused = pause_menu.visible
|
||||
|
||||
func _on_resume_pressed():
|
||||
var pause_menu = get_node_or_null("PauseMenu")
|
||||
if pause_menu:
|
||||
pause_menu.visible = false
|
||||
get_tree().paused = false
|
||||
|
||||
func _on_settings_pressed():
|
||||
var pause_menu = get_node_or_null("PauseMenu")
|
||||
var settings_panel = get_node_or_null("SettingsPanel")
|
||||
if pause_menu:
|
||||
pause_menu.visible = false
|
||||
if settings_panel:
|
||||
settings_panel.visible = true
|
||||
|
||||
# Sync settings UI with current state
|
||||
var joystick_toggle = settings_panel.get_node_or_null("Panel/VBox/JoystickToggle")
|
||||
if joystick_toggle and touch_controls:
|
||||
joystick_toggle.set_pressed_no_signal(touch_controls.joystick_enabled)
|
||||
|
||||
var size_slider = settings_panel.get_node_or_null("Panel/VBox/ButtonSizeRow/ButtonSizeSlider")
|
||||
if size_slider and touch_controls:
|
||||
size_slider.set_value_no_signal(touch_controls.button_size)
|
||||
|
||||
var opacity_slider = settings_panel.get_node_or_null("Panel/VBox/OpacityRow/OpacitySlider")
|
||||
if opacity_slider and touch_controls:
|
||||
opacity_slider.set_value_no_signal(touch_controls.button_opacity)
|
||||
|
||||
func _on_quit_match_pressed():
|
||||
get_tree().paused = false
|
||||
# Properly disconnect from Nakama match
|
||||
_cleanup_multiplayer()
|
||||
# Return to lobby or main menu
|
||||
get_tree().change_scene_to_file("res://scenes/lobby.tscn")
|
||||
|
||||
func _on_settings_back_pressed():
|
||||
var pause_menu = get_node_or_null("PauseMenu")
|
||||
var settings_panel = get_node_or_null("SettingsPanel")
|
||||
if settings_panel:
|
||||
settings_panel.visible = false
|
||||
if pause_menu:
|
||||
pause_menu.visible = true
|
||||
|
||||
func _on_button_size_changed(value: float):
|
||||
if touch_controls:
|
||||
touch_controls.button_size = value
|
||||
touch_controls._save_settings()
|
||||
|
||||
func _on_opacity_changed(value: float):
|
||||
if touch_controls:
|
||||
touch_controls.button_opacity = value
|
||||
touch_controls._save_settings()
|
||||
|
||||
func _on_joystick_toggled(enabled: bool):
|
||||
if touch_controls:
|
||||
touch_controls.set_joystick_enabled(enabled)
|
||||
touch_controls._save_settings()
|
||||
|
||||
+209
-39
@@ -76,39 +76,6 @@ current = true
|
||||
fov = 35.5
|
||||
size = 23.0
|
||||
|
||||
[node name="NetworkPanel" type="Panel" parent="."]
|
||||
anchors_preset = 5
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -185.0
|
||||
offset_top = 25.0
|
||||
offset_right = 185.0
|
||||
offset_bottom = 78.0
|
||||
grow_horizontal = 2
|
||||
theme_override_styles/panel = ExtResource("5_dvx6y")
|
||||
|
||||
[node name="NetworkInfo" type="HBoxContainer" parent="NetworkPanel"]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme_override_constants/separation = 50
|
||||
alignment = 1
|
||||
|
||||
[node name="NetworkSideDisplay" type="Label" parent="NetworkPanel/NetworkInfo"]
|
||||
layout_mode = 2
|
||||
text = "Network Side"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="UniquePeerID" type="Label" parent="NetworkPanel/NetworkInfo"]
|
||||
layout_mode = 2
|
||||
text = "Unique Peer ID"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="PlayerboardPanel" type="PanelContainer" parent="."]
|
||||
anchors_preset = 4
|
||||
anchor_top = 0.5
|
||||
@@ -9375,9 +9342,9 @@ anchor_top = 1.0
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 1.0
|
||||
offset_left = -200.0
|
||||
offset_top = -87.0
|
||||
offset_top = -52.0
|
||||
offset_right = 200.0
|
||||
offset_bottom = -53.0
|
||||
offset_bottom = -18.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 0
|
||||
theme_override_styles/panel = ExtResource("5_dvx6y")
|
||||
@@ -9397,11 +9364,13 @@ theme_override_constants/separation = 4
|
||||
anchors_preset = 1
|
||||
anchor_left = 1.0
|
||||
anchor_right = 1.0
|
||||
offset_left = -294.0
|
||||
offset_left = -211.0
|
||||
offset_top = 15.0
|
||||
offset_right = -18.0
|
||||
offset_bottom = 215.0
|
||||
offset_bottom = 166.0
|
||||
grow_horizontal = 0
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="LeaderboardPanel"]
|
||||
layout_mode = 2
|
||||
@@ -9518,8 +9487,8 @@ horizontal_alignment = 2
|
||||
[node name="GoalsTimer" type="PanelContainer" parent="."]
|
||||
offset_left = 20.0
|
||||
offset_top = 20.0
|
||||
offset_right = 120.0
|
||||
offset_bottom = 90.0
|
||||
offset_right = 97.0
|
||||
offset_bottom = 97.0
|
||||
|
||||
[node name="VBox" type="VBoxContainer" parent="GoalsTimer"]
|
||||
layout_mode = 2
|
||||
@@ -9537,6 +9506,207 @@ theme_override_font_sizes/font_size = 12
|
||||
text = "seconds"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="GlobalMatchTimer" type="PanelContainer" parent="."]
|
||||
anchors_preset = 5
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -80.0
|
||||
offset_top = 24.0
|
||||
offset_right = 80.0
|
||||
offset_bottom = 74.0
|
||||
grow_horizontal = 2
|
||||
|
||||
[node name="VBox" type="VBoxContainer" parent="GlobalMatchTimer"]
|
||||
layout_mode = 2
|
||||
alignment = 1
|
||||
|
||||
[node name="TimerLabel" type="Label" parent="GlobalMatchTimer/VBox"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 28
|
||||
text = "3:00"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="PauseMenu" type="CanvasLayer" parent="."]
|
||||
process_mode = 3
|
||||
layer = 10
|
||||
visible = false
|
||||
|
||||
[node name="Background" type="ColorRect" parent="PauseMenu"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
color = Color(0, 0, 0, 0.7)
|
||||
|
||||
[node name="Panel" type="PanelContainer" parent="PauseMenu"]
|
||||
anchors_preset = 8
|
||||
anchor_left = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -150.0
|
||||
offset_top = -150.0
|
||||
offset_right = 150.0
|
||||
offset_bottom = 150.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="NetworkPanel" type="Panel" parent="PauseMenu/Panel"]
|
||||
layout_mode = 2
|
||||
theme_override_styles/panel = ExtResource("5_dvx6y")
|
||||
|
||||
[node name="NetworkInfo" type="HBoxContainer" parent="PauseMenu/Panel/NetworkPanel"]
|
||||
layout_mode = 1
|
||||
anchors_preset = 5
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -135.0
|
||||
offset_right = 135.0
|
||||
offset_bottom = 23.0
|
||||
grow_horizontal = 2
|
||||
theme_override_constants/separation = 50
|
||||
alignment = 1
|
||||
|
||||
[node name="NetworkSideDisplay" type="Label" parent="PauseMenu/Panel/NetworkPanel/NetworkInfo"]
|
||||
layout_mode = 2
|
||||
text = "Network Side"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="UniquePeerID" type="Label" parent="PauseMenu/Panel/NetworkPanel/NetworkInfo"]
|
||||
layout_mode = 2
|
||||
text = "Unique Peer ID"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="VBox" type="VBoxContainer" parent="PauseMenu/Panel"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 15
|
||||
alignment = 1
|
||||
|
||||
[node name="Title" type="Label" parent="PauseMenu/Panel/VBox"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 24
|
||||
text = "PAUSED"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="Spacer" type="Control" parent="PauseMenu/Panel/VBox"]
|
||||
custom_minimum_size = Vector2(0, 20)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="ResumeBtn" type="Button" parent="PauseMenu/Panel/VBox"]
|
||||
custom_minimum_size = Vector2(200, 45)
|
||||
layout_mode = 2
|
||||
text = "Resume"
|
||||
|
||||
[node name="SettingsBtn" type="Button" parent="PauseMenu/Panel/VBox"]
|
||||
custom_minimum_size = Vector2(200, 45)
|
||||
layout_mode = 2
|
||||
text = "Settings"
|
||||
|
||||
[node name="QuitBtn" type="Button" parent="PauseMenu/Panel/VBox"]
|
||||
custom_minimum_size = Vector2(200, 45)
|
||||
layout_mode = 2
|
||||
text = "Quit Match"
|
||||
|
||||
[node name="SettingsPanel" type="CanvasLayer" parent="."]
|
||||
process_mode = 3
|
||||
layer = 11
|
||||
visible = false
|
||||
|
||||
[node name="Background" type="ColorRect" parent="SettingsPanel"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
color = Color(0, 0, 0, 0.8)
|
||||
|
||||
[node name="Panel" type="PanelContainer" parent="SettingsPanel"]
|
||||
anchors_preset = 8
|
||||
anchor_left = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -200.0
|
||||
offset_top = -200.0
|
||||
offset_right = 200.0
|
||||
offset_bottom = 200.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="VBox" type="VBoxContainer" parent="SettingsPanel/Panel"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 12
|
||||
alignment = 1
|
||||
|
||||
[node name="Title" type="Label" parent="SettingsPanel/Panel/VBox"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 22
|
||||
text = "Settings"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="Spacer" type="Control" parent="SettingsPanel/Panel/VBox"]
|
||||
custom_minimum_size = Vector2(0, 10)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="TouchHeader" type="Label" parent="SettingsPanel/Panel/VBox"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 16
|
||||
text = "Touch Controls"
|
||||
|
||||
[node name="ButtonSizeRow" type="HBoxContainer" parent="SettingsPanel/Panel/VBox"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="SettingsPanel/Panel/VBox/ButtonSizeRow"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
text = "Button Size"
|
||||
|
||||
[node name="ButtonSizeSlider" type="HSlider" parent="SettingsPanel/Panel/VBox/ButtonSizeRow"]
|
||||
custom_minimum_size = Vector2(150, 0)
|
||||
layout_mode = 2
|
||||
min_value = 50.0
|
||||
max_value = 120.0
|
||||
value = 70.0
|
||||
|
||||
[node name="OpacityRow" type="HBoxContainer" parent="SettingsPanel/Panel/VBox"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="SettingsPanel/Panel/VBox/OpacityRow"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
text = "Button Opacity"
|
||||
|
||||
[node name="OpacitySlider" type="HSlider" parent="SettingsPanel/Panel/VBox/OpacityRow"]
|
||||
custom_minimum_size = Vector2(150, 0)
|
||||
layout_mode = 2
|
||||
max_value = 1.0
|
||||
step = 0.1
|
||||
value = 0.7
|
||||
|
||||
[node name="JoystickToggle" type="CheckButton" parent="SettingsPanel/Panel/VBox"]
|
||||
layout_mode = 2
|
||||
button_pressed = true
|
||||
text = "Enable Virtual Joystick"
|
||||
|
||||
[node name="Spacer2" type="Control" parent="SettingsPanel/Panel/VBox"]
|
||||
custom_minimum_size = Vector2(0, 15)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="BackBtn" type="Button" parent="SettingsPanel/Panel/VBox"]
|
||||
custom_minimum_size = Vector2(0, 40)
|
||||
layout_mode = 2
|
||||
text = "Back"
|
||||
|
||||
[connection signal="pressed" from="Menu/Host" to="." method="_on_host_pressed"]
|
||||
[connection signal="pressed" from="Menu/Join" to="." method="_on_join_pressed"]
|
||||
[connection signal="text_submitted" from="MessageInput" to="." method="_on_message_input_text_submitted"]
|
||||
[connection signal="pressed" from="PauseMenu/Panel/VBox/ResumeBtn" to="." method="_on_resume_pressed"]
|
||||
[connection signal="pressed" from="PauseMenu/Panel/VBox/SettingsBtn" to="." method="_on_settings_pressed"]
|
||||
[connection signal="pressed" from="PauseMenu/Panel/VBox/QuitBtn" to="." method="_on_quit_match_pressed"]
|
||||
[connection signal="value_changed" from="SettingsPanel/Panel/VBox/ButtonSizeRow/ButtonSizeSlider" to="." method="_on_button_size_changed"]
|
||||
[connection signal="value_changed" from="SettingsPanel/Panel/VBox/OpacityRow/OpacitySlider" to="." method="_on_opacity_changed"]
|
||||
[connection signal="toggled" from="SettingsPanel/Panel/VBox/JoystickToggle" to="." method="_on_joystick_toggled"]
|
||||
[connection signal="pressed" from="SettingsPanel/Panel/VBox/BackBtn" to="." method="_on_settings_back_pressed"]
|
||||
|
||||
+169
-15
@@ -78,6 +78,17 @@ var spawn_point_selected = false
|
||||
|
||||
# Action for hilighter
|
||||
var highlighted_spawn_points = []
|
||||
var _is_highlighting: bool = false
|
||||
|
||||
# Character selection and animation
|
||||
@onready var anim_player: AnimationPlayer = $AnimationPlayer
|
||||
@onready var character_bob: Node3D = $Bob
|
||||
@onready var character_masbro: Node3D = $Masbro
|
||||
@onready var character_gatot: Node3D = $Gatot
|
||||
@onready var character_oldpop: Node3D = $Oldpop
|
||||
|
||||
var selected_character: String = "Masbro" # Default character (matches tscn default visibility)
|
||||
const AVAILABLE_CHARACTERS: Array[String] = ["Bob", "Masbro", "Gatot", "Oldpop"]
|
||||
|
||||
|
||||
@export var movement_range: int = 1:
|
||||
@@ -124,6 +135,9 @@ func _ready():
|
||||
|
||||
_init_managers()
|
||||
|
||||
# Initialize character selection from LobbyManager
|
||||
_setup_character()
|
||||
|
||||
# Early setup for bots
|
||||
if is_bot == true or is_in_group("Bots"):
|
||||
# Initialize behavior tree for bots
|
||||
@@ -162,11 +176,13 @@ func _ready():
|
||||
enhanced_gridmap.initialize_astar()
|
||||
enhanced_gridmap.set_diagonal_movement(use_diagonal_movement)
|
||||
|
||||
# Request current spawn positions before highlighting
|
||||
request_spawn_positions_update()
|
||||
|
||||
highlight_available_spawn_points()
|
||||
# Remove this line as goals are now managed by the host
|
||||
# Skip manual spawn selection if random spawn is enabled
|
||||
# Host will assign positions via RPC
|
||||
if not LobbyManager.get_randomize_spawn():
|
||||
# Request current spawn positions before highlighting
|
||||
request_spawn_positions_update()
|
||||
highlight_available_spawn_points()
|
||||
# Remove this line as goals are now managed by the host
|
||||
#append_random_goals()
|
||||
|
||||
playerboard.resize(25)
|
||||
@@ -181,21 +197,26 @@ func _ready():
|
||||
if enhanced_gridmap:
|
||||
enhanced_gridmap.initialize_astar()
|
||||
enhanced_gridmap.set_diagonal_movement(use_diagonal_movement)
|
||||
current_position = find_valid_starting_position()
|
||||
update_player_position(current_position)
|
||||
# Only set position if not using random spawn (host will assign via RPC)
|
||||
# AND not already assigned
|
||||
if not LobbyManager.get_randomize_spawn() and not spawn_point_selected:
|
||||
current_position = find_valid_starting_position()
|
||||
update_player_position(current_position)
|
||||
|
||||
#append_random_goals()
|
||||
playerboard.resize(25)
|
||||
playerboard.fill(-1)
|
||||
|
||||
# Ensure proper initial positioning
|
||||
global_position = Vector3(
|
||||
current_position.x * cell_size.x + cell_size.x * 0.5,
|
||||
1.0,
|
||||
current_position.y * cell_size.z + cell_size.z * 0.5
|
||||
)
|
||||
if is_multiplayer_authority():
|
||||
rpc("sync_position", current_position)
|
||||
# Ensure proper initial positioning (only if NOT using random spawn and not already positioned)
|
||||
# When random spawn is enabled, the host assigns positions via set_spawn_position RPC
|
||||
if not LobbyManager.get_randomize_spawn() and not spawn_point_selected:
|
||||
global_position = Vector3(
|
||||
current_position.x * cell_size.x + cell_size.x * 0.5,
|
||||
1.0,
|
||||
current_position.y * cell_size.z + cell_size.z * 0.5
|
||||
)
|
||||
if is_multiplayer_authority():
|
||||
rpc("sync_position", current_position)
|
||||
|
||||
func _init_managers():
|
||||
movement_manager = load("res://scripts/managers/player_movement_manager.gd").new()
|
||||
@@ -233,6 +254,125 @@ func _init_managers():
|
||||
add_child(powerup_manager)
|
||||
powerup_manager.initialize(self, enhanced_gridmap)
|
||||
|
||||
# =============================================================================
|
||||
# Character Selection
|
||||
# =============================================================================
|
||||
|
||||
func set_character(character_name: String) -> void:
|
||||
"""Show only the selected character model and hide others. Updates AnimationPlayer root."""
|
||||
if character_name not in AVAILABLE_CHARACTERS:
|
||||
push_warning("Invalid character name: %s" % character_name)
|
||||
return
|
||||
|
||||
selected_character = character_name
|
||||
|
||||
# Hide all character models
|
||||
if character_bob: character_bob.visible = false
|
||||
if character_masbro: character_masbro.visible = false
|
||||
if character_gatot: character_gatot.visible = false
|
||||
if character_oldpop: character_oldpop.visible = false
|
||||
|
||||
# Show selected character and update AnimationPlayer root
|
||||
var active_character: Node3D = null
|
||||
match character_name:
|
||||
"Bob":
|
||||
if character_bob:
|
||||
character_bob.visible = true
|
||||
active_character = character_bob
|
||||
"Masbro":
|
||||
if character_masbro:
|
||||
character_masbro.visible = true
|
||||
active_character = character_masbro
|
||||
"Gatot":
|
||||
if character_gatot:
|
||||
character_gatot.visible = true
|
||||
active_character = character_gatot
|
||||
"Oldpop":
|
||||
if character_oldpop:
|
||||
character_oldpop.visible = true
|
||||
active_character = character_oldpop
|
||||
|
||||
# Update AnimationPlayer's root node to point to active character
|
||||
if anim_player and active_character:
|
||||
anim_player.root_node = anim_player.get_path_to(active_character)
|
||||
# Start with idle animation
|
||||
play_idle_animation()
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func sync_character(character_name: String) -> void:
|
||||
"""Sync character selection across all clients."""
|
||||
set_character(character_name)
|
||||
|
||||
func _setup_character() -> void:
|
||||
"""Initialize character based on LobbyManager selection or defaults."""
|
||||
var character_name = "Masbro" # Default
|
||||
var player_authority_id = get_multiplayer_authority()
|
||||
|
||||
# Look up character from LobbyManager for this player (works for all players)
|
||||
if LobbyManager:
|
||||
var players = LobbyManager.get_players()
|
||||
for player_data in players:
|
||||
if player_data.get("id") == player_authority_id:
|
||||
character_name = player_data.get("character", "Masbro")
|
||||
break
|
||||
|
||||
set_character(character_name)
|
||||
|
||||
# If this is our local player, also sync to other clients for late joiners
|
||||
if is_multiplayer_authority():
|
||||
rpc("sync_character", character_name)
|
||||
|
||||
# =============================================================================
|
||||
# Animation Functions
|
||||
# =============================================================================
|
||||
|
||||
# Animation speed multiplier for fast-paced gameplay
|
||||
const ANIMATION_SPEED: float = 2.0
|
||||
|
||||
func play_walk_animation() -> void:
|
||||
"""Play walking animation at increased speed."""
|
||||
if anim_player and anim_player.has_animation("animation-pack/walk_forward"):
|
||||
anim_player.play("animation-pack/walk_forward", -1, ANIMATION_SPEED)
|
||||
|
||||
func play_pickup_animation() -> void:
|
||||
"""Play pickup/grab tile animation at increased speed."""
|
||||
if anim_player and anim_player.has_animation("animation-pack/take_tile_1"):
|
||||
anim_player.play("animation-pack/take_tile_1", -1, ANIMATION_SPEED)
|
||||
|
||||
func play_put_animation() -> void:
|
||||
"""Play put/drop tile animation at increased speed."""
|
||||
if anim_player and anim_player.has_animation("animation-pack/drop_tile_1"):
|
||||
anim_player.play("animation-pack/drop_tile_1", -1, ANIMATION_SPEED)
|
||||
|
||||
func play_special_animation() -> void:
|
||||
"""Play special ability animation (backflip) at increased speed."""
|
||||
if anim_player and anim_player.has_animation("animation-pack/backflip_1"):
|
||||
anim_player.play("animation-pack/backflip_1", -1, ANIMATION_SPEED * 1.5)
|
||||
|
||||
func play_idle_animation() -> void:
|
||||
"""Play idle animation at normal speed."""
|
||||
if anim_player and anim_player.has_animation("animation-pack/idle"):
|
||||
anim_player.play("animation-pack/idle")
|
||||
|
||||
# =============================================================================
|
||||
# Screen Shake
|
||||
# =============================================================================
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func trigger_screen_shake(shake_type: String) -> void:
|
||||
"""Trigger screen shake effect. Called via RPC when targeted or completing goals."""
|
||||
var main = get_tree().get_root().get_node_or_null("Main")
|
||||
if main:
|
||||
var screen_shake_manager = main.get_node_or_null("ScreenShakeManager")
|
||||
if screen_shake_manager:
|
||||
match shake_type:
|
||||
"targeted":
|
||||
screen_shake_manager.shake_targeted()
|
||||
"goal":
|
||||
screen_shake_manager.shake_goal_complete()
|
||||
_:
|
||||
screen_shake_manager.shake_light()
|
||||
|
||||
# Add function to check if position is at finish line
|
||||
func is_at_finish_line() -> bool:
|
||||
return race_manager.is_at_finish_line()
|
||||
@@ -1121,6 +1261,20 @@ func sync_position(pos: Vector2i):
|
||||
current_position.y * cell_size.z + cell_size.z * 0.5
|
||||
) + cell_offset
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func set_spawn_position(pos: Vector2i):
|
||||
"""Set spawn position - used by random spawn system."""
|
||||
current_position = pos
|
||||
spawn_point_selected = true
|
||||
# Clear any spawn highlights
|
||||
clear_spawn_highlights()
|
||||
# Update visual position
|
||||
global_position = Vector3(
|
||||
current_position.x * cell_size.x + cell_size.x * 0.5,
|
||||
cell_size.y,
|
||||
current_position.y * cell_size.z + cell_size.z * 0.5
|
||||
) + cell_offset
|
||||
|
||||
func highlight_valid_obstacle_cells():
|
||||
action_manager.highlight_valid_obstacle_cells()
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ spacing_glyph = 5
|
||||
|
||||
[node name="CharacterBody3D" type="CharacterBody3D"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.5, 0)
|
||||
collision_layer = 2
|
||||
script = ExtResource("1_qecr4")
|
||||
cell_size = Vector3(1, 1, 1)
|
||||
use_diagonal_movement = true
|
||||
|
||||
@@ -205,7 +205,9 @@ func on_goal_completed(player: Node, time_remaining: float):
|
||||
|
||||
# Clear playerboard tiles (they convert to powerup bar reward)
|
||||
player.playerboard.fill(-1)
|
||||
player.rpc("sync_playerboard", player.playerboard)
|
||||
# Use main scene's RPC which properly looks up player by ID on each client
|
||||
if main_scene:
|
||||
main_scene.rpc("sync_playerboard", peer_id, player.playerboard)
|
||||
|
||||
# Regenerate goals for this player
|
||||
regenerate_goals_for_player(player)
|
||||
@@ -250,7 +252,9 @@ func _process_cycle_end_for_all_players():
|
||||
|
||||
# Clear playerboard
|
||||
player.playerboard.fill(-1)
|
||||
player.rpc("sync_playerboard", player.playerboard)
|
||||
# Use main scene's RPC which properly looks up player by ID
|
||||
if main_scene:
|
||||
main_scene.rpc("sync_playerboard", peer_id, player.playerboard)
|
||||
|
||||
# Generate new goals
|
||||
regenerate_goals_for_player(player)
|
||||
@@ -291,7 +295,11 @@ func regenerate_goals_for_player(player: Node):
|
||||
int_goals.append(g)
|
||||
|
||||
player.goals = int_goals
|
||||
player.rpc("sync_goals", int_goals)
|
||||
|
||||
# Use main scene's RPC which properly looks up player by ID on each client
|
||||
var peer_id = player.get_multiplayer_authority()
|
||||
if main_scene:
|
||||
main_scene.rpc("sync_player_goals", peer_id, int_goals)
|
||||
|
||||
# =============================================================================
|
||||
# Tile Randomization
|
||||
|
||||
@@ -12,6 +12,7 @@ signal ready_state_changed(player_id: int, is_ready: bool)
|
||||
signal all_players_ready()
|
||||
signal game_starting()
|
||||
signal match_duration_changed(duration_seconds: int)
|
||||
signal randomize_spawn_changed(enabled: bool)
|
||||
signal character_changed(player_id: int, character_name: String)
|
||||
signal area_changed(area_name: String)
|
||||
signal player_list_changed()
|
||||
@@ -26,6 +27,9 @@ var local_player_name: String = "Player"
|
||||
# Match duration in seconds (configurable in lobby by host)
|
||||
var match_duration: int = 180 # Default 3 minutes
|
||||
|
||||
# Randomize spawn locations (configurable in lobby by host)
|
||||
var randomize_spawn: bool = true # Default enabled
|
||||
|
||||
# Character and area selection
|
||||
var available_characters: Array[String] = ["Bob", "Gatot", "Masbro", "Oldpop"]
|
||||
var available_areas: Array[String] = ["Desert", "Forest", "City", "Factory"]
|
||||
@@ -161,6 +165,21 @@ func sync_match_duration(duration_seconds: int) -> void:
|
||||
func get_match_duration() -> int:
|
||||
return match_duration
|
||||
|
||||
func set_randomize_spawn(enabled: bool) -> void:
|
||||
"""Host sets randomize spawn. Syncs to all clients."""
|
||||
randomize_spawn = enabled
|
||||
if is_host:
|
||||
rpc("sync_randomize_spawn", enabled)
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_randomize_spawn(enabled: bool) -> void:
|
||||
"""Sync randomize spawn from host to clients."""
|
||||
randomize_spawn = enabled
|
||||
emit_signal("randomize_spawn_changed", enabled)
|
||||
|
||||
func get_randomize_spawn() -> bool:
|
||||
return randomize_spawn
|
||||
|
||||
# =============================================================================
|
||||
# Character Selection
|
||||
# =============================================================================
|
||||
|
||||
@@ -70,6 +70,10 @@ func simple_move_to(grid_position: Vector2i) -> bool:
|
||||
# All checks passed, perform move
|
||||
rotate_towards_target(grid_position)
|
||||
|
||||
# Play walk animation
|
||||
if player.has_method("play_walk_animation"):
|
||||
player.play_walk_animation()
|
||||
|
||||
var path = [Vector2(player.current_position.x, player.current_position.y), Vector2(grid_position.x, grid_position.y)]
|
||||
path.pop_front()
|
||||
|
||||
|
||||
@@ -54,6 +54,10 @@ func grab_item(grid_position: Vector2i) -> bool:
|
||||
if not player.is_multiplayer_authority():
|
||||
return false
|
||||
|
||||
# Play pickup animation
|
||||
if player.has_method("play_pickup_animation"):
|
||||
player.play_pickup_animation()
|
||||
|
||||
# === Optimistic Local Update (immediate visual feedback) ===
|
||||
# Apply changes locally first, server will validate/sync
|
||||
enhanced_gridmap.set_cell_item(cell, -1) # Remove item visually immediately
|
||||
@@ -82,7 +86,9 @@ func grab_item(grid_position: Vector2i) -> bool:
|
||||
if multiplayer.is_server():
|
||||
# HOST/SERVER: Broadcast to all clients
|
||||
main.rpc("sync_grid_item", cell.x, cell.y, cell.z, -1)
|
||||
player.rpc("sync_playerboard", player.playerboard)
|
||||
# Use main's RPC which properly looks up player by ID on each client
|
||||
var peer_id = player.get_multiplayer_authority()
|
||||
main.rpc("sync_playerboard", peer_id, player.playerboard)
|
||||
player.has_performed_action = true
|
||||
player.consume_action_points(1)
|
||||
player.rpc("force_action_state_none")
|
||||
@@ -138,13 +144,17 @@ func _execute_grab(grid_pos: Vector2i, cell: Vector3i, item_id: int):
|
||||
player.playerboard[target_slot] = item_id
|
||||
|
||||
# 3c. Broadcast the new playerboard state to all clients
|
||||
player.rpc("sync_playerboard", player.playerboard)
|
||||
var peer_id = player.get_multiplayer_authority()
|
||||
main.rpc("sync_playerboard", peer_id, player.playerboard)
|
||||
|
||||
# 3d. Consume action points
|
||||
# 3d. Check if goal is completed (SERVER-SIDE - this triggers goal regeneration for clients!)
|
||||
_check_goal_completion()
|
||||
|
||||
# 3e. Consume action points
|
||||
player.has_performed_action = true
|
||||
player.consume_action_points(1)
|
||||
|
||||
# 3e. Reset the UI for the player who acted
|
||||
# 3f. Reset the UI for the player who acted
|
||||
player.rpc("force_action_state_none")
|
||||
|
||||
return true
|
||||
@@ -306,6 +316,10 @@ func auto_put_item() -> bool:
|
||||
var cell = Vector3i(target_pos.x, 1, target_pos.y)
|
||||
|
||||
if player.is_multiplayer_authority():
|
||||
# Play put animation
|
||||
if player.has_method("play_put_animation"):
|
||||
player.play_put_animation()
|
||||
|
||||
# === Optimistic Local Update (immediate visual feedback) ===
|
||||
enhanced_gridmap.set_cell_item(cell, item) # Add item to grid visually immediately
|
||||
player.playerboard[put_slot] = -1 # Remove from playerboard immediately
|
||||
@@ -627,6 +641,10 @@ func _check_goal_completion():
|
||||
if powerup_manager:
|
||||
powerup_manager.add_goal_completion_reward()
|
||||
|
||||
# Trigger screen shake for goal completion
|
||||
if player.is_multiplayer_authority() and player.has_method("trigger_screen_shake"):
|
||||
player.trigger_screen_shake("goal")
|
||||
|
||||
# Notify GoalsCycleManager for scoring
|
||||
var main = player.get_tree().get_root().get_node_or_null("Main")
|
||||
if main:
|
||||
|
||||
@@ -6,6 +6,7 @@ const MAX_POINTS: int = 12
|
||||
const POINTS_PER_BAR: int = 4
|
||||
const MAX_BARS: int = 4
|
||||
const HOLO_PICKUPS_PER_BAR: int = 4
|
||||
const SPECIAL_COOLDOWN: float = 4.0 # 4 second cooldown
|
||||
|
||||
var player: Node3D
|
||||
var enhanced_gridmap: Node
|
||||
@@ -13,6 +14,7 @@ var enhanced_gridmap: Node
|
||||
# Power-up state
|
||||
var current_points: int = 0
|
||||
var holo_pickup_count: int = 0
|
||||
var special_cooldown_timer: float = 0.0 # Current cooldown remaining
|
||||
|
||||
signal points_changed(current: int, max_points: int)
|
||||
signal bar_filled()
|
||||
@@ -21,6 +23,12 @@ signal effect_used()
|
||||
func initialize(p_player: Node3D, p_gridmap: Node):
|
||||
player = p_player
|
||||
enhanced_gridmap = p_gridmap
|
||||
set_process(true)
|
||||
|
||||
func _process(delta):
|
||||
# Update cooldown timer
|
||||
if special_cooldown_timer > 0:
|
||||
special_cooldown_timer -= delta
|
||||
|
||||
# =============================================================================
|
||||
# Holo Tile Pickup
|
||||
@@ -79,17 +87,29 @@ func use_special_effect():
|
||||
player.rpc("display_message", "Not enough power-up!", 3)
|
||||
return false
|
||||
|
||||
# Check cooldown
|
||||
if special_cooldown_timer > 0:
|
||||
player.rpc("display_message", "Special on cooldown! (%.1fs)" % special_cooldown_timer, 3)
|
||||
return false
|
||||
|
||||
# Consume 1 bar
|
||||
current_points -= POINTS_PER_BAR
|
||||
emit_signal("effect_used")
|
||||
emit_signal("points_changed", current_points, MAX_POINTS)
|
||||
|
||||
# Start cooldown
|
||||
special_cooldown_timer = SPECIAL_COOLDOWN
|
||||
|
||||
# Play special animation (backflip)
|
||||
if player.has_method("play_special_animation"):
|
||||
player.play_special_animation()
|
||||
|
||||
# Trigger random special effect via SpecialTilesManager
|
||||
var special_tiles_manager = player.get_node_or_null("SpecialTilesManager")
|
||||
if special_tiles_manager:
|
||||
special_tiles_manager.trigger_random_effect()
|
||||
|
||||
print("[PowerUp] Player %s used special effect! Remaining: %d/%d points" % [player.name, current_points, MAX_POINTS])
|
||||
print("[PowerUp] Player %s used special effect! Remaining: %d/%d points, Cooldown: %.1fs" % [player.name, current_points, MAX_POINTS, SPECIAL_COOLDOWN])
|
||||
|
||||
if player.is_multiplayer_authority():
|
||||
rpc("sync_points", current_points)
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
extends Node
|
||||
|
||||
# ScreenShakeManager - Handles camera shake effects for impact feedback
|
||||
|
||||
var camera: Camera3D
|
||||
var shake_intensity: float = 0.0
|
||||
var shake_duration: float = 0.0
|
||||
var shake_timer: float = 0.0
|
||||
var original_position: Vector3
|
||||
|
||||
# Shake presets
|
||||
const SHAKE_TARGETED: Dictionary = {"intensity": 0.15, "duration": 0.4}
|
||||
const SHAKE_GOAL_COMPLETE: Dictionary = {"intensity": 0.1, "duration": 0.3}
|
||||
const SHAKE_LIGHT: Dictionary = {"intensity": 0.05, "duration": 0.2}
|
||||
|
||||
func initialize(p_camera: Camera3D):
|
||||
"""Initialize with specific camera instance."""
|
||||
camera = p_camera
|
||||
if camera:
|
||||
original_position = camera.position
|
||||
print("[ScreenShakeManager] Initialized with camera: ", camera.name)
|
||||
else:
|
||||
push_warning("[ScreenShakeManager] Initialized with null camera")
|
||||
|
||||
func _process(delta):
|
||||
if shake_timer > 0:
|
||||
shake_timer -= delta
|
||||
if camera:
|
||||
var shake_offset = Vector3(
|
||||
randf_range(-shake_intensity, shake_intensity),
|
||||
randf_range(-shake_intensity, shake_intensity),
|
||||
randf_range(-shake_intensity * 0.5, shake_intensity * 0.5)
|
||||
)
|
||||
camera.position = original_position + shake_offset
|
||||
|
||||
if shake_timer <= 0:
|
||||
_reset_camera()
|
||||
|
||||
func _reset_camera():
|
||||
if camera:
|
||||
camera.position = original_position
|
||||
shake_timer = 0.0
|
||||
shake_intensity = 0.0
|
||||
|
||||
func shake(intensity: float, duration: float):
|
||||
"""Trigger camera shake with given intensity and duration."""
|
||||
if not camera:
|
||||
push_warning("Screen shake requested but no camera assigned!")
|
||||
return
|
||||
|
||||
if camera:
|
||||
# If already shaking, reset camera first to get true original position
|
||||
if shake_timer > 0:
|
||||
camera.position = original_position
|
||||
else:
|
||||
original_position = camera.position
|
||||
|
||||
shake_intensity = intensity
|
||||
shake_duration = duration
|
||||
shake_timer = duration
|
||||
|
||||
func shake_targeted():
|
||||
"""Called when local player is targeted by opponent's powerup."""
|
||||
shake(SHAKE_TARGETED.intensity, SHAKE_TARGETED.duration)
|
||||
|
||||
func shake_goal_complete():
|
||||
"""Called when local player completes a goal."""
|
||||
shake(SHAKE_GOAL_COMPLETE.intensity, SHAKE_GOAL_COMPLETE.duration)
|
||||
|
||||
func shake_light():
|
||||
"""Light shake for minor events."""
|
||||
shake(SHAKE_LIGHT.intensity, SHAKE_LIGHT.duration)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bg43ds58dip7u
|
||||
@@ -139,6 +139,12 @@ func _execute_burn_tiles():
|
||||
if main:
|
||||
main.rpc("sync_grid_item", cell.x, cell.y, cell.z, -1)
|
||||
|
||||
# Trigger screen shake on the targeted opponent
|
||||
if opponent.is_multiplayer_authority():
|
||||
opponent.rpc("trigger_screen_shake", "targeted")
|
||||
else:
|
||||
opponent.rpc_id(opponent.get_multiplayer_authority(), "trigger_screen_shake", "targeted")
|
||||
|
||||
print("[SpecialTiles] BURN_TILES: Removed %d tiles around %s" % [positions.size(), opponent.name])
|
||||
player.rpc("display_message", "Burned tiles near opponent!")
|
||||
|
||||
@@ -177,6 +183,12 @@ func _execute_freeze_player():
|
||||
opponent.set("is_frozen", true)
|
||||
_create_unfreeze_timer(opponent, FREEZE_DURATION)
|
||||
|
||||
# Trigger screen shake on the frozen opponent
|
||||
if opponent.is_multiplayer_authority():
|
||||
opponent.rpc("trigger_screen_shake", "targeted")
|
||||
else:
|
||||
opponent.rpc_id(opponent.get_multiplayer_authority(), "trigger_screen_shake", "targeted")
|
||||
|
||||
print("[SpecialTiles] FREEZE_PLAYER: Froze %s for %ds" % [opponent.name, FREEZE_DURATION])
|
||||
player.rpc("display_message", "Froze an opponent!")
|
||||
opponent.rpc("display_message", "You are frozen!")
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
extends CanvasLayer
|
||||
|
||||
# TouchControlsManager - Manages mobile touch controls including virtual joystick and action buttons
|
||||
|
||||
signal grab_pressed
|
||||
signal put_pressed
|
||||
signal special_pressed
|
||||
|
||||
# Touch control nodes
|
||||
var virtual_joystick: Control
|
||||
var grab_button: Button
|
||||
var put_button: Button
|
||||
var special_button: Button
|
||||
var settings_button: Button
|
||||
|
||||
# Settings - persisted to config file
|
||||
const CONFIG_PATH = "user://touch_controls_settings.cfg"
|
||||
var button_size: float = 70.0
|
||||
var button_opacity: float = 0.7
|
||||
var joystick_enabled: bool = true
|
||||
var touch_buttons_enabled: bool = true # Master toggle for action buttons (grab, put, special)
|
||||
var joystick_position: Vector2 = Vector2(120, -120) # Relative to bottom-left
|
||||
var button_positions: Dictionary = {
|
||||
"grab": Vector2(-200, -240), # Relative to bottom-right
|
||||
"put": Vector2(-120, -160),
|
||||
"special": Vector2(-200, -80)
|
||||
}
|
||||
var button_scale: float = 1.0
|
||||
|
||||
# Reference to main scene and player
|
||||
var main_scene: Node3D
|
||||
var local_player: Node3D
|
||||
|
||||
func initialize(p_main: Node3D):
|
||||
main_scene = p_main
|
||||
_create_touch_ui()
|
||||
_load_settings()
|
||||
|
||||
func set_player(p_player: Node3D):
|
||||
local_player = p_player
|
||||
|
||||
func _create_touch_ui():
|
||||
print("[TouchControls] Creating touch UI...")
|
||||
# Use layer 10 - above regular UI but below pause menu
|
||||
layer = 10
|
||||
|
||||
# Create main container
|
||||
var container = Control.new()
|
||||
container.name = "TouchControls"
|
||||
container.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
container.mouse_filter = Control.MOUSE_FILTER_PASS # Pass input to children
|
||||
add_child(container)
|
||||
|
||||
# Create virtual joystick (bottom-left)
|
||||
var joystick_script = load("res://scripts/ui/virtual_joystick.gd")
|
||||
virtual_joystick = Control.new()
|
||||
virtual_joystick.set_script(joystick_script)
|
||||
virtual_joystick.name = "VirtualJoystick"
|
||||
virtual_joystick.set_anchors_preset(Control.PRESET_BOTTOM_LEFT)
|
||||
|
||||
# Use standard size from joystick script defaults (radius 60 -> size 160)
|
||||
var joy_size = Vector2(160, 160)
|
||||
virtual_joystick.custom_minimum_size = joy_size
|
||||
virtual_joystick.size = joy_size
|
||||
|
||||
# Position relative to Bottom-Left anchor
|
||||
# joystick_position (120, -120) interpreted as margin from anchor
|
||||
# x=120 (right from left edge), y=-120 (up from bottom edge - implies bottom margin)
|
||||
# We want the *center* or *bottom-left* corner?
|
||||
# Assuming (120, -120) is top-left corner of the control relative to anchor?
|
||||
# Let's align bottom-left corner of control to (120, -120) from screen bottom-left
|
||||
# Screen Bottom-Left is (0, 1) in normalized anchors.
|
||||
# offset_left = 120
|
||||
# offset_bottom = -120 (120px up from bottom)
|
||||
# offset_top = -120 - 160 = -280
|
||||
# offset_right = 120 + 160 = 280
|
||||
|
||||
virtual_joystick.offset_left = 120
|
||||
virtual_joystick.offset_top = -280
|
||||
virtual_joystick.offset_right = 280
|
||||
virtual_joystick.offset_bottom = -120
|
||||
|
||||
virtual_joystick.direction_changed.connect(_on_joystick_direction)
|
||||
container.add_child(virtual_joystick)
|
||||
|
||||
# Create action buttons (bottom-right)
|
||||
grab_button = _create_action_button("Grab", "👋", button_positions.grab)
|
||||
put_button = _create_action_button("Put", "📦", button_positions.put)
|
||||
special_button = _create_action_button("Special", "⚡", button_positions.special)
|
||||
|
||||
container.add_child(grab_button)
|
||||
container.add_child(put_button)
|
||||
container.add_child(special_button)
|
||||
|
||||
# Create settings button (top-right corner)
|
||||
settings_button = Button.new()
|
||||
settings_button.name = "SettingsBtn"
|
||||
settings_button.text = "⚙"
|
||||
settings_button.set_anchors_preset(Control.PRESET_TOP_RIGHT)
|
||||
settings_button.offset_left = -70 # Use offsets instead of position for anchored controls
|
||||
settings_button.offset_right = -20
|
||||
settings_button.offset_top = 70
|
||||
settings_button.offset_bottom = 120
|
||||
settings_button.custom_minimum_size = Vector2(50, 50)
|
||||
settings_button.mouse_filter = Control.MOUSE_FILTER_STOP # Ensure it receives input
|
||||
settings_button.pressed.connect(_on_settings_pressed)
|
||||
_style_button(settings_button, 0.5)
|
||||
container.add_child(settings_button)
|
||||
|
||||
# Always visible now - controlled by settings toggle
|
||||
# Can be hidden via settings if user doesn't want touch controls on desktop
|
||||
visible = true
|
||||
|
||||
func _create_action_button(button_name: String, icon: String, pos: Vector2) -> Button:
|
||||
var btn = Button.new()
|
||||
btn.name = button_name + "Btn"
|
||||
btn.text = icon
|
||||
btn.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
|
||||
# Use offsets strictly for anchored positioning
|
||||
# pos.x and pos.y are negative offsets from bottom-right (e.g. -200, -240)
|
||||
btn.offset_left = pos.x
|
||||
btn.offset_top = pos.y
|
||||
btn.offset_right = pos.x + button_size
|
||||
btn.offset_bottom = pos.y + button_size
|
||||
|
||||
btn.custom_minimum_size = Vector2(button_size, button_size)
|
||||
btn.pivot_offset = Vector2(button_size / 2, button_size / 2) # Center pivot for scaling
|
||||
|
||||
# Connect signals
|
||||
btn.button_down.connect(func(): _on_button_pressed(button_name))
|
||||
btn.button_up.connect(func(): _on_button_released(button_name))
|
||||
|
||||
_style_button(btn, button_opacity)
|
||||
return btn
|
||||
|
||||
func _style_button(btn: Button, opacity: float):
|
||||
var style = StyleBoxFlat.new()
|
||||
style.bg_color = Color(0.2, 0.2, 0.25, opacity)
|
||||
style.border_width_left = 2
|
||||
style.border_width_top = 2
|
||||
style.border_width_right = 2
|
||||
style.border_width_bottom = 2
|
||||
style.border_color = Color(0.647, 0.996, 0.224, 0.8)
|
||||
style.corner_radius_top_left = 15
|
||||
style.corner_radius_top_right = 15
|
||||
style.corner_radius_bottom_right = 15
|
||||
style.corner_radius_bottom_left = 15
|
||||
btn.add_theme_stylebox_override("normal", style)
|
||||
|
||||
var pressed_style = style.duplicate()
|
||||
pressed_style.bg_color = Color(0.4, 0.8, 0.4, opacity + 0.2)
|
||||
btn.add_theme_stylebox_override("pressed", pressed_style)
|
||||
|
||||
var hover_style = style.duplicate()
|
||||
hover_style.bg_color = Color(0.3, 0.3, 0.35, opacity)
|
||||
btn.add_theme_stylebox_override("hover", hover_style)
|
||||
|
||||
btn.add_theme_font_size_override("font_size", 28)
|
||||
|
||||
func _on_joystick_direction(direction: Vector2i):
|
||||
if local_player and local_player.has_method("simple_move_to"):
|
||||
var target_pos = local_player.current_position + direction
|
||||
local_player.movement_manager.simple_move_to(target_pos)
|
||||
|
||||
func _on_button_pressed(button_name: String):
|
||||
if not local_player:
|
||||
return
|
||||
|
||||
# Visual feedback - scale up
|
||||
var btn: Button
|
||||
match button_name:
|
||||
"Grab": btn = grab_button
|
||||
"Put": btn = put_button
|
||||
"Special": btn = special_button
|
||||
|
||||
if btn:
|
||||
var tween = create_tween()
|
||||
tween.tween_property(btn, "scale", Vector2(1.15, 1.15), 0.1)
|
||||
|
||||
# Trigger action
|
||||
match button_name:
|
||||
"Grab":
|
||||
emit_signal("grab_pressed")
|
||||
if local_player.has_method("grab_item"):
|
||||
local_player.grab_item(local_player.current_position)
|
||||
"Put":
|
||||
emit_signal("put_pressed")
|
||||
if local_player.has_method("auto_put_item"):
|
||||
local_player.auto_put_item()
|
||||
"Special":
|
||||
emit_signal("special_pressed")
|
||||
var powerup_mgr = local_player.get_node_or_null("PowerUpManager")
|
||||
if powerup_mgr and powerup_mgr.has_method("use_special_effect"):
|
||||
powerup_mgr.use_special_effect()
|
||||
|
||||
func _on_button_released(button_name: String):
|
||||
var btn: Button
|
||||
match button_name:
|
||||
"Grab": btn = grab_button
|
||||
"Put": btn = put_button
|
||||
"Special": btn = special_button
|
||||
|
||||
if btn:
|
||||
var tween = create_tween()
|
||||
tween.tween_property(btn, "scale", Vector2(1.0, 1.0), 0.1)
|
||||
|
||||
func _on_settings_pressed():
|
||||
# Open settings panel in main scene
|
||||
if main_scene:
|
||||
var settings_panel = main_scene.get_node_or_null("SettingsPanel")
|
||||
if settings_panel:
|
||||
settings_panel.visible = true
|
||||
print("[TouchControls] Opening settings panel")
|
||||
else:
|
||||
print("[TouchControls] SettingsPanel not found in main scene")
|
||||
|
||||
func _is_touch_device() -> bool:
|
||||
# Check if running on mobile
|
||||
return OS.has_feature("android") or OS.has_feature("ios") or OS.has_feature("web_android") or OS.has_feature("web_ios")
|
||||
|
||||
func _load_settings():
|
||||
"""Load touch control settings from config file."""
|
||||
var config = ConfigFile.new()
|
||||
var err = config.load(CONFIG_PATH)
|
||||
if err != OK:
|
||||
print("[TouchControls] No saved settings found, using defaults")
|
||||
return
|
||||
|
||||
# Load settings values
|
||||
button_size = config.get_value("touch_controls", "button_size", 70.0)
|
||||
button_opacity = config.get_value("touch_controls", "button_opacity", 0.7)
|
||||
button_scale = config.get_value("touch_controls", "button_scale", 1.0)
|
||||
joystick_enabled = config.get_value("touch_controls", "joystick_enabled", true)
|
||||
touch_buttons_enabled = config.get_value("touch_controls", "touch_buttons_enabled", true)
|
||||
|
||||
# Load button positions
|
||||
var grab_pos = config.get_value("touch_controls", "grab_position", Vector2(-200, -240))
|
||||
var put_pos = config.get_value("touch_controls", "put_position", Vector2(-120, -160))
|
||||
var special_pos = config.get_value("touch_controls", "special_position", Vector2(-200, -80))
|
||||
button_positions = {"grab": grab_pos, "put": put_pos, "special": special_pos}
|
||||
|
||||
# Apply loaded settings
|
||||
_apply_settings()
|
||||
print("[TouchControls] Settings loaded")
|
||||
|
||||
func _save_settings():
|
||||
"""Save touch control settings to config file."""
|
||||
var config = ConfigFile.new()
|
||||
|
||||
config.set_value("touch_controls", "button_size", button_size)
|
||||
config.set_value("touch_controls", "button_opacity", button_opacity)
|
||||
config.set_value("touch_controls", "button_scale", button_scale)
|
||||
config.set_value("touch_controls", "joystick_enabled", joystick_enabled)
|
||||
config.set_value("touch_controls", "touch_buttons_enabled", touch_buttons_enabled)
|
||||
config.set_value("touch_controls", "grab_position", button_positions.grab)
|
||||
config.set_value("touch_controls", "put_position", button_positions.put)
|
||||
config.set_value("touch_controls", "special_position", button_positions.special)
|
||||
|
||||
var err = config.save(CONFIG_PATH)
|
||||
if err != OK:
|
||||
push_error("[TouchControls] Failed to save settings: %s" % err)
|
||||
else:
|
||||
print("[TouchControls] Settings saved")
|
||||
|
||||
func _apply_settings():
|
||||
"""Apply current settings to UI elements."""
|
||||
# Apply joystick visibility
|
||||
if virtual_joystick:
|
||||
virtual_joystick.visible = joystick_enabled
|
||||
|
||||
# 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
|
||||
|
||||
print("[TouchControls] Applying settings: JoystickEnabled=", joystick_enabled, " ButtonsVisible=", buttons_visible)
|
||||
|
||||
if grab_button:
|
||||
grab_button.visible = buttons_visible
|
||||
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
|
||||
|
||||
if put_button:
|
||||
put_button.visible = buttons_visible
|
||||
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 special_button:
|
||||
special_button.visible = buttons_visible
|
||||
special_button.scale = Vector2(button_scale, button_scale)
|
||||
special_button.offset_left = button_positions.special.x
|
||||
special_button.offset_top = button_positions.special.y
|
||||
special_button.offset_right = button_positions.special.x + button_size
|
||||
special_button.offset_bottom = button_positions.special.y + button_size
|
||||
|
||||
# Force layer update
|
||||
visible = true
|
||||
|
||||
# =============================================================================
|
||||
# Public Settings API
|
||||
# =============================================================================
|
||||
|
||||
func set_touch_buttons_enabled(enabled: bool):
|
||||
"""Enable or disable all action buttons (grab, put, special)."""
|
||||
touch_buttons_enabled = enabled
|
||||
_apply_settings()
|
||||
|
||||
func set_joystick_enabled(enabled: bool):
|
||||
"""Enable or disable the virtual joystick (and all touch controls)."""
|
||||
joystick_enabled = enabled
|
||||
_apply_settings()
|
||||
|
||||
func set_button_scale(p_scale: float):
|
||||
"""Set scale for all action buttons."""
|
||||
button_scale = p_scale
|
||||
_apply_settings()
|
||||
|
||||
func set_button_position(button_name: String, new_position: Vector2):
|
||||
"""Update position of a specific button."""
|
||||
button_positions[button_name] = new_position
|
||||
match button_name:
|
||||
"grab":
|
||||
if grab_button:
|
||||
grab_button.offset_left = new_position.x
|
||||
grab_button.offset_top = new_position.y
|
||||
grab_button.offset_right = new_position.x + button_size
|
||||
grab_button.offset_bottom = new_position.y + button_size
|
||||
"put":
|
||||
if put_button:
|
||||
put_button.offset_left = new_position.x
|
||||
put_button.offset_top = new_position.y
|
||||
put_button.offset_right = new_position.x + button_size
|
||||
put_button.offset_bottom = new_position.y + button_size
|
||||
"special":
|
||||
if special_button:
|
||||
special_button.offset_left = new_position.x
|
||||
special_button.offset_top = new_position.y
|
||||
special_button.offset_right = new_position.x + button_size
|
||||
special_button.offset_bottom = new_position.y + button_size
|
||||
|
||||
func get_button_positions() -> Dictionary:
|
||||
"""Get current button positions for settings UI."""
|
||||
return button_positions.duplicate()
|
||||
|
||||
func get_settings() -> Dictionary:
|
||||
"""Get all current settings as a dictionary."""
|
||||
return {
|
||||
"button_size": button_size,
|
||||
"button_opacity": button_opacity,
|
||||
"button_scale": button_scale,
|
||||
"joystick_enabled": joystick_enabled,
|
||||
"touch_buttons_enabled": touch_buttons_enabled,
|
||||
"button_positions": button_positions.duplicate()
|
||||
}
|
||||
|
||||
func show_controls():
|
||||
visible = true
|
||||
|
||||
func hide_controls():
|
||||
visible = false
|
||||
@@ -0,0 +1 @@
|
||||
uid://b54tfa0n6kogi
|
||||
@@ -20,6 +20,7 @@ var arrange_button
|
||||
var playerboard_ui
|
||||
|
||||
var local_player_character
|
||||
var _previous_playerboard_state: Array = []
|
||||
|
||||
enum ActionState {
|
||||
NONE,
|
||||
@@ -122,9 +123,6 @@ func update_playerboard_ui():
|
||||
if center_index != -1 and center_index < goals.size():
|
||||
var goal_value = goals[center_index]
|
||||
|
||||
# Check if player has collected this tile
|
||||
var _has_tile = item == goal_value
|
||||
|
||||
if item != -1:
|
||||
# Player has a tile in this slot - show it at full brightness
|
||||
match item:
|
||||
@@ -150,6 +148,35 @@ func update_playerboard_ui():
|
||||
8: slot.texture = item_tex[2]
|
||||
9: slot.texture = item_tex[3]
|
||||
10: slot.texture = item_tex[4]
|
||||
# Non-center slots always full brightness
|
||||
slot.modulate = Color.WHITE
|
||||
|
||||
# Check for new special tile placement to trigger effect
|
||||
if i < _previous_playerboard_state.size():
|
||||
var prev_item = _previous_playerboard_state[i]
|
||||
# If slot was empty or different, and now has a special tile (7-10)
|
||||
if item != prev_item and item >= 7 and item <= 10:
|
||||
_pulse_slot_effect(slot)
|
||||
|
||||
# Update cache
|
||||
_previous_playerboard_state = local_player_character.playerboard.duplicate()
|
||||
|
||||
func _pulse_slot_effect(slot: Control):
|
||||
"""Visual feedback when a special tile is placed."""
|
||||
var tween = create_tween()
|
||||
|
||||
# Reset scale first to be safe
|
||||
slot.scale = Vector2.ONE
|
||||
slot.pivot_offset = slot.size / 2 # Center pivot
|
||||
|
||||
# Pop effect
|
||||
tween.tween_property(slot, "scale", Vector2(1.4, 1.4), 0.15).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
||||
tween.tween_property(slot, "scale", Vector2(1.0, 1.0), 0.2).set_trans(Tween.TRANS_BOUNCE).set_ease(Tween.EASE_OUT)
|
||||
|
||||
# Flash effect
|
||||
var original_modulate = slot.modulate
|
||||
slot.modulate = Color(1.5, 1.5, 1.5) # Overbright
|
||||
tween.parallel().tween_property(slot, "modulate", original_modulate, 0.3)
|
||||
|
||||
func update_button_states():
|
||||
if not local_player_character or local_player_character.is_in_group("Bots"):
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
extends Control
|
||||
|
||||
# VirtualJoystick - Touch joystick for mobile movement control
|
||||
# Provides 8-directional movement input
|
||||
|
||||
signal direction_changed(direction: Vector2i)
|
||||
signal joystick_released
|
||||
|
||||
@export var dead_zone: float = 0.2
|
||||
@export var clamp_zone: float = 0.8
|
||||
@export var joystick_radius: float = 60.0
|
||||
@export var knob_radius: float = 25.0
|
||||
@export var repeat_delay: float = 0.3 # Initial delay before repeat
|
||||
@export var repeat_rate: float = 0.15 # Repeat rate for continuous movement
|
||||
|
||||
var base_color: Color = Color(1, 1, 1, 0.4)
|
||||
var knob_color: Color = Color(1, 1, 1, 0.7)
|
||||
var pressed_color: Color = Color(0.4, 0.9, 0.4, 0.8)
|
||||
|
||||
var is_pressed: bool = false
|
||||
var touch_index: int = -1
|
||||
var center_position: Vector2
|
||||
var current_direction: Vector2 = Vector2.ZERO
|
||||
var last_grid_direction: Vector2i = Vector2i.ZERO
|
||||
|
||||
var _repeat_timer: float = 0.0
|
||||
var _initial_repeat: bool = true
|
||||
|
||||
func _ready():
|
||||
# Set minimum size for touch target
|
||||
custom_minimum_size = Vector2(joystick_radius * 2 + 40, joystick_radius * 2 + 40)
|
||||
center_position = size / 2
|
||||
|
||||
# Enable touch input
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
set_process(true)
|
||||
|
||||
func _draw():
|
||||
# Draw base circle
|
||||
var base_circle_color = pressed_color if is_pressed else base_color
|
||||
draw_circle(center_position, joystick_radius, base_circle_color)
|
||||
draw_arc(center_position, joystick_radius, 0, TAU, 64, Color.WHITE, 2.0)
|
||||
|
||||
# Draw knob
|
||||
var knob_pos = center_position + current_direction * joystick_radius * clamp_zone
|
||||
var knob_circle_color = pressed_color if is_pressed else knob_color
|
||||
draw_circle(knob_pos, knob_radius, knob_circle_color)
|
||||
draw_arc(knob_pos, knob_radius, 0, TAU, 32, Color.WHITE, 1.5)
|
||||
|
||||
# Draw direction indicators (8 directions)
|
||||
for i in range(8):
|
||||
var angle = i * TAU / 8
|
||||
var line_start = center_position + Vector2.from_angle(angle) * (joystick_radius * 0.6)
|
||||
var line_end = center_position + Vector2.from_angle(angle) * (joystick_radius * 0.9)
|
||||
draw_line(line_start, line_end, Color(1, 1, 1, 0.3), 2.0)
|
||||
|
||||
func _process(delta: float):
|
||||
# Handle continuous movement while holding joystick
|
||||
if is_pressed and last_grid_direction != Vector2i.ZERO:
|
||||
_repeat_timer -= delta
|
||||
if _repeat_timer <= 0:
|
||||
emit_signal("direction_changed", last_grid_direction)
|
||||
_repeat_timer = repeat_rate
|
||||
_initial_repeat = false
|
||||
|
||||
func _gui_input(event: InputEvent):
|
||||
if event is InputEventScreenTouch:
|
||||
if event.pressed:
|
||||
_start_touch(event.index, event.position)
|
||||
elif event.index == touch_index:
|
||||
_end_touch()
|
||||
|
||||
elif event is InputEventScreenDrag:
|
||||
if event.index == touch_index:
|
||||
_update_touch(event.position)
|
||||
|
||||
# Mouse support for testing
|
||||
elif event is InputEventMouseButton:
|
||||
if event.button_index == MOUSE_BUTTON_LEFT:
|
||||
if event.pressed:
|
||||
_start_touch(0, event.position)
|
||||
else:
|
||||
_end_touch()
|
||||
|
||||
elif event is InputEventMouseMotion:
|
||||
if is_pressed and touch_index == 0:
|
||||
_update_touch(event.position)
|
||||
|
||||
func _start_touch(index: int, pos: Vector2):
|
||||
is_pressed = true
|
||||
touch_index = index
|
||||
_repeat_timer = repeat_delay # Use longer initial delay
|
||||
_initial_repeat = true
|
||||
_update_touch(pos)
|
||||
queue_redraw()
|
||||
|
||||
func _end_touch():
|
||||
is_pressed = false
|
||||
touch_index = -1
|
||||
current_direction = Vector2.ZERO
|
||||
last_grid_direction = Vector2i.ZERO
|
||||
_repeat_timer = 0.0
|
||||
_initial_repeat = true
|
||||
emit_signal("joystick_released")
|
||||
queue_redraw()
|
||||
|
||||
func _update_touch(pos: Vector2):
|
||||
var diff = pos - center_position
|
||||
var distance = diff.length()
|
||||
|
||||
if distance > 0:
|
||||
current_direction = diff.normalized() * clampf(distance / joystick_radius, 0, clamp_zone)
|
||||
else:
|
||||
current_direction = Vector2.ZERO
|
||||
|
||||
# Convert to 8-directional grid movement
|
||||
var grid_dir = _get_grid_direction(current_direction)
|
||||
if grid_dir != last_grid_direction:
|
||||
last_grid_direction = grid_dir
|
||||
if grid_dir != Vector2i.ZERO:
|
||||
emit_signal("direction_changed", grid_dir)
|
||||
|
||||
queue_redraw()
|
||||
|
||||
func _get_grid_direction(dir: Vector2) -> Vector2i:
|
||||
if dir.length() < dead_zone:
|
||||
return Vector2i.ZERO
|
||||
|
||||
# Determine 8-directional output
|
||||
var angle = dir.angle()
|
||||
|
||||
# Divide circle into 8 sectors (each 45 degrees)
|
||||
var sector = int(round(angle / (TAU / 8))) % 8
|
||||
if sector < 0:
|
||||
sector += 8
|
||||
|
||||
match sector:
|
||||
0: return Vector2i(1, 0) # East
|
||||
1: return Vector2i(1, 1) # Southeast
|
||||
2: return Vector2i(0, 1) # South
|
||||
3: return Vector2i(-1, 1) # Southwest
|
||||
4: return Vector2i(-1, 0) # West
|
||||
5: return Vector2i(-1, -1) # Northwest
|
||||
6: return Vector2i(0, -1) # North
|
||||
7: return Vector2i(1, -1) # Northeast
|
||||
|
||||
return Vector2i.ZERO
|
||||
|
||||
func get_direction() -> Vector2i:
|
||||
"""Get the current grid direction for polling."""
|
||||
return last_grid_direction
|
||||
@@ -0,0 +1 @@
|
||||
uid://djiml4sh61dc1
|
||||
Reference in New Issue
Block a user