feat: Implement Tekton roaming NPC with movement, combat, carry, throw, and knock mechanics.

This commit is contained in:
Yogi Wiguna
2026-02-19 17:29:14 +08:00
parent a7a8106b7e
commit e90cbfe246
11 changed files with 235 additions and 84 deletions
+44 -18
View File
@@ -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")