Third-person player characters now animate every action with readable intent (all networked, cel style preserved): - Look-around: upper body follows the owner's camera pitch — distributed over spine/neck/head so aiming up/down reads on the whole silhouette (driven by the already-synced HeadPivot rotation on remotes) - Reload: the baked PistolReload clip plays as a one-shot whenever the equipped weapon starts reloading (local edge-detect poll -> new synced action counter replays it on every peer) - Grenade throw: new Throw clip baked from the library's overhead swing (Sword_Attack retarget); triggers on throw, synced the same way - Shot recoil: visible kick on the shooter's model (shoulders snap back, forearms rise, fast decay) driven by the existing fire-effects RPC — remote players' shots now look like shots - Slide: trailing arm braces against the ground for balance on top of the feet-first pose - Armed idle confirmed in capture: weapon held at ready, not arms-down Plumbing: synced_action/synced_action_seq added to all three player spawners' replication configs; ACTIONS table on the model maps names to clips + lock times. Verified: 18 clips resolve (incl Throw), smoke 0 failures, movement 11/11, acoustics PASSED; third-person run + armed idle captures clean. Co-Authored-By: Claude Fable 5 <[email protected]>
73 lines
2.2 KiB
GDScript
73 lines
2.2 KiB
GDScript
extends SceneTree
|
|
|
|
## Dev tool: launch with rendering, screenshot the main menu and a level,
|
|
## then quit. Run:
|
|
## godot --path . -s res://debug/visual_capture.gd -- <output_dir> [scene_path]
|
|
## Screenshots land in <output_dir>/shot_menu.png, shot_level.png, shot_tp_run.png
|
|
## and shot_aerial.png. scene_path defaults to the test level.
|
|
|
|
var _frames := 0
|
|
var _out_dir := "."
|
|
var _scene := "res://scenes/maps/test_level/test_level.tscn"
|
|
|
|
|
|
func _initialize() -> void:
|
|
var args := OS.get_cmdline_user_args()
|
|
if args.size() > 0:
|
|
_out_dir = args[0]
|
|
if args.size() > 1:
|
|
_scene = args[1]
|
|
change_scene_to_file("res://ui/main_menu/main_menu.tscn")
|
|
|
|
|
|
func _process(_delta: float) -> bool:
|
|
_frames += 1
|
|
if _frames == 60:
|
|
_shot("menu")
|
|
var nm = root.get_node_or_null("NetworkManager")
|
|
if nm and nm.has_method("start_singleplayer_match"):
|
|
nm.start_singleplayer_match("Deathmatch")
|
|
change_scene_to_file(_scene)
|
|
elif _frames == 240:
|
|
_shot("level")
|
|
# Third person + run forward, to eyeball model grounding and animation
|
|
for p in root.find_children("*", "CharacterBody3D", true, false):
|
|
if p.has_method("set_third_person") and p.is_multiplayer_authority():
|
|
p.set_third_person(true)
|
|
Input.action_press("move_forward")
|
|
elif _frames == 300:
|
|
_shot("tp_run")
|
|
Input.action_release("move_forward")
|
|
elif _frames == 338:
|
|
_shot("tp_idle")
|
|
# Aerial overview of the whole map
|
|
var cam := root.get_camera_3d()
|
|
if cam:
|
|
var aerial := Camera3D.new()
|
|
current_scene.add_child(aerial)
|
|
if _scene.contains("neon_alley"):
|
|
aerial.far = 2000.0
|
|
aerial.global_position = Vector3(0, 400, 300)
|
|
else:
|
|
aerial.global_position = Vector3(0, 55, 42)
|
|
aerial.look_at(Vector3(0, 0, -5), Vector3.UP)
|
|
aerial.current = true
|
|
elif _frames == 340:
|
|
_shot("aerial")
|
|
# Fixed street-level view for deterministic style comparisons
|
|
var cam := root.get_camera_3d()
|
|
if cam:
|
|
cam.global_position = Vector3(3, 2.2, 20)
|
|
cam.look_at(Vector3(-8, 4, -18), Vector3.UP)
|
|
elif _frames == 360:
|
|
_shot("street")
|
|
return true
|
|
return false
|
|
|
|
|
|
func _shot(tag: String) -> void:
|
|
var img := root.get_viewport().get_texture().get_image()
|
|
var path := _out_dir + "/shot_" + tag + ".png"
|
|
img.save_png(path)
|
|
print("visual_capture: saved ", path)
|