feat: Introduce Tekton character model, materials, and associated control scripts.

This commit is contained in:
Yogi Wiguna
2026-02-12 15:49:23 +08:00
parent 8837349896
commit 4d7ee61e7d
4 changed files with 84 additions and 25 deletions
+4 -1
View File
@@ -1874,7 +1874,10 @@ func sync_throw_tekton(target_pos: Vector2i):
var tekton = carried_tekton var tekton = carried_tekton
carried_tekton = null carried_tekton = null
is_carrying_tekton = false is_carrying_tekton = false
tekton.set_carried(false) if tekton.has_method("set_thrown"):
tekton.set_thrown(true)
else:
tekton.set_carried(false)
# Visual Arc Tween # Visual Arc Tween
var start_pos = tekton.global_position var start_pos = tekton.global_position
+3 -1
View File
@@ -362,6 +362,7 @@ blend_shape_mode = 0
[sub_resource type="Animation" id="Animation_0jd8m"] [sub_resource type="Animation" id="Animation_0jd8m"]
resource_name = "tekton_idle" resource_name = "tekton_idle"
length = 0.875 length = 0.875
loop_mode = 1
tracks/0/type = "position_3d" tracks/0/type = "position_3d"
tracks/0/imported = true tracks/0/imported = true
tracks/0/enabled = true tracks/0/enabled = true
@@ -569,6 +570,7 @@ _data = {
} }
[node name="tekton" type="Node3D" unique_id=2052742928] [node name="tekton" type="Node3D" unique_id=2052742928]
transform = Transform3D(0.15, 0, 0, 0, 0.15, 0, 0, 0, 0.15, 0, 0, 0)
[node name="Armature" type="Node3D" parent="." unique_id=1122137922] [node name="Armature" type="Node3D" parent="." unique_id=1122137922]
transform = Transform3D(1.1701986, 0, 0, 0, 1.1701986, 0, 0, 0, 1.1701986, 0, 0, 0) transform = Transform3D(1.1701986, 0, 0, 0, 1.1701986, 0, 0, 0, 1.1701986, 0, 0, 0)
@@ -586,7 +588,7 @@ bones/1/parent = 0
bones/1/rest = Transform3D(0.99999994, -6.3734255e-08, -2.29742e-07, -1.065814e-14, 0.9636076, -0.26732078, 2.384186e-07, 0.2673208, 0.96360755, -1.4760682e-14, 1.0773813, 0) bones/1/rest = Transform3D(0.99999994, -6.3734255e-08, -2.29742e-07, -1.065814e-14, 0.9636076, -0.26732078, 2.384186e-07, 0.2673208, 0.96360755, -1.4760682e-14, 1.0773813, 0)
bones/1/enabled = true bones/1/enabled = true
bones/1/position = Vector3(-1.4760682e-14, 1.0773813, 0) bones/1/position = Vector3(-1.4760682e-14, 1.0773813, 0)
bones/1/rotation = Quaternion(0.13007852, 0.02469786, 0.04730596, 0.9900666) bones/1/rotation = Quaternion(0.13489334, -1.18119765e-07, 1.6080534e-08, 0.9908601)
bones/1/scale = Vector3(0.99999994, 1, 0.99999994) bones/1/scale = Vector3(0.99999994, 1, 0.99999994)
bones/2/name = "Bone.002" bones/2/name = "Bone.002"
bones/2/parent = 1 bones/2/parent = 1
+75 -21
View File
@@ -7,11 +7,12 @@ signal movement_finished
@export var current_position: Vector2i = Vector2i(0, 0) @export var current_position: Vector2i = Vector2i(0, 0)
@export var health: int = 3 @export var health: int = 3
@export var movement_speed: float = 0.4 # Seconds per step @export var movement_speed: float = 2.0 # Seconds per step (Slower walk)
var enhanced_gridmap: Node var enhanced_gridmap: Node
var is_moving: bool = false var is_moving: bool = false
var is_carried: bool = false var is_carried: bool = false
var is_thrown: bool = false
var carrier: Node3D = null var carrier: Node3D = null
var tween: Tween var tween: Tween
@@ -31,7 +32,7 @@ func sync_position(pos: Vector2i):
update_visual_position() update_visual_position()
func update_visual_position(): func update_visual_position():
if is_carried: return if is_carried or is_thrown: return
# Align with floor height (matching player's typical grounded height) # Align with floor height (matching player's typical grounded height)
var floor_y = 0.05 var floor_y = 0.05
@@ -43,7 +44,7 @@ func update_visual_position():
position = Vector3(current_position.x + 0.2, floor_y, current_position.y + 0.2) position = Vector3(current_position.x + 0.2, floor_y, current_position.y + 0.2)
func move_to(target_pos: Vector2i): func move_to(target_pos: Vector2i):
if is_moving or is_carried: return if is_moving or is_carried or is_thrown: return
# Validate # Validate
if not enhanced_gridmap.is_position_valid(target_pos): if not enhanced_gridmap.is_position_valid(target_pos):
@@ -63,11 +64,14 @@ func move_to(target_pos: Vector2i):
var target_rot = atan2(dir.x, dir.z) var target_rot = atan2(dir.x, dir.z)
rotation.y = target_rot rotation.y = target_rot
play_animation("tekton_move")
tween = create_tween() 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_property(self , "position", target_world_pos, movement_speed).set_trans(Tween.TRANS_LINEAR).set_ease(Tween.EASE_IN_OUT)
tween.tween_callback(func(): tween.tween_callback(func():
current_position = target_pos current_position = target_pos
is_moving = false is_moving = false
play_animation("tekton_idle")
emit_signal("movement_finished") emit_signal("movement_finished")
) )
@@ -76,7 +80,6 @@ func move_to(target_pos: Vector2i):
@rpc("any_peer", "call_remote", "reliable") @rpc("any_peer", "call_remote", "reliable")
func sync_movement(target_pos: Vector2i): func sync_movement(target_pos: Vector2i):
# Clients execute the move visually
move_to(target_pos) move_to(target_pos)
# --- COMBAT / INTERACTION --- # --- COMBAT / INTERACTION ---
@@ -98,6 +101,10 @@ func on_hit(attacker: Node = null, intensity: float = 1.0):
func set_carried(state: bool, p_carrier: Node3D = null): func set_carried(state: bool, p_carrier: Node3D = null):
is_carried = state is_carried = state
carrier = p_carrier carrier = p_carrier
if is_carried:
play_animation("tekton_idle")
is_moving = false is_moving = false
if tween: tween.kill() if tween: tween.kill()
@@ -111,9 +118,33 @@ func set_carried(state: bool, p_carrier: Node3D = null):
else: else:
controller.call("_start_timer") controller.call("_start_timer")
if not state: if not state and not is_thrown:
update_visual_position() update_visual_position()
@rpc("any_peer", "call_local")
func set_thrown(state: bool):
is_thrown = state
if is_thrown:
# If we are thrown, we are not carried anymore, but we must stay disabled
if is_carried:
set_carried(false) # This will re-enable controller, so we must disable it again below
var controller = get_node_or_null("TektonController")
if controller:
controller.set_physics_process(false)
if controller.get("timer"):
controller.timer.stop()
# play_animation("RESET") # Optional: ensure no animation plays? Or just idle.
# User said "don't play ANY animation".
# If we play nothing, it freezes on last frame.
# Maybe just don't call anything new, and let previous (idle) stick?
# Or stop?
var anim_player = get_node_or_null("Visuals/tekton/Armature/AnimationPlayer")
if not anim_player: anim_player = find_child("AnimationPlayer", true, false)
if anim_player: anim_player.stop()
func _process(delta): func _process(delta):
if is_carried and is_instance_valid(carrier): if is_carried and is_instance_valid(carrier):
# Carry on head: offset Y by approx carrier height (e.g. 1.25) # Carry on head: offset Y by approx carrier height (e.g. 1.25)
@@ -125,31 +156,36 @@ var original_scales: Array[Vector3] = []
func _ready(): func _ready():
# Cache meshes and their initial scales # Cache meshes and their initial scales
# We wait a frame to ensure all children are ready and transforms applied _cache_meshes(self)
await get_tree().process_frame
var meshes = find_children("*", "MeshInstance3D", true) func _cache_meshes(node: Node):
for mesh in meshes: if node is MeshInstance3D:
mesh_cache.append(mesh) mesh_cache.append(node)
original_scales.append(mesh.scale) original_scales.append(node.scale)
for child in node.get_children():
_cache_meshes(child)
func _flash_damage(): 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
if mesh_cache.is_empty():
return var tween_flash = create_tween()
for i in range(mesh_cache.size()): for i in range(mesh_cache.size()):
var mesh = mesh_cache[i] var mesh = mesh_cache[i]
if is_instance_valid(mesh): var original_scale = original_scales[i]
var base_scale = original_scales[i]
var t = create_tween() # Pop effect
t.tween_property(mesh, "scale", base_scale * 1.2, 0.1) tween_flash.parallel().tween_property(mesh, "scale", original_scale * 1.3, 0.1).set_trans(Tween.TRANS_BOUNCE).set_ease(Tween.EASE_OUT)
t.tween_property(mesh, "scale", base_scale, 0.1) tween_flash.parallel().tween_property(mesh, "scale", original_scale, 0.2).set_delay(0.1)
var is_recovering: bool = false # True when shrunk/waiting var is_recovering: bool = false # True when shrunk/waiting
var recovery_timer: Timer
@rpc("any_peer", "call_local", "reliable") @rpc("any_peer", "call_local", "reliable")
func on_thrown_landing(attacker: Node = null, intensity: float = 1.0): func on_thrown_landing(attacker: Node = null, intensity: float = 1.0):
"""Called when Tekton lands after being thrown or knocked.""" """Called when Tekton lands after being thrown or knocked."""
is_thrown = false
# Prevent double-triggering (re-entrancy) # Prevent double-triggering (re-entrancy)
if is_recovering: if is_recovering:
return return
@@ -302,3 +338,21 @@ func spawn_tiles_around(count: int = 4):
if main: if main:
main.rpc("sync_grid_item", pos.x, 1, pos.y, item_id) main.rpc("sync_grid_item", pos.x, 1, pos.y, item_id)
spawned += 1 spawned += 1
func play_animation(anim_name: String):
# Try specific user path first
var anim_player = get_node_or_null("Visuals/tekton/Armature/AnimationPlayer")
# If not found, try finding recursive
if not anim_player:
anim_player = find_child("AnimationPlayer", true, false)
if anim_player and anim_player.has_animation(anim_name):
anim_player.play(anim_name)
# print("[Tekton] Playing animation: %s" % anim_name)
else:
if not anim_player:
print("[Tekton] AnimationPlayer NOT FOUND! Scene tree:")
print_tree_pretty()
else:
print("[Tekton] Animation '%s' NOT FOUND in AnimationPlayer!" % anim_name)
+2 -2
View File
@@ -3,8 +3,8 @@ extends Node
# TektonController - AI for Tekton NPC # TektonController - AI for Tekton NPC
# Handles random movement logic. # Handles random movement logic.
@export var move_interval_min: float = 2.0 @export var move_interval_min: float = 0.1
@export var move_interval_max: float = 5.0 @export var move_interval_max: float = 0.1
var tekton: Node3D var tekton: Node3D
var enhanced_gridmap: Node var enhanced_gridmap: Node