feat: Implement Tekton roaming NPC with movement, combat, carry, throw, and knock mechanics.
This commit is contained in:
@@ -221,7 +221,6 @@ layout_mode = 2
|
||||
text = "PROFILE"
|
||||
|
||||
[node name="LobbyPanel" type="Control" parent="." unique_id=1745714811]
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
|
||||
+27
-5
@@ -486,11 +486,20 @@ func _setup_client_game():
|
||||
|
||||
# Wait shorter time for host to be ready, then request full sync to correct positions/state
|
||||
await get_tree().create_timer(1.0).timeout
|
||||
rpc_id(1, "request_full_player_sync", my_id)
|
||||
|
||||
# Only request grid if we remain connected
|
||||
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
|
||||
rpc_id(1, "request_full_grid_sync")
|
||||
# Ensure we see the server (Peer 1)
|
||||
if 1 in multiplayer.get_peers():
|
||||
rpc_id(1, "request_full_player_sync", my_id)
|
||||
rpc_id(1, "request_full_grid_sync")
|
||||
else:
|
||||
print("Client: Connected but Peer 1 not found yet. Retrying in 1s...")
|
||||
await get_tree().create_timer(1.0).timeout
|
||||
if 1 in multiplayer.get_peers():
|
||||
rpc_id(1, "request_full_player_sync", my_id)
|
||||
rpc_id(1, "request_full_grid_sync")
|
||||
|
||||
|
||||
|
||||
func _auto_start_from_lobby():
|
||||
"""Called when main.tscn is loaded from lobby - game is already connected."""
|
||||
@@ -516,6 +525,9 @@ func _auto_start_from_lobby():
|
||||
|
||||
func _start_game():
|
||||
if multiplayer.is_server():
|
||||
# Allow socket/peer to stabilize before blasting RPCs
|
||||
await get_tree().create_timer(2.0).timeout
|
||||
|
||||
GameStateManager.start_game()
|
||||
rpc("sync_game_start", GameStateManager.players, TurnManager.turn_based_mode)
|
||||
if TurnManager.turn_based_mode:
|
||||
@@ -525,6 +537,7 @@ func _start_game():
|
||||
|
||||
# Start the global match timer (this also starts the first cycle)
|
||||
if LobbyManager.game_mode == "Stop n Go":
|
||||
# Only Server starts the mode logic (arena setup, missions, etc)
|
||||
if stop_n_go_manager:
|
||||
stop_n_go_manager.start_game_mode()
|
||||
elif goals_cycle_manager:
|
||||
@@ -693,7 +706,10 @@ func spawn_tekton_npc():
|
||||
# Generate a consistent ID/Name for sync (add index to ensure uniqueness)
|
||||
var tekton_id = Time.get_ticks_msec() + spawned_count
|
||||
_create_tekton(valid_pos, tekton_id)
|
||||
rpc("sync_spawn_tekton", valid_pos, tekton_id)
|
||||
|
||||
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
|
||||
if multiplayer.get_peers().size() > 0:
|
||||
rpc("sync_spawn_tekton", valid_pos, tekton_id)
|
||||
|
||||
spawned_count += 1
|
||||
print("[Main] Spawned Tekton %d at %s" % [spawned_count, valid_pos])
|
||||
@@ -752,6 +768,11 @@ func _precalculate_static_positions():
|
||||
func spawn_static_tektons():
|
||||
"""Spawn fixed static tektons using StaticTektonManager."""
|
||||
if not multiplayer.is_server(): return
|
||||
|
||||
# Disable for Stop n Go mode
|
||||
if LobbyManager.game_mode == "Stop n Go":
|
||||
return
|
||||
|
||||
var enhanced_gridmap = $EnhancedGridMap
|
||||
if not enhanced_gridmap: return
|
||||
|
||||
@@ -1679,7 +1700,8 @@ func _deferred_init_leaderboard():
|
||||
|
||||
# Request leaderboard sync from server for accurate data
|
||||
if not multiplayer.is_server():
|
||||
rpc_id(1, "request_leaderboard_sync")
|
||||
if 1 in multiplayer.get_peers():
|
||||
rpc_id(1, "request_leaderboard_sync")
|
||||
else:
|
||||
# Server can update directly
|
||||
_update_leaderboard_display()
|
||||
|
||||
+44
-18
@@ -22,12 +22,16 @@ var display_name: String:
|
||||
if name_label:
|
||||
name_label.text = _display_name
|
||||
|
||||
# Sync to other peers if we are authority
|
||||
if is_multiplayer_authority() and is_inside_tree():
|
||||
# Sync to other peers if we are authority and connected
|
||||
if is_multiplayer_authority() and is_inside_tree() and can_rpc():
|
||||
rpc("sync_display_name", _display_name)
|
||||
get:
|
||||
return _display_name
|
||||
|
||||
# Helper to check network status
|
||||
func can_rpc() -> bool:
|
||||
return multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED
|
||||
|
||||
# Special effect states
|
||||
var is_frozen: bool = false
|
||||
var is_invisible: bool = false
|
||||
@@ -55,7 +59,7 @@ var is_attack_mode: bool = false:
|
||||
_apply_tint_recursive(self , Color.WHITE)
|
||||
|
||||
# Sync to others if we are the authority
|
||||
if is_multiplayer_authority():
|
||||
if is_multiplayer_authority() and can_rpc():
|
||||
rpc("sync_attack_mode", is_attack_mode)
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
@@ -199,7 +203,7 @@ func _ready():
|
||||
pointer.visible = is_multiplayer_authority()
|
||||
|
||||
# Sync name to other peers if this is our local player or a bot we own
|
||||
if is_multiplayer_authority():
|
||||
if is_multiplayer_authority() and can_rpc():
|
||||
rpc("sync_display_name", display_name)
|
||||
|
||||
# Wait briefly to ensure proper scene setup and server recognition
|
||||
@@ -239,25 +243,21 @@ func _ready():
|
||||
current_position = _find_random_spawn_position()
|
||||
update_player_position(current_position)
|
||||
spawn_point_selected = true
|
||||
rpc("set_spawn_position", current_position)
|
||||
rpc("notify_spawn_selected", current_position)
|
||||
if can_rpc():
|
||||
rpc("set_spawn_position", current_position)
|
||||
rpc("notify_spawn_selected", current_position)
|
||||
|
||||
# Assign bot character (deterministic based on ID to match lobby preview)
|
||||
# Bot IDs start from 2 (host is 1)
|
||||
# Lobby slots are 0-indexed in UI loop, but bots fill empty slots.
|
||||
# Use name.to_int() because all bots have authority 1 (Server)
|
||||
var bot_id_val = name.to_int()
|
||||
var bot_characters = ["Bob", "Gatot", "Masbro", "Oldpop"]
|
||||
# Map bot ID to character index. Bot 2 -> Index 1. Bot 3 -> Index 2.
|
||||
# Formula: (bot_id - 1) % size
|
||||
var char_index = (bot_id_val - 1) % bot_characters.size()
|
||||
var bot_char_name = bot_characters[char_index]
|
||||
set_character(bot_char_name)
|
||||
if is_multiplayer_authority():
|
||||
if is_multiplayer_authority() and can_rpc():
|
||||
rpc("sync_character", bot_char_name)
|
||||
|
||||
# Sync bot status to network
|
||||
if is_multiplayer_authority():
|
||||
if is_multiplayer_authority() and can_rpc():
|
||||
rpc("sync_bot_status", true)
|
||||
|
||||
# Continue to manager initialization...
|
||||
@@ -282,7 +282,7 @@ func _ready():
|
||||
current_position.y * 1 + 1 * 0.5
|
||||
)
|
||||
target_visual_position = global_position
|
||||
if is_multiplayer_authority():
|
||||
if is_multiplayer_authority() and can_rpc():
|
||||
rpc("sync_position", current_position)
|
||||
else:
|
||||
target_visual_position = global_position
|
||||
@@ -407,7 +407,7 @@ func _setup_character() -> void:
|
||||
set_character(character_name)
|
||||
|
||||
# If this is our local player, also sync to other clients for late joiners
|
||||
if is_multiplayer_authority():
|
||||
if is_multiplayer_authority() and can_rpc():
|
||||
rpc("sync_character", character_name)
|
||||
|
||||
# =============================================================================
|
||||
@@ -987,7 +987,8 @@ func _process(delta):
|
||||
_verify_timer += delta
|
||||
if _verify_timer >= 3.0:
|
||||
_verify_timer = 0.0
|
||||
rpc("ping_existence")
|
||||
if can_rpc():
|
||||
rpc("ping_existence")
|
||||
else:
|
||||
# Client-side visual smoothing
|
||||
# Only interpolate if NOT running a movement tween, OR if the drift is large (teleport/snap)
|
||||
@@ -1024,7 +1025,8 @@ var last_sent_position: Vector3
|
||||
func _physics_process(delta):
|
||||
if is_multiplayer_authority():
|
||||
if global_position.distance_squared_to(last_sent_position) > 0.001:
|
||||
rpc("remote_set_position", global_position)
|
||||
if can_rpc():
|
||||
rpc("remote_set_position", global_position)
|
||||
last_sent_position = global_position
|
||||
|
||||
# NOTE: Finish line checking removed - game uses cycle-based goals system now
|
||||
@@ -1158,6 +1160,30 @@ func _find_random_spawn_position() -> Vector2i:
|
||||
print("Warning: No gridmap for random spawn")
|
||||
return Vector2i.ZERO
|
||||
|
||||
# Special handling for Stop n Go mode (22x10 Grid)
|
||||
if LobbyManager.game_mode == "Stop n Go":
|
||||
var available_positions = []
|
||||
# Scan the 22x10 grid
|
||||
for x in range(22):
|
||||
for z in range(10):
|
||||
var pos = Vector2i(x, z)
|
||||
# Check if position is walkable (not a wall/obstacle)
|
||||
# We check Floor 0 item. Assuming walls are identifiable.
|
||||
# In setup_arena, walls are TILE_OBSTACLE (4).
|
||||
# We should check if it is NOT TILE_OBSTACLE.
|
||||
var item = enhanced_gridmap.get_cell_item(Vector3i(x, 0, z))
|
||||
# Assuming 4 is obstacle, and -1 is void. 0 is walkable, 2 is safe zone.
|
||||
if item != -1 and item != 4:
|
||||
if not is_position_occupied(pos):
|
||||
available_positions.append(pos)
|
||||
|
||||
if available_positions.size() > 0:
|
||||
var rng = RandomNumberGenerator.new()
|
||||
rng.randomize()
|
||||
return available_positions[rng.randi() % available_positions.size()]
|
||||
return Vector2i(10, 5) # Fallback center
|
||||
|
||||
|
||||
var available_positions = []
|
||||
|
||||
# Scan the grid for valid walkable floor tiles that are not occupied
|
||||
@@ -1893,7 +1919,7 @@ func grab_tekton():
|
||||
# Find nearby Tekton
|
||||
var tekton = _find_nearby_tekton()
|
||||
if tekton:
|
||||
if is_multiplayer_authority():
|
||||
if is_multiplayer_authority() and can_rpc():
|
||||
rpc("sync_grab_tekton", tekton.get_path())
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
|
||||
Reference in New Issue
Block a user