Skip to content

Pets, Canon's questline, the wreck, and the September scene revamp - #8

Merged
joshuaroll merged 19 commits into
mainfrom
feat/pets-academy-island
Sep 12, 2026
Merged

joshuaroll merged 19 commits into
mainfrom
feat/pets-academy-island

Conversation

@joshuaroll

Copy link
Copy Markdown
Owner

Eighteen commits from the island branch: pets and island systems, pet abilities and Canon's questline, the diving suit and the wreck story, one Fredward drawn by one model, the scene-art skill, and the September scene revamp (shared character kit, rubber-hose passes on every location, map details, scene and character CI gates, PNG renderer).

npm run verify is green locally. Composed into bitcryptic-org/world for the M6 cutover.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Cr1HRkEPpcngrJyKuceTys

joshuaroll and others added 18 commits September 4, 2026 03:18
Adds the pet system and the systems around it, plus fixes for bugs found
along the way.

Pets (js/pets.js, check:pets in the gate)
  Five creatures, one per clue device, caught by solving and housed in the
  cottage. Three rules the check enforces: a hinted solve never earns a pet,
  feeding never says "wrong", and there is no neglect mechanic. Drawn in the
  croc plushie's language rather than as one recoloured blob. The hedgehog
  has three quill styles.

  Gated behind a catching method (bcw_pet_catcher), which Canon's quest will
  grant. The gate checks run BEFORE BCWPets.onSolve(), because that call
  marks a species seen and would otherwise burn the one-time offer on a
  player who cannot act on it.

Island systems
  materials, cottage, weather, islanders, notebook, graduation, dailyTerminal.

Fixes
  - blackjack: a stale setTimeout chain dealt into the next hand after the
    table was closed and reopened; doubling raised the bet permanently, so
    the following hand silently wagered double. Blackjack now pays even money.
  - the bucket: base 1 -> 2, and 2 tiers -> 6, because the shop only shows the
    next unowned tier and the category vanished once you owned the second.
  - fish_collection asked for 10 unique species and there are 9, so it could
    never fire. Now 5, plus The Full Shoal for all of them.
  - the Mounted Fish palette tile was ungated: anyone could place a trophy
    they had not bought.
  - window curtains used rx="0 0 6 6", CSS shorthand in an attribute that
    takes one length, erroring on every house open.
  - Tower Guardian runes had no text-anchor and ran off the breastplate.
  - Mark's dome was two quadratic curves, i.e. a tent, not a hemisphere.
  - sawtoothMantle forced to an odd tooth count: an even count puts a valley
    on the centre line and the quills read lopsided.

Also: POG/BOSTON toggles that appear only once redeemed and never take the
yacht away, per-object colourways, a PNG export of the house, the ANGAS code
granting The Minute Watch, enumerations on all 27 daily clues, and Simon's
MANIFEST clue reduced to a clean anagram (both clue changes approved).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VqfPVC3ZMY6Gz1Ak2TrETR
Ten ideas from the Club Penguin and Layton research, built.

PETS NOW DO SOMETHING (idea 04)
The black puffle rode the cart and did nothing, and that is what
everybody remembers being disappointed by. The green puffle fetched
coins and everybody remembers using it. So each creature performs its
own device on text the player chooses: the croc shuffles letters, the
hedgehog marks a run buried across a word end, the ram reads backwards,
the chameleon slides a seam through an enumeration, the cat threads one
fragment through another.

Never coins. This island already pays in coins and in material, and a
creature that prints currency devalues what the player earned. And
never an answer: each ability does the manual labour a solver does by
hand and returns the possibility space. check:pets asserts this by
demanding the croc return arrangements no dictionary would.

ONE COMES WITH YOU
A HUD badge, not a map marker, because the island has no player avatar
to walk beside and the badge is reachable from inside a clue modal,
which is when a solver actually wants it. Swapping happens at the
cottage, which gives the house a reason to exist past decoration.

THE LABOUR GRADUATES (idea 10)
Spiritfarer is 94% positive and its best negative review is fair: "it
is not good on average", because the verbs at hour forty are the verbs
at hour one. So every ability has a second tier earned by feeding a pet
its own device cheese, which makes the progression the pairing quiz
played out over weeks. Material yield tiers the same way, off unaided
solves. Both are ratchets; nothing decays.

TWO WAYS TO A CREATURE (idea 08)
Pets sat entirely behind a secret terminal code. That is the shape of
Layton's most-criticised gate and reviewers named it. Now Fredward's
lure, found at a wreck that needs no code and anybody can reach by
boat, catches exactly one creature and then sulks. Canon's questline
upgrades it to the real thing. Nobody is locked out of the warmest
content; everybody who wants all five still wants the quest.

