Skip to content

S10: The player, visibly bionic - #374

Merged
jamescrosswell merged 4 commits into
mainfrom
task/357-bionic-player
Oct 22, 2026
Merged

jamescrosswell merged 4 commits into
mainfrom
task/357-bionic-player

Conversation

@jamescrosswell

@jamescrosswell jamescrosswell commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

The player's figure in every combination of the hydraulic legs, the datajack and the infrared eye, in every pose, made from the player's whole-FLUX figure. The side view shows whichever combination player_state says is fitted, and swaps it when that changes. Residents' figures are untouched.

Closes #357

Please look at

The previews branch also has each screenshot on its own, and the scripts and the full-edit pipeline that made them.

Full edits or overlays: overlays, and that's what ships

I made both:

  • Full edits: each combination's frame is an edit of the same frame one part short. For example, legs, then legs and datajack, then all three.
  • Overlays: each part is painted once on each frame, as an edit of the whole player's frame. What that edit changed near the part is then laid over the whole player's frames for every combination (bionics.changed, bionics.laid). This is "painted once" per frame, not once overall: the legs are drawn differently in every pose.
Full edits Overlays
Paintings 45 frames 20 frames
FLUX about 40 min for the first pass, then 23 more to repaint the eye combinations about 20 min, then 13 to repaint the eye one size and try the run legs again
Atlases 7 × ~46 KB the same, since they're laid when the build writes them
Consistency Each edit redraws the whole figure, so the face, hair and backpack shift a little between combinations The player is identical in every combination except for the parts, and each part is identical in every combination it's in
Failures Edits compound. In the first pass, adding the eye turned the datajack's light red, or painted both eyes, in 8 of the 12 frames with both. Asking again fixed most, but one frame still lost the datajack, and the eye grew and glowed Each part is checked once, on its own
More bionics 2ⁿ combinations × frames n parts × frames

At play size, the full edits' best frames look about as good as the overlays. The overlays' seams are feathered and I can't see them. I recommend overlays, so the pipeline ships them. The full-edit code is on the previews branch if you'd rather have them.

The overlays are laid at build time, so the client still just swaps one atlas per combination. If later stages add many bionics, the same layers could be laid in the client instead, so the art grows by parts rather than combinations.

The eye's side

Both eyes show in three-quarter view, and the client mirrors every figure to face west. One painting per combination, with the eye in the near socket, by the ear, means the eye swaps sockets as the player turns. Every figure's hair parting and leading leg already do the same. Painting the far socket too would need the eye painted again on every frame, plus a figure chosen by facing. The eye would also show only as a red speck by the nose whenever the player faced away from its socket. The eye costs the most socially, so I'd rather it always shows. Either socket gets the same figure (playerFigureId).

How it's checked

Every part's frame gets #337's colour, face and scale checks against the whole player's standing figure. It also has to show its part (bionics.missing), or it's painted again:

  • steel over 12% of the legs. The whole player's sneakers have about 6% grey shading; the hydraulic legs have 13 to 23%.
  • the green light
  • one red lens, not two, no wider than 17% of the head. The walk's lenses are about 15%, so the eye doesn't grow when the player stops.

Three changes make those checks work for the parts:

  • The face check spares the lens and its rim.
  • A frame may show the parts' steel, red and green without them counting as strays.
  • With the legs, the standing colour check looks for the trousers' colour in the shorts.

test_content.py runs all of this on the committed atlases, along with the residents' existing checks. It also checks that:

  • every combination is the whole player's size
  • the whole player's own frames show none of the parts
  • the lens is within 2 atlas pixels of one size in every frame

