From 6aede0a38283c4cc28e220a146c6f6c40ac6a1d9 Mon Sep 17 00:00:00 2001 From: adtpdn Date: Wed, 7 Jan 2026 05:41:38 +0800 Subject: [PATCH] feat: Implement cycle timer toggle, refactor continuous input, and improve movement synchronization. --- .vscode/settings.json | 2 +- _daily_basis/report_2026-01-07.md | 23 ++++++ scenes/lobby.gd | 38 +++++++--- scenes/lobby.tscn | 17 +++++ scenes/player.gd | 17 +++-- scripts/bot_controller.gd | 5 +- scripts/managers/goals_cycle_manager.gd | 17 +++++ scripts/managers/lobby_manager.gd | 26 +++++++ scripts/managers/player_input_manager.gd | 79 ++++++++++++--------- scripts/managers/player_movement_manager.gd | 35 ++++++++- scripts/managers/playerboard_manager.gd | 9 ++- 11 files changed, 210 insertions(+), 58 deletions(-) create mode 100644 _daily_basis/report_2026-01-07.md diff --git a/.vscode/settings.json b/.vscode/settings.json index 3a199ec..b7cfc8f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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" } \ No newline at end of file diff --git a/_daily_basis/report_2026-01-07.md b/_daily_basis/report_2026-01-07.md new file mode 100644 index 0000000..1b957fc --- /dev/null +++ b/_daily_basis/report_2026-01-07.md @@ -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). diff --git a/scenes/lobby.gd b/scenes/lobby.gd index 19e3afa..2ac0da1 100644 --- a/scenes/lobby.gd +++ b/scenes/lobby.gd @@ -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() diff --git a/scenes/lobby.tscn b/scenes/lobby.tscn index f0d22e1..bdc7973 100644 --- a/scenes/lobby.tscn +++ b/scenes/lobby.tscn @@ -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 diff --git a/scenes/player.gd b/scenes/player.gd index dbc8849..1ca43c6 100644 --- a/scenes/player.gd +++ b/scenes/player.gd @@ -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,8 +1079,9 @@ 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 - action_manager.clear_highlights() - action_manager.clear_playerboard_highlights() + if action_manager: + action_manager.clear_highlights() + action_manager.clear_playerboard_highlights() # ----------------------------------------------------------------- @@ -1214,10 +1219,12 @@ func highlight_occupied_playerboard_slots(): action_manager.highlight_occupied_playerboard_slots() func clear_highlights(): - action_manager.clear_highlights() + if action_manager: + action_manager.clear_highlights() func clear_playerboard_highlights(): - action_manager.clear_playerboard_highlights() + if action_manager: + action_manager.clear_playerboard_highlights() func rotate_towards_target(target_pos: Vector2i): movement_manager.rotate_towards_target(target_pos) diff --git a/scripts/bot_controller.gd b/scripts/bot_controller.gd index fd6be2e..2169255 100644 --- a/scripts/bot_controller.gd +++ b/scripts/bot_controller.gd @@ -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 diff --git a/scripts/managers/goals_cycle_manager.gd b/scripts/managers/goals_cycle_manager.gd index 3af9dfe..8325a5e 100644 --- a/scripts/managers/goals_cycle_manager.gd +++ b/scripts/managers/goals_cycle_manager.gd @@ -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 @@ -57,6 +64,16 @@ func _process(delta): # Update cycle timer if cycle is active 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 diff --git a/scripts/managers/lobby_manager.gd b/scripts/managers/lobby_manager.gd index ddc4503..540a6ef 100644 --- a/scripts/managers/lobby_manager.gd +++ b/scripts/managers/lobby_manager.gd @@ -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 diff --git a/scripts/managers/player_input_manager.gd b/scripts/managers/player_input_manager.gd index b75dbaf..cea79c2 100644 --- a/scripts/managers/player_input_manager.gd +++ b/scripts/managers/player_input_manager.gd @@ -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 + # 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 - 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() - 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: @@ -65,7 +74,7 @@ func handle_unhandled_input(event): var click_position = player.raycast_to_grid(from, to) if click_position in player.highlighted_spawn_points: if player.select_spawn_point(click_position): - return + return # Turn-based mouse input if not player.is_multiplayer_authority() or (TurnManager.turn_based_mode and (not player.is_my_turn or movement_manager.is_moving)): diff --git a/scripts/managers/player_movement_manager.gd b/scripts/managers/player_movement_manager.gd index 3f847c3..3396c1e 100644 --- a/scripts/managers/player_movement_manager.gd +++ b/scripts/managers/player_movement_manager.gd @@ -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 diff --git a/scripts/managers/playerboard_manager.gd b/scripts/managers/playerboard_manager.gd index c9b79c1..7567231 100644 --- a/scripts/managers/playerboard_manager.gd +++ b/scripts/managers/playerboard_manager.gd @@ -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