Declining the lure un-sees the species, so saving it for somebody else
is a choice rather than a trap.

CANON'S QUESTLINE
Five errands recovering five things a crossing shed. The sediment
principle made literal: fetching objects out of the landscape is
dredging a settled man's dropped life out of the terrain, so the quest
structure and the metaphysics are the same act.

  * One step per mission is randomised PER SAVE (idea 01). Club Penguin
    did this in three of eleven missions and it is the cheapest
    integrity mechanism in the genre. Written wrong the first time,
    minting the seed on every read, which silently gave every save in
    the world identical missions while every test still passed;
    check:quest now asserts the seed is written down once.
  * The last errand has no gadget (idea 02). Canon opens the drawer,
    looks in it, and says so. The spike is the Academy's argument
    stated as a mechanic.
  * Artifacts arrive as NOTEBOOK PAGES (idea 03), with room to write
    under them. The notebook was fully built and the quest used none of
    it; the alternative was an inventory screen, which is a thing you
    look at once.
  * Nothing ever expires (idea 09). 52% of Layton's puzzles no longer
    exist because they were download-only and the servers closed.
    check:quest fails if a clock enters this file.

THE ISLAND NOTICES (idea 07)
All five islanders react to the creature at your heel and to how far
into the errands you are. No line resolves a planted ambiguity: the
Dock Keeper still greets it as a returner and still does not explain
himself.

ALSO
BCWSave.ALL_KEYS is exported. It had drifted twice, always silently,
because it could not be checked from outside; now a test asserts every
new key reaches export, backup and account deletion, and it fails by
name when one is missing.

Gates: 39 scripts parse, clue freeze 1357e386021d intact, pets and
quest checks pass under mutation, 9 sync-coverage tests including two
new ones. The three e2e failures on this branch are pre-existing
harness 404s; the baseline fails five.

Scripts for both stories are drafted, reviewed twice and awaiting G3
sign-off in workshop/drafts. No clue was written, composed or reworded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ai78b5KJDmPPwfCCoXwo3E
visitWreck() has been calling startStory('wreck') against a story that
was never written. The guard in startStory returns before touching the
DOM, so the failure was invisible: the panel closed, the map label
appeared, and nothing opened. Worse, the first-visit flag was consumed
on the way past, so any save that reached the wreck could only ever be
routed to wreck_return afterwards.

Both stories now exist, and wreck_return is written to stand alone for
exactly that reason: a save that burned the flag before today gets it
as a cold introduction rather than a recap of a scene it never saw.

WHAT THE STORY DOES
Fredward Fishington lives in the wreck and is the happiest person in
the game. He catalogues creatures, he is delighted to have a visitor,
and when you ask him where home is he says the wreck, warmly, and means
it. Nothing narrates what just happened; the step ends on the crab,
which has not moved.

He also gets the strongest argument in either script, because the lore
ledger refuses to say whether this island is a threat or the one refuge
from being audited, and instructs that both readings stay planted. If
he were only ever charming that ambiguity would resolve by omission:

  "Nobody comes down to see whether I have kept up, and nobody ever
   has, and so every single thing in this book is in here because it
   is true and for no other reason at all."

He is right, and the player has just come from an island that scores
them. He never says the word Argus and does not know it.

He hands over the lure on the way in rather than at the end, so a
player who stops halfway still has the thing they were given.

TWO PLACEHOLDERS THE RUNNING GAME FILLS
{{DRAWING}} rotates the page he shows you across four creatures, by
visit rather than at random, so coming down twice running is not a
loop. {{CATCHER}} branches on whether the real catcher is already
yours. Both halves read cold. Substitution rather than four
near-identical steps, which is where content drifts.

SCENE ART
42 scenes across two new files. hidden.js carries the listening post:
Canon from behind in every frame, never a face, nine screens showing an
island where nothing happens, and one amber lamp on the big screen that
is his brother four hundred feet down. The pencil runs parallel through
every scene until he takes the tin, and stays fallen after.

Verified: every scene renders with real geometry and no console errors,
no duplicate ids within either file, and no collisions between them.

Note for later, not fixed here: five gradient ids DO collide between
the older scene files (goldGlow, lanternGlow, lanternGlow2, oceanGrad,
starGlow across cafe/library/skyship/town/beach/docks/observatory). Two
scenes in one document can inherit each other's gradients. Pre-existing
and out of scope for this change.

Gates: 41 scripts parse, clue freeze 1357e386021d intact, pets and
quest checks pass, 9 sync-coverage tests. Played through in a browser:
12 beats, art on every one, all three decoys fire, completion registers,
the lure is granted, the map label appears, no errors.

Copy is unsigned and batched for G3. No clue was written.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ai78b5KJDmPPwfCCoXwo3E
18 scenes. The first pass broke the one piece of direction that
mattered, so this is mostly a note about that.

