- Air: direct steering bends existing velocity toward input without changing
its magnitude (air_steer_rate), on top of Quake accelerate with raised
gains (air_strafe_accel 55->90, per-tick cap 1.2->2.5). No-input air drag
nearly removed so held momentum carries. Steering skips opposing input to
avoid flipping through zero.
- Dash: charge system on the machine — dash_charges (2) spend instantly,
each recharges in dash_cooldown (2 s). get_dash_cooldown_remaining() now
reports time to the NEXT charge (0 while one is banked) so existing HUDs
keep working; get_dash_charges() added for pips.
- Tests: dash tests updated to the charge API + new coverage for
spend-2-then-recharge; 11/11 pass, spawn smoke clean.
Co-Authored-By: Claude Fable 5 <[email protected]>
- Generated CC0-style prototype grid textures (0.5 m cells) and a shared
LevelMaterials factory: world-space triplanar mapping over every code-built
level (test level, dust2, procedural arena) with the existing color coding
kept as tints. Readable surfaces at speed instead of flat color boxes;
materials are cached per tint so identical surfaces batch.
- .gdignore + .gitignore the raw downloaded asset packs in addons/ whose
overlong paths broke Godot import scans and spammed git warnings.
- Replaced the rotted FSM test file (invalid call(t) on Callables, stateless
stub nodes, hung forever without quitting) with a real suite: 10 tests
covering state registration, jump bookkeeping, wall-run momentum
preservation and upward-carry cap, per-player dash cooldown (regression for
the old shared static), dash speed stacking, chain soft cap, landing
events, and slide entry momentum. New headless entrypoint:
godot --headless --path . -s res://movement/tests/run_fsm_tests.gd
Co-Authored-By: Claude Fable 5 <[email protected]>
The shipped skin GLB only had Idle/Walk/Run/Jump, so Slide, WallRun, Crouch,
Dash, Fall and Land all fell back to the wrong clips. Rebuilt via the Blender
pipeline from the library-skeleton rigged model (the old miku_proper_anim was
on a different skeleton, so retargeting silently baked flat clips — and the
shooter pose layer's DEF-* bone names never matched it either).
miku.glb now ships Idle, Walk, Run, Sprint, Jump, Fall, Land, CrouchIdle,
CrouchWalk, Dash (roll), Death, Hit, Dance from the Quaternius Universal
Animation Library (CC0). Verified per-bone keyframe motion in the export and
rendered pose stills for deformation QA (no crossed arms / stretched feet).
Also removed five stale experimental skin GLBs + extracted textures and the
one-off editor import scripts; skins.json now lists only the working skin.
Co-Authored-By: Claude Fable 5 <[email protected]>
- Quake-style air-strafe accelerate (real speed gain from strafing; external
speed from dash/rockets never clamped)
- Wall run: keeps entry momentum (incl. upward carry), accelerates along the
wall instead of hard-setting velocity, gravity fades in over the run
- Slide: slope physics (gravity projected along floor accelerates downhill),
flat-ground decel instead of multiplicative friction, entry boost, direct
slide->wallrun and slide->dash transitions
- Dash: cooldown 10s -> 2s, per-player (was a static shared across instances),
momentum fully kept on exit, FOV punch event
- Grapple: taut pendulum with active rope control (W reels in, S pays out),
release keeps swing energy
- Wall climb: carries upward momentum in, jump-off kick, shared vault helper
- Crouch capsule: single owner in the machine, smoothly lerped, ceiling check;
camera eye height follows via crouch factor (no more head snapping)
- Landing: machine emits 'land' events scaled by impact; camera dip + pitched
down thud; footsteps get pitch variation
- Camera rig: event-driven (land dip, dash FOV kick, vault pitch impulse),
slide roll tilt
- Model: Jump vs Fall split by vertical velocity, Land one-shot on heavy
landings, wall-run body lean (synced to remotes via synced_wall_side)
Co-Authored-By: Claude Fable 5 <[email protected]>
Godot import artifacts that pair with already-committed assets:
- miku_test_Image_0..3.png: textures extracted from miku_test.glb on import
- .uid files for spawn_smoke_test.gd and audio_manager.gd
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Layer a procedural SkeletonModifier3D (ShooterPoseModifier) on top of the base
clip so the character reads like a movement-shooter avatar:
- Directional lean: banks into the movement direction (right/left/back) and
blends smoothly for diagonals, driven by velocity relative to facing.
- Slide: leans the torso back and pitches the head up to look forward, instead
of the base clip's forward-torso/legs-out "spine break".
- Airborne: plays a composed Jump pose (weapon ready) rather than a flailing
fall.
- Weapon hold: the base clip already keeps the arms down (weapon at the hip);
on ADS both arms lift and swing in toward centre-front to aim. ADS is read
from the active weapon and synced (synced_is_ads) so remote players raise
their weapons too.
The controller feeds movement direction + ADS to SkinnedPlayerModel.set_locomotion()
each frame (works for local and remote via synced velocity/rotation). All
rotations are authored in skeleton space (fwd=+Z, up=+Y, right=-X) and converted
per-bone; tuning constants are at the top of ShooterPoseModifier.
Verified by rendering the poses (idle/strafe/back/slide/ads) in a real Godot
viewport. Smoke test 30/30.
Co-Authored-By: Claude Fable 5 <[email protected]>
The model animated but deformed wrong: arms crossed the torso and foot heels
stretched to the model's center-bottom. Two root causes, both fixed:
1. Pose mismatch. Sketchfab models are modelled in an A-pose (arms angled
down) but the Quaternius library rests in a T-pose. Binding the A-pose mesh
to T-pose arm bones misweighted the arms, and the library clips (which
rotate arms ~70° down from the T rest) then shoved the already-drooped arms
across the body.
Fix: autorig.fit_arms_to_mesh() reorients each arm bone chain along the
model's real arm direction; merge_animations retargets every clip from the
library's T-pose rest onto the fitted rest via world-space constraint bake
(Copy Rotation/Location + nla.bake), so world motion is reproduced
regardless of the rest difference.
2. Feet-to-center stretch. The library's deform-flagged `root` bone runs up
the body centre-line; nearest-bone weighting assigned inner-foot/heel verts
to it, stretching them to center when animated.
Fix: exclude root/control/mechanism bones from weighting (_NON_DEFORM_HINTS).
Also upgraded nearest-bone weighting from rigid (1 bone/vert) to a smooth
inverse-distance blend across the nearest 4 bones, so joints deform cleanly.
Bone-heat still fails on Miku's joined hair/clothing mesh, so this is the
primary path.
Verified by rendering Idle/Walk/Run poses (Blender): natural stance, arms at
sides swinging correctly, feet planted, proper running stride. Smoke test
still 30/30.
Co-Authored-By: Claude Fable 5 <[email protected]>
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]>
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]>
Verified the full chain on a real Sketchfab model (Miku UID -> rigged ->
13 clips -> loads and animates in-engine, 24/24 smoke checks pass).
Fixes found while running it for real:
- sketchfab_import: read .sketchfab_token as UTF-16/BOM too (PowerShell's
`echo >` writes UTF-16LE, which crashed the UTF-8 reader)
- autorig: rig to the animation library's OWN skeleton when _library.glb
is present (zero retargeting); robust binding — fall back to rigid
nearest-bone weights when Blender bone-heat fails on complex meshes
(Miku's joined hair/clothing gave 0 weighted verts); export as
parent_type=ARMATURE with no modifier (Blender 5.x only emits a glTF
skin in that exact form — a lingering Armature modifier gave skins:0);
export_apply=False (applying modifiers baked away the skin); flatten
the Sketchfab empty hierarchy before binding
- merge_animations: support a single multi-clip library GLB via
LIBRARY_CLIP_MAP; version-safe fcurve access for Blender 4.4+/5.x
slotted actions (Action.fcurves was removed)
Assets/tooling:
- bundle assets/characters/animations/_library.glb — CC0 Quaternius
Universal Animation Library (13 game clips), + .gdignore so Godot
skips the pipeline-input folder
- requirements.txt (stdlib-only; documents Blender-python separation)
- pipeline docs updated for the bundled library + swap instructions
Co-Authored-By: Claude Fable 5 <[email protected]>
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]>