Diagnosed rather than guessed. The martlet failed three times over ninety
minutes while the gryphon succeeded in the same background run, so it was
neither the service nor the account nor the window. Regenerating it with the
single word "bare" replaced and nothing else changed brought it back
immediately.
It comes from armourWords at naturalArmour 0 — Midjourney reads it as nudity
— and thirteen creatures carried it, every one of them a creature with no
natural armour. That band says "thin-skinned, nothing between it and a blow"
now.
BANNED returned its hits as `warnings` for the caller to print. A warning in
a log nobody reads is precisely what let this stall three times, because the
failure it predicts is a job that never arrives and never fails, which looks
exactly like the service being slow. promptFor throws now, as it does for an
over-long negative list, and says why the refusal is invisible. All 204 specs
still build; a planted "corpse" is refused.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The gryphon fix added six armour negatives and three weapon ones, which took
the prompt to eight --no terms. This project has a recorded case where ten or
more never returned a grid at all while the same prompt with four came back
in two minutes — and a job that never arrives is indistinguishable from a
moderation refusal, because those are ephemeral and never appear in the
channel either. It cost twenty minutes of wondering whether the account was
out of fast hours, and the note about it was written down precisely so it
would not be diagnosed from scratch a second time. I walked into it anyway.
Trimmed to three armour terms and one weapon term, which puts every prompt at
five. promptFor now throws above six and names the offending terms, so the
next person to reach for a long negative list is stopped at the tool rather
than by a half-hour stall. All 204 specs build under the cap.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The illustrations were sitting in art/creatures/, where nothing looked for
them. build-packs resolves a creature's picture by stem under art/portraits/,
so none of the 47 reached the game.
tools/make-art.py (was make-tokens.py) now derives both from one plate:
art/portraits/<key>.webp and art/tokens/<key>.webp. Same drawing, so the
token cannot disagree with the portrait — the argument mj-queue already makes
for deriving prompts from statblocks.
Webp because every one of the 309 art files already here is one, at 160-220
KB. The raw Midjourney plates are 2.3 MB each; 47 of them would have put 93
MB into a 29 MB repository permanently, for pictures nothing renders at that
size. At quality 88 a plate is ~300 KB with the pencil hatching intact. The
plates stay on disk as working files and are gitignored.
The token lookup in build-packs was a hardcoded `.png`, so the first webp
token would have silently fallen back to the portrait and Foundry would have
gone back to cropping a 2:3 plate into a circle. Portraits and tokens now
share one resolver and one extension list.
146 of 147 npcs carry a portrait and 46 a derived token.
Also: the gryphon came back as a mech a second time. The first fix branched
the armour SIMILE on era while both arms still opened with the word
"armoured", and that word is what Midjourney draws — changing the reasoning
is not the same as changing the cause. No natural creature's prompt contains
"armour" now, and negativesFor() asks for plate, machinery and worn armour to
be left out, because a word choice is an argument and a --no is closer to a
guarantee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three fail-open classifiers, each reading a field that is not where the
bestiary keeps the answer. Found by generating one creature before
spending the allowance on forty-eight; art/creatures/gryphon-u1.png is a
WINGLESS gryphon, from a spec whose bodyPlan is "winged".
1. anatomyWords() read `species`. Shape lives in `bodyPlan` — that is the
field selecting the hit-location table. 44 of the 100 new creatures are
species "baseline" with bodyPlan quadruped or winged, so every one fell
to the silent branch and asked for no shape at all.
2. isPersonSpec() ended `if (tag) return true` — an unrecognised role tag
means person. MONSTER_TAGS listed three tags; the bestiary uses twelve,
so 43 creatures were prompted as "portrait of X, waist-up portrait".
Now bodyPlan decides first, structurally, because a tag list is free
text that grows and bodyPlan is enforced by the engine.
3. ROLE_CLASS knew the same three tags, so HERALDRY fell through to
"carries nothing manufactured and armour >= 4 -> anomalous". Six
centuries of carved stone were classed as far-side machinery, and the
armour clause duly asked for a mech. ROLE_CLASS now carries all twelve,
and the heavy-armour wording follows era: machinery for future and
anomalous, "hide and scale thick as carved stone" for the rest.
Also in this commit: all 100 creatures re-pointed onto the natural attacks
added in 3feac39 — 41 of 100 changed, the rest keeping grasp or punch
because that is genuinely what they do. Three manoeuvres stay as prose and
say so explicitly: dun_cow's and bull_azure's charge, gryphon's stoop and
drop, tup's blind side. Portraits redrawn, since era decides their hue.
All 23 guards pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Mac bridge posts /imagine to Discord with a user token. It is not ported here and
should not be: Midjourney's terms since 27 May 2026 forbid automated access, there is
no official API, and the bridge's own memory note records Discord silently dropping
its interactions from 22 July — which is what enforcement looks like from inside. A
Windows clone would be a second self-bot on the same account.
So this does everything except talk to Discord, which turns out to be most of the
work. `next` derives a prompt from the creature's spec and puts it on the clipboard;
you paste it yourself; `file` or `watch` renames the download to the canonical name
and retires the generated placeholder. One at a time, because the memory note is
explicit that concurrent jobs in one channel steal each other's grids.
Prompts are derived, not written — anatomy from the location table, scale from SIZ,
build from STR against SIZ, plating from naturalArmour — so they cannot drift from the
statblock. Three things learned by reading the data rather than assuming it:
- tactics is written for a GM, not an illustrator. "Advances by bounds, 15 m per
round" put a rate of fire in an art prompt. role is the one-line essence and is
already the sentence you would hand an artist. Sentences carrying mechanics are
dropped; dates and ages are kept.
- era is the wrong test for whether something is a person. It classified Mr Bewley of
the Registry and everyone in 1881 Cordera as monsters, because their kit is old.
- 18 creatures are species "baseline" and are not human-shaped — a black dog, a case
file that eats case files, a machine that has decided it is the site. The prompt
refuses to describe a body it cannot verify, and `list` names them. That is a
statblock to write, not something to paper over here.
`sheet` emits a prompt sheet that the existing gen-art.mjs parses unchanged, so if the
bridge ever works again the whole bestiary drives from it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>