diff --git a/public/assets/img/critic_disappointed.png b/public/assets/img/critic_disappointed.png new file mode 100644 index 0000000..162db64 Binary files /dev/null and b/public/assets/img/critic_disappointed.png differ diff --git a/public/assets/img/critic_horrified.png b/public/assets/img/critic_horrified.png new file mode 100644 index 0000000..7fd8d87 Binary files /dev/null and b/public/assets/img/critic_horrified.png differ diff --git a/public/assets/img/critic_impressed.png b/public/assets/img/critic_impressed.png new file mode 100644 index 0000000..1fb4d82 Binary files /dev/null and b/public/assets/img/critic_impressed.png differ diff --git a/public/assets/img/critic_intrigued.png b/public/assets/img/critic_intrigued.png new file mode 100644 index 0000000..e252c20 Binary files /dev/null and b/public/assets/img/critic_intrigued.png differ diff --git a/public/assets/img/critic_neutral.png b/public/assets/img/critic_neutral.png new file mode 100644 index 0000000..9915da4 Binary files /dev/null and b/public/assets/img/critic_neutral.png differ diff --git a/scripts/cf-flux.py b/scripts/cf-flux.py new file mode 100755 index 0000000..ad01fcc --- /dev/null +++ b/scripts/cf-flux.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +"""One FLUX-1-schnell image via Cloudflare Workers AI (10k free neurons/day). +Usage: cf-flux.py [steps] [seed] +Creds come from backnforth/.env — never printed, never committed.""" +import base64 +import json +import os +import sys +import urllib.request + +out, prompt = sys.argv[1], sys.argv[2] +steps = int(sys.argv[3]) if len(sys.argv) > 3 else 8 +seed = int(sys.argv[4]) if len(sys.argv) > 4 else 0 + +env = {} +with open(os.path.expanduser('~/Documents/backnforth/.env')) as f: + for line in f: + if '=' in line and not line.startswith('#'): + k, _, v = line.strip().partition('=') + env[k] = v + +req = urllib.request.Request( + f"https://api.cloudflare.com/client/v4/accounts/{env['CLOUDFLARE_ACCOUNT_ID']}/ai/run/@cf/black-forest-labs/flux-1-schnell", + data=json.dumps({'prompt': prompt, 'steps': steps, 'seed': seed}).encode(), + headers={'Authorization': f"Bearer {env['CLOUDFLARE_API_TOKEN']}", 'Content-Type': 'application/json'}, +) +r = json.load(urllib.request.urlopen(req, timeout=120)) +if not r.get('success'): + sys.exit('CF error: ' + json.dumps(r.get('errors'))) +with open(out, 'wb') as f: + f.write(base64.b64decode(r['result']['image'])) +print(out) diff --git a/src/game/game.ts b/src/game/game.ts index 543f9f9..40d2183 100644 --- a/src/game/game.ts +++ b/src/game/game.ts @@ -641,7 +641,8 @@ export class Game { } beginDay(): void { - eye.show(); // he watches you cook + eye.critic = !this.dailyDate && !this.demoMode && this.save.day % 10 === 0; + eye.show(); // he watches you cook — or on the tenth day, HE does document.body.classList.toggle('shadow', this.shadowMode); const day = this.save.day; // Procedural days are seeded BY the day, not by a running stream: a reload @@ -999,7 +1000,7 @@ export class Game { if (v.grade === 'S') extras.push('The Critic: I came to write a takedown. I cannot. This is the worse outcome — for me.'); else if (v.grade === 'A') extras.push('The Critic: competent. The word has never once appeared in a good review.'); else if (v.grade === 'F') extras.push('The Critic: I will be gentle in print. This kitchen has suffered enough today.'); - else extras.push(`The Critic: the ${v.worst.label.toLowerCase()} betrayed you. It will be the headline.`); + else extras.push(`The Critic: the ${v.worst.label.toLowerCase().replace(/^the /, '')} betrayed you. It will be the headline.`); } // THE MOTHER: a lively starter firms the crumb on toaster days. A dead // one is on the shelf where he can see it, and he can smell a coward. diff --git a/src/scenes/judge.ts b/src/scenes/judge.ts index 0decc01..a6398ae 100644 --- a/src/scenes/judge.ts +++ b/src/scenes/judge.ts @@ -33,7 +33,7 @@ export class JudgeView implements View { onNext: (() => void) | null = null; /** One-shot lines the game adds to this verdict (streaks, regulars' memory). */ extraLines: string[] = []; - /** Every 10th day THE CRITIC seats himself — same face, drained of warmth. */ + /** Every 10th day THE CRITIC seats himself — his own face, his own notebook. */ critic = false; /** The one-line result SHARE copies to the clipboard. */ shareText = ''; @@ -265,8 +265,7 @@ export class JudgeView implements View { this.extraLines = []; this.lastCaption = { day: order.day, grade: verdict.grade, total: verdict.total, line: lines[1] ?? lines[0] ?? '' }; - this.faceEl.src = assetUrl(`/assets/img/judge_${judgeFace(verdict.grade)}.png`); - this.faceEl.style.filter = this.critic ? 'grayscale(1) contrast(1.25)' : ''; + this.faceEl.src = assetUrl(`/assets/img/${this.critic ? 'critic' : 'judge'}_${judgeFace(verdict.grade)}.png`); this.stampEl.textContent = verdict.grade; this.stampEl.className = `judge-stamp grade-${verdict.grade}`; // re-trigger the stamp animation diff --git a/src/ui/eye.ts b/src/ui/eye.ts index 70f7f74..fe36084 100644 --- a/src/ui/eye.ts +++ b/src/ui/eye.ts @@ -9,6 +9,8 @@ export type Mood = 'neutral' | 'intrigued' | 'impressed' | 'disappointed' | 'hor */ class Eye { private el: HTMLImageElement | null = null; + /** Critic days: the corner watcher is the man with the notebook. */ + critic = false; private timer: number | null = null; private current: Mood = 'neutral'; @@ -32,7 +34,7 @@ class Eye { private set(m: Mood): void { this.current = m; - this.ensure().src = assetUrl(`/assets/img/judge_${m}.png`); + this.ensure().src = assetUrl(`/assets/img/${this.critic ? 'critic' : 'judge'}_${m}.png`); } show(): void {