Avatars actually show; camera stops hugging the player
checkpoint / checkpoint (push) Successful in 1m40s

- SpringArm3D was colliding with the player's own capsule (same layer) ->
  camera yanked up against the head. add_excluded_object(self) in _ready +
  gentler default pitch, shorter arm, lower pivot.
- AvatarSystem.build_current_avatar() is now self-sufficient: loads the
  catalog itself and falls back to a bare default loadout if login didn't
  prime one. So the in-world player always shows an avatar, never the
  capsule, as long as the assets are packed (they are now).
- New AvatarSystem.assemble_avatar(): builds a standalone avatar with a
  throwaway loader, touching no shared state -- so it's safe to call once
  for the local player AND once per remote peer. (load_avatar()'s shared
  loader reset()s and freed the previous avatar on every call.)
- game.gd: remote players render as the assembled avatar too (shared default
  look, tinted per-peer, name tag) instead of a plain capsule.

Client-side -- rebuild the exe.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
This commit is contained in:
2026-09-05 23:52:10 +00:00
parent 54bc763dff
commit 80f24bfd01
4 changed files with 98 additions and 22 deletions
+49 -2
View File
@@ -270,6 +270,53 @@ func set_loadout_from_dict(loadout_dict: Dictionary) -> bool:
## into a scene (e.g. onto the in-world player). Returns null if there's no
## loadout or the catalog isn't loaded or assembly failed.
func build_current_avatar() -> Node3D:
if current_loadout == null or not is_catalog_loaded:
# Self-sufficient: load the catalog if needed, and fall back to a bare
# default loadout when login didn't prime one -- the player should always
# get *an* avatar, never a placeholder capsule, as long as the assets exist.
if not ensure_catalog():
push_warning("AvatarSystem: catalog unavailable; cannot build avatar")
return null
return load_avatar(current_loadout)
if current_loadout == null:
var bases := get_assets_for_slot("base_body")
if bases.is_empty():
return null
set_loadout_from_dict({"base_body": bases[0], "slots": {}})
return assemble_avatar(current_loadout)
## Assemble a standalone avatar node from [param loadout] using a throwaway
## loader -- unlike load_avatar(), this touches no shared state, so it's safe
## to call many times (local player + one per remote peer). Returns null only
## if the assets are genuinely missing.
func assemble_avatar(loadout) -> Node3D:
if loadout == null or not is_catalog_loaded:
return null
var validation := catalog.validate_loadout(loadout)
if not validation.valid:
push_warning("AvatarSystem: invalid loadout, skipping: %s" % str(validation.errors))
return null
var l = AvatarLoader.new()
l.init_loader(catalog)
var base := l.load_base_body(loadout.base_body)
if base == null:
l.free()
return null
for slot_id in AvatarSlotRegistry.SLOT_IDS:
var v = loadout.slots.get(slot_id)
if slot_id == "accessory":
if v is Array:
for aid in v:
if aid is String and not (aid as String).is_empty():
l.load_attachment(slot_id, aid)
elif v is String and not (v as String).is_empty():
l.load_attachment(slot_id, v)
for slot_id in AvatarSlotRegistry.BODY_PART_SLOTS:
var v = loadout.slots.get(slot_id)
if v is String and not (v as String).is_empty():
l.set_base_mesh_visible(slot_id, false)
l.free()
return base