Notes

  • The client side:
    • world/assets.ts keeps the fitted body from player_state (playerBody), and dollFor('player') picks the figure for it.
    • SideDoll wears the new figure when it changes, through a new SpriteFigure.wear. It also catches up if the body changed while its first atlas loaded.
    • Both are tested with a stand-in Phaser (dolls/phaser.fixture.ts).
    • side-doll.ts and poses.ts aren't S5's, so side-scene.ts is untouched.
    • S5 has since merged playerState.latest. world/assets.ts subscribes at module load as well, so it doesn't depend on it.
  • simile dolls paint now waits for the GPU (flux.wait_for_gpu). It waits while another mflux, simile papercraft or simile dolls run, or an Ollama model, is using it, but not for itself or the uv run that started it.
  • A player look painted by the creator's doll job gets no bionic figures. The game has never drawn job-painted player figures; it uses content/dolls/player.
  • Taste calls:
    • The datajack: FLUX draws it as a chrome band round the neck with a green port, however it's asked not to. It reads at play size and it's the same in every frame, so I kept it.
    • The legs: shorts with steel shins and the same sneakers, so the trousers' and shoes' colours still check and the legs read at play size.
    • The eye: the smaller red lens in a thin rim, which beat a second prompt's bigger, glowing lens. The sitting frame keeps a thin arm to the ear, like a monocle, after rerolls. The running passing step's lens is a little darker; rerolling it only gave bigger lenses with arms, so it stays.
    • In the two run frames, a strip of bare knee shows between the hem and the steel. Rerolling both legs frames kept the strip, so the reviewed paintings stay.
  • Rerolls (build.REROLLS):
    • the datajack's stride, whose light was a 2-pixel speck
    • the eye's standing frame, whose lens was twice the walk's
    • the eye's passing step, painted dark
    • the eye's sitting frame, outsize too
  • FLUX: about 98 minutes of painting in all, with model loads, including 13 for the review's repaints.

🤖 Generated with Claude Code

jamescrosswell and others added 2 commits October 23, 2026 05:11
The player's figure with every combination of the hydraulic legs, the
datajack and the infrared eye, in every pose. Each part is painted once
on every frame as a FLUX edit of the whole player's frame, checked, and
laid over the whole player's frames where it changed them, so every
combination is the same player but for its parts. The side view's player
wears whichever combination player_state says is fitted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jamescrosswell

Copy link
Copy Markdown
Contributor Author

Changes requested. The art and the overlay approach hold up. Three fixes and a docstring trim.

What I checked:

  • pytest tests/dolls: 411 passed. Vitest on dolls, side and world: 544 passed.
  • Merges cleanly with main and with S6: Salvage, and selling it #373.
  • No schema change, and player.* and the residents' files are untouched.
  • 322 KB of WebP added.
  • I extracted every frame from the atlases. Outside the parts, the combinations differ from player by at most 3 pixels, which is WebP loss. Each part is pixel-identical in every combination it's in.
  • No seams or halos at 4×, in any pose.
  • Full edits drifting is plain on the sheets: the datajack's light goes red and the eye grows. Overlays are the right call.
  • The eye in the near socket doesn't conflict with Body or the plan.

Changes

  1. The legs check passes a frame with no legs. The whole player's own frames already measure 0.050 to 0.064 "steel" in the legs (stand 0.064, stride 0.056, sit 0.050), from the grey shading on the white sneakers. That is above the 0.042 bar in test_every_frame_shows_the_bionics_fitted (LEAST 0.06 × SHOWN 0.7) in 6 of 7 frames. Stand is even above the build's 0.06.
    • Real legs frames measure 0.185 to 0.259.
    • Fix: leave the shoes out of the legs region in bionics.parts/missing, or raise LEAST[HYDRAULIC_LEGS] to about 0.12.
    • Add a negative test: missing(frame("player", f), FITTED, f) names every part on the committed atlas.
    • The datajack and eye checks do fail correctly. The whole player has 0 green and 0 red pixels, a second red eye gives two eyes, and a cheek smudge still fails the spared face check (0.22).
  2. The swap can miss a player_state that arrives while the atlas loads. SideScene.figure() picks dollFor(id) before addFigureAtlas, but SideDoll only subscribes in its constructor, after the load. A change in between is never heard, and the player keeps the stale figure until the next change. A one-line fix in side-doll.ts: after subscribing, if (dollFor(PLAYER_FIGURE) !== figure) void this.wear(dollFor(PLAYER_FIGURE)).
  3. Nothing tests the swap. The figure choice is covered (playerFigureId, PlayerBody, dollFor from player_state). Nothing exercises SpriteFigure.wear or SideDoll's subscription, including the case in 2. side/street-view.test.ts already mocks Phaser, so this can be done the same way.
  4. Cut these docstrings to one line. Move anything worth keeping to content/dolls/README.md, which already says most of it.
    • New: bionics.py module docstring (4 lines), SHORTS, PALETTE, REACH, missing, changed, laid; build.py fitted_fingerprint, paint_parts, fit, build_fitted (3 lines); flux.py fitted; dolls/bionics.ts playerFigureId; sprite-figure.ts wear.
    • Grown: build.py paint_frame and files (1 to 2 lines), attempt (4 to 5); measure.py strays (2 to 3); sprites.py frames (4 to 5); world/assets.ts dollFor (1 to 2).