wreck_8 is the beat where the player asks Fredward where home is and he
answers with the wreck, warmly, meaning it. The script's art note is
explicit: no emphasis, no lighting change, nothing dramatic, the cold
has to come from composition only. The first version put him centred
under a spotlight in a pool of amber, which is exactly the emphasis
that direction forbids and which tells the reader how to feel about a
line whose whole power is that nobody in it has noticed anything.

It now uses byte-identical lamp fills, radii, opacity and flicker
timings to wreck_1, and the same sand wash and corner darkening, so
nothing in the frame is stronger than any other deck scene. He is
turned away: you get the back of the helmet, a bolt ring, a crown seam,
and the far rim of the faceplate catching the lamp he is looking at.
The player is small and still in the foreground with the lure held
down, unlooked at. The crab has not moved.

Also fixed: the faceplate across all eight appearances, which was a
yellow disc with two dots and a curve and read as a smiley. It is now
dark recessed glass with a face dimly behind it at 0.82 opacity, a
heavy brass rim and eight bolts, with a specular sweep so the glass
reads as glass. The expression differs per scene, and in wreck_10 the
pupils drift a millimetre off and settle back over nine seconds while
the smile never moves.

Scene 5 was rebuilt to the REVISED art note: the creatures are gone and
the water behind him is deliberately empty, because the step is about
asking rather than chasing and putting creatures at the rail would
illustrate the opposite of what he is saying.

Every one of the 18 steps has its own art and neither story inherits a
previous scene. Both terminate with a complete step. No duplicate ids
within the file and no collisions with hidden.js or any other scene
file.

Gates: 41 scripts parse, full verify green, no console errors on any
scene.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ai78b5KJDmPPwfCCoXwo3E
wreck_6 is the hero prop shot: Fredward holding out the thing he built,
proud of it and slightly embarrassed by it. It was reading as a golden
birdcage held by two green mounds.

Two causes. The palms had no thumbs, so they were ellipses rather than
hands. And the lure was drawn at jewellery scale in bright polished
brass, which is the opposite of the register: it is wire and knotted
line and a dented bell.

The fix that mattered was not anatomy. Adding fingers to two facing
ellipses made it worse, because two hands pointing at each other fuse
into one symmetrical shape and the result read as a moth. The hands now
angle apart with open water between them, each with a forearm, a cuff,
a palm heel, fanned foreshortened fingers and a thumb thrown wide, and
they differ in size and angle so they are not a mirror of each other.

The lure is down to 0.42 scale and fully de-goldened: no #ffd700 and no
white anywhere in the scene, tarnished brass and cloudy sea glass, with
plier marks and tarnish patches. Every part that made it look
hand-made survives, the twisted-shut wire, the mismatched solder blobs,
the chipped float, the dent in the bell.

Worth recording: a comment in an earlier draft of this scene contained
a double hyphen, which is illegal inside an XML comment and would have
rendered the whole scene blank. Both check:syntax and node --check pass
it happily, because they parse the JS and never look inside the string.
Rendering is the only thing that catches it, which is why every scene
in this file is now checked for it.

Gates: full verify green, 18 scenes, all valid XML, no duplicate ids,
no collisions with any other scene file, no illegal comments.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ai78b5KJDmPPwfCCoXwo3E
Every existing check parses the JavaScript and never looks inside the
string, so a scene can be valid JS and broken markup with nothing
anywhere reporting it. check:scenes looks inside.

