The reticle was five white ColorRects, pasted byte-identically into three
level runtime scripts. The vitals were two stock ProgressBars with a flat
colour override, inlined 1200 lines into the movement controller. The ammo
count — the number a shooter's player looks at most — was drawn by the LEVEL,
in a black rounded panel that shared nothing with the menus, and it had a
special case in it (`elif active_weapon is DoubleBarrelShotgun`) because that
weapon never declared a name or a capacity.
Everything that describes A PLAYER now belongs to ui/player_hud.gd, and a
level owns the level. The immediate symptom that fixed: the screen had two
ammo panels on it at once, in two different styles, overlapping in the corner.
What each piece now says, rather than merely shows:
ui/crosshair.gd one drawn reticle instead of five rectangles, so it can
BLOOM — open with speed, airtime and each shot, snap shut
on ADS. That is the accuracy readout of the whole game and
five ColorRects could not express it. Every stroke is
drawn twice, ink underneath, because a 2 px white line
disappears over pale concrete exactly when aim matters.
The hit confirmation is the same cross at 45 degrees, so
it lands where the eye already is.
ui/vital_bar.gd segmented, so remaining health can be COUNTED rather than
estimated, with a drain ghost that holds the old value for
a beat — the gap between fill and ghost is the size of the
hit, which a bar that merely gets shorter never tells you.
ui/ability_chip.gd dash and grapple as a wipe across a chip rather than a
tinted JPEG with a 12 px number under it. A shape changing
size is readable in peripheral vision; 12 px type is not.
chain meter promoted out of the debug panel. Movement is this game's
first stated pillar and chaining is its skill expression,
so the count is a score, not a diagnostic.
The numerals moved OFF the bars and beside them. Text centred on a two-tone
bar cannot be given a colour that beats both the fill and the trough — that is
the 2.4:1 debug/ui_contrast_check.gd measured on the old HUD — so this fixes it
at the source rather than leaning on an outline to rescue it.
debug/hud_layout_check.gd measures where every element actually lands, which is
how three real bugs were found rather than squinted at: `set_anchors_preset`
moves the anchors and LEAVES THE OFFSETS, so the reticle spanned the viewport
with a size of exactly (0,0) and drew itself in the top-left corner; a
PRESET_CENTER applied after the ring's own `_ready` undid its centring; and a
BOX CONTAINER's own `alignment` is what pushes content to an edge, not a
SHRINK_END flag on the box, whose minimum width depends on children that may be
hidden. The ammo card was hanging off the right edge of the screen because of
the last one.
DoubleBarrelShotgun now declares `weapon_name` and `max_shells` like every
other weapon, and the four inline `2`s are gone.
spawn smoke 0 failures, 11/11 movement tests, contrast 108/108, layout PASS.
Co-Authored-By: Claude Opus 5 <[email protected]>
The theme was a comic one — cream paper, ink borders, papaya. It is now a
charged one: near-black violet, hot papaya, and a lightning yellow spent
nowhere except the instant a button is pressed. Chips are cut with two sharp
corners and two round ones on a diagonal, which is as close to a skew as a
StyleBoxFlat gets and is the difference between a button that reads calm and
one that reads fast. BoltRule draws the motif itself, struck a third of the way
along its rule rather than centred, so it reads as something that HIT the line.
The pause menu was 900 lines of hand-rolled UI that never referenced UITheme at
all, so it rendered in Godot's default grey. It now applies the theme — and
applies it to its own root Control, not only to the Window, because a Control
inherits from its nearest Control ANCESTOR and this screen hangs off a
CanvasLayer, which is not one. That was invisible at first: the parts built with
UITheme.title() carry their own overrides and looked right next to a list and a
button that did not.
And it now has a Character screen. The roster on the left, the character
themselves on the right, turning — a name in a dropdown is not a character
selection screen. The preview is a real SkinnedPlayerModel in its own world, so
it shows exactly what will spawn: the same cel look, the same per-class
outlines, the same cloth and hair on springs. Selecting applies immediately;
there is nothing destructive to confirm, and applying on selection means the
character behind the menu changes as you arrow the list, which IS the
comparison.
PlayerMovementController.set_skin() is the supported way in. Both halves of a
skin change are easy to do by halves — `synced_skin_id` is what REMOTE peers
rebuild from, and only their _process watches it, so setting the property alone
would change everyone else's view of you and not your own.
Checked rather than asserted. debug/character_picker_check.gd walks the whole
roster and proves each entry loads a skeleton, animations, a surface table and
body surfaces — and that the skeleton is MOVING, because the clip name is a
variable this class sets on itself and reads "Idle" just as happily when nothing
is ticking. debug/ui_capture.gd and debug/roster_capture.gd photograph the
screens and every character, which is how three things were found that no
assertion could see: the theme break above, a preview showing the back of the
character's head, and a turntable that carried on from the last character so the
third one you looked at was side-on.
Known and not fixed here: momo's idle pose is wrong — arms overhead and a
pinched waist. Her rig, not the picker; every other character is correct.
Co-Authored-By: Claude Opus 5 <[email protected]>
Four separate third-person defects, one shared root: the arms were posed at
art-directed ANGLES that merely approximated the gun, and a stuck one-shot
flag kept handing them back to the animation clip.
- The rifle hold was silently DEAD after the first reload or throw. The gate
trusted AnimationNodeOneShot's `active` parameter, which never clears, so
clip_owns_arms stayed true forever and the clip drove the arms while the
gun hung off the hand. `_upper_lock` (a timer we own) is now the
authority, and the one-shot is explicitly faded out when it expires.
This alone is why the gun was never held correctly.
- Arms are now solved with real two-bone IK (`_ik_arm`) onto points derived
from the WEAPON: the stock is anchored in the shoulder pocket, the grip
and foregrip fall out along the barrel, and the support hand is placed on
the actual handguard (sliding inboard if the model is too long to reach).
Verified numerically: the hand lands within 2mm of its target.
- `_measure_weapon` measures the model's AABB along its barrel and re-seats
it so the hand sits at a realistic pistol-grip point. Weapon models put
their origin anywhere — the M4's was 10cm from the muzzle end, so parking
its stock in the shoulder drove the hand INTO the shoulder and folded the
arm up behind the head.
- Recoil now shows for the OWNER in third person: server_play_fire_effects
only ever fired for remote shooters, so the local player saw nothing. The
ammo counter dropping now kicks the model. The kick rides in the gun's aim
direction so the IK carries BOTH hands up with it, instead of rotating the
arms and shoving the support hand off the gun.
- Reload no longer flips the gun. The library's pistol-reload rotates the
wrist the gun is parented to, which turned the rifle upside-down (mag to
the sky) while the hand reached down for it. The hold now keeps the right
arm through a reload and the support hand does the magazine work at the
real mag well, under the receiver.
anim_capture gains recoil shots and lets ADS settle before framing;
debug/dump_bones.gd prints a skin's bone hierarchy.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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]>
- 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]>
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]>
New Acoustics system (globals/acoustics.gd):
- Occlusion: rays from listener to source collect up to 3 occluding bodies;
each surface's material adds transmission loss and lowers the lowpass
cutoff. A gunshot through wood stays warm (-8dB, 1.4kHz), brick muffles
hard (-15dB, 600Hz), concrete harder (-18dB, 450Hz), metal keeps more
highs (-11dB, 1kHz), glass barely muffles (-5dB, 2.6kHz). Applied via
each AudioStreamPlayer3D's attenuation filter.
- Early reflections: loud sounds (gunfire, explosions) probe 8 directions
for nearby walls; each close surface plays a quiet, delayed (path/343ms),
material-filtered copy from the mirror point — shots audibly slap back
off structures so players can triangulate. Sub-30ms echoes are skipped
(they perceptually merge with the direct sound).
- Surface classification: builder-set acoustic_material meta -> node-name
keywords -> metallic-material inference -> concrete default; cached.
Integration:
- AudioManager.play_3d applies occlusion at fire time and re-filters all
playing positional sounds every 0.12s (walking behind a wall mid-sound
audibly muffles it); flight loops opt in via "acoustic_occluded" group
- Remote players' gunshots were entirely SILENT — rpc_play_fire_effects now
plays the right weapon's shot at the shooter's position, occluded and
reflected like everything else
- Remote players' footsteps were also silent — now positional + occluded,
cadence scaled to their speed
- Test level tagged: brick arena walls, wooden platforms/obstacles, metal
wall-run corridor/rails/dash platforms; dust2 sandstone reads as brick
Tests: debug/acoustics_test.gd (12 checks: per-material loss ordering,
multi-wall stacking, reflection delay/filter, name classification) — all
pass; movement 11/11; smoke 0 failures.
Co-Authored-By: Claude Fable 5 <[email protected]>
Audio:
- All ballistic weapons now fire real firearm recordings (The Free Firearm
Sound Library, CC0): AK-47 uses actual AK-47 shots, M4=AR-15, MP7=PPSh,
AWP=Mosin Nagant, DMR=Savage 10, shotgun=Mossberg/Nova, nailgun=Ruger .22,
rocket/mortar layered with real booms. Plasma keeps its energy sound.
- Reload is real lever-action foley instead of interface clicks
- Projectile flight loops localized: unit_size 50->6, max_db 6->0,
max_distance 45 — no more mortar/rocket drone in every player's ear;
loops also got proper loop_end (was a zero-length loop)
- Flight loops lowpassed into soft whooshes instead of sci-fi engine drones
- Menu music converted to OGG with native full-track looping (47s seamless,
replaces the broken manual WAV loop points)
Animations (movement-shooter pass):
- Rebaked miku.glb with 4 new clips from the CC0 library: Grapple (swim
reach — reads as a swing), PistolIdle (armed idle with weapon up),
PistolShoot, PistolReload
- Armed idle: standing with a weapon now uses PistolIdle instead of
arms-at-sides
- Slide pose overhaul: legs kick out front (lead/trail thighs, knees
straighten) — feet-first slide instead of "sitting in a crouch"
- Wall run: forward drive pitch + inner arm reaches out to the wall
- Per-transition blend times: dash/hit/jump cut fast (0.05-0.08s),
locomotion cross-fades smoothly (0.2-0.25s); CrouchWalk speed-scales
Fixes:
- Third-person camera no longer renders the first-person viewmodel layer
(the gun/arms floated in front of the third-person view)
Verified: 11/11 movement tests, smoke test 0 failures (17 clips resolved),
third-person run screenshot shows grounded animated model, no floating gun.
Co-Authored-By: Claude Fable 5 <[email protected]>
Audio (all CC0 — Kenney packs + OpenGameArt, see assets/sounds/SOURCES.md):
- Replace every procedural synth WAV with forged sounds (ffmpeg pitch/layer
mixes): distinct fire sounds per weapon with 2-3 variations, explosions,
footsteps x5, land/jump/dash/vault, knife swing, hit/kill confirms,
bullet impacts, reload, flight loops, UI hover/click/confirm/error,
match jingles, menu music
- AudioManager.stream_for(): AudioStreamRandomizer per sound id — every
weapon now gets variation + pitch randomization on each shot
- Explosions and bullet impacts play positional audio; landing has its own
sound instead of a pitched footstep; ambient wind bed on every map
Visuals:
- ExplosionVFX: shared cel-shaded burst (white-hot stepped core, ink
shockwave ring, star spikes, flat smoke puffs) replaces the orange
sphere in both local and remote-replay paths
- Comic star muzzle flashes on all weapons; unified cel tracer bolts with
ink outlines across hitscan/shotgun/remote paths
- Impact decals: hard-stepped ink-splat gradients instead of soft airbrush
- Toon shading on first-person view models + arms, third-person weapons,
and the procedural humanoid fallback (no hull outlines on FBX weapons —
their hard normals tear the inverted hull)
- Fix: giant soft "blob" highlight on floors — toon rim/specular disabled
on level-geometry materials (pre-existing artifact since the cel commit)
- Fix: own third-person weapon rendered into the first-person camera
(shadows-only now covers first_person_mode)
UI:
- UITheme: comic theme on the root window — Bangers display font (OFL),
paper panels with thick ink borders + hard drop shadows, papaya accent,
themed buttons/inputs/popups; hover/click sounds on all menu buttons
- Main menu: tilted comic wordmark, sunset toon diorama, looping menu music
- Match HUD: themed scoreboard/killfeed/kill counter
Viewports:
- 4x MSAA project-wide + viewmodel/diorama viewports (crisp ink lines)
- Viewmodel camera near plane 0.01; third-person camera FOV syncs settings
- debug/visual_capture.gd: dev tool to screenshot menu + level for review
Verified: 11/11 movement tests, spawn smoke test 0 failures, before/after
screenshot comparison of menu and level.
Co-Authored-By: Claude Fable 5 <[email protected]>
- SkinnedPlayerModel: generic play_oneshot() (generalizes the Land lock),
Hit and Dance in the clip table (Dance loops), restartable clips
- Hit flinch: victims visibly react on every peer's screen via the existing
damage broadcast — no new RPC
- Dance emote on B: toggles while grounded and idle, breaks on any movement;
synced to remotes via synced_is_dancing (all three spawner configs)
- Anime radial speed-lines overlay (canvas shader): fades in past ~1.2x walk
speed, spikes on dash, widescreen-corrected clear center
- New input action 'emote' (B) in project.godot
11/11 FSM tests, spawn smoke 0 failures.
Co-Authored-By: Claude Fable 5 <[email protected]>
- 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]>
- 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]>
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 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]>
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]>
- Add miku_rigged_animated.glb (3.27MB) with 18-bone skeleton and 6 animations
(Idle, Walk, Run, Jump, Crouch, Death) via Blender auto-rig script
- Add tools/rig_and_animate.py: reusable Blender script for rigging any humanoid
mesh with Mixamo-compatible bone naming
- Update SkinnedPlayerModel: scale_factor, first-person mode, animation state
matching (Idle/Walk/Run/Jump/Crouch/Death)
- Update level_runtime.gd: use skinned model for local player, procedural
humanoid for remote players
- Update skin_manager.gd and test_level_builder.gd to use animated model
- Fix Godot 4.2.1 'is not Type' syntax in 5 weapon files
- Add editor/import_miku_to_tscn.gd for editor-based GLB import
- Fix tab character in _build_elevated_walkways() call (line 63)
- Fix untyped 'pos' variable in grapple pillar loop (line 376)
- Remove fog_sky_affinity (Godot 4.3+ only property)
- Replace preload() with load() for runtime script loading
- Replace 'is DoubleBarrelShotgun' type check with duck-typing
- Fix 'is not OfflineMultiplayerPeer' syntax in grenade spawn
- Replace absf() with abs() in state_machine, state_air, state_ground
- Update test_level.tscn to remove stale UID reference
All fixes verified: PARSE OK and scene runs without script errors
on Godot 4.2.1 headless.