THE FACES: every regular gets a portrait, the jar gets three, the dish gets one

Twenty-two assets off the Cloudflare FLUX lane (scripts/gen-art-batch.sh
— idempotent, fixed seed per character, ~5s each, barely a dent in the
10k daily neurons):

- Thirteen regulars on their tickets, clipped into the corner like a
  loyalty card photo: Deidre's floral cardigan, Ray's flat cap, the
  child on tiptoes with a fistful of coins, the bloke from two doors
  down in his dressing gown, the delivery mid-yawn at 6am, the lunch
  rush as three impatient torsos, the pavlova order in her pearls. The
  inspector's ticket uses his own portrait; a name with no face made
  simply shows no frame (onerror removes it).
- THE MOTHER's jar rendered in all three states on the title button:
  bubbling over the rim, sulking under grey liquor, and dead with a
  tiny white surrender flag planted in the crust.
- Six dish heroes on the scorecard beside the ask: the skewers, the
  pan, the sliced rib eye, the whites bowl, the poached egg mid-drip,
  the benedict.

Verified live: pavlova portrait on day 20's ticket, benedict table on
21's, jar_sulking at gap 3, hero_eggs on the eggs scorecard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-21 02:37:15 +10:00
parent 720e4dd5df
commit d998b74abc
27 changed files with 101 additions and 3 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 KiB

47
scripts/gen-art-batch.sh Executable file
View File

@ -0,0 +1,47 @@
#!/bin/bash
# gen-art-batch.sh — the full 2D art pass on the Cloudflare FLUX lane.
# Regulars' ticket portraits, THE MOTHER's jar states, dish heroes.
# Idempotent: skips anything already in public/assets/img.
set -uo pipefail
cd "$(dirname "$0")/.."
S="${SCRATCH:-/tmp/toastsim-art}"; mkdir -p "$S"
STYLE="claymation stop-motion figurine, plain light grey background, soft studio lighting, chunky stylized game asset, waist-up portrait"
JSTYLE="claymation stop-motion style, plain light grey background, soft studio lighting, chunky stylized game asset"
gen() { # gen <outname> <seed> <prompt>
local name="$1" seed="$2" prompt="$3"
[ -f "public/assets/img/$name.png" ] && { echo "= $name exists"; return; }
./scripts/cf-flux.py "$S/$name.raw" "$prompt" 8 "$seed" >/dev/null \
&& sips -s format png -Z 768 "$S/$name.raw" --out "public/assets/img/$name.png" >/dev/null 2>&1 \
&& echo "+ $name" || echo "! $name FAILED"
}
# The regulars — one fixed seed each so reruns keep the same person.
gen who_deidre 101 "$STYLE, elderly woman with lavender-grey perm, floral cardigan, large round glasses on a chain, clutching a small handbag, expectant pursed smile"
gen who_ray 102 "$STYLE, middle-aged working man in a flat cap and worn brown work jacket, stubble, mug of tea in hand, easy-going squint"
gen who_a_child 103 "$STYLE, small child on tiptoes at a counter, oversized striped jumper, gap-toothed grin, holding out a fistful of coins"
gen who_a_man_in_a_hurry 104 "$STYLE, businessman with loosened tie and rumpled suit, briefcase under one arm, checking his wristwatch, harried expression"
gen who_a_regular 105 "$STYLE, comfortable older man in a knitted vest with a folded newspaper under his arm, content half-smile, coffee mug"
gen who_someone_new 106 "$STYLE, young woman with a short bob and denim jacket, looking around uncertainly, holding a menu, hopeful nervous smile"
gen who_the_bloke_from_two_doors_down 107 "$STYLE, unshaven man in a tartan dressing gown over day clothes, slippers, holding his own mug from home, sheepish grin"
gen who_the_chef_on_her_day_off 108 "$STYLE, woman with hair in a tight bun, chef whites jacket open over a band t-shirt, arms crossed, appraising professional stare"
gen who_the_delivery_at_6am 109 "$STYLE, courier in a hi-vis vest and beanie mid-yawn, cardboard box under one arm, heavy eyelids"
gen who_the_lunch_rush 110 "$STYLE, three impatient office workers crowded shoulder to shoulder, all checking phones and watches, tight group"
gen who_the_man_at_table_nine 111 "$STYLE, mysterious lone man in a long grey overcoat, fedora resting on the table edge, steepled fingers, unreadable face"
gen who_the_pavlova_order 112 "$STYLE, grand formidable elderly woman in pearls and a silk scarf, chin high, gloved hands folded, expecting perfection"
gen who_the_benedict_table 113 "$STYLE, refined woman in a cream blazer with gold earrings, holding a menu closed, patient knowing smile"
# THE MOTHER — three states of one jar.
gen jar_bubbling 201 "$JSTYLE, single glass mason jar of sourdough starter, cloth cap tied with string, contents risen and bubbly, lively froth over the rim, warm and healthy"
gen jar_sulking 202 "$JSTYLE, single glass mason jar of sourdough starter, cloth cap tied with string, contents flat and sunken with a thin grey liquid layer on top, gloomy"
gen jar_dead 203 "$JSTYLE, single glass mason jar of long-dead sourdough starter, cloth cap askew, contents collapsed dark and crusted, a small wilted flag stuck in it, funereal"
# Dish heroes — the plate on the scorecard's order line.
gen hero_grill 301 "$JSTYLE, plate of chargrilled mushroom skewers with proper dark grill marks, single plate centered"
gen hero_pan 302 "$JSTYLE, cast iron pan with golden butter-basted mushrooms glistening, single pan centered"
gen hero_steak 303 "$JSTYLE, sliced rib eye steak on a wooden board, rosy pink centre, resting juices, single board centered"
gen hero_eggs 304 "$JSTYLE, glass bowl of whipped glossy egg whites beside three brown eggshells, single bowl centered"
gen hero_poach 305 "$JSTYLE, single perfect poached egg on a slotted spoon, white silky and intact, droplet falling"
gen hero_benedict 306 "$JSTYLE, eggs benedict on a toasted muffin, hollandaise dripping down, single plate centered"
echo done

