@tool
## Pose Library Test Scene — connects bridge, exports skeleton for posing,
## and handles switching between edit mode (bridge drives skeleton) and
## preview mode (ProceduralAnimator drives skeleton for locomotion).
##
## Usage:
## 1. Open this scene in Godot editor (bridge auto-starts)
## 2. In Blender, connect + create poses in Asset Browser
## 3. Select a pose in Blender's Godot panel → edit it live
## 4. Click "Start Preview" → character walks with WASD using the poses
extends Node3D
const ProceduralAnimatorScript := preload("res://scenes/player/procedural/procedural_animator.gd")
const PoseLibraryClass := preload("res://scenes/player/procedural/pose_library.gd")
@export var auto_start: bool = true
@export var socket_path: String = "/tmp/blender_bridge.sock"
@export_group("Pose Library Status")
@export var active_pose: String = "(none)"
@export var pose_count: int = 0
@export var pose_names_list: String = "(empty)"
@export var preview_active: bool = false
@export var preview_speed: float = 3.0
@export var save_library_now: bool = false: set = _on_save_library
@export_group("Procedural Animation Tuning")
@export_range(0.0, 1.0, 0.01) var hip_drop_max: float = 0.45
@export_range(0.0, 30.0, 0.5) var transition_speed_idle: float = 6.0
@export_range(0.0, 30.0, 0.5) var transition_speed_walk: float = 8.0
@export_range(0.0, 30.0, 0.5) var transition_speed_jump: float = 10.0
@export_range(0.0, 30.0, 0.5) var transition_speed_land: float = 12.0
@export_range(0.0, 30.0, 0.5) var transition_speed_fall: float = 5.0
@export_group("Arm Physics")
@export_range(0.5, 1.0, 0.01) var arm_drag: float = 0.85
@export_range(0.0, 1.0, 0.01) var arm_stiffness_loose: float = 0.85
@export_range(0.0, 1.0, 0.01) var arm_stiffness_stiff: float = 0.7
@export_group("Wall Climbing")
@export_range(0.5, 5.0, 0.1) var climb_speed_up: float = 2.0
@export_range(0.5, 5.0, 0.1) var climb_speed_lateral: float = 3.0
@export_range(0.05, 0.5, 0.01) var climb_step_threshold: float = 0.25
@export_range(0.05, 0.3, 0.01) var climb_arc_height: float = 0.15
@export_group("Pose Preview Buttons")
@export var show_idle: bool = false: set = _show_idle
@export var show_walk_a: bool = false: set = _show_walk_a
@export var show_walk_b: bool = false: set = _show_walk_b
@export var show_run_a: bool = false: set = _show_run_a
@export var show_run_b: bool = false: set = _show_run_b
@export var show_jump_launch: bool = false: set = _show_jump_launch
@export var show_jump_air: bool = false: set = _show_jump_air
@export var show_fall: bool = false: set = _show_fall
@export var show_land: bool = false: set = _show_land
@export var show_roll_start: bool = false: set = _show_roll_start
@export var capture_current: bool = false: set = _on_capture_current
func _show_idle(v: bool) -> void: _show_pose(v, "idle", "show_idle")
func _show_walk_a(v: bool) -> void: _show_pose(v, "walk_a", "show_walk_a")
func _show_walk_b(v: bool) -> void: _show_pose(v, "walk_b", "show_walk_b")
func _show_run_a(v: bool) -> void: _show_pose(v, "run_a", "show_run_a")
func _show_run_b(v: bool) -> void: _show_pose(v, "run_b", "show_run_b")
func _show_jump_launch(v: bool) -> void: _show_pose(v, "jump_launch", "show_jump_launch")
func _show_jump_air(v: bool) -> void: _show_pose(v, "jump_air", "show_jump_air")
func _show_fall(v: bool) -> void: _show_pose(v, "fall", "show_fall")
func _show_land(v: bool) -> void: _show_pose(v, "land", "show_land")
func _show_roll_start(v: bool) -> void: _show_pose(v, "roll_start", "show_roll_start")
func _show_pose(value: bool, pose_name: String, prop_name: String) -> void:
if not value or not Engine.is_editor_hint():
return
set(prop_name, false)
_apply_pose_to_skeleton(pose_name)
func _on_save_library(value: bool) -> void:
if not value or not Engine.is_editor_hint():
return
save_library_now = false
if _pose_lib:
ResourceSaver.save(_pose_lib, POSE_LIB_PATH)
print("[PoseTest] PoseLibrary manually saved to ", POSE_LIB_PATH)
func _apply_pose_to_skeleton(pose_name: String) -> void:
if _pose_lib == null or _skeleton == null:
print("[PoseTest] No pose lib or skeleton")
return
var pose: Dictionary = _pose_lib.get_pose(pose_name)
if pose.is_empty():
print("[PoseTest] Pose '", pose_name, "' is empty (no bone data)")
return
for bone_name: String in pose:
var idx: int = _skeleton.find_bone(bone_name)
if idx >= 0:
var xform: Transform3D = pose[bone_name]
_skeleton.set_bone_pose_position(idx, xform.origin)
_skeleton.set_bone_pose_rotation(idx, xform.basis.get_rotation_quaternion())
_skeleton.set_bone_pose_scale(idx, xform.basis.get_scale())
print("[PoseTest] Applied pose '", pose_name, "' to skeleton (", pose.size(), " bones)")
func _on_capture_current(value: bool) -> void:
if not value or not Engine.is_editor_hint():
return
capture_current = false
if _skeleton == null or _pose_lib == null or active_pose == "(none)":
print("[PoseTest] Cannot capture: no skeleton, lib, or active pose")
return
var pose: Dictionary = {}
for i in range(_skeleton.get_bone_count()):
var bone_name: String = _skeleton.get_bone_name(i)
var pos: Vector3 = _skeleton.get_bone_pose_position(i)
var rot: Quaternion = _skeleton.get_bone_pose_rotation(i)
var scl: Vector3 = _skeleton.get_bone_pose_scale(i)
pose[bone_name] = Transform3D(Basis(rot).scaled(scl), pos)
_on_pose_captured(active_pose, pose)
print("[PoseTest] Manually captured current skeleton as '", active_pose, "'")
var _bridge: BlenderBridge
var _started: bool = false
var _preview_active: bool = false
func _sync_tuning_to_animator() -> void:
if _proc_animator == null:
return
_proc_animator.hip_drop_max = hip_drop_max
_proc_animator.fade_speed_idle = transition_speed_idle
_proc_animator.fade_speed_walk = transition_speed_walk
_proc_animator.fade_speed_jump = transition_speed_jump
_proc_animator.fade_speed_land = transition_speed_land
_proc_animator.fade_speed_fall = transition_speed_fall
var _proc_animator: Node = null
var _pose_lib: Resource = null
var _skeleton: Skeleton3D = null
const POSE_LIB_PATH: String = "res://scenes/player/procedural/tumble_poses.tres"
func _ready() -> void:
if Engine.is_editor_hint():
return
# (The creature spawner that stood here retired with the creature system, #114 phase
# 5: creatures are bots, placed by a room's own spawner object.)
# The player in the "player" group: what bot tiers measure distance from.
var player: Node = $PlayerProcedural
if player and not player.is_in_group("player"):
player.add_to_group("player")
func _enter_tree() -> void:
if not Engine.is_editor_hint():
return
call_deferred("_deferred_start")
func _deferred_start() -> void:
if not auto_start or _started:
return
if not is_inside_tree():
return
_bridge = $BlenderBridge as BlenderBridge
if _bridge == null:
push_warning("[PoseTest] BlenderBridge node not found")
return
# Find skeleton and set up ProceduralAnimator
var player: Node = $PlayerProcedural
if player:
_skeleton = _find_child_of_type(player, "Skeleton3D") as Skeleton3D
if _skeleton:
# Create ProceduralAnimator (inactive until preview)
var animator = ProceduralAnimatorScript.new()
animator.name = "ProceduralAnimator"
animator.active = false
_skeleton.add_child(animator)
_proc_animator = animator
print("[PoseTest] ProceduralAnimator added to ", _skeleton.get_path())
# Disable AnimationPlayer so bridge can drive bones
var anim_player: AnimationPlayer = _find_child_of_type(player, "AnimationPlayer") as AnimationPlayer
if anim_player:
anim_player.active = false
# Load or create PoseLibrary
if ResourceLoader.exists(POSE_LIB_PATH):
_pose_lib = load(POSE_LIB_PATH)
else:
_pose_lib = PoseLibraryClass.new()
if _proc_animator and _pose_lib:
_proc_animator.pose_lib = _pose_lib
_proc_animator.hip_drop_max = hip_drop_max
_sync_tuning_to_animator()
# Connect bridge signals
_bridge.connect("pose_captured", _on_pose_captured)
_bridge.connect("pose_selected", _on_pose_selected)
_bridge.connect("pose_preview_changed", _on_pose_preview_changed)
_bridge.connect("pose_library_updated", _on_pose_library_updated)
# Start server and export
_bridge.start_server(socket_path)
if player:
_bridge.export_scene(player)
_bridge.auto_sync = true
_started = true
print("[PoseTest] Server started — waiting for Blender connection")
func _on_pose_captured(pose_name: String, bone_transforms: Dictionary) -> void:
if _pose_lib == null:
return
# Update the pose slot in memory (no disk save — Godot autosave handles that)
var poses: Dictionary = _pose_lib.get("poses")
poses[pose_name] = bone_transforms
_pose_lib.set("poses", poses)
pose_count = poses.size()
# Show which poses have data vs empty
var status_parts: PackedStringArray = []
for key in poses.keys():
var bone_count: int = poses[key].size() if poses[key] is Dictionary else 0
status_parts.append(str(key) + "(" + str(bone_count) + ")")
pose_names_list = ", ".join(status_parts) if status_parts.size() > 0 else "(empty)"
notify_property_list_changed()
print("[PoseTest] Captured pose '", pose_name, "' with ", bone_transforms.size(), " bones")
func _on_pose_selected(pose_name: String) -> void:
active_pose = pose_name
notify_property_list_changed()
print("[PoseTest] Active pose: '", pose_name, "'")
func _on_pose_preview_changed(active: bool) -> void:
_preview_active = active
preview_active = active
notify_property_list_changed()
print("[PoseTest] Preview mode: ", "ON" if active else "OFF")
if active:
# Save PoseLibrary to disk before preview
if _pose_lib:
var dir := DirAccess.open("res://")
if dir:
dir.make_dir_recursive("scenes/player/procedural")
ResourceSaver.save(_pose_lib, POSE_LIB_PATH)
print("[PoseTest] PoseLibrary saved to ", POSE_LIB_PATH)
# Enable ProceduralAnimator, disable bridge skeleton import
if _proc_animator:
_proc_animator.pose_lib = _pose_lib
_proc_animator.active = true
else:
# Disable ProceduralAnimator, let bridge drive skeleton again
if _proc_animator:
_proc_animator.active = false
func _on_pose_library_updated() -> void:
# Reload from disk if bridge saved it (e.g., from pose_names: command)
if ResourceLoader.exists(POSE_LIB_PATH):
_pose_lib = load(POSE_LIB_PATH)
if _proc_animator:
_proc_animator.pose_lib = _pose_lib
var poses: Dictionary = _pose_lib.get("poses") if _pose_lib else {}
pose_count = poses.size()
pose_names_list = ", ".join(poses.keys()) if poses.size() > 0 else "(empty)"
notify_property_list_changed()
print("[PoseTest] PoseLibrary updated: ", pose_count, " poses — ", pose_names_list)
func _exit_tree() -> void:
if not Engine.is_editor_hint():
return
if _bridge != null and _started:
_bridge.auto_sync = false
_bridge.stop_server()
_started = false
print("[PoseTest] Server stopped")
func _process(_delta: float) -> void:
if not Engine.is_editor_hint() or not _started:
return
# F6 to re-export
if Input.is_key_pressed(KEY_F6):
var player: Node = $PlayerProcedural
if player:
_bridge.export_scene(player)
print("[PoseTest] Re-exported (F6)")
# Sync tuning sliders to animator every frame (cheap, enables live tweaking)
if _preview_active and _proc_animator:
_sync_tuning_to_animator()
# Feed fake movement to ProceduralAnimator during preview
if _preview_active and _proc_animator:
# Simple oscillating walk for preview
var _t: float = Time.get_ticks_msec() / 1000.0
_proc_animator.horizontal_speed = preview_speed
_proc_animator.is_grounded = true
_proc_animator.vertical_velocity = 0.0
func _find_child_of_type(node: Node, type_name: String) -> Node:
for child in node.get_children():
if child.get_class() == type_name:
return child
var found: Node = _find_child_of_type(child, type_name)
if found:
return found
return null