feat: Sketchfab-to-game character pipeline, FP/TP animated views, client-auth netcode, audio system

Character pipeline (tools/):
- sketchfab_import.py: search/download via Sketchfab Download API with
  license/attribution tracking (SKETCHFAB_API_TOKEN)
- autorig.py: headless Blender auto-rig — fits a Mixamo-named skeleton
  to unrigged humanoids, binds automatic weights
- merge_animations.py: merges the shared Mixamo-skeleton animation
  library onto any rigged character, strips root motion, canonical names
- pipeline.py: one command chains download -> rig -> animate -> register

In-game skin system:
- SkinManager autoload reads skins.json (auto-written by the pipeline)
- SkinnedPlayerModel rewritten: canonical clips with fallback chains,
  blend times, speed-scaled locomotion, weapon bone attachment
- First-person: full animated body for the owner, head hidden via
  SkeletonModifier3D; third-person: full model for other players
- Skin selector in main menu; skin id synced in multiplayer
- Fixed GLBLoader crash (GLTFDocument.get_animation_count doesn't exist)

Multiplayer sync overhaul:
- Movement is now client-authoritative: the owning peer simulates
  locally (no input round-trip), server keeps health/kills/death
- Remote players interpolate synced_position/velocity with
  extrapolation and snap-on-teleport
- Knockback/impulses routed to the simulating peer

Audio:
- AudioManager autoload: SFX/Weapons/Footsteps/UI/Music buses, pooled
  3D players, variation + pitch randomization, auto-registration from
  assets/sounds

Docs: 3D_ASSET_PIPELINE.md rewritten end-to-end, new ASSET_SOURCES.md
(non-procedural animation/map/sound sources) and SOUND_DESIGN.md.
Verified with debug/spawn_smoke_test.gd (headless: 24/24 checks pass).

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Nicholas Butzke
2026-07-06 02:28:58 -04:00
co-authored by Claude Fable 5
parent 552338112e
commit d05477c135
52 changed files with 1933 additions and 466 deletions
+12 -8
View File
@@ -92,30 +92,34 @@ func _spawn_player(pid: int) -> CharacterBody3D:
# Dynamic Spawning
player.position = _get_dynamic_spawn_position()
# Server Synchronizer (Host is the ground truth)
# Server Synchronizer — host-owned gameplay state (health, kills, death)
var server_sync = MultiplayerSynchronizer.new()
server_sync.name = "ServerSynchronizer"
server_sync.set_multiplayer_authority(1) # Host always controls these
var server_rep_config = SceneReplicationConfig.new()
server_rep_config.add_property(":position")
server_rep_config.add_property(":synced_movement_state")
server_rep_config.add_property(":synced_movement_speed")
server_rep_config.add_property(":synced_is_crouching")
server_rep_config.add_property(":health")
server_rep_config.add_property(":shield")
server_rep_config.add_property(":is_dead")
server_rep_config.add_property(":synced_grapple_point")
server_rep_config.add_property(":synced_is_grapple_shooting")
server_sync.replication_config = server_rep_config
player.add_child(server_sync)
# Client Synchronizer (Client dictates their aim and loadout setup)
# Client Synchronizer — owner-simulated movement, aim, animation state.
# Movement is client-authoritative for instant response; remote peers
# interpolate synced_position/velocity (see PlayerMovementController).
var client_sync = MultiplayerSynchronizer.new()
client_sync.name = "MultiplayerSynchronizer"
client_sync.set_multiplayer_authority(pid)
var client_rep_config = SceneReplicationConfig.new()
client_rep_config.add_property(":synced_position")
client_rep_config.add_property(":synced_velocity")
client_rep_config.add_property(":rotation")
client_rep_config.add_property("HeadPivot:rotation")
client_rep_config.add_property(":synced_movement_state")
client_rep_config.add_property(":synced_movement_speed")
client_rep_config.add_property(":synced_is_crouching")
client_rep_config.add_property(":synced_grapple_point")
client_rep_config.add_property(":synced_is_grapple_shooting")
client_rep_config.add_property(":synced_skin_id")
client_rep_config.add_property(":synced_weapon_path")
client_rep_config.add_property(":synced_loadout_p1")
client_rep_config.add_property(":synced_loadout_p2")
@@ -63,15 +63,9 @@ func _spawn_player(pid: int) -> CharacterBody3D:
server_sync.name = "ServerSynchronizer"
server_sync.set_multiplayer_authority(1) # Host always controls these
var server_rep_config = SceneReplicationConfig.new()
server_rep_config.add_property(":position")
server_rep_config.add_property(":synced_movement_state")
server_rep_config.add_property(":synced_movement_speed")
server_rep_config.add_property(":synced_is_crouching")
server_rep_config.add_property(":health")
server_rep_config.add_property(":shield")
server_rep_config.add_property(":is_dead")
server_rep_config.add_property(":synced_grapple_point")
server_rep_config.add_property(":synced_is_grapple_shooting")
server_sync.replication_config = server_rep_config
player.add_child(server_sync)
@@ -80,8 +74,16 @@ func _spawn_player(pid: int) -> CharacterBody3D:
client_sync.name = "MultiplayerSynchronizer"
client_sync.set_multiplayer_authority(pid)
var client_rep_config = SceneReplicationConfig.new()
client_rep_config.add_property(":synced_position")
client_rep_config.add_property(":synced_velocity")
client_rep_config.add_property(":rotation")
client_rep_config.add_property("HeadPivot:rotation")
client_rep_config.add_property(":synced_movement_state")
client_rep_config.add_property(":synced_movement_speed")
client_rep_config.add_property(":synced_is_crouching")
client_rep_config.add_property(":synced_grapple_point")
client_rep_config.add_property(":synced_is_grapple_shooting")
client_rep_config.add_property(":synced_skin_id")
client_rep_config.add_property(":synced_weapon_path")
client_rep_config.add_property(":synced_loadout_p1")
client_rep_config.add_property(":synced_loadout_p2")