bb34764d59875c7777edcef8da8c2e76eef9837b
124
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0ce626ad42 | jump ani fixing and transition animations | ||
|
|
833e936bcd | ani fixed | ||
|
|
9358746582 | ani | ||
|
|
922983429e | big | ||
|
|
61669627db |
chore(debug): track the UID Godot generated for menu_capture.gd
Godot writes a .uid sidecar for every script and this one landed after the commit that added debug/menu_capture.gd. All 140 of the others are in version control; an untracked one drifts to a different UID on the next machine that imports the project. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
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]>
|
||
|
|
986179854d |
feat(modes): matches now end, and something is at stake when they do
There was a string — `current_gamemode`, always "Deathmatch" — an OptionButton
with one entry in it, and a five-minute timer that counted to zero, set
`match_active = false`, and did nothing else. No winner was declared, no summary
appeared, the clock froze at 00:00, and players carried on shooting each other
in a match that had stopped counting. Nothing was tracked beyond a running kill
count. A match did not end so much as stop mattering, and everything a player
does in the last minute only matters if there is a last minute.
globals/game_mode.gd answers the three questions a mode has to answer — how you
score, when it ends, who won — for three modes:
Deathmatch 25 frags or 10 minutes.
Team Deathmatch two squads to 50, teams balanced by COUNT on join (a 4v4
that loses three from one side must refill the short side;
round-robin on join order leaves it 4v1 forever), friendly
fire off and enforced on the server before damage is even
broadcast — a mode where the damage lands but the kill does
not count is worse than either.
Gun Game every kill promotes you a rung and swaps your weapon. The
score IS the rung and the limit IS the ladder's length, so
finishing the ladder and reaching the score limit are the
same event and only one win condition exists.
`check_win` is a pure function of the stats and the clock, deliberately, because
a win condition that can only be exercised by playing a whole match is one
nobody tests — and the previous one never was. debug/game_mode_check.gd runs 30
cases over it: a tie is a DRAW rather than a win for whoever came first out of
the dictionary; a team match is decided on the TEAM's total, which a per-player
check never reaches; every ladder rung names a weapon that exists, or that rung
softlocks the mode; and promoting past the top clamps, because the winning kill
promotes before the match-end RPC lands.
Tracking now covers score, team, current streak, best streak and ladder rung.
Score and kills are separate numbers because in Gun Game they coincide and in
anything with an objective they would not. The scoreboard ranks by score, shows
the mode's own noun for it, and puts the limit on the top line — a win condition
players cannot see is one they cannot play toward.
ui/match_summary.gd gives the ending somewhere to happen: who won, WHY (time and
frag limit are different stories about the same scoreline), team totals, full
standings, and a way out that is not alt-F4. Play Again is host-only on a server.
Three bugs found on the way, all of which only became bugs once matches could
actually end: loading a level reset the clock but not the scores, so the second
match on a server would have ended on its first kill; `.rpc()` on an offline
peer does not call locally, so singleplayer had no killfeed and no stat sync at
all; and a rocket already in the air at the whistle could change the result
after the summary was on screen.
spawn smoke 0, game modes 30/30, movement 11/11.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
f1a4f7df52 |
feat(emotes): five dances, built like animation, behind a radial dial
The shared clip library ships exactly one `Dance_Loop`, and five copies of one
clip is not five dances. What the runtime does have is a procedural pose layer
over a real skeleton with spring-driven hair and cloth, which is enough — if
the motion is constructed the way an animator would construct it rather than
the way a programmer reaches for first.
Wiring sine waves to bones is that first reach, and everyone can tell. A raw
sine moves fastest through the middle and slowest at the ends by the same
amount on every channel, all in phase, forever. It floats. It has no weight, no
accent, and no sense that one part of the body is driving and the rest is
following. Four principles fix it, and all four are cheap:
OVERLAP the body is a chain. Hips lead, spine follows a beat later,
head last. One subtraction — `beat - lag * i` — and the spring
solver then carries it out through the hair and skirt for free,
because the dance layer runs before it.
ACCENT a dance HITS poses. `shape` bends the wave so it hangs at the
extremes and snaps between them, which is what a key-and-
breakdown pass produces by hand.
WEIGHT the HIPS translate, not just rotate. A body that never leaves
its own axis reads as a puppet on a stick.
CONTRAST Robot deliberately breaks all of the above — zero lag,
quantised motion — and reads as mechanical precisely because
the other four do not.
Spin spots its head: it holds a heading against the turn and whips round to
catch up, which is what a real dancer does to keep from getting dizzy and the
most recognisable thing about a turn.
The dial is a radial menu because every option is then the SAME DISTANCE from
where the pointer starts — the choice is a direction, and a direction becomes
muscle memory in a way "the fourth row down" does not. Selection is by ANGLE
alone, so a flick and a careful nudge do the same thing. HOLD to open, release
to commit; a tap too short to have aimed replays the last emote, which is what
the button did before, so the old habit still works. Pressing while already
dancing just stops — having to aim at something in order to STOP would be the
most annoying possible way to build this.
debug/dance_check.gd asserts the overlap, and getting it to measure that took
four wrong measurements, each of which is now a comment where it was made:
- correlating the hips' TRANSLATION against the head's position relative to
them compared two different quantities at different periods; it ranked the
Robot, whose lag is zero by construction, as the most overlapped routine.
- a signed scalar `angle * sign of the axis's largest component` is
DISCONTINUOUS — as a rocking bone passes back through rest the axis flips —
so smooth Two-Step measured a full-range jump per frame, which is exactly
what quantised motion looks like.
- a bone's GLOBAL rotation carries every ancestor's, so the head correlates
with the hips at lag zero however delayed the head itself is.
- and the hips and head are driven by different channels anyway.
Measuring two links of the SAME chain, as local rotation vectors, agrees with
the authored lag: Spin measures 9 frames against 8.4 authored, Two-Step 7
against 6.6, Robot 0. The Robot is checked on the property it actually has —
its jump per frame is 0.41 of its range against 0.03-0.06 for the others.
RigRoles is pulled out of ShooterPoseModifier so the dance layer resolves bones
the same way rather than carrying a second copy. Two copies is how a rig ends up
animating correctly under one modifier and not the other.
spawn smoke 0 failures, 11/11 movement, 21/21 weapon-hold pairs, contrast 108/108.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
a13ae50f95 |
feat(rig): every weapon gets its own hold, so the silhouette names the gun
`_apply_rifle_hold` did exactly what its name said, to everything. A knife, an
AWP and a rocket launcher were all solved as a rifle — stock in the shoulder
pocket, support hand out along the barrel, muzzle on the aim line — so in third
person every character stood the same way whatever they carried, and the only
thing distinguishing a sniper from a shotgun was the ~30 cm of gun mesh in
their hands. At the distance an enemy is usually seen that is nothing.
It costs more than looking wrong. A character's pose is the fastest available
answer to "what is about to happen to me": a tube on a shoulder means take
cover, a blade held low means they have to close, a rifle at low ready means
they have not seen you. One hold throws all of that away. It is also the thing
HoYoverse's team say they chase in Zenless Zone Zero — characters read by
silhouette first, and they refuse to settle on one construction method because
one method limits how distinguishable the results can be.
weapons/weapon_hold_profiles.gd gives each of the twelve weapons a style, and a
style is not a bundle of slider values. Three of its differences cannot be
expressed on the rifle solve at all, and those are the ones that carry:
support WHERE the off hand goes and HOW IT IS TURNED there — wrapped round
a handguard, cupped against the firing fist, hooked under a tube
palm-up, or released entirely. Sending a hand somewhere new without
re-orienting it gives a hand teleported to the new spot still shaped
for the old one.
mount whether the weapon's rear sits IN the shoulder pocket, ON TOP of the
shoulder, or nowhere near it.
head whether the head comes down to the stock, or leans away to clear a
tube. The cheek weld is the sniper silhouette, and its inverse is
what says a launcher is resting on that shoulder.
A blade RELEASES the off arm back to the animation, so it swings with the run
cycle instead of gripping a handguard that is not there — most of what makes a
one-handed weapon read as one-handed — and closes a full fist, because an index
left straight along a knife handle reads as a mistake, not as discipline.
Layered strictly UNDER the existing tuning, so aria's hand-tuned AK-47 hold is
byte-for-byte what it was. WeaponHoldTuning.default_for is weapon-aware now,
which it had to be: the rig lab SAVES every knob it shows, so without it,
opening the lab on the knife and pressing save would silently overwrite the
blade profile with the rifle spec and put the character back to holding a knife
like an AK with nothing to indicate it had happened.
debug/weapon_hold_check.gd asserts the consequence, not the plumbing — storing
an enum and reading it back proves nothing. It measures where the hands and head
ACTUALLY end up, from inside the modifier pass (outside it, Godot restores the
local poses and every weapon reports an identical rifle) and in the shoulder's
own frame, because the hold breathes and two samples of the SAME weapon
otherwise differ by more than two different weapons do. 21 weapon pairs, all
distinguishable; the knife's off-hand weight measured at 0.01.
Two findings only measuring produced. Pushing `gun_fore` further out for the
sniper does NOTHING — the reach solver slides the support hand back down the
handguard until the arm can get there, so it landed at 0.388 m against the
rifle's 0.387. Raising the whole weapon is what makes a scoped rifle read.
And one only LOOKING produced, via debug/hold_capture.gd: the shotgun's barrel
passed through the character's chest. Every assertion passed — the hands were
exactly where they had been asked to go — but +x is toward the centreline, so
dropping the pocket and pushing it across at once swings the muzzle into the
torso.
rig_anchor_check still reports aria: her hand-tuned wrist rotates the anchor
offset. Pre-existing, and this halves it — it was 2 failures on main, now 1.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
c4bcbc7fd1 |
feat(ui): the first-person HUD is one thing, in the game's own hand
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]>
|
||
|
|
414026f001 |
feat(ui): every control state is checked for readability, not trusted
The palette has two light accents and one very dark one, and a control changes its FILL on hover and press. Paper text that reads at 18:1 on the resting near-black chip inverts to paper-on-yellow the moment the pointer arrives, which is 1.1:1 — invisible. That is the standard way a stylised UI becomes unreadable, and it was live on the OptionButton dropdown that every settings row uses: PopupMenu draws its papaya hover fill but keeps `font_color` unless `font_hover_color` is set, and it was not set. So the label now follows the fill. `ink_for(fill)` picks the legible glyph colour by contrast ratio, and every state's text, icon and outline is derived from its own fill through it — including the states nobody remembers exist: `hover_pressed` on a toggle (a CheckButton read as OFF while you touched it), icon colours on a CheckBox that is all icon, and the list hover that used to be the same papaya as list SELECTION, so the row you pointed at looked like the row you had chosen. debug/ui_contrast_check.gd interrogates the BUILT theme rather than the palette — a table compared against itself agrees by construction and catches nothing — and fails below the WCAG floor. It found the PopupMenu gap, a Tree hover asking for a `font_hovered_color` Godot does not have, and the health bar's readout at 2.4:1 over its own fill. That last one is not fixable as a colour pair: a centred readout straddles a hot papaya fill and a near-black trough, and no single colour beats both. What carries it is the heavy ink outline this theme puts on every glyph, which is its first stated rule and the same mechanism that keeps menu text legible straight over the 3D scene. The check models that as a fallback route — outline vs backdrop 3:1, glyph vs outline 4.5:1, at least 4 px — granted only where the backdrop genuinely varies, and substituting two ratios for one rather than waiving the requirement. Disabled text moves from 3.2:1 to 5.2:1 on the way past. A greyed-out "Start Match" is information; an illegible smudge is not. Also styled, because the theme had simply never mentioned them and Godot's defaults are grey-on-grey: scrollbars, Tree, SpinBox, ProgressBar, tooltips, LineEdit read-only and selected text. 108 pairs checked, all passing. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
742d68e318 |
chore(characters): Aria's AK-47 hold, tuned in the lab
The first tuning pass saved through the rig lab, and the first content in assets/characters/weapon_holds.json. Aria holding the AK-47 at low ready: weapon scaled to 0.53, both wrists set, both elbow poles placed, and both hands moved along and off the barrel line. Authored by hand at the sliders, not derived — which is the whole point of the file. Every value here is one the code cannot work out from the skeleton, and the layering means it applies to this character and this weapon only; every other character still gets what the code derives. Also picks up the .uid Godot generated for debug/wrist_gun_check.gd, which was committed a moment before its companion existed. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
e97de9aafd |
fix(weapons): the wrist turns the hand, and the gun stays on the aim line
The weapon is a child of a BoneAttachment3D on the trigger hand, so the two were welded by construction. Every degree `wrist_r` turned swung the barrel the same degree off the aim line — and took with it every control that could have corrected for it, because they are all expressed relative to that same hand. There was no combination of sliders that aligned a hand to a gun, which is the one thing the knob exists for. Both outcomes are now computed where the hand's local pose is set: the rotation the hand would take without the wrist offset, and the one it takes with it. The hand gets the second; the difference between them is exactly the counter-rotation the weapon mount needs, in the hand's own local frame, and `SkinnedPlayerModel._hold_weapon_still` applies it to the mount each frame. The gun ends up precisely where the solver put it. That also gives the two controls a clean split, which is what makes them usable together: TRIGGER / SUPPORT WRIST (hold) turns the HAND, gun stays on the aim line Grip roll / pitch / yaw (anchors) turns the GUN inside the hand Identity when `wrist_r` is untuned, so a character nobody has tuned mounts its weapon exactly as before. Applied in `_process` rather than inside the modifier pass on purpose. The gun's mount is not something the skeleton owns, and the compensated value only changes when a slider moves or the ADS blend travels, so one frame of lag is a fraction of a degree; reaching into the modifier to touch a scene node would be worse. wrist_gun_check asserts both halves, because only asserting the first is how this shipped broken: the hand must TURN, or the knob does nothing, and the gun must NOT, or the knob cannot be used. Across both poses and all three axes the hand turns 28.2-28.7 degrees for a 0.5 rad knob and the gun moves 0.1-0.6 — against the ~28 it would move if it were still following the wrist. The residue is the arm's own IK settling, since the hand's rotation feeds the chain that places the shoulder. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
a97ccca13c |
feat(rig lab): wrists in three axes, and sliders that belong to the pose on screen
Three things the lab could not do. WRISTS. Each hand had one scalar, a twist about the barrel. That is the only axis a hand wrapping a cylinder is free in ONCE the arc onto the barrel is solved — which is true of the support hand, was never true of the trigger hand, and in neither case left a way to cock a wrist forward or break it inward. Both now take pitch, yaw and roll, applied in the GUN's frame so the three sliders mean the same thing whether the muzzle is down at low ready or level down the sights. Zero is exactly the old behaviour, since the roll term defaulted to zero too. HAND POINTS. `gun_stock` and `gun_fore` are the distances along the weapon at which each hand sits, and they were labelled by what they measure rather than by whose hand it is. They now say TRIGGER and SUPPORT, next to the off-barrel shifts for the same two hands, so the four controls that place a hand read as four controls that place a hand. POSES. The hold's knobs are now per pose, and the lab shows one pose's at a time. Half of them mean something different at low ready than down the sights; showing both sets at once meant every slider on screen was for one of two poses with nothing saying which. Selecting a pose rebuilds the panel. Two poses, not four, and deliberately: the runtime blends between exactly two holds on `ads`. Running and Crouched are locomotion states that still use the low-ready hold, so they edit the same numbers — and the heading says so, rather than letting someone tune "Running" and wonder why standing still changed. Offering four independent tunings would be inventing a capability the code does not have, and the fourth would silently do nothing. `pitch` is the case that forced the design: down the sights the muzzle follows the CAMERA, so there is nothing there to tune. It exists at low ready and nowhere else, and a spec table where a knob names the poses it applies to is what lets that be said instead of shipping a control that does nothing. hold_pose_check asserts both halves — that no pose shows another's knobs, that aiming offers no muzzle pitch, that the heading names the hold being edited, and that all twelve wrist axes turn the hand they name. Its first version reported every wrist axis as moving the hand by 0.0 degrees, which is precisely the answer it would have given if the wrists had never been implemented: it read `get_bone_pose_rotation` from a SceneTree script, and Godot restores every bone's local pose after the modifier pass. The repo has a reference section about exactly this and it still cost a cycle. Measured through a PoseProbe, every axis turns its hand ~20 degrees. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
b1c8bab714 |
feat(rig lab): drag the anchors themselves, not just the gun under them
The lab could move the GUN and not the anchor points the hands are solved onto. `grip_offset` slides the weapon around inside the fist; `gun_fore` and `gun_stock` are distances ALONG the barrel, so the trigger and support hands could travel up and down the weapon's own axis and nowhere else. Nothing could take a hand off that axis, which is what a handguard below the bore, an angled foregrip, or a pistol whose grip is nowhere near its barrel line all need. Two things fix that. `grip_shift` and `fore_shift` give the two hand anchors real three-dimensional freedom, expressed in the GUN's own across/up/along frame so a sideways nudge stays sideways as the weapon pitches between low ready and ADS. Zero is exactly the old behaviour. Their z overlaps the along-axis distances, which is redundant and deliberate: keeping those separate is what lets the reach solver slide the support hand back down the handguard without undoing a considered sideways offset. And the markers are now draggable. They already showed the anchors; now they are handles. The one under the mouse swells and draws through the body — depth testing is right for judging whether a hand reached its target and wrong for a handle, because at any useful framing the hands occlude all three. Verified three ways, and each one had to be rebuilt once: anchor_shift_check first compared absolute positions and reported a 3.5 mm error that was the character BREATHING — there is a sin() on the muzzle pitch, so no anchor is ever in the same place twice. Measuring each anchor relative to the one it hangs off, rotated into the current gun basis, cancels the breathing, the ADS blend and the recoil exactly. 48 checks, six characters, both poses. anchor_drag_check asserts the drag writes the knob the MOUSE asked for, derived independently from the camera: 0.00-0.01 mm on all three. It does not assert the marker lands under the cursor, because it does not — the anchors hang off the shoulder and the arm chasing them moves the shoulder, so a drag settles at 0.77x-1.13x. Small enough to ignore interactively. That feedback first read as 1.5x-1.8x, because the cases were compounding on each other, and waiting LONGER for the pose to settle made it worse rather than better — which is the opposite of how a settling error behaves and is what gave it away. The buttstock case also failed for a while on a bug entirely in the test: it read an absent knob as zero when `pocket_hip` defaults to (30, -70, 60) mm. The lab has a note about that trap in `_reset`. It is just as easy to walk into from a test, and now has one there too. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
700d0925d7 |
tools: measure rest poses, and diagnose momo
momo's idle plays with her arms overhead and her waist pinched, and every assertion in the suite passes on her. The first suspicion was the retarget's rest-relative delta: it applies "what the clip does to the LIBRARY's rest" to THIS rig's rest, which quietly assumes the two rests are alike. rest_pose_check.gd tests that, and disproves it — miku's arms rest 41° off the library's and taila's 32°, and both animate correctly. The delta retarget handles a rest-pose difference, which is what it is for. Recorded in the reference so nobody spends that hour again. Writing the tool reproduced this project's own recurring mistake in miniature. Measuring "the direction from a bone to its first child" reported kiyoko's and aria's legs 71° off the library — because a thigh's first child is as likely to be a skirt bone as a shin, and it was measuring the hang of a skirt panel. Pointing it at the next limb BY ROLE dropped both to 1°. What momo actually has: `Root_001` through `Root_007` are in her driven_bones, and they are her HAIR roots — Hair_A is dominated by Root_001_001, Root_007 and Root_005. The animation is keying bones the spring solver is supposed to own, which is non-negotiable #2 broken by the role resolver rather than by a clip. The surface table corroborates it: her hair surfaces report 1.8% and 9.4% chain share, because most of their vertices belong to bones in no chain at all. Not fixed here. `Root_00N` matches no COSMETIC stem, and adding "root" to the stems would cost a rig whose actual root is called `Root` its hips. The structural fix is that a bone whose geometry is dominated by a mesh classified `hair` is a hair bone whatever it is called — which the surface table makes answerable at build time, and did not when momo was imported. It needs a Blender re-run and re-verification of all six characters. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
08ae85b286 |
feat(characters): rig anchors, and a lab that edits any tuning group
The weapon lab could tune how a character HOLDS a gun. It could not tune where
the gun sits in the hand, and that is a different question with a different
scope: a hold is per character and weapon, an anchor is a fact about the hand.
RigAnchors adds it. A hand bone's origin is the WRIST, not the palm, and how far
down the palm a grip should sit — and how the gun rolls in the fingers — depends
on how big that character's hand is and how the artist posed the thumb. It
cannot be derived, it differs per character, and it is small. So it is an offset
that defaults to identity, and identity means exactly what the code derived
before anchors existed: an untuned character is bit-for-bit unchanged.
Deliberately NOT a fixed rotation on the mount. There used to be one, and a bone
attachment is expressed in the BONE's axes, which no two rigs agree on — that
constant is why the hand mount points were wrong on every character. The derived
mount stays derived; the anchor is a nudge on top of it, and nothing here spells
a bone name.
The layering, the JSON round trip and the res://-then-user:// write are now
TuningStore, because none of that was ever specific to weapons and two copies of
it would mean two places for "an exported build's tuning pass is silently
discarded" to come back. WeaponHoldTuning is built on it with its file format
unchanged.
The lab is a rig lab now, and it grew three things:
ANCHORS a second knob group. It cost a spec table — everything in the lab is
written against "which specs, which file, keyed on what" rather than
twice against the two groups, so anchors got sliders, live preview,
reset, save and clipboard for free. A third group is a third row in
GROUPS.
CLIP audition one animation on its own. The four pose buttons are the
states the game drives; watching a whole clip end to end is how you
see where a retarget went wrong, and there was no way to do it.
SURFACES what the importer decided each surface IS, and a click to isolate a
class. Isolating is how the decision gets CHECKED: click `hair` and
anything else still standing was misclassified. Hidden by swapping
in a transparent material rather than hiding the node, because a
mesh is not one class — Miku's body, face and hair are three
surfaces of one mesh.
And it takes the game's theme, applied to its own panel rather than only to the
Window, for the same reason the pause menu needed it: a CanvasLayer is not a
Control, so theme inheritance stops at one.
debug/rig_anchor_check.gd asserts the physical consequence rather than the
plumbing — an anchor system is easy to build so that the sliders move, the file
saves, the JSON round-trips and the gun does not budge. All six characters move
their weapon by exactly the offset asked for, measured in the attachment's frame
because a world-space delta on an animating skeleton is mostly the idle
animation, and all six return exactly to the derived mount when it is cleared.
Also fixes BoltRule, whose zigzag was self-intersecting: draw_colored_polygon
triangulates, and a crossing outline fails triangulation and draws nothing at
all except a console full of "Invalid polygon data". Same silhouette, walked as
a closed loop, with the ink edge as a polyline rather than a grown polygon —
growing a concave shape from its centroid reintroduces the crossing.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
26f7c2c622 |
feat(ui): one high-voltage theme, and pick your character from the escape menu
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]> |
||
|
|
53f175ed6d |
feat(characters): say what every surface IS, and light it accordingly
A character has arrived as separate body, garment and hair meshes since the
pipeline stopped joining them — but nothing recorded which was which, so every
system downstream re-guessed from the material. That guess ("untextured and
nearly black means ink") had already rendered the mannequin's flat yellow body
as a black silhouette once.
The question is answerable once, at build time, where the mesh, the weights and
the skeleton are all in hand. tools/surface_map.py answers it three ways, in
order of how much it trusts them: the material name, which on VRoid exports is
formal and on hand-authored models is still explicit; the weights, which are
decisive when the name says nothing — a surface pulled by the skirt chain is a
skirt whatever it is called; and the material flags, which catch the model's own
line-work. The answer goes in the rig sidecar next to the roles and the chains,
and SkinSurfaces reads it.
All eighteen of Taila's surfaces, and every surface of the other five skins,
now resolve from the table with nothing falling through to the heuristic
(debug/surface_class_check.gd). The heuristic stays as the fallback, which is
the one job it was ever right for.
What that buys immediately is per-class art direction, which was impossible
while every surface had to take numbers calibrated on skin. Hair takes a much
thinner line — at the body's 5 mm each strand's hull swallows its neighbour and
the head reads as a solid dark cap. Cloth takes a heavier line and a crisper
terminator, because a garment's silhouette is most of what separates a character
from the background at range. Accessories take the heaviest. `body` is unchanged
on purpose, so the look this was all calibrated against does not move.
That required moving the outline from the instance to the surface: Miku's body,
face and hair are three surfaces of ONE mesh, so an instance-wide overlay could
only ever give all three the same weight.
Two things found on the way, fixed here because they are one line each: the
surface classifier skips meshes with no vertex groups, which drops the stray
42-vertex Icosphere that rides inside every shipped skin — two older tools
already skipped it by spelling its name — and load_model now clears _rig_info,
which a model with no skeleton used to inherit from the last character loaded.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
33d07b3717 |
feat(tools): a weapon hold lab for tuning each character's grip in 3D
I have been guessing at numbers that want an eye on them. This is the environment to set them instead. godot --path . res://debug/weapon_lab.tscn Pick a character, pick a weapon, pick a pose, drag sliders, press Save. Thirteen knobs — weapon size, support-hand distance along the barrel, grip to buttstock, muzzle pitch at low ready, both wrist rolls, three finger curl amounts, the stock pocket for low-ready and for aiming, and both elbow poles. Three markers show the points being solved for: red trigger grip, green support hand, blue buttstock. If a hand is not ON its marker the IK could not reach it, which is a different problem from the marker being in the wrong place, and the two used to be indistinguishable. Results land in assets/characters/weapon_holds.json, resolved in layers so a number can be set once and contradicted where it matters: defaults every character, every weapon skins.<skin>._all this character, every weapon skins.<skin>.<weapon> this character, this weapon An empty file means "use what the code derives", so the game runs exactly as before until something is actually tuned. The knobs that CAN be derived from the skeleton still are — mount rotation, wrist frame, weapon size — and their sliders read "auto" at zero rather than silently overriding. Two things this had to get right to be honest rather than merely present. Slider defaults come from the same table as the code defaults, because a slider parked at 0 beside a code default of 1.0 means the first touch of that slider silently switches finger curl off. And the markers are depth-tested: drawn through the body they look like they are floating in front of the chest when they are really behind an arm, which is the exact wrong impression for judging whether a hand is on its target. `-- shot <path> [skin] [weapon]` renders one framed close-up and quits, so the lab can be checked without a human at the controls — which is how both of those bugs were caught. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
270d5f0973 |
chore: track Godot uid files for the new debug tools
Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
040b595397 |
feat(anim): point the legs where the character is actually going
The clip library has one forward locomotion cycle and no strafe or backpedal clips, so a character sidestepping ran forwards on the spot while sliding sideways. Nothing in the animation said which way they were travelling and a body lean was carrying the whole burden of telling the player. Yaw the HIPS onto the travel direction and unwind it up the spine. The legs hang off the hips, so the whole stride turns with them and it costs no new animation; the chest keeps facing roughly where the player aims. Past about a right angle the hips cannot follow, so the cycle plays in reverse and the legs point the other way — a real backpedal instead of a moonwalk. The regime is hysteretic and the yaw is eased, so crossing between them reads as a pivot, which is what a person does there. The lean moved into the travel frame with it. Leaning "forward" along the facing while the legs run off to one side leans them sideways relative to their own stride, which is what being dragged rather than running feels like. It is also driven by how hard the character is moving rather than by signed forward input, so a sidestep leans into its own stride instead of standing straight up. debug/travel_dir_check.gd measures it. How far the stride points from the actual direction of travel: forward 9° fwd-diagonals 5-6° backpedal 2° back-diagonal 2° pure sidestep 39-42°, which is the deliberate hip cap Two things worth knowing about that tool. It reads bone poses from inside the modifier pass, because Godot restores them afterwards and anything read later is the animation with the pose layer missing. And it averages over a full stride: a run cycle twists the torso against the hips by tens of degrees twice per stride, so a single-frame sample measures the clip, not the layer. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
0dd9d01ac7 |
fix(cloth): solve the garment instead of repairing it five times
The skirt glitched when the character moved and the thigh still came through it. Both came from the same place: the solver integrated one spring per bone and then ran four more passes behind it — resolve the collision against the target, resolve it again against the answer, relax the cross-panel links and rebuild every pose from the corrected tips, then walk a separate ancestor "lift" — each writing bone poses the next read back and partly undid. The lift wrote poses that were never fed back into the spring state at all, so every frame began by pulling against a pose the springs did not know about. Replaced with one position-based solve, the shape Magica Cloth 2's BoneCloth uses. Every JOINT is a particle, so a bone's head can move; predict with inertia in the anchor's frame; relax length, bend, backstop, the cross-panel links and the colliders together; convert to rotations once at the end. A contact with no rotational leverage is now resolved by the panel moving, which is what a bodily chain push, an ancestor lift and a drape weight were each approximating separately. Measured, at a dead-still idle and over a movement sweep: idle jitter (skirt) 0.53 -> 0.025 deg/frame, worst 24 -> 1.9 settling after a dash 103 -> 18 mm of leg left inside the skirt fall / air / walk 82 -> 40, 96 -> 75, 72 -> 76 mm run / slide / dash unchanged, ~95 mm The idle buzz and the failure to come home after a hard move are gone — those were the "glitches out". Peak clipping in a run, a slide and a dash is NOT fixed and is still around 95 mm. Four things this turned up on the way: - debug/cloth_clip_check.gd was measuring the animation, not the render. Godot restores bone poses after the modifier pass, so reading them with force_update_all_bone_transforms() afterwards sees nothing any modifier did. It reported the same ~95 mm with collision fully enabled and with it commented out. It now observes from inside the pass. Every number ever taken from this tool before now was measuring the wrong pose. - The collision hulls came from ten farthest-point samples per bone, which describe a panel's corners and hem and leave its MIDDLE unsampled — exactly where a thigh comes through. Built from the real mesh at load time instead. - The drape term is gone. It was there to move a panel the old solver could not, and once the solver could, it was worse in every state but a walk and cost 20x in stability: its target sat inside the leg the collision was pushing out of, so the two ran against each other forever. - Cost was 10.9 ms per character. The inner loop rebuilt every capsule and reallocated the hull array for every (bone, collider, pass). Now 2.6 ms at full quality with a distance LOD behind it. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
374d9f9822 | feat: implement automated 3D character pipeline with retargeting and rig management tools | ||
|
|
1e12c36cca |
tools: give the limb deform check a baseline that validates itself
The measurement was comparing skinned lengths against BIND-POSE lengths. A
character is never in bind pose, so ordinary posing registered as deformation:
with the animation frozen it still reported 0.42x-2.56x edge ratios, and during
a run it reported edges collapsing to 0.08x and blowing out to 5.2x. Those
numbers were an artefact of the metric, not the model.
The baseline is now built from the skeleton's REST transforms, so every ratio
reads exactly 1.00 on an unposed model and the tool validates itself. Corrected
readings on Taila through the full runtime stack, worst over run, walk, jump,
fall, slide and dash:
lengthwise stretch 1.07 Body, 1.08 ClothB, 1.09 ClothCAndW, 1.03 shell
cross-section 0.97-0.99 knee, 0.85 hip, 0.92-0.94 ankle
That is ordinary linear-blend skinning.
Also records, in the header, the two ways this measurement was previously wrong,
because both nearly produced a bad "fix":
* the bind-pose baseline above;
* flagging vertices weighted across "non-adjacent" leg bones with
max_slot - min_slot >= 2, which flags shin+foot+toe — a perfectly normal
contiguous run. A repair pass built on that was written and reverted before
it shipped; it was stripping legitimate toe and shin influences.
Checks that did come back clean and are worth not repeating: bone pose bases are
pure rotations to 0.00001, bone lengths never drift from their rest offsets, and
of 4019 coincident-vertex groups not one carries mismatched weights, so there
are no split seams tearing at the joints.
No character or gameplay code changed. skin_leg_repair.gd is byte-identical in
behaviour to the last commit; only its header comment gained a pointer to this
tool.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
||
|
|
204846bf09 |
tools: measure limb deformation instead of eyeballing it
Adds debug/limb_deform_check.gd, which skins the leg vertices by hand and compares them against the rest pose across a sweep of run, walk, jump, fall, slide and dash. It reports lengthwise stretch (bending a knee can only shrink this, so growth is genuine stretching) and cross-section loss (the linear-blend "candy wrapper" collapse at a bent joint). No gameplay or character code changes in this commit. Current readings on Taila, through the full runtime stack: lengthwise stretch 1.05 worst (Body, ClothB, ClothCAndW), 1.03 outline shell cross-section 0.93 knee, 0.84 hip, 0.91 ankle Sanity check on the method: the measured rest span of the leg vertex cloud is 0.818 m against 0.843 m of thigh+shin bone length, so the skinning maths is producing real positions rather than plausible-looking noise. That is ordinary linear-blend skinning. Combined with the earlier findings — no bone pose basis deviating from a pure rotation by more than 0.00001, and no bone length drifting by more than 0.0000 m — the leg geometry is not squashing or stretching in any measurable way, so there is nothing here to fix blind. Committing the instrument so the next round starts from a number. Co-Authored-By: Claude Opus 4.8 <[email protected]> |
||
|
|
35ada4f34a |
fix: unlink the ankle cuffs, stop the reload head-dive, soften every blend
Three animation complaints, measured before changing anything. There is NO squash-and-stretch. Instrumented every bone every frame: the worst deviation of a pose basis from a pure rotation is 0.00000. Every clip's position tracks match the skeleton's rest pose to within a millimetre (only DEF-hips translates, as it should) and no scale key deviates from 1. The "squishing" is a deep BEND, not a scale: the shared library's PistolReload was authored for someone watching their own hands, and it dives the head 33° with the whole neck chain at 67°. At that depth Taila's head/hair weights pinch and the skull changes shape. Dropping neck+head from the upper-body one-shot's filter leaves them on the locomotion layer: head dive is now 14° and the neck chain 48°, and the character keeps looking downrange through a reload, which is what a shooter wants anyway. Trade-off: Throw/Hit no longer swing the head. The ankle cuffs are not linked by the rig or the clips — the foot bones swing independently (left/right Z correlation -0.94). They are linked by SKIN WEIGHTS: 16 vertices belonging to the LEFT cuff, sitting at x = -0.002 just across the centre line, are weighted to the RIGHT leg, so the far foot drags a band of cuff across the gap. Position alone cannot classify them (the cuff they belong to spans x = 0.00 .. 0.05), so SkinWeightRepair decides by CONNECTIVITY: a vertex that disagrees with 80%+ of the vertices it shares triangles with gets its leg influences mirrored. Ankle-height cross-leg triangles go 24 -> 0 on Taila; the pass is conservative enough that Miku needs exactly 1 vertex and her face/hair/skirt are untouched (only leg bones are ever considered). Meshes with blend shapes are skipped rather than rebuilt. Blending: nothing cuts hard any more. Base cross-fade 0.15 -> 0.22, locomotion 0.28-0.30 (Idle/Walk/Run/Sprint switch constantly as speed drifts across their thresholds, which is where the snapping showed most), reaction moves raised off their near-instant values but still snappy, and the upper-body one-shot's own fades widened to 0.14 in / 0.22 out. Co-Authored-By: Claude Opus 4.8 <[email protected]> |
||
|
|
d8b0c08611 |
fix: Taila reads like her source render — no white rim, matte hair
Two separate causes behind "too glossy with a thin white outline": 1. The model carries its OWN cel line-work as extra, UNTEXTURED surfaces — an inverted-hull outline shell plus eye-line/highlight cards (Taila names them FullBlack / EyesFullBlack / EyesInvL / EyesHL). They are authored to read as flat black, but the glTF import hands them a default near-white albedo, and apply_toon_recursive then LIT them. That is the thin white rim: a pale, toon-shaded outline shell tracing every hair strand. They now render unshaded flat ink (flat white for the "HL" highlight card). 2. The skin textures already have cel shading painted in. Stacking our hard 3-tone break on top produced a bright stripe that slid across the hair as the camera moved — the "gloss". Characters now get a soft terminator and an almost-invisible second step, so the painted shading carries the form. Both live in a new LevelMaterials.apply_character_look(), called only for imported character GLBs, so props and level geometry keep exactly the crisp banding they were calibrated with. The one shared change is that the toon shader's second-step darkness is now the mid_tone uniform, defaulting to the 0.82 that was hardcoded — no change for existing callers. Character shadow tint (0.64, 0.56, 0.72) is measured off the Sketchfab reference render rather than guessed: sampling the hair there, shadow/midtone lands near (0.63, 0.53, 0.70). Green drops hardest, which is what keeps copper hair copper instead of washing it to brown. debug/character_look_capture.gd renders the raw import beside our treatment under matched lighting — the comparison this was tuned against. Co-Authored-By: Claude Opus 4.8 <[email protected]> |
||
|
|
1172e465c1 | chore: add UID file for bone dumping debug utility | ||
|
|
0d125dc03f |
fix: third-person gun actually held — IK to the weapon, real recoil, no flip
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]> |
||
|
|
df0bf18e0f |
feat: real per-weapon reload/recoil choreography + knife slash, fully matte hair
Weapons no longer reload by spinning the gun 360 (that was the upside-down
flip; nothing detached). New ViewmodelAnim module drives per-style manual-of-
arms sequences on the existing model_root + named arm pivots, with a temporary
magazine/shell/rocket/cell prop that visibly leaves and returns to the gun:
- mag (M4/AK/MP7/DMR): roll gun to present the well, hand rips the mag
down and away, fresh mag up, seat, charge. Gun stays UPRIGHT.
- boltmag (AWP): mag swap + a distinct bolt-cycle rock.
- break (double barrel): hinge open, flick both shells out, drop fresh
shells in, snap shut.
- tube (rocket launcher): tip the tube in, shove a rocket home, shoulder.
- cell (plasma/nail gun): glowing energy cell swapped on the side.
Per-shot viewmodel recoil kick (ViewmodelAnim.apply_kick) scaled by each
weapon's recoil_amplitude, so an AWP slams and an MP7 chatters — replaces the
old flat model behaviour. Weapons expose reload_style; base classes call
play_reload / stop (on unequip) / apply_kick.
Knife: the old barely-visible flick is now a real diagonal slash that
alternates backhand/forehand, whips the blade through screen centre, and
fires the third-person melee arm-swing action (synced to other players).
Hair/materials: specular fully to 0 (even a 2% stepped glint swept as shine
across Taila's hair when the camera moved); rim trimmed to a whisper. Fully
matte cloth/hair now.
Verify: debug/fp_weapon_capture.gd screenshots each weapon's reload phases,
the knife slash, and the fire kick.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
||
|
|
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]> |
||
|
|
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]> |
||
|
|
d0c746d084 |
feat: Taila — second anime character through a hardened import pipeline
New selectable skin: Taila (original anime character by Partaevil, CC-BY, license kept alongside the model), cel-shaded with ink outlines, running the full 18-clip animation set + the new rifle-hold pose layer. Pipeline hardening learned the hard way (each of these produced a broken character before the fix): - tools/strip_rig.py: strips a foreign rig (VRoid/Mixamo names bake FLAT clips in the name-based retarget), keeps only meshes skinned to that rig (scene props were joining into the player model), recentres feet-on-origin, and reroutes image textures into Principled base color for re-export. - tools/unlit_to_pbr.py: KHR_materials_unlit anime models carry their albedo in emissiveTexture over a black base — Blender 5.1 drops the texture on import, rendering the character pitch black. Rewrites the GLB JSON to standard textured PBR before Blender ever sees it. Also dropped the Elle import attempt: her rest pose is seated (posed scene), which the T-pose autorig cannot use. Co-Authored-By: Claude Fable 5 <[email protected]> |
||
|
|
6c227b5538 |
feat: real two-hand rifle hold — armed animations finally read as a shooter
The clips were playing all along, but every locomotion state used unarmed jog arms while the gun floated at the hip, so nothing looked animated. - ShooterPoseModifier now REPLACES the arm chain with a direction-based FK rifle hold (low-ready at hip, shouldered on ADS) instead of nudging the unarmed clip additively; the wrist is solved so the muzzle points exactly along the aim line, gun kept upright (no-roll constraint). - Per-arm release logic: reload/throw/hit one-shots, dance, death, the slide ground-brace, wall-run reach and grapple free the authored clips. - Third-person weapon: fixed re-hide on weapon swap while in third person, scaled to character proportions, gripped along the hand. - Armed idle uses Idle (arms owned by the hold) instead of arms-crossed PistolIdle base. - FP viewmodel arms: character-styled sleeve + teal cuff + skin hand instead of the blue slabs. - debug/anim_capture.gd: front+side screenshots of every movement state, one-shot action and ADS for visual regression of the model pipeline. Co-Authored-By: Claude Fable 5 <[email protected]> |
||
|
|
7920aecec5 |
feat: player animation quality pass — aim follow, reload/throw, recoil
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]> |
||
|
|
04d91dea31 |
feat: street-level detail pass — real roads, streetlights, trees, cables
Closes more of the gap to hand-authored city art at eye level: - Kenney City Kit Roads (CC0) installed: every street in the 10x10 grid is now real road meshes — lane-marked straights (single stretched mesh per segment) and line-marked crossroad tiles at all 81 intersections, tinted to sit under the toon lighting without clipping - Kit streetlights (curved arms over the road) along both central avenues, with slim pole colliders kept for grapples - Kenney Nature Kit trees replace the cone/box stand-ins in shrine courtyards and plaza planters (original kit materials kept — the toon swap mangled their vertex colors into cyan) - Overhead cable bundles sag across the east-west streets (the classic urban-Japan sky clutter), drawn as ink lines; central avenue kept clear - Construction sites get cones + barriers from the roads kit - Per-building tint jitter so repeated kit models read as distinct shops Verified via street/aerial captures (dark marked asphalt, green trees, detailed facades); movement 11/11, smoke 0 failures, acoustics ALL PASSED. Co-Authored-By: Claude Fable 5 <[email protected]> |
||
|
|
f62f4f2503 | feat: add Kenney road and nature assets and implement map builder script | ||
|
|
b0af2ea9ee |
feat: Akiba Crossing city — 100 blocks with unique landmarks + detail pass
Scaled the single block to a full 10x10 city grid (48m cells, 16m streets, 640m across) with a hand-authored layout so the fabric stays varied: Block types: - S standard retail (70%): paired parametric buildings facing the N-S streets, seeded heights/colors per block; ~half get a mid-block alley with fire-escape climbs so no two blocks play identically - T landmark towers: 8-10 floors on a walkable retail podium, mega corner sign, double-sided rooftop crown billboard (skyline wayfinding) - J shrine courtyards: walled court, torii, slide-roof hall, lanterns, trees — the green quiet break in the fabric - M market streets: dense stall rows + lantern strings between low buildings (close-quarters cover mazes) - P plaza cluster at the city centre with planter cover and a giant 4-face screen tower at the exact middle (the Crossing) - C construction sites: open slab frame to wallrun, access ramp, container stacks, and a 28m crane whose jib is the city's tallest grapple anchor City systems: - Brick rail viaduct runs the full east-west width on per-block piers with vending arcades under alternating arches + slide ramps at the centre - Central avenues x=0/z=0 get matched streetlights and zebra crossings at every block line; brick wall rings the district; city-haze fog for depth - Detail pass per building: window bands every floor, water tanks/antenna masts seeded on roofs, denser sign stacking Perf/physics: decoration (signs, windows, leaves, lamp heads) is render-only MeshInstances; only gameplay surfaces are static bodies, all acoustics-tagged. Spawns distributed across block types facing centre. Verified: full-city aerial (all 100 blocks, aligned grid), avenue-level capture (clean canyon, viaduct vista); movement 11/11, smoke 0 failures, acoustics ALL PASSED. Co-Authored-By: Claude Fable 5 <[email protected]> |
||
|
|
233f88aee3 |
feat: full cel treatment for Neon Alley — flat toon colors + ink outlines
- LevelMaterials.flat(): toon-banded FLAT color material (no greybox grid texture) for dressed maps; cached per tint, rim/spec off - Neon Alley overrides _box_static: every structure renders flat cel color; prop-scale pieces (<10m) get an inverted-hull ink outline scaled to their size. Building-scale boxes skip the hull — the inflated plates smear into black voids at close range (found via fixed street-camera capture) - Pylon wires render as flat ink lines; warmer wood + hotter vermilion for flat shading (the grid multiply used to brighten tints) - visual_capture: deterministic street-level shot after the aerial for style comparisons Street capture now reads properly BoomerangX: flat lavender asphalt, hard light bands, torii framed down the underpass, neon strips + sign glow. Co-Authored-By: Claude Fable 5 <[email protected]> |
||
|
|
0813d0b40d |
feat: Neon Alley — urban-Japan movement map built on the acoustic materials
New map (scenes/maps/neon_alley/, auto-appears in the level selectors): Layout for movement flow: - Main N-S street = the long sightline, crossed by a pedestrian bridge (slide ramps up both ends, underpass cover below, railed high ground) - West machiya row: wooden two-story houses with a jump route (awning -> balcony -> roof deck) and 25-degree slide roofs facing the street - East concrete towers with glass storefronts; brick wallrun alley between them (AC units to vault); metal fire-escape platform route to the roofs; angled rooftop billboard as cover + grapple anchor - Three power pylons with crossarms over the street chain a grapple route; the torii gate crossbeam at the north shrine plaza is a fourth anchor - South market: wooden stalls with neon canopies + metal vending machines = scattered mid cover; shipping container corner piece - Spawns ring the block and face the map center Acoustics as level design: every surface is tagged (machiya/stalls/torii wood, towers concrete, storefronts glass, alley+perimeter brick, escapes/ AC/rails/container metal) so occlusion tells you which structure a shot came through and reflections differ per street. Style: sunset sky + fixed dusk-lavender ambient (calibrated for the toon shader's 1/PI light response), warm low sun, neon sign columns, shop signs, lantern strings, billboard glow, underpass strip lights. Verified: aerial + street-level + third-person captures; movement 11/11, smoke 0 failures, acoustics ALL PASSED. Co-Authored-By: Claude Fable 5 <[email protected]> |
||
|
|
a51595e3b9 |
feat: material-aware sound occlusion + early reflections for localization
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]> |
||
|
|
a9b0775de3 |
feat: real gunshots, localized 3D audio, seamless music, better animations
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]> |
||
|
|
6c8b0ddd28 |
feat: cel-shaded look & feel overhaul — real audio, comic UI, themed VFX
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]> |
||
|
|
5bcbcfdc3f |
feat: emotive animations + anime speed lines — hit flinch, dance emote (B)
- 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]> |
||
|
|
37c15bed1a |
feat: cel-shaded look — toon shader, ink outlines, stylized environment
- toon.gdshader: banded diffuse with cool-tinted shadows (3 tones), stepped specular, rim light, and an optional world-triplanar albedo path so level geometry keeps the 0.5 m grid with no UVs - toon_outline.gdshader: inverted-hull ink outline, applied via material_overlay so it works on skinned (deforming) meshes - LevelMaterials: tinted() now returns toon grid materials; toonify()/ apply_toon_recursive() convert imported character/prop materials in place - SkinnedPlayerModel: characters get toon shading + outline on load - LevelEnvironment: shared stylized environment for all maps — saturated anime sky, linear tonemap (filmic/ACES crush cel bands), bloom for emissives, saturation+contrast grade, light depth haze; test level and dust2 now use it and the procedural arena (which had NO environment) gets it plus runtime toonification of its baked geometry Co-Authored-By: Claude Fable 5 <[email protected]> |
||
|
|
198345177a |
feat: world-grid level materials + real movement test suite
- 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]> |
||
|
|
0ba14a9469 |
feat: overhaul movement for flow — momentum-preserving states, smooth crouch, landing feel
- 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]> |
||
|
|
c4a538a469 |
chore: add miku_test skin textures and script .uid files
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]> |
||
|
|
29fdca3565 |
feat: shooter animation feel — directional lean, slide, composed airborne, weapon hold
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]> |