View File

@ -31,6 +31,7 @@ import { DAYS, proceduralOrder, nameBrowning, prepAsk, type Order, type PrepStep
import { el, Panel } from '../ui/hud';
import { Title } from '../ui/title';
import { eye } from '../ui/eye';
import { assetUrl } from '../scenes/assets';
const SAVE_KEY = 'toastsim.save';
@ -1088,6 +1089,12 @@ export class Game {
this.ticketEl.innerHTML = '';
el('div', 'ticket-day', this.ticketEl, `DAY ${o.day}`);
el('div', 'ticket-who', this.ticketEl, o.who);
// The regular's face, clipped to the ticket. The inspector uses his own.
const faceImg = el('img', 'ticket-face', this.ticketEl) as HTMLImageElement;
const slug = o.who.replace(', again', '').toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
faceImg.alt = '';
faceImg.src = assetUrl(o.who.includes('inspector') ? '/assets/img/judge_neutral.png' : `/assets/img/who_${slug}.png`);
faceImg.onerror = () => faceImg.remove(); // a face we never made — no frame, no fuss
if (o.day % 10 === 0 && !this.dailyDate) el('div', 'ticket-critic', this.ticketEl, 'THE CRITIC is in today. The flaw will be the headline.');
if (this.rushDay(o.day) && !this.dailyDate) el('div', 'ticket-critic', this.ticketEl, this.rushFirst !== null ? 'TICKET TWO — the first is dying on the pass.' : 'RUSH HOUR — two tickets on the rail. The first will wait for the second.');
const demos = (window as unknown as { __t?: { demos?: Record<number, unknown> } }).__t?.demos;

View File

@ -194,6 +194,15 @@ export class JudgeView implements View {
// Grill and bruschetta days don't have a browning/spread to name — show who
// asked and what for, not the unread toast defaults.
// The dish itself, in miniature, beside the ask.
const heroKey = order.benedict ? 'benedict' : order.poach ? 'poach' : order.eggs ? 'eggs' : order.steak ? 'steak' : order.grill ? 'grill' : order.pan ? 'pan' : null;
this.orderEl.parentElement?.querySelector('.judge-hero')?.remove();
if (heroKey) {
const hero = el('img', 'judge-hero', this.orderEl.parentElement ?? this.orderEl) as HTMLImageElement;
hero.alt = '';
hero.src = assetUrl(`/assets/img/hero_${heroKey}.png`);
this.orderEl.parentElement?.insertBefore(hero, this.orderEl);
}
this.orderEl.textContent = order.grill
? `Day ${order.day} · ${order.who} asked for the barbie`
: order.benedict

View File

@ -806,3 +806,33 @@ body:has(#ui > .title) #touchpad { display: none; }
color: #b8d89a;
transition: all 0.2s;
}
/* The regular's face on the ticket. */
.ticket-face {
position: absolute;
top: 10px;
right: 12px;
width: 54px;
height: 54px;
object-fit: cover;
border-radius: 50%;
border: 2px solid #cbb89a;
background: #e8dcc4;
}
.jar-img {
width: 26px;
height: 26px;
object-fit: cover;
border-radius: 6px;
vertical-align: middle;
margin-right: 8px;
}
.judge-hero {
width: 52px;
height: 52px;
object-fit: cover;
border-radius: 10px;
border: 2px solid #3a2c20;
float: right;
margin: 0 0 6px 10px;
}

View File

@ -52,8 +52,13 @@ export class Title {
if (save.starter) {
const st = save.starter;
const jar = el('button', 'btn ghost jar-btn', txt, '');
const jarImg = el('img', 'jar-img', jar) as HTMLImageElement;
jarImg.alt = '';
const jarTxt = el('span', undefined, jar, '');
const label = () => {
const gap = save.maxDay - st.fed;
const gap0 = save.maxDay - st.fed;
jarImg.src = assetUrl(`/assets/img/jar_${gap0 >= 5 ? 'dead' : gap0 >= 3 ? 'sulking' : 'bubbling'}.png`);
const gap = gap0;
const name = st.deaths > 0 ? `THE MOTHER (${['second', 'third', 'fourth', 'umpteenth'][Math.min(st.deaths - 1, 3)]} of her name)` : 'THE MOTHER';
if (gap >= 5) return `\u{1FAD9} ${name} has gone still. begin again \u2014 she forgives, mostly`;
if (gap === 0) return `\u{1FAD9} ${name} \u2014 fed. bubbling like gossip`;
@ -61,13 +66,13 @@ export class Title {
if (gap === 4) return `\u{1FAD9} ${name} \u2014 one day from the end. FEED HER NOW`;
return `\u{1FAD9} ${name} \u2014 hungry (${gap} days). the crumb suffers`;
};
jar.textContent = label();
jarTxt.textContent = label();
jar.addEventListener('click', () => {
const gap = save.maxDay - st.fed;
if (gap >= 5) { st.deaths++; st.born = save.maxDay; }
st.fed = save.maxDay;
onFeed?.();
jar.textContent = label();
jarTxt.textContent = label();
jar.classList.add('fed');
window.setTimeout(() => jar.classList.remove('fed'), 700);
});