Commit Graph
6 Commits
Author SHA1 Message Date
Nicholas ButzkeandClaude Opus 5 2efc21b18d feat(menu): PLAY is one press, and the level cards are photographs
Reaching a game was Singleplayer → pick a level. Reaching a multiplayer game
was Multiplayer → Host Game → wait → pick a level → wait, and the gamemode
dropdown had exactly one entry which set a string nothing read.

PLAY now starts the last map and mode played, and says which — "Akiba Crossing
• Deathmatch" under the button, so pressing it is a promise rather than a leap.
Everything else on the home screen is a detour from that, which is the right
shape for a menu: the common case is a button, not a path. Hosting is one press
and lands in the lobby already hosting; the lobby puts map, mode, players and
start on ONE screen; Enter connects, so typing an address does not then require
reaching for the mouse.

This is the lesson HoYoverse published about Zenless Zone Zero's first months
more loudly than anything else they have written. Their postmortem on the TV
mode names three complaints — it took too long, it sat between the player and
the combat, and there was too much of it early — and 1.2 removed it from the
story entirely rather than shortening it. Time spent BEFORE the thing the
player came for is not neutral, it is a cost.

The cards were a two-stop gradient generated from two colours in a meta file,
whose hover state was `use_hdr = true` — not a visible change on any of them.
They are photographs now, shot by debug/map_preview_capture.gd, which took
three attempts to get right and each attempt is a comment in the file:

  - framing each map from OUTSIDE by merging every VisualInstance3D's AABB
    produced five tiny dioramas floating on a table, and one solid black
    rectangle. That is a minimap, and a minimap is not a photograph.
  - standing at a spawn point fixed three maps and left two black: fps_blockout
    is genuinely dark and most of its spawns face an unlit wall, and
    procedural_arena builds its geometry at runtime so no fixed offset is
    reliably inside it.
  - so the tool now RENDERS several vantages and scores each result — mean
    luminance times its standard deviation, because brightness alone picks the
    empty sky and variance alone picks a high-contrast corner of a dark room.
    Their product picks a photograph. A black rectangle passes any check that
    only asks whether the camera ended up somewhere sensible.

Also fixed, and caught by looking at the screenshot: the selected mode chip was
unreadable. Its glyph is ink on volt, and the theme gave every button a 5 px INK
outline — an ink glyph inside an ink outline is not outlined, it is five pixels
fatter, and on a small chip that is a solid blob. Button labels now carry no
outline at all, which is the same reasoning debug/ui_contrast_check.gd already
encodes: an outline separates a glyph from a backdrop it cannot beat alone, and
a label on a solid chip does not have that problem. The chips keep their heavy
ink border, so nothing about the drawn look changes.

`current_gamemode` is normalised on the way in, because it used to hold a
display string and six callers still pass one. An unrecognised id compares
unequal to GameMode.GUN_GAME and would silently disable that mode's weapon
issuing — half a mode is worse than none.

The menu's background character is the player's OWN skin holding their weapon,
rather than the box-and-capsule mannequin that stood there before.

spawn smoke 0, game modes 30/30, movement 11/11, contrast 108/108, HUD layout
PASS, weapon holds 0, dances 0.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-07-28 12:40:50 -04:00
Nicholas ButzkeandClaude Fable 5 1ad3563e5a feat: animations follow the mechanics — upper/lower split + real grapple pose
Two fidelity gaps closed:

- Reload/throw/shoot/hit used to REPLACE the whole-body clip, so reloading
  mid-slide snapped the character upright on standing legs. The model now
  runs a runtime AnimationTree (clips -> Transition -> TimeScale -> OneShot)
  whose one-shot layer is FILTERED to upper-body bones: the arms play the
  action while the legs keep sliding/running/falling. Land stays full-body.
  The rifle hold releases exactly while the one-shot node reports active.
