feat: Introduce Tekton character model, materials, and associated control scripts.
This commit is contained in:
+75
-21
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user