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
+3
View File
@@ -1874,6 +1874,9 @@ func sync_throw_tekton(target_pos: Vector2i):
var tekton = carried_tekton
carried_tekton = null
is_carrying_tekton = false
if tekton.has_method("set_thrown"):
tekton.set_thrown(true)
else:
tekton.set_carried(false)
# Visual Arc Tween
+3 -1
View File
@@ -362,6 +362,7 @@ blend_shape_mode = 0
[sub_resource type="Animation" id="Animation_0jd8m"]
resource_name = "tekton_idle"
length = 0.875
loop_mode = 1
tracks/0/type = "position_3d"
tracks/0/imported = true
tracks/0/enabled = true
@@ -569,6 +570,7 @@ _data = {
}
[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]
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/enabled = true
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/2/name = "Bone.002"
bones/2/parent = 1
+74 -20
View File
@@ -7,11 +7,12 @@ 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
@export var movement_speed: float = 2.0 # Seconds per step (Slower walk)
var enhanced_gridmap: Node
var is_moving: bool = false
var is_carried: bool = false
var is_thrown: bool = false
var carrier: Node3D = null
var tween: Tween
@@ -31,7 +32,7 @@ func sync_position(pos: Vector2i):
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)
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)
func move_to(target_pos: Vector2i):
if is_moving or is_carried: return
if is_moving or is_carried or is_thrown: return
# Validate
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)
rotation.y = target_rot
play_animation("tekton_move")
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():
current_position = target_pos
is_moving = false
play_animation("tekton_idle")
emit_signal("movement_finished")
)
@@ -76,7 +80,6 @@ func move_to(target_pos: Vector2i):
@rpc("any_peer", "call_remote", "reliable")
func sync_movement(target_pos: Vector2i):
# Clients execute the move visually
move_to(target_pos)
# --- 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):
is_carried = state
carrier = p_carrier
if is_carried:
play_animation("tekton_idle")
is_moving = false
if tween: tween.kill()
@@ -111,9 +118,33 @@ func set_carried(state: bool, p_carrier: Node3D = null):
else:
controller.call("_start_timer")
if not state:
if not state and not is_thrown:
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):
if is_carried and is_instance_valid(carrier):
# Carry on head: offset Y by approx carrier height (e.g. 1.25)
@@ -125,31 +156,36 @@ 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)
_cache_meshes(self)
func _cache_meshes(node: Node):
if node is MeshInstance3D:
mesh_cache.append(node)
original_scales.append(node.scale)
for child in node.get_children():
_cache_meshes(child)
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()):
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)
var original_scale = original_scales[i]
# Pop effect
tween_flash.parallel().tween_property(mesh, "scale", original_scale * 1.3, 0.1).set_trans(Tween.TRANS_BOUNCE).set_ease(Tween.EASE_OUT)
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 recovery_timer: Timer
@rpc("any_peer", "call_local", "reliable")
func on_thrown_landing(attacker: Node = null, intensity: float = 1.0):
"""Called when Tekton lands after being thrown or knocked."""
is_thrown = false
# Prevent double-triggering (re-entrancy)
if is_recovering:
return
@@ -302,3 +338,21 @@ func spawn_tiles_around(count: int = 4):
if main:
main.rpc("sync_grid_item", pos.x, 1, pos.y, item_id)
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
# Handles random movement logic.
@export var move_interval_min: float = 2.0
@export var move_interval_max: float = 5.0
@export var move_interval_min: float = 0.1
@export var move_interval_max: float = 0.1
var tekton: Node3D
var enhanced_gridmap: Node