- Grapple no longer plays the library's horizontal-swim clip. The zip pose
  is procedural: Fall as the airborne base, the spine pivots to fly along
  the line to the actual anchor point (capped so overhead shots don't fold
  the body), the head sights the anchor, legs trail behind, the FREE left
  hand reaches up the rope — and the right hand keeps holding the rifle.
  The anchor point feeds in from the controller for local and remote
  players (synced_grapple_point).
- anim_capture: deterministic open-ground teleport + two new regression
  shots (reload-while-sliding, grapple zip toward a real anchor).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-20 22:51:04 -04:00
Nicholas ButzkeandClaude Fable 5 87b8ae70df feat: Fortnite-style OTS camera + Alt free-look orbit in third person
- Third-person camera chain is now HeadPivot > OrbitPivot > ShoulderOffset
  (0.55m right, 0.12m up) > SpringArm(2.6m, 14 deg) — the character sits
  slightly left of screen centre over-the-shoulder instead of dead centre
  behind. Aim is unchanged (still the first-person camera).
- Hold Alt in third person to orbit the camera freely around the character
  with the mouse (yaw wraps, pitch clamped -69..+29 deg); the character
  keeps facing and aiming where it was. Release Alt and the camera springs
  back behind the shoulder.
- debug/orbit_capture.gd: screenshots the OTS framing, a driven orbit, and
  the spring-back for visual regression.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-19 23:59:44 -04:00
Nicholas ButzkeandClaude Fable 5 22cb0a58b7 fix: T-posing skinned models — autorig produced an orphan skin with no weights
The model animated its skeleton but rendered a permanent T-pose: the mesh
wasn't skin-bound. Root cause was in autorig's binding, introduced while
chasing an earlier "skins:0" export:

- The earlier "skins:0" was actually caused by bone-heat weighting failing
  (0 weighted verts → empty skin gets dropped), NOT by the Armature modifier.
- The "fix" then stripped the Armature modifier and used parent_type=ARMATURE.
  That makes the glTF exporter emit a skin OBJECT but with no node.skin
  reference and no per-vertex JOINTS/WEIGHTS — an orphan skin. The mesh then
  renders its bind pose (T-pose) forever while the skeleton animates unseen.

Fix: keep the standard ARMATURE_AUTO result (Armature modifier + vertex
groups). With the nearest-bone fallback ensuring real weights, the exporter
now writes a COMPLETE skin (verified: meshnode.skin=0, JOINTS/WEIGHTS present).

Also:
- skinned_player_model: _ensure_meshes_bound() re-binds any skinned mesh whose
  skeleton NodePath doesn't resolve at load — graceful degradation instead of
  a silent T-pose for imperfect GLBs.
- smoke test now asserts the spawned player's mesh is bound to its skeleton,
  so this class of bug fails the test instead of shipping. 30/30 pass.
- regenerated miku_test.glb with the corrected pipeline.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-06 18:11:26 -04:00
Nicholas ButzkeandClaude Fable 5 e2fbc424a7 feat: third-person camera toggle (V) + fix first-person showing inside the model
The first-person head-shrink (SkeletonModifier3D) left the camera looking at
the inside of the neck/torso/hair. Replaced it: the owner's model now renders
shadows-only in first person (clean FPS view, still fully animated and visible
to others and in shadows).

Added a third-person toggle (V / toggle_camera_view) so you can actually see
your own animated model — the easiest way to verify a new skin's animations:
- over-the-shoulder SpringArm3D camera (wall-collision aware) on the local
  player; press V to swap, press again to return
- firing still uses the first-person camera, so aim is unchanged
- reveals the owner's model via set_owner_visible() on both SkinnedPlayerModel
  and HumanoidModel; hides the first-person weapon viewmodel while in TP
- death forces first person so the toggle can't fight the death cam

Smoke test extended to cover the toggle (camera boom created, becomes current,
round-trips) — 28/28 checks pass for both color and GLB skins.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-06 17:18:16 -04:00
Nicholas ButzkeandClaude Fable 5 d05477c135 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]>
2026-07-06 02:28:58 -04:00