extends Node3D # Tekton - Roaming NPC # Moves around the grid and spawns tiles when attacked. signal movement_finished @export var current_position: Vector2i = Vector2i(0, 0) @export var health: int = 3 @export var movement_speed: float = 0.4 # Seconds per step var enhanced_gridmap: Node var is_moving: bool = false var is_carried: bool = false var carrier: Node3D = null var tween: Tween const SIDE_OFFSET = 0.35 # Distance from center func initialize(start_pos: Vector2i, p_gridmap: Node): current_position = start_pos enhanced_gridmap = p_gridmap # Grounded and Side-positioned # We use a consistent side offset (e.g. North-West corner of the tile) update_visual_position() @rpc("any_peer", "call_local", "reliable") func sync_position(pos: Vector2i): current_position = pos update_visual_position() func update_visual_position(): if is_carried: return # Align with floor height (matching player's typical grounded height) var floor_y = 0.05 if enhanced_gridmap and "cell_size" in enhanced_gridmap: floor_y = enhanced_gridmap.cell_size.y # Side offset: place it near the edge # Using NW corner (+0.2, +0.2) instead of center (+0.5, +0.5) position = Vector3(current_position.x + 0.2, floor_y, current_position.y + 0.2) func move_to(target_pos: Vector2i): if is_moving or is_carried: return # Validate if not enhanced_gridmap.is_position_valid(target_pos): return is_moving = true var floor_y = 0.05 if enhanced_gridmap and "cell_size" in enhanced_gridmap: floor_y = enhanced_gridmap.cell_size.y var target_world_pos = Vector3(target_pos.x + 0.2, floor_y, target_pos.y + 0.2) # Rotation var dir = target_world_pos - position if dir.length_squared() > 0.01: var target_rot = atan2(dir.x, dir.z) rotation.y = target_rot tween = create_tween() tween.tween_property(self , "position", target_world_pos, movement_speed).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) tween.tween_callback(func(): current_position = target_pos is_moving = false emit_signal("movement_finished") ) if is_multiplayer_authority(): rpc("sync_movement", target_pos) @rpc("any_peer", "call_remote", "reliable") func sync_movement(target_pos: Vector2i): # Clients execute the move visually move_to(target_pos) # --- COMBAT / INTERACTION --- func on_hit(attacker: Node = null, intensity: float = 1.0): """Called when hit by a player attack or knock. Intensity: 0.5 for throw, 1.0+ for knock.""" print("[Tekton] Hit by %s! Intensity: %.1f" % [attacker.name if attacker else "Unknown", intensity]) # Visual Reaction (Flash red) _flash_damage() # Spawn Tiles if is_multiplayer_authority(): var tile_count = int(8 * intensity) # Base 8 tiles for 1.0 intensity spawn_tiles_around(tile_count) @rpc("any_peer", "call_local") func set_carried(state: bool, p_carrier: Node3D = null): is_carried = state carrier = p_carrier is_moving = false if tween: tween.kill() # Disable/Enable controller var controller = get_node_or_null("TektonController") if controller: controller.set_physics_process(not state) if controller.get("timer"): if state: controller.timer.stop() else: controller.call("_start_timer") if not state: update_visual_position() func _process(delta): if is_carried and is_instance_valid(carrier): # Carry on head: offset Y by approx carrier height (e.g. 1.25) global_position = carrier.global_position + Vector3(0, 1.5, 0) rotation = carrier.rotation var mesh_cache: Array[MeshInstance3D] = [] var original_scales: Array[Vector3] = [] func _ready(): # Cache meshes and their initial scales # We wait a frame to ensure all children are ready and transforms applied await get_tree().process_frame var meshes = find_children("*", "MeshInstance3D", true) for mesh in meshes: mesh_cache.append(mesh) original_scales.append(mesh.scale) func _flash_damage(): # If cache empty (e.g. called before ready), try to populate or just skip custom scaling if mesh_cache.is_empty(): return for i in range(mesh_cache.size()): var mesh = mesh_cache[i] if is_instance_valid(mesh): var base_scale = original_scales[i] var t = create_tween() t.tween_property(mesh, "scale", base_scale * 1.2, 0.1) t.tween_property(mesh, "scale", base_scale, 0.1) @rpc("any_peer", "call_local", "reliable") func on_thrown_landing(attacker: Node = null): """Called when Tekton lands after being thrown.""" print("[Tekton] Landed! Shrinking and waiting...") # Disable movement/interaction logic temporarily var controller = get_node_or_null("TektonController") if controller and controller.get("timer"): controller.timer.stop() # Visual Shrink # Use cached meshes if available, else find them (but can't restore accurately if not cached) if mesh_cache.is_empty(): # Fallback if _ready hasn't run or failed # We'll just define the user's specific vector as fallback target for the sphere # But better to rely on cache. pass for i in range(mesh_cache.size()): var mesh = mesh_cache[i] if is_instance_valid(mesh): var base_scale = original_scales[i] var t = create_tween() t.tween_property(mesh, "scale", base_scale * 0.5, 0.2).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK) # Wait 1 seconds await get_tree().create_timer(1.0).timeout # Grow back for i in range(mesh_cache.size()): var mesh = mesh_cache[i] if is_instance_valid(mesh): var base_scale = original_scales[i] var t = create_tween() t.tween_property(mesh, "scale", base_scale, 0.2).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK) # Resume AI if controller and controller.has_method("_start_timer"): if is_multiplayer_authority() and not is_carried: controller._start_timer() # Spawn tiles (as requested "tekton will spawn a tiles around that floor also") if is_multiplayer_authority(): spawn_tiles_around(8) # Standard amount # Floor Freeze (Visual/Instant - Run on all clients locally) _temporarily_change_floor(current_position, 1, 6, 3.0) func _temporarily_change_floor(center: Vector2i, radius: int, new_id: int, duration: float): if not enhanced_gridmap: return # Run locally on all clients to ensure instant feedback without network delay var changed_cells = {} # pos: original_id for x in range(-radius, radius + 1): for y in range(-radius, radius + 1): var pos = center + Vector2i(x, y) if enhanced_gridmap.is_position_valid(pos): var cell_3d = Vector3i(pos.x, 0, pos.y) var original = enhanced_gridmap.get_cell_item(cell_3d) # Only change if not already the new ID (avoid redundant updates or overriding existing freeze) if original != new_id: changed_cells[pos] = original # Set locally immediately enhanced_gridmap.set_cell_item(cell_3d, new_id) await get_tree().create_timer(duration).timeout # Restore locally for pos in changed_cells: var original = changed_cells[pos] var current_cell = Vector3i(pos.x, 0, pos.y) var current = enhanced_gridmap.get_cell_item(current_cell) # Only restore if it hasn't been changed to something else in meantime if current == new_id: enhanced_gridmap.set_cell_item(current_cell, original) # Stun nearby players handled by Thrower (Player.gd) or here? # Player.gd handles the stun call because it knows the impact zone context better? # Actually, Player.gd calls this function. Player.gd *also* iterates players to stun them. # That is fine. func spawn_tiles_around(count: int = 4): """Spawns a mix of normal and special tiles in a radius.""" if not enhanced_gridmap: return var radius = 2 var rng = RandomNumberGenerator.new() rng.randomize() print("[Tekton] Spawning %d tiles around %s" % [count, current_position]) var spawned = 0 var attempts = 0 while spawned < count and attempts < 25: attempts += 1 var x = rng.randi_range(-radius, radius) var y = rng.randi_range(-radius, radius) var pos = current_position + Vector2i(x, y) # Don't overwrite the Tekton's own cell? Or do? # Maybe avoid center. if x == 0 and y == 0: continue if enhanced_gridmap.is_position_valid(pos): # 50% chance to spawn something if rng.randf() > 0.5: continue # Determine Type var item_id: int var roll = rng.randf() if roll < 0.6: # 60% Normal Tile (7-10) item_id = rng.randi_range(7, 10) elif roll < 0.9: # 30% PowerUp (11-14) item_id = rng.randi_range(11, 14) else: # 10% Obstacle/Trap (optional) item_id = -1 # Clear? if item_id != -1: var main = get_tree().get_root().get_node_or_null("Main") if main: main.rpc("sync_grid_item", pos.x, 1, pos.y, item_id) spawned += 1