Notes for the user, not blockers

  • Datajack: a chrome neck band with a green port, the same in every frame. It reads at play size, but as a collar more than a port.
  • Legs: shorts over steel shins in the same sneakers. They read well. In the two run frames, a strip of bare knee shows between the hem and the steel.
  • Eye: the lens is about twice as big standing (73 px) as in the walk and run frames (36 to 39 px), so it pops when the player stops. The run_pass lens is darker.
  • Eye side: the eye swaps sockets when mirrored, like the hair parting. Anything S11 says about which eye won't match the art.
  • Sitting: the eye's sitting frame has a thin monocle arm to the ear.
  • Wait for the GPU: flux.wait_for_gpu is outside S10's job, though it's in dolls/. It waits as long as any Ollama model is loaded.

jamescrosswell and others added 2 commits October 23, 2026 06:14
…ught up

- The legs check asked for 6% steel, which the whole player's sneakers
  nearly reach; it asks for 12%, and a test checks the whole player shows
  no part.
- A lens wider than 17% of the head is too big: the standing and sitting
  eyes are painted again to the walk's size.
- The side view's player wears the fitted figure if the body changed while
  its first atlas loaded, and SpriteFigure.wear and SideDoll's following
  are tested with a stand-in Phaser.
- One-line docstrings; the detail is in content/dolls/README.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 22, 2026
@jamescrosswell

Copy link
Copy Markdown
Contributor Author

Approved. All four items are fixed. Re-checked at ed7e84b:

  • Tests: pytest tests/dolls: 417 passed. Vitest on dolls, side and world: 645 passed. Merges cleanly with main and with S6: Salvage, and selling it #373.
  • Legs check: LEAST[legs] 0.12 now fails the whole player's frames, which measure 0.050 to 0.064. The legs frames measure at least 0.185. test_the_whole_player_shows_no_bionics names every part on each committed player frame.
  • Swap:
    • SideDoll catches up after subscribing.
    • With the re-check removed, catches up with a bionic fitted while its first atlas loaded fails.
    • The sprite-figure tests drive the real wear → addFigureAtlas → paint → draw path, with real frame choice and out-of-order loads.
  • Eyes: lenses are now 6 to 8 px wide in every frame. Standing is 44 px of lens, against 73 before and 37 to 38 walking. At 4×, the standing and sitting eyes match the walk's.
  • No other figure changed: this round, only the four eye combinations' atlases changed. player.* and the residents are untouched. Outside the parts, every combination is still within 4 px of player.
  • Docstrings: every one I listed is one line now.

Non-blocking: stops following once it's gone passes even without the unsubscribe. A destroyed figure's wear fails at this.scene.textures anyway, so the test can't tell. Asserting that console.warn isn't called would make it bite.

@jamescrosswell
jamescrosswell marked this pull request as ready for review October 22, 2026 17:46
@jamescrosswell
jamescrosswell merged commit 1837e0a into main Oct 22, 2026
7 checks passed
@jamescrosswell
jamescrosswell deleted the task/357-bionic-player branch October 22, 2026 17:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

S10: The player, visibly bionic

1 participant