feat: Implement the main game scene, core logic, UI, and manager setup for Tekton Dash.
This commit is contained in:
@@ -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
|
||||
# =============================================================================
|
||||
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dyovwailce5tf
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://c67yq846u8y68
|
||||
Reference in New Issue
Block a user