feat: Implement cycle timer toggle, refactor continuous input, and improve movement synchronization.
This commit is contained in:
Vendored
+1
-1
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"godotTools.editorPath.godot4": "c:\\Users\\danchie\\Documents\\Godot 4.4\\Godot_v4.4-stable_win64.exe"
|
||||
"godotTools.editorPath.godot4": "/home/beng/Documents/Godots/Godot_v4.4.1-stable_linux.x86_64"
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
[ ADT's Report ]
|
||||
|
||||
Updated the `tekton-enet` ( Armageddon Multiplayer ) on branch `launcher`
|
||||
|
||||
**Movement & Responsiveness**
|
||||
|
||||
✅ **Smooth Grid Movement** - Replaced "jittery" `EASE_IN_OUT` tweening with `TRANS_LINEAR` for fluid, constant-speed movement. Introduced input buffering to allow players to queue their next move seamlessly, eliminating "stop-start" delays between tiles.
|
||||
|
||||
✅ **Precise Control** - Refined the movement logic to enforce a strict "One-Tap = One-Tile" rule, preventing accidental double-movements while maintaining the ability to hold keys for continuous motion.
|
||||
|
||||
✅ **Rotation Fix** - Unlocked character rotation during movement animations. Characters now naturally face their intended direction immediately, even when chaining rapid turns.
|
||||
|
||||
**Lobby UI & Settings**
|
||||
|
||||
✅ **Disable Reset Timer** - Added a host-controlled option to disable the automatic 30-second playerboard shuffle. This allows for "endless" rounds that only reset upon goal completion.
|
||||
|
||||
✅ **UI Refactor** - Updated the Lobby UI to use cleaner, static scene-based components (`.tscn`) for settings, replacing legacy dynamic code. The "Enable Timer" toggle now sits correctly alongside other room settings.
|
||||
|
||||
**Bug Fixes & Stability**
|
||||
|
||||
✅ **'Put' Action Fix** - Fixed a critical bug where the 'Put' action (S key) failed in Real-Time mode due to incorrect Action Point calculations. It now functions correctly regardless of game mode.
|
||||
|
||||
✅ **Crash Prevention** - Patched a potential crash in `player.gd` by guarding `clear_highlights()` calls. This prevents runtime errors when the player script is running in contexts without a full game environment (like the Lobby character preview).
|
||||
+27
-11
@@ -32,6 +32,8 @@ extends Control
|
||||
@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
|
||||
@onready var enable_timer_check = $LobbyPanel/TopBar/SettingsSection/EnableTimerCheck
|
||||
@onready var enable_timer_label = $LobbyPanel/TopBar/SettingsSection/EnableTimerLabel
|
||||
|
||||
# UI References - Player Slots
|
||||
@onready var players_container = $LobbyPanel/PlayersContainer
|
||||
@@ -112,6 +114,7 @@ func _ready():
|
||||
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)
|
||||
enable_timer_check.toggled.connect(_on_enable_timer_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)
|
||||
@@ -129,6 +132,7 @@ func _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.enable_cycle_timer_changed.connect(_on_enable_cycle_timer_changed)
|
||||
LobbyManager.character_changed.connect(_on_character_changed)
|
||||
LobbyManager.area_changed.connect(_on_area_changed)
|
||||
LobbyManager.player_list_changed.connect(_update_player_slots)
|
||||
@@ -269,10 +273,11 @@ 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 _on_random_spawn_toggled(toggled_on):
|
||||
LobbyManager.set_randomize_spawn(toggled_on)
|
||||
|
||||
func _on_enable_timer_toggled(toggled_on):
|
||||
LobbyManager.set_enable_cycle_timer(toggled_on)
|
||||
|
||||
func _update_random_spawn_label(enabled: bool) -> void:
|
||||
if random_spawn_label:
|
||||
@@ -328,14 +333,17 @@ func _on_room_joined(room_data: Dictionary) -> void:
|
||||
# Duration: host sees dropdown, clients see text
|
||||
duration_option.visible = is_host
|
||||
duration_text_label.visible = not is_host
|
||||
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())
|
||||
|
||||
enable_timer_check.visible = is_host
|
||||
enable_timer_label.visible = not is_host
|
||||
|
||||
# Update values from LobbyManager
|
||||
_on_match_duration_changed(LobbyManager.match_duration)
|
||||
_on_randomize_spawn_changed(LobbyManager.randomize_spawn)
|
||||
_on_enable_cycle_timer_changed(LobbyManager.enable_cycle_timer)
|
||||
|
||||
# Area selector: only host can interact
|
||||
area_left_btn.disabled = not is_host
|
||||
@@ -378,8 +386,16 @@ func _on_match_duration_changed(duration_seconds: int) -> void:
|
||||
_update_duration_text_label(duration_seconds)
|
||||
|
||||
func _on_randomize_spawn_changed(enabled: bool) -> void:
|
||||
if not LobbyManager.is_host:
|
||||
_update_random_spawn_label(enabled)
|
||||
if random_spawn_check:
|
||||
random_spawn_check.set_pressed_no_signal(enabled)
|
||||
if random_spawn_label:
|
||||
random_spawn_label.text = "Random \u2713" if enabled else "Random \u2717"
|
||||
|
||||
func _on_enable_cycle_timer_changed(enabled: bool) -> void:
|
||||
if enable_timer_check:
|
||||
enable_timer_check.set_pressed_no_signal(enabled)
|
||||
if enable_timer_label:
|
||||
enable_timer_label.text = "Timer \u2713" if enabled else "Timer \u2717"
|
||||
|
||||
func _on_character_changed(_player_id: int, _character_name: String) -> void:
|
||||
_update_player_slots()
|
||||
|
||||
@@ -296,6 +296,23 @@ theme_override_colors/font_color = Color(0.647, 0.996, 0.224, 1)
|
||||
theme_override_font_sizes/font_size = 11
|
||||
text = "Random ✓"
|
||||
|
||||
[node name="TimerSpacer" type="Control" parent="LobbyPanel/TopBar/SettingsSection"]
|
||||
custom_minimum_size = Vector2(15, 0)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="EnableTimerCheck" type="CheckButton" parent="LobbyPanel/TopBar/SettingsSection"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 11
|
||||
button_pressed = true
|
||||
text = "Enable Timer"
|
||||
|
||||
[node name="EnableTimerLabel" 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 = "Timer ✓"
|
||||
|
||||
[node name="HostBanner" type="PanelContainer" parent="LobbyPanel"]
|
||||
layout_mode = 1
|
||||
anchors_preset = 5
|
||||
|
||||
+8
-1
@@ -771,7 +771,7 @@ func start_movement_along_path(path: Array, clear_visual: bool = true):
|
||||
target_position = Vector2i(path[-1].x, path[-1].y)
|
||||
|
||||
var tween = create_tween()
|
||||
tween.set_trans(Tween.TRANS_CUBIC)
|
||||
tween.set_trans(Tween.TRANS_LINEAR)
|
||||
tween.set_ease(Tween.EASE_IN_OUT)
|
||||
|
||||
for point in path:
|
||||
@@ -793,6 +793,10 @@ func start_movement_along_path(path: Array, clear_visual: bool = true):
|
||||
if clear_visual:
|
||||
enhanced_gridmap.clear_path_visualization()
|
||||
|
||||
# Check for buffered input
|
||||
if movement_manager and movement_manager.has_method("_on_movement_finished"):
|
||||
movement_manager._on_movement_finished()
|
||||
|
||||
# Only restore UI state if this is a human player
|
||||
if not (is_bot or is_in_group("Bots")):
|
||||
# Restore movement range highlights if it was the player's turn
|
||||
@@ -1075,6 +1079,7 @@ func force_action_state_none():
|
||||
var main = get_tree().get_root().get_node_or_null("Main")
|
||||
if main and main.ui_manager:
|
||||
main.ui_manager.current_action_state = main.ui_manager.ActionState.NONE
|
||||
if action_manager:
|
||||
action_manager.clear_highlights()
|
||||
action_manager.clear_playerboard_highlights()
|
||||
|
||||
@@ -1214,9 +1219,11 @@ func highlight_occupied_playerboard_slots():
|
||||
action_manager.highlight_occupied_playerboard_slots()
|
||||
|
||||
func clear_highlights():
|
||||
if action_manager:
|
||||
action_manager.clear_highlights()
|
||||
|
||||
func clear_playerboard_highlights():
|
||||
if action_manager:
|
||||
action_manager.clear_playerboard_highlights()
|
||||
|
||||
func rotate_towards_target(target_pos: Vector2i):
|
||||
|
||||
@@ -323,8 +323,9 @@ func _try_move() -> bool:
|
||||
_is_processing_action = true
|
||||
_current_action = "moving"
|
||||
|
||||
# Wait for movement tween (approx 0.25s) plus small delay
|
||||
await get_tree().create_timer(0.3).timeout
|
||||
# Wait for movement to finish (signal from movement manager)
|
||||
await actor.movement_manager.movement_finished
|
||||
|
||||
|
||||
if not is_instance_valid(self): return true
|
||||
_is_processing_action = false
|
||||
|
||||
@@ -38,6 +38,13 @@ func _ready():
|
||||
|
||||
func initialize(main: Node):
|
||||
main_scene = main
|
||||
if LobbyManager:
|
||||
LobbyManager.enable_cycle_timer_changed.connect(_on_enable_cycle_timer_changed)
|
||||
|
||||
func _on_enable_cycle_timer_changed(enabled: bool):
|
||||
# If disabled mid-cycle, the timer will just freeze in _process
|
||||
# If enabled mid-cycle, it will resume
|
||||
pass
|
||||
|
||||
func _process(delta):
|
||||
# Update global match timer if active
|
||||
@@ -58,6 +65,16 @@ func _process(delta):
|
||||
if not is_cycle_active:
|
||||
return
|
||||
|
||||
# Skip countdown if timer is disabled
|
||||
var lobby_manager = get_tree().get_root().get_node_or_null("Main/LobbyManager")
|
||||
# Note: LobbyManager is an Autoload, so we can access it directly via 'LobbyManager'
|
||||
|
||||
if LobbyManager and not LobbyManager.get_enable_cycle_timer():
|
||||
# If timer is disabled, we just don't decrement.
|
||||
# But we still keep is_cycle_active = true so the phase is "active" (allowing actions)
|
||||
# just without the clock ticking down.
|
||||
return
|
||||
|
||||
current_cycle_timer -= delta
|
||||
|
||||
if current_cycle_timer <= 0:
|
||||
|
||||
@@ -30,6 +30,10 @@ var match_duration: int = 180 # Default 3 minutes
|
||||
# Randomize spawn locations (configurable in lobby by host)
|
||||
var randomize_spawn: bool = true # Default enabled
|
||||
|
||||
# Timer setting
|
||||
var enable_cycle_timer: bool = true # Default enabled
|
||||
signal enable_cycle_timer_changed(enabled: bool)
|
||||
|
||||
# Character and area selection
|
||||
var available_characters: Array[String] = ["Bob", "Gatot", "Masbro", "Oldpop"]
|
||||
var available_areas: Array[String] = ["Desert", "Forest", "City", "Factory"]
|
||||
@@ -180,6 +184,25 @@ func sync_randomize_spawn(enabled: bool) -> void:
|
||||
func get_randomize_spawn() -> bool:
|
||||
return randomize_spawn
|
||||
|
||||
# =============================================================================
|
||||
# Timer Setting
|
||||
# =============================================================================
|
||||
|
||||
func set_enable_cycle_timer(enabled: bool) -> void:
|
||||
"""Host sets enable cycle timer. Syncs to all clients."""
|
||||
enable_cycle_timer = enabled
|
||||
if is_host:
|
||||
rpc("sync_enable_cycle_timer", enabled)
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_enable_cycle_timer(enabled: bool) -> void:
|
||||
"""Sync enable cycle timer from host to clients."""
|
||||
enable_cycle_timer = enabled
|
||||
emit_signal("enable_cycle_timer_changed", enabled)
|
||||
|
||||
func get_enable_cycle_timer() -> bool:
|
||||
return enable_cycle_timer
|
||||
|
||||
# =============================================================================
|
||||
# Character Selection
|
||||
# =============================================================================
|
||||
@@ -272,6 +295,8 @@ func start_game() -> void:
|
||||
|
||||
# Sync match duration to all clients before starting
|
||||
rpc("sync_match_duration", match_duration)
|
||||
# Sync timer setting
|
||||
rpc("sync_enable_cycle_timer", enable_cycle_timer)
|
||||
|
||||
# Notify all clients to start
|
||||
rpc("_on_game_starting")
|
||||
@@ -394,3 +419,4 @@ func reset() -> void:
|
||||
match_duration = 180 # Reset to default 3 minutes
|
||||
selected_area = "Desert"
|
||||
local_character_index = 0
|
||||
enable_cycle_timer = true
|
||||
|
||||
@@ -9,6 +9,43 @@ func initialize(p_player: Node3D, p_movement_manager: Node, p_race_manager: Node
|
||||
movement_manager = p_movement_manager
|
||||
race_manager = p_race_manager
|
||||
|
||||
func _process(delta):
|
||||
# Early return conditions
|
||||
if not is_instance_valid(player) or not player.is_multiplayer_authority() or player.is_bot or player.is_in_group("Bots"):
|
||||
return
|
||||
|
||||
if TurnManager.turn_based_mode:
|
||||
return
|
||||
|
||||
# Continuous movement input
|
||||
var target_position = player.current_position
|
||||
|
||||
if Input.is_action_pressed("move_north"):
|
||||
target_position += Vector2i(0, -1)
|
||||
elif Input.is_action_pressed("move_northeast"):
|
||||
target_position += Vector2i(1, -1)
|
||||
elif Input.is_action_pressed("move_east"):
|
||||
target_position += Vector2i(1, 0)
|
||||
elif Input.is_action_pressed("move_southeast"):
|
||||
target_position += Vector2i(1, 1)
|
||||
elif Input.is_action_pressed("move_south"):
|
||||
target_position += Vector2i(0, 1)
|
||||
elif Input.is_action_pressed("move_southwest"):
|
||||
target_position += Vector2i(-1, 1)
|
||||
elif Input.is_action_pressed("move_west"):
|
||||
target_position += Vector2i(-1, 0)
|
||||
elif Input.is_action_pressed("move_northwest"):
|
||||
target_position += Vector2i(-1, -1)
|
||||
|
||||
# Action inputs (still momentary)
|
||||
if Input.is_action_just_pressed("action_grab"):
|
||||
player.grab_item(player.current_position)
|
||||
elif Input.is_action_just_pressed("action_put"):
|
||||
player.auto_put_item()
|
||||
|
||||
if target_position != player.current_position:
|
||||
movement_manager.simple_move_to(target_position)
|
||||
|
||||
func handle_unhandled_input(event):
|
||||
# Early return if not authorized human player
|
||||
if not player.is_multiplayer_authority() or player.is_bot or player.is_in_group("Bots"):
|
||||
@@ -19,41 +56,13 @@ func handle_unhandled_input(event):
|
||||
if not main:
|
||||
return
|
||||
|
||||
# --- Real-time Keyboard/Touch Input ---
|
||||
if not TurnManager.turn_based_mode and not movement_manager.is_moving:
|
||||
var target_position = player.current_position
|
||||
var input_handled = true
|
||||
|
||||
if Input.is_action_just_pressed("move_north"):
|
||||
target_position += Vector2i(0, -1)
|
||||
elif Input.is_action_just_pressed("move_northeast"):
|
||||
target_position += Vector2i(1, -1)
|
||||
elif Input.is_action_just_pressed("move_east"):
|
||||
target_position += Vector2i(1, 0)
|
||||
elif Input.is_action_just_pressed("move_southeast"):
|
||||
target_position += Vector2i(1, 1)
|
||||
elif Input.is_action_just_pressed("move_south"):
|
||||
target_position += Vector2i(0, 1)
|
||||
elif Input.is_action_just_pressed("move_southwest"):
|
||||
target_position += Vector2i(-1, 1)
|
||||
elif Input.is_action_just_pressed("move_west"):
|
||||
target_position += Vector2i(-1, 0)
|
||||
elif Input.is_action_just_pressed("move_northwest"):
|
||||
target_position += Vector2i(-1, -1)
|
||||
elif Input.is_action_just_pressed("action_grab"):
|
||||
player.grab_item(player.current_position)
|
||||
elif Input.is_action_just_pressed("action_put"):
|
||||
player.auto_put_item()
|
||||
else:
|
||||
input_handled = false
|
||||
|
||||
if target_position != player.current_position:
|
||||
movement_manager.simple_move_to(target_position)
|
||||
|
||||
if input_handled:
|
||||
player.get_viewport().set_input_as_handled()
|
||||
# Turn-based mouse input (handled in unhandled_input)
|
||||
if not player.is_multiplayer_authority() or (TurnManager.turn_based_mode and (not player.is_my_turn or movement_manager.is_moving)):
|
||||
return
|
||||
# --- End Real-time Input ---
|
||||
|
||||
# --- Real-time Keyboard/Touch Input moved to _process ---
|
||||
|
||||
# Handle spawn point selection if not yet selected
|
||||
|
||||
# Handle spawn point selection if not yet selected
|
||||
if not player.spawn_point_selected and player.highlighted_spawn_points.size() > 0:
|
||||
|
||||
@@ -20,8 +20,12 @@ func initialize(p_player: Node3D, p_gridmap: Node):
|
||||
if "use_diagonal_movement" in player:
|
||||
use_diagonal_movement = player.use_diagonal_movement
|
||||
|
||||
signal movement_finished
|
||||
var buffered_direction: Vector2i = Vector2i.ZERO
|
||||
var current_move_direction: Vector2i = Vector2i.ZERO
|
||||
|
||||
func _process(delta):
|
||||
if player and not is_moving:
|
||||
if player:
|
||||
_handle_rotation(delta)
|
||||
|
||||
func _handle_rotation(delta):
|
||||
@@ -37,7 +41,20 @@ func rotate_towards_target(target_pos: Vector2i):
|
||||
player.rpc("sync_rotation", target_rotation)
|
||||
|
||||
func simple_move_to(grid_position: Vector2i) -> bool:
|
||||
if not player.is_multiplayer_authority() or is_moving:
|
||||
if is_moving:
|
||||
# Calculate direction for buffering
|
||||
var direction = grid_position - player.current_position
|
||||
|
||||
# FIX: Only buffer if direction is DIFFERENT from current move (prevents overshoot)
|
||||
# We need to know current move direction. We can infer it or store it.
|
||||
# For now, let's assume if we are moving, we don't buffer same direction.
|
||||
# But we need to know what the current direction is.
|
||||
# Let's add a variable `current_move_direction` to the class first.
|
||||
if direction != current_move_direction:
|
||||
buffer_move_input(direction)
|
||||
return false
|
||||
|
||||
if not player.is_multiplayer_authority():
|
||||
return false
|
||||
|
||||
# Check if player is frozen
|
||||
@@ -80,12 +97,26 @@ func simple_move_to(grid_position: Vector2i) -> bool:
|
||||
var path = [Vector2(player.current_position.x, player.current_position.y), Vector2(grid_position.x, grid_position.y)]
|
||||
path.pop_front()
|
||||
|
||||
current_move_direction = grid_position - player.current_position
|
||||
|
||||
# Use the existing RPC to move (assuming player still has this RPC or we move it here)
|
||||
# For now, we'll call the player's RPC method
|
||||
player.rpc("start_movement_along_path", path, not (player.is_bot or player.is_in_group("Bots")))
|
||||
|
||||
return true
|
||||
|
||||
func buffer_move_input(direction: Vector2i):
|
||||
buffered_direction = direction
|
||||
|
||||
func _on_movement_finished():
|
||||
if buffered_direction != Vector2i.ZERO:
|
||||
var target = player.current_position + buffered_direction
|
||||
buffered_direction = Vector2i.ZERO
|
||||
simple_move_to(target)
|
||||
else:
|
||||
current_move_direction = Vector2i.ZERO
|
||||
emit_signal("movement_finished")
|
||||
|
||||
func move_to_clicked_position(grid_position: Vector2i) -> bool:
|
||||
if not player.is_multiplayer_authority() or is_moving or player.action_points <= 0:
|
||||
return false
|
||||
|
||||
@@ -25,7 +25,9 @@ func _normalize_tile(tile: int) -> int:
|
||||
# =============================================================================
|
||||
|
||||
func grab_item(grid_position: Vector2i) -> bool:
|
||||
if not enhanced_gridmap or player.action_points <= 0:
|
||||
var has_ap = player.action_points > 0 if TurnManager.turn_based_mode else true
|
||||
|
||||
if not enhanced_gridmap or not has_ap:
|
||||
return false
|
||||
|
||||
var cell = Vector3i(grid_position.x, 1, grid_position.y)
|
||||
@@ -209,7 +211,10 @@ func bot_try_grab_item() -> bool:
|
||||
# =============================================================================
|
||||
|
||||
func auto_put_item() -> bool:
|
||||
if not enhanced_gridmap or player.action_points <= 0 or player.is_bot or player.is_in_group("Bots"):
|
||||
# Check AP only if in turn-based mode
|
||||
var has_ap = player.action_points > 0 if TurnManager.turn_based_mode else true
|
||||
|
||||
if not enhanced_gridmap or not has_ap or player.is_bot or player.is_in_group("Bots"):
|
||||
return false
|
||||
|
||||
# Step 1: Find empty adjacent (or current) grid cells
|
||||
|
||||
Reference in New Issue
Block a user