It found four things on its first run. One is a real bug and is fixed
here: town_3 uses url(#townSoftGlow), but that filter is defined in
town_0. Shown on its own the reference resolves to nothing and the
group renders unblurred, so the scene only looked right when the
earlier scene happened to still be in the DOM. Confirmed in a browser
before and after. town_3 now carries its own copy under its own id.

The check is deliberately tiered, and the tiering was measured rather
than assumed. The game injects scenes with innerHTML, which is HTML
parsing and forgiving, so most malformed markup degrades instead of
blanking the panel. I rendered the suspect scenes to find out which
was which before deciding what should fail a build.

  FAILS: a scene that is not a string or is not a well-formed <svg>,
  and a url(#id) pointing at nothing in the same scene, which renders
  the shape flat.

  WARNS: duplicate gradient ids across files (five of these predate
  this work; two scenes in one document borrow each other's
  gradients), a double hyphen inside a comment (illegal XML, harmless
  under innerHTML, instantly fatal to an XML parser or a serialiser),
  and the two forest scenes drawn at 500x280 rather than the house
  500x260. Those last are somebody's compositions and reflowing them
  is a deliberate job, not a thing to do on the way past.

One subtlety worth recording: scenes alias, and the idiom
`STORY_SCENES['town_1'] = STORY_SCENES['town_0']` means the same string
appears under several keys. Counting it once per key inflated the total
to 144 and reported every id in an aliased scene as colliding with
itself, which buried nine real warnings under sixty phantom ones.

Mutation-tested: breaking a url(#id) fails by name, a wrong viewBox
warns rather than fails.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ai78b5KJDmPPwfCCoXwo3E
The wreck sits on the seabed a long way down and the player was
reaching it by clicking a button. No boat, no suit, no preparation,
nothing spent, no reason the water was survivable. The one place on the
island that wanted a threshold had none, and arriving at the bottom
carried no weight.

Now an old hardhat suit hangs in the dock keeper's shack with three
pieces missing, and he knows exactly where all three went.

  HELMET   Lexicon Library   propping up a shelf, because it was the
                             heaviest brass thing anybody had
  BOOTS    Puzzle Workshop   full of bolts, because they are lead-soled
                             and will not tip over
  HOSE     Cluey Cove        coiled in a tidal pool with weed grown
                             through it, so it stopped being a hose and
                             started being scenery

Each is somewhere it would plausibly have ended up, absorbed into a
place by falling out of use, which is what this island does to things.

THIS IS A LEAD-UP AND NOT A GATE, AND THAT IS ENFORCED
The wreck is deliberately the un-gated half of the pet system: no
terminal code, so a player who never finds a secret still reaches the
warmest thing here. A subquest in front of it is one refactor away from
undoing that, and it would be the exact failure the Layton research
named, content behind a threshold nobody was told about.

So check:suit asserts, and each assertion was mutation-tested:

  * every piece is named out loud, by place, before it can be collected
  * no piece may sit at a location that needs a terminal code, which
    would put the wreck behind a code by the back door
  * nothing is collectable before the dock keeper has explained it, or
    the first thing a player finds is a brass helmet with no story
  * A SAVE THAT ALREADY REACHED THE WRECK CAN ALWAYS GO BACK

That last one is the trap this could most easily have fallen into. The
subquest shipped after the wreck did, so a returning player would have
been told they can no longer visit somewhere they have already been.
Verified in a browser on a save with the visit flag already set: the
docks button still reads "Back Down to the Wreck" and goes straight
down.

No piece needs a clue, a code or a story. The work is noticing, not
solving. Order does not matter.

One button on the docks, two states: it opens the shack until the suit
is whole and dives afterwards. A disabled second button sitting beside
it would be a control that exists to say no.

Played through in a browser: a new player is offered nothing until they
ask, all three collect in any order, a held piece cannot be taken
twice, the suit is worn once, and the dive opens the story.

Gates: 43 scripts parse, full verify green, 9 sync-coverage tests.
Copy is unsigned and batched for G3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ai78b5KJDmPPwfCCoXwo3E
An agent mid-redraw assembles scenes from underscore-prefixed fragments
in scenes/, and I swept three of them plus a screenshot script into the
previous commit. index.html script-tags every scene file explicitly so
a stray one is inert, but this repo IS the deploy source: an untracked
file here is one .vercelignore gap away from being a URL.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ai78b5KJDmPPwfCCoXwo3E
Joshua reviewed all 18 wreck scenes: the colouring, concepts and small
movements were good, the character and object consistency was not.
wreck_3 and 4 looked weird, wreck_5 did not make sense, the hands in
wreck_6 were unnatural, and wreck_7 had lost his legs.

They share one cause. There was no character model, so every scene
hand-built him at whatever size felt right in isolation. Measured
across the file, his helmet radius ranged from 18 to 25 with no scale
transform to explain it. That is why the catalogue book came out three
times too large in two scenes, why wreck_5 had no Fredward in it at
all, why wreck_6 was two disembodied mitts, and why wreck_7 and
wreck_8 both shipped as a torso with no legs. wreck_8's missing legs
had survived two of my own reviews.

Now there is one fred(), one player(), and shared book, table, mat and
crab. Everything derives from helmet radius, with fredFootDrop(h) so a
caller who knows the deck line can stand a man on it. Helmet radius is
21 to 26 in every scene, and the only variation left is camera
distance.

The palette did not change. The brand book is explicit that island art
is not brand and art colours are never swept, and Joshua had said the
colouring was good. One palette edit crept into a draft and was
reverted; the legibility problem it was solving turned out to be limb
thickness.

TWO LOCKED SPECS, in the format the Cryptic Croc spec established

characters/FREDWARD_DESIGN_CONTEXT.md and PLAYER_DESIGN_CONTEXT.md:
proportion table in helmet radii, wreck_2 named canonical at h=21, the
seven parts that must always be present, the faceplate treatment, the
overlap rule, a do-not-change list, and a design journey recording
what drifted so nobody re-derives it.

The player spec records the mirroring trap that caused "I don't get the
scene" in wreck_7: face:'left' mirrors the group, so custom arm markup
is authored in mirrored space, and getting it backwards had the player
reaching away from the object being handed to them.

CHECK:CHARACTER, WHICH CATCHES WHAT LOOKING DOES NOT

Three failure modes, each one seen in this file, none visible when you
review a scene on its own:

  * a hand drawn with no helmet, which is a disembodied glove
  * a helmet drawn with no boots, which is a legless torso
  * a helmet scaled independently of the body, which is a giant head

The third was found by this check after the first rebuild. Two return
scenes had lost their disembodied hands by inflating the helmet to
r=56.8, against 21 to 26 everywhere else, until his head was wider than
the book. Same disease as the original drift, pointing the other way.
The threshold is calibrated against the file rather than guessed.

Mutation-tested: model removed, a body part dropped, opaque pure white
introduced.

Gates: all 8 green. 51 scenes render with no console errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ai78b5KJDmPPwfCCoXwo3E
The scale guard added in the previous commit did not work, and finding
that out took three attempts, so the failures are worth recording.

WHAT WAS WRONG

Version one keyed on the word "boot" to detect a legless figure. The
model emits no such comment, so it warned about every figure in the
file, including the canonical scene. A guard that fires on everything
is a guard people learn to scroll past.

Version two measured the drop from the helmet to the lowest drawn
point. The first circle in a scene's markup is usually a lamp glow
rather than a head, so the origin was wrong and it warned about
wreck_2, which is the reference every other scene is measured against.

Version three inferred scale from the largest circle radius. It caught
the two scenes that happened to emit a literal <circle r="...">, which
is why it looked like it worked. Mutation testing showed it did not:
doubling the model's own scale variable from 21 to 58 produced a giant
head that the check passed without comment, because the model composes
helmets from computed values and those scenes were invisible to it.

WHAT IT DOES NOW

fred() and player() stamp data-fred-h and data-player-h on the group
they return, so the check reads the number rather than inferring it.
Every figure in the file now reports: 21 in every mid-shot, 27.99 for
the documented close-up, 22 in the two return scenes, 13.5 for the
player. The mutation that defeated all three earlier versions now fails
by name.

This is a change to the art code to make the art checkable, which is
the right direction. Inferring a property from rendered output was
always going to be approximate; having the thing that knows the number
write it down is not.

Also folded in: the arms override treated an empty string as "no
override" and silently drew the default arms as well as the scene's, so
wreck_return_3 briefly had four. Now tested with !== undefined, letting
a scene suppress the default and draw its own for correct z-order.

FREDWARD_DESIGN_CONTEXT.md gains the two rules the failures taught:
the camera moves and the anatomy does not, resize the prop and never
the helmet; and cropping by the frame edge is a camera decision while
cropping by furniture is a bug.

Gates: all 8 green, 18 scenes render with no console errors, no
warnings from check:character.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ai78b5KJDmPPwfCCoXwo3E
Joshua reviewed the 51 shipped scenes and named about fifteen defects:
arms broken, an arm too long, chairs broken, animations bad, a hatch
that makes no sense, an object that reads as drug paraphernalia. Rather
than fix fifteen things I had two agents research the craft and audit
every scene from the live DOM.

171 defects. 137 of them are instances of 13 systemic faults, so the
scattergun symptom list is really four missing disciplines: no prop
library, no layering rule, no animation vocabulary, no legibility test.

MEASURED, NOT GUESSED

  * All 599 animations across both files run linear. Not one keySplines
    anywhere. Nothing accelerates and nothing settles, which is why the
    animation reads as bad everywhere at once.
  * 43 of them share a single duration and 39 share another, so a scene
    pulses in unison.
  * 24 limbs end in nothing: a round line-cap, a bare ellipse, or a
    rect. wHand() exists and six scenes never call it.
  * Every chair is a back panel plus two rear legs, no seat and no front
    legs, then rotated as a whole group so the legs go off plumb: one
    drives through the floor, the other floats.
  * Props are resized per scene. The mat runs 26 to 84 wide, the crab
    radius 6.4 to 10.1, Canon's head 14 to 19.

TWO HARD BUGS, FIXED HERE

wreck_return_4 had 17 <g> against 18 </g>. A leftover hand-built player
survived after the player() call and its stray close tag ended the
scene group early, so it rendered as a smear in the corner and
everything after it escaped its group. innerHTML repairs this silently,
which is why nothing caught it. check:scenes now fails on unbalanced
groups, and it fires on that scene.

The rope in wreck_return_5 carried translate(94,196) with a
non-additive animateTransform starting at 94,200, so the authored
position never applied and it sat 4px low forever. Now additive.

Seven more non-additive transforms remain on placed groups. They need
restaging rather than a blind sweep, and one of them is the cause of
"he is holding the book with his feet": the figure swings 10 units in
2.6s while its placement transform is being replaced.

Also corrected: the proportion comment at the top of wreck.js said the
figure is 13 h tall. The locked model and the rendered figure are both
8.65 h. Anyone building from that comment drew him 50% too tall.

THE SKILL

.claude/skills/scene-art/SKILL.md, registered in the workspace
CLAUDE.md. Every rule in it is one of the 13 faults written as the
thing that prevents it, with the numbers from the research: breathing
at 3.75 to 5s with keyTimes 0/0.333/1 because resting inhale to exhale
is 1:2 and a symmetric loop reads as a machine; keySplines all four
values in [0,1] because bounce curves are spec-invalid in SMIL and
silently do not run; len(keySplines) == len(keyTimes) - 1 because a
miscount kills the animation dead rather than degrading it.

Research and audit kept at workshop/svg-scene-craft-research.md and
workshop/scene-defect-audit.md.

Gates: all 8 green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ai78b5KJDmPPwfCCoXwo3E
The hands had been redrawn three times and still read as combs. The
research settles why, and I verified the number before acting on it:

At Fredward's scale a hand is 21 units long. ANSUR II (n=6,068,
directly measured) gives breadth as 0.457 of length, so 9.6 units
across. Split four ways that is a 2.09-unit finger with a 0.58-unit
gap, against a ~2-unit floor for the smallest renderable void.

FOUR FINGERS ARE NOT DRAFTABLE AT THIS SIZE. The comb is not a drawing
failure; the comb is what four fingers degrade into. A fourth careful
redraw would have failed identically.

It also explains the giant-hand fault that recurred three times and
that I kept sending back: at correct size the four-finger construction
stops working, so each pass scaled the hand up until it did. Scaling up
was the symptom, not the fix.

The construction at this scale is a MITT: one massed lobe plus a thumb
lobe on the SIDE at mid-height, which every tutorial tradition agrees
is the feature that makes a hand read. Fredward's mitt is also
correct rather than a compromise, because standard diving dress
genuinely used mittens.

THE ONE-CONSTANT FIX

WGLOVE was '#40614e'. WSUIT_B was '#40614e'. Identical. The glove had
no value separation from the sleeve, so it read as a blob regardless of
its shape, which is precisely the problem the Mickey glove was invented
to solve. Now '#a3bfa9'.

ALSO IN THIS PASS

Arms rebuilt to the measured taper, 1.00 : 0.87 : 0.49
shoulder:elbow:wrist. The shape fact that matters is that the forearm
is nearly as thick as the biceps, so the taper is forearm-to-wrist, not
a cone from the shoulder. A stroke has uniform width, so a tapered limb
is a filled path with two edges; stroke-profile was proposed in 2014,
never advanced, and no vendor implemented it.

Chair rotations ran to -20 degrees, which reads as a chair toppling
rather than one turned aside. Capped at 8. The helper was already
correct: legs vertical to floorY, only the body rotating.

hidden_end_3 had a brow, a nose and a moustache and NO EYES, which is
what made the face read blank; the torso closed at y=96 with nothing
under it; and both hands were bare ellipses. All three fixed.

THE SOUNDING SPOON IS NOW THE SOUNDING FORK

A spoon at 40px is an oval on a string. Three art passes produced a
bell, a bathysphere and a blob, because the problem was the object and
not the drawing. A tuning fork has an unmistakable silhouette at any
size and states its own function: it is FOR finding out what a thing
sounds like. Still made from what washes up, per the rule every gadget
obeys. The id is unchanged so no save, scene or check moves.

This matters more than it did, because the first errand's badge now
grants the gadget permanently.

The size ladder is in the scene-art skill so this cannot recur.

Gates: all 8 green. 634 animation tags, all eased, zero miscounted
keySplines.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ai78b5KJDmPPwfCCoXwo3E
Joshua: "You messed up the bodies in the hidden series."

He is right. I added a rim light to make the silhouette read after
asking for more contrast, and I drew it as a bright cyan stroke INSIDE
the shape, down the middle of a black figure rather than along its
outer edge. On every wide shot it read as a tear through the body.

Canon is a pure silhouette, a shape you never see into, lit only by the
screens in front of him. A pure silhouette reads best as a pure shape.
The rim was added out of a worry that the form was unreadable, and the
form was not unreadable.

HID_RIM is now the skin value, so all 79 call sites become no-ops in
one place rather than 79 edits, and a future pass can reinstate it
correctly (outer edge, following the light) by changing one constant.
Seven further rim strokes were hardcoded on figures rather than using
the constant, and those are removed outright. The fork's sound rings
use the same colour and are deliberately kept.

WHAT IS STILL WRONG, AND WHY I AM NOT GUESSING AT IT

The wrists. Hands and arms are drawn as SEPARATE shapes butted
together: the arm tapers to a wrist width, then a hand shape starts
with its own wrist band, and the seam shows. It reads as a hand stuck
on a stick rather than one growing out of an arm. That is a real fault
and it is not fixed here.

Joshua also said to study animation or stop animating hand motions. I
checked before agreeing to either: there are no hand or arm rotations
anywhere in the file. The three animateTransforms in the hidden series
are a lamp, a screen and the fork. So the failure is static
construction, and turning animation off would fix nothing. I am
researching wrist joins and G1 continuity properly rather than
attempting a third redraw from description.

Gates: all 8 green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ai78b5KJDmPPwfCCoXwo3E
Two problems, one root cause: a construction boundary was being
rendered as a visible boundary.

THE WRIST

hidArm() and hidHand() were separate functions that shared nothing. The
arm tapered to a wrist width derived from SHOULDER width; the hand
began with its own band derived from HEAD radius. Measured, the hand's
mouth was 1.93x the arm's wrist, so the hand flared to nearly double
the limb it was meant to continue. That is most of what made a hand
read as a glove on the end of a pole.

Worse, the wrist band's own comment said "so the hand has a boundary
with the arm". I had built the seam deliberately.

Three changes, no call site touched:

  * hidWristW(r) is the ONE wrist width. Arm and hand now agree
    exactly, 1.00x.
  * The hand's wrist band is gone. The mass runs BACKWARD past the
    wrist by 0.22H and the forearm covers the far end. Canon is a
    single flat fill, so an overlap between two shapes of the same
    colour is literally invisible; this is the cutout-animation answer
    and it keeps every call site's `rot`, which a one-path
    reconstruction would have broken.
  * The mouth pinches rather than flaring. Measured, forearm : wrist :
    hand-breadth is 1.76 : 1.00 : 1.61, so the wrist is narrower than
    BOTH neighbours. A smooth interpolation from arm to hand is not a
    wrist.

THE RIM, AND WHY IT WAS NEVER GOING TO WORK

I removed it last commit for being drawn inside the silhouette. The
research found the harder reason, and it is the one that matters:

A rim light requires a source BEHIND the subject. Canon's light is nine
screens in FRONT of him and slightly above. That is a key light. It
cannot physically produce a rim, so no amount of correct edge geometry
would have saved it; it was in the wrong place because the light is in
the wrong place for the effect.

Leaving HID_RIM off is therefore correct rather than a workaround. The
research notes the file's current state is right, and keeps the full
rule and SVG for the one legitimate case, a narrative temple rim.

Also recorded there: "duplicate path offset outward" does not work,
because scale() is not an offset and true Bezier offsetting is not
expressible in SVG. An inner rim needs a double-width stroke clipped to
the silhouette with a directional gradient mask so it BREAKS rather
than outlines.

Research: workshop/wrist-and-rim-research.md.

Gates: all 8 green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ai78b5KJDmPPwfCCoXwo3E
Joshua: "you did not fix the hands, they look floppy and disconnected
to the wrists."

He was right, and the previous commit's claim was wrong. I fixed the
MOUTH of the hand so it agreed with the arm, and left the MASS at its
old size, so the join matched and then immediately ballooned.

Measured from the rendered path data rather than guessed:

  forearm end     5.92 units
  wrist           4.60 units
  hand breadth   10.56 units

  hand : wrist = 2.29x, where the measured human ratio is 1.61x.

A hand nearly twice the size of the limb it hangs from is exactly what
floppy and disconnected looks like. The mass was derived from HAND
LENGTH while the wrist was derived from the ARM, so the two could
never agree; fixing either one alone leaves the other wrong, which is
what happened three times.

There is now ONE ratio chain and all three widths come off the arm,
because the arm is the thing the hand has to belong to:

  hidForearmW(r)          the limb, as drawn
  hidWristW(r)   = /1.76  the wrist PINCHES, narrower than both
  hidHandB(r)    = *1.61  and the hand follows from the wrist

Verified: forearm:wrist is 1.76x and hand:wrist is 1.61x, exactly.

The lesson, which is now in the comment: every previous attempt fixed
one link and left the others. A ratio chain has to be solved as a
chain.

Gates: all 8 green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ai78b5KJDmPPwfCCoXwo3E
Joshua: "rethink the Canon's arms entirely you have a fundamentally
incorrect idea about anatomy." He was right, and it was conceptual
rather than a bad number.

The old build was TWO TAPERING TUBES joined at a point: a cone from
shoulder to elbow, another cone from elbow to wrist. Measured, it ran
1.00 : 0.84 : 0.30 shoulder to wrist against a real 1.00 : 0.87 : 0.49.
That is a spike, not a limb.

Three errors, and the third one is why every previous fix failed:

1. IT STARTED AT SHOULDER WIDTH. Biceps girth is 0.304 of shoulder
   girth, so the arm is under a third of the shoulder mass. Drawing the
   limb at full shoulder width makes a tube with a head on top. There
   is now a deltoid cap at 1.34x the biceps, sitting over the joint,
   because the shoulder has to be visibly wider than the arm below it.

2. THE FOREARM NEVER SWELLED, AND STRUCTURALLY COULD NOT. hidLimb()
   interpolates between a start width and an end width, so it can only
   ever produce a cone; there was no way to express a bulge at all. But
   the forearm reaches its MAXIMUM a third of the way down from the
   elbow, the standard anthropometric girth site, and only then drops,
   and that drop is the dramatic one at 43%. An arm is a fat forearm
   ending in a thin wrist, not a taper from the shoulder.

   The forearm is now TWO segments with the swell between them, which
   is the only way a two-point primitive can describe a bulge. The
   elbow is the narrowest point on the whole limb, which it should be.

3. THE WRIST WAS 0.30 OF THE SHOULDER instead of 0.49, which is what
   made everything hanging off it look like a balloon on a thread. The
   previous commit chased that ratio by shrinking the HAND. The wrist
   was the thing that was wrong.

All widths now derive from the biceps, which is the arm's own
reference, and the hand reads the same numbers. Verified exact:
biceps : forearm-max : wrist = 1.00 : 0.87 : 0.49, hand : wrist = 1.61,
deltoid 1.34x the biceps, and the forearm max is wider than the elbow.

Gates: all 8 green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ai78b5KJDmPPwfCCoXwo3E
Joshua: "these arms are even worse", with a reference sheet of cartoon
arms and a pointer to Toontown hands.

He was right, and the reference explains four failed attempts at once. I
had been solving the wrong problem. Every previous version chased
ANATOMY: ANSUR II circumference ratios, biceps girth at 0.304 of the
shoulder, deltoid caps, forearm swell one third down from the elbow.
Each was measured correctly and each was wrong, because Canon is a
stylised cartoon figure and cartoon limbs are not anatomical.

What the reference actually shows, and it inverts nearly everything:

  THE ARM IS A CONSTANT-WIDTH TUBE. No taper anywhere. No shoulder
  mass, no biceps, no elbow, no forearm swell. A bent noodle of one
  thickness end to end. All that anatomical structure was noise, and
  the elaborate three-segment build of the last commit was noise with
  more steps.

  THE HAND IS BIGGER THAN THE ARM, about 2.2x its width. I had spent
  four passes making the hand SMALLER to match a tapering wrist. In
  this idiom the hand is the largest thing on the limb.

  A CUFF SEPARATES THEM. The band I deleted two commits ago as "the
  seam" is the correct construction. It stages the size jump instead of
  hiding it, which is exactly what lets a big hand read as attached to
  a thin arm. Restored.

The arm is now a single stroke with round caps, because a stroke has
uniform width by definition and that is precisely what is wanted. The
filled-path taper machinery existed only to express a taper that should
never have been there.

Also fixed: the hand was 1.70x longer than wide, a finger shape, because
its length came off head radius while its breadth came off the arm. A
cartoon mitt is roughly as wide as it is long. Now 0.95, matching the
reference.

The lesson worth keeping: I was measuring real arms for a drawing that
was never trying to be one. Four correct measurements of the wrong
thing.

Gates: all 8 green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ai78b5KJDmPPwfCCoXwo3E
…ocation redrawn

The scene upgrade measured in the 2026-09-08 handoff, committed whole: the
shared character kit (scenes/characters.js), the rubber-hose passes on every
location, the map's town plaza and environment details, the scene and
character CI gates that check them, and the PNG renderer used to look at
every scene before believing it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cr1HRkEPpcngrJyKuceTys
@vercel

vercel Bot commented Sep 12, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
bit-cryptic-world Ready Ready Preview Sep 12, 2026 12:09am UTC

The swag overlay and panel sit at 10040 and 10041; the confirm sat at 10002,
so the one dialog a player must always be able to cancel could be painted
over. e2e/hardening asserts the confirm outranks every other layer.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cr1HRkEPpcngrJyKuceTys
@joshuaroll
joshuaroll merged commit 93142b2 into main Sep 12, 2026
6 of 8 checks passed

This branch was successfully deployed

1 active deployment
Preview — 227413fb Deployed Sep 12, 2026 by vercel[bot]
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.

1 participant