diff --git a/scenes/main.gd b/scenes/main.gd index 0d713e7..ba28c75 100644 --- a/scenes/main.gd +++ b/scenes/main.gd @@ -504,6 +504,9 @@ func _start_game(): var all_players = get_tree().get_nodes_in_group("Players") ui_manager.initialize_leaderboard_with_players(all_players) + # Spawn Tekton NPC + spawn_tekton_npc() + func _assign_random_spawn_positions(): """Assign spawn positions distributed to 4 corners (2 per corner for 8 players).""" var enhanced_gridmap = $EnhancedGridMap @@ -596,6 +599,54 @@ func _assign_random_spawn_positions(): spawn_index += 1 +# ============================================================================= +# Tekton NPC Management +# ============================================================================= + +func spawn_tekton_npc(): + """Spawn a Tekton NPC at a random location.""" + if not multiplayer.is_server(): return + + # Find random valid position + var enhanced_gridmap = $EnhancedGridMap + if not enhanced_gridmap: return + + var valid_pos = Vector2i(-1, -1) + for _i in range(20): # Try 20 times + var x = randi() % enhanced_gridmap.columns + var y = randi() % enhanced_gridmap.rows + var cell = Vector3i(x, 0, y) + if enhanced_gridmap.get_cell_item(cell) == 0: # Walkable floor + valid_pos = Vector2i(x, y) + break + + if valid_pos != Vector2i(-1, -1): + # Generate a consistent ID/Name for sync + var tekton_id = Time.get_ticks_msec() + _create_tekton(valid_pos, tekton_id) + rpc("sync_spawn_tekton", valid_pos, tekton_id) + +@rpc("call_remote", "reliable") +func sync_spawn_tekton(pos: Vector2i, tekton_id: int): + _create_tekton(pos, tekton_id) + +func _create_tekton(pos: Vector2i, tekton_id: int): + var node_name = "Tekton_%d" % tekton_id + if has_node(node_name): return + + var tekton_scene = load("res://scenes/tekton.tscn") + var tekton = tekton_scene.instantiate() + tekton.name = node_name + add_child(tekton) + + # Initialize + if tekton.has_method("initialize"): + if has_node("EnhancedGridMap"): + tekton.initialize(pos, $EnhancedGridMap) + + print("[Main] Spawned Tekton at %s (ID: %d)" % [pos, tekton_id]) + + # ============================================================================= # Player Management # ============================================================================= diff --git a/scenes/tekton.tscn b/scenes/tekton.tscn new file mode 100644 index 0000000..603aa90 --- /dev/null +++ b/scenes/tekton.tscn @@ -0,0 +1,27 @@ +[gd_scene format=3 uid="uid://dpi7acioph6kk"] + +[ext_resource type="Script" uid="uid://dyovwailce5tf" path="res://scripts/tekton.gd" id="1_tekton"] +[ext_resource type="Script" uid="uid://c67yq846u8y68" path="res://scripts/tekton_controller.gd" id="2_controller"] +[ext_resource type="PackedScene" uid="uid://bye8rbeqmxy1m" path="res://assets/models/meshes/tekton/tekton_walking.glb" id="3_mesh"] + +[sub_resource type="BoxShape3D" id="BoxShape3D_tekton"] +size = Vector3(0.8, 1, 0.8) + +[node name="Tekton" type="Node3D" unique_id=119914767 groups=["Tektons"]] +script = ExtResource("1_tekton") + +[node name="TektonController" type="Node" parent="." unique_id=1658331083] +script = ExtResource("2_controller") + +[node name="Visuals" type="Node3D" parent="." unique_id=1698719440] + +[node name="tekton_walking" parent="Visuals" unique_id=1701195793 instance=ExtResource("3_mesh")] +transform = Transform3D(0.15, 0, 0, 0, 0.15, 0, 0, 0, 0.15, 0, 0, 0) + +[node name="HitArea" type="Area3D" parent="." unique_id=2139590311] +collision_layer = 4 +collision_mask = 2 + +[node name="CollisionShape3D" type="CollisionShape3D" parent="HitArea" unique_id=818146069] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0) +shape = SubResource("BoxShape3D_tekton") diff --git a/scripts/tekton.gd b/scripts/tekton.gd new file mode 100644 index 0000000..19cf038 --- /dev/null +++ b/scripts/tekton.gd @@ -0,0 +1,132 @@ +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 tween: Tween + +func initialize(start_pos: Vector2i, p_gridmap: Node): + current_position = start_pos + enhanced_gridmap = p_gridmap + + # Snap to grid visual (Center on tile) + # User wanted Z=1.0. X should ideally be 0.5 (center). + # Ensuring consistent offset across Init, Sync, and Move. + position = Vector3(current_position.x + 0.5, 1.0, current_position.y + 1.0) + + # NO RPC HERE - Spawn RPC handles initial position sync + +@rpc("any_peer", "call_local", "reliable") +func sync_position(pos: Vector2i): + current_position = pos + position = Vector3(current_position.x + 0.5, 1.0, current_position.y + 1.0) + +func move_to(target_pos: Vector2i): + if is_moving: return + + # Validate + if not enhanced_gridmap.is_position_valid(target_pos): + return + + # Check simple collision (optional, can be expanded) + # For now, Tekton walks through things or we check elsewhere? + # Controller should check validity. + + is_moving = true + var world_pos = Vector3(target_pos.x + 0.5, 1.0, target_pos.y + 1.0) + + # Rotation + var dir = world_pos - position + if dir.length_squared() > 0.1: + var target_rot = atan2(dir.x, dir.z) + rotation.y = target_rot + + # Tween Movement (Match Z+0.5 offset for symmetry during movement as requested) + var final_world_pos = Vector3(target_pos.x + 0.5, 1.0, target_pos.y + 0.5) + tween = create_tween() + tween.tween_property(self, "position", final_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): + """Called when hit by a player attack.""" + print("[Tekton] Hit by %s!" % (attacker.name if attacker else "Unknown")) + + # Visual Reaction (Flash red) + _flash_damage() + + # Spawn Tiles + if is_multiplayer_authority(): + spawn_tiles_around() + +func _flash_damage(): + var meshes = find_children("*", "MeshInstance3D", true) + for mesh in meshes: + var original_modulate = mesh.transparency + # Quick flash hack or shader param? + # If standard material, maybe just modulate visibility or scale + var t = create_tween() + t.tween_property(mesh, "scale", Vector3(1.2, 1.2, 1.2), 0.1) + t.tween_property(mesh, "scale", Vector3(1.0, 1.0, 1.0), 0.1) + +func spawn_tiles_around(): + """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 tiles around %s" % current_position) + + for x in range(-radius, radius + 1): + for y in range(-radius, radius + 1): + 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) diff --git a/scripts/tekton.gd.uid b/scripts/tekton.gd.uid new file mode 100644 index 0000000..88071e9 --- /dev/null +++ b/scripts/tekton.gd.uid @@ -0,0 +1 @@ +uid://dyovwailce5tf diff --git a/scripts/tekton_controller.gd b/scripts/tekton_controller.gd new file mode 100644 index 0000000..0ebadd8 --- /dev/null +++ b/scripts/tekton_controller.gd @@ -0,0 +1,70 @@ +extends Node + +# TektonController - AI for Tekton NPC +# Handles random movement logic. + +@export var move_interval_min: float = 2.0 +@export var move_interval_max: float = 5.0 + +var tekton: Node3D +var enhanced_gridmap: Node +var timer: Timer +var rng = RandomNumberGenerator.new() + +func _ready(): + if not is_multiplayer_authority(): + set_process(false) + return # Only server/authority runs AI + + tekton = get_parent() + # Wait for tekton to be ready and have gridmap + await get_tree().create_timer(1.0).timeout + enhanced_gridmap = tekton.enhanced_gridmap + + rng.randomize() + + # Setup Timer + timer = Timer.new() + add_child(timer) + timer.one_shot = true + timer.timeout.connect(_on_timer_timeout) + _start_timer() + +func _start_timer(): + var time = rng.randf_range(move_interval_min, move_interval_max) + timer.start(time) + +func _on_timer_timeout(): + if not is_instance_valid(tekton) or not is_instance_valid(enhanced_gridmap): + return + + _attempt_move() + _start_timer() + +func _attempt_move(): + if tekton.is_moving: return + + var neighbors = enhanced_gridmap.get_neighbors(tekton.current_position, 0) + var valid_moves = [] + + for n in neighbors: + if n.is_walkable: + if _is_occupied(n.position): + continue + valid_moves.append(n.position) + + if valid_moves.size() > 0: + var target = valid_moves[rng.randi() % valid_moves.size()] + tekton.move_to(target) + +func _is_occupied(pos: Vector2i) -> bool: + var players = get_tree().get_nodes_in_group("Players") + for p in players: + if p.current_position == pos: + return true + # Also check other Tektons? + var tektons = get_tree().get_nodes_in_group("Tektons") + for t in tektons: + if t != tekton and t.current_position == pos: + return true + return false diff --git a/scripts/tekton_controller.gd.uid b/scripts/tekton_controller.gd.uid new file mode 100644 index 0000000..8bc5a48 --- /dev/null +++ b/scripts/tekton_controller.gd.uid @@ -0,0 +1 @@ +uid://c67yq846u8y68