SLOPSHOPPER

toons

Animated cartoons of what Claude is doing, drawn under the spinner while it works

newpanespinnerguardcommandtoast
★ 27v0.1.0MITupdated 2026-10-02achimala/claude-toons
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · toons
│ ┃ toons ✕ › fix the failing auth test and add an audit log c╭────────────────╮ │ ┃ Cartoons : hidden ▾ │ toons │ │ ┃ Director model : Sonnet 5.5 ▾ ⏺ Read(src/auth.ts) │ toons: API 0: │ │ ┃ New scene : every 15 seconds ▾ ⎿ Read 6 lines ╰────────────────╯ │ ┃ Director thinks : off (cheaper) ▾ ⏺ Update(src/auth.ts) │ ┃ Scenes : mix: ready-made, fresh ⎿ Added 2 lines, removed 1 line │ ┃ Scene styles : mix (3D, pixel art, tex ⏺ Bash(bun test) │ ┃ Toggle any time with /toons, even while ⎿ 3 pass, 1 fail │ ┃ Claude works │ ┃ ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ Per hour of Claude working │ ┃ Claude's own work not measured yet after ~ ✻ Worked for 42s · done 4:20 PM │ ┃ Cartoons ~$5.12 ~240 scenes, esti │ ┃ › /toons │ ┃ Each model at this pace ⎿ toons: Cartoons off: no scenes are requested or drawn until you │ ┃ Haiku 4.5 ~$2.56/hr ~240 scenes, c │ ┃ > Sonnet 5.5 ~$5.12/hr ~240 scenes │ ┃ Opus 5.5 ~$8.81/hr ~240 scenes │ ┃ │ ┃ Usage │ ┃ Your plan 5-hour 31% │ ┃ This session 0 scenes · $0.00 and 2 r │ ┃ │ ┃ On a subscription neither line is charged │ ┃ separately: both count toward the same │ ┃ usage limits, each in proportion to its ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · toons
Cartoons : hidden ▾ Director model : Sonnet 5.5 ▾ New scene : every 15 seconds ▾ Director thinks : off (cheaper) ▾ Scenes : mix: ready-made, fresh ones for news (~ Scene styles : mix (3D, pixel art, text art) ▾ Toggle any time with /toons, even while Claude works Per hour of Claude working Claude's own work not measured yet after ~10 min of Claude Cartoons ~$5.12 ~240 scenes, estimate Each model at this pace Haiku 4.5 ~$2.56/hr ~240 scenes, cheapest > Sonnet 5.5 ~$5.12/hr ~240 scenes Opus 5.5 ~$8.81/hr ~240 scenes Usage Your plan 5-hour 31% This session 0 scenes · $0.00 and 2 ready-made, free On a subscription neither line is charged separately: both count toward the same usage limits, each in proportion to its cost. Hidden or idle: nothing.
README

claude-toons

Little animated cartoons of what Claude is doing, drawn under the spinner in Claude Code while it works. Clawd, the Claude Code mascot, stars in every scene, and the scene follows the work: tests become a castle siege, a bug hunt becomes a safari, a long build becomes a rocket on the pad.

Clawd in a cartoon under the Claude Code spinner

An independent project, not affiliated with or endorsed by Anthropic.

What it costs

On default settings, about 1 to 3% on top of what Claude Code already uses.

Most scenes come from a library of 138 ready-made scenes that ship with the plugin and cost nothing. A model is asked to draw a fresh scene only when you start a task or something fails, and at most once a minute. Each of those costs about 2 to 4 cents at API prices.

The range depends on how you work. Long tasks land near 1%, because one fresh scene covers minutes of Claude's work. Rapid short back-and-forth lands near 3%, because nearly every message starts a task. These figures assume Claude Code itself runs on Opus. On a cheaper model the share is larger.

The requests use your Claude Code login and nothing else. On a subscription they count toward your plan's usage limits like any other Claude use. On an API key they're billed to that key. Nothing is spent while Claude is idle or the cartoons are hidden.

The "Scenes" setting picks where scenes come from:

ScenesExtra on top of Claude Code
Ready-made onlyNothing. No requests are made.
Mix (default)~1–3%
Fresh only, a new scene every 15 seconds~5–10%
Fresh only, as fast as possible~15–25%

/toons settings shows what the cartoons have actually cost you, next to what Claude's own work costs, once there's enough to measure.

Install

You need Claude Code with plugin hook modules, an early-access feature. This was built on version 2.1.287.

  1. Clone the repo:
   git clone https://github.com/achimala/claude-toons ~/src/claude-toons
  1. Try it for one session:
   claude --plugin-dir ~/src/claude-toons

Give Claude any task. The cartoons appear under the spinner while it works.

  1. To load it in every session, add the folder to the env block of ~/.claude/settings.json with its full path, then restart Claude Code:
   {
     "env": {
       "CLAUDE_CODE_PLUGIN_DIRS": "/Users/you/src/claude-toons"
     }
   }

To update, run git pull in the folder. A running session picks up the change on its own. To uninstall, remove the folder from CLAUDE_CODE_PLUGIN_DIRS and delete it.

Nothing shows up? Run claude --debug and look for a toons line. If it says hook modules are turned off, your Claude Code doesn't have the feature yet. Cartoons only appear while Claude is working, and only in the terminal.

Using it

  • /toons shows or hides the cartoons, even mid-task. Hidden means no scenes are requested at all. The choice is remembered across sessions.
  • /toons settings opens the settings and the cost figures. The same settings are in /config under "Toons".
SettingWhat it does
Scenes"Ready-made only" is free. "Mix", the default, adds fresh scenes for news. "Fresh only" draws every scene new, at several times the cost.
Scene stylesA mix, or only 3D, pixel art or text art, or everything but 3D.
Director modelThe model that draws fresh scenes. Sonnet by default. Haiku is cheaper, Opus is more inventive.
New sceneHow often fresh scenes may be requested, in "fresh only".
Director thinksLets the model think before each fresh scene. Off by default, since thinking costs more.

License

MIT. See LICENSE.

Source 10 files
hooks/register.tsx 737 lines
1import type { EngineInterface, Register } from 'claude-code'
2
3import {
4  GAP_MS,
5  MODELS,
6  MODEL_NAMES,
7  PACES,
8  addTokens,
9  costOf,
10  emptyStats,
11  isModel,
12  isPace,
13  money,
14  spendKey,
15  summarizeCost,
16  type Model,
17  type Pace,
18  type Spend,
19  type Stats,
20  type Window,
21} from './cost'
22import { createDealer, deal, isInteresting, isStockDue, phaseOf, whatOf, type Dealer, type Phase } from './library'
23import { STYLE_SETS, URL, createThread, isStyleSet, type Narration, type StyleSet } from './narrator'
24import { SCENES } from './scenes'
25import { cleanScript, stage, type Script } from './script'
26
27const PLUGIN = 'toons'
28const ROWS = 9
29// How long the band takes to rise to its full height when the spinner shows.
30const GROW_MS = 700
31// How long the log may stay quiet mid-turn before a "still going" line.
32const QUIET_MS = 20_000
33// How long a scene plays before a quiet stretch asks for its next beat. A
34// scene animates on its own, so until then quiet costs nothing: no request
35// is made just to hear that the scene should keep playing.
36const BEAT_MS = 90_000
37// With the stock of ready-made scenes on, the director is asked only for news
38// worth a scene of its own (the task, a failure, the turn's end, a broken
39// scene), and no more often than this; everything else deals a stock scene.
40const LIVE_MS = 60_000
41
42// Where scenes come from: ready-made ones only (free, nothing is requested),
43// ready-made with fresh ones for news (the default), or fresh ones only.
44const SOURCES = ['mix', 'ready-made only', 'fresh only'] as const
45type Source = (typeof SOURCES)[number]
46// Settings saved before there were three: on was the mix, off fresh only.
47const sourceOf = (v: unknown): Source => (v === 'off' ? 'fresh only' : SOURCES.includes(v as Source) ? (v as Source) : 'mix')
48// The settings pane, and the slash command that toggles the cartoons or opens it.
49const PANE = 'toons'
50const COMMAND = 'toons'
51
52// The buddy waking up, until its first scene arrives.
53const FIRST = cleanScript({
54  background: { effect: 'plasma', palette: ['#5a2416', '#b4532f', '#d97757', '#f5c4a8'], speed: 0.6, intensity: 0.3 },
55  actors: [{ kind: 'clawd', frames: [], fps: 0, x: 'max(w*0.45, w - t*14)', y: '3', color: '#d97757', say: '', sayAt: 0 }],
56  particles: [{ glyphs: '·∘°', count: 12, x: 'rand(k)*w', y: 'mod(rand(k+3)*h - t*(0.5+rand(k+5)), h)', color: '#d97757' }],
57}) as Script
58
59// The arguments worth narrating, per tool, cut to a line.
60const ARGS = ['command', 'file_path', 'pattern', 'path', 'url', 'query', 'description', 'subagent_type'] as const
61
62function describe(input: Record<string, unknown>) {
63  const parts = ARGS.map(key => input[key])
64    .filter((v): v is string => typeof v === 'string' && v.length > 0)
65    .map(v => v.replace(/\s+/g, ' ').slice(0, 100))
66
67  return parts.length > 0 ? ` \`${parts.join(' | ')}\`` : ''
68}
69
70const clamp01 = (v: number) => Math.min(1, Math.max(0, v))
71
72// Stats as stored, made safe to add to.
73function cleanStats(raw: unknown): Stats {
74  const stats = emptyStats()
75  if (typeof raw !== 'object' || raw === null) return stats
76  const r = raw as { buddy?: Record<string, unknown>; main?: unknown }
77  const spend = (v: unknown): Spend | undefined => {
78    const s = v as Partial<Spend> | null
79    if (!s || typeof s !== 'object' || typeof s.usd !== 'number' || typeof s.scenes !== 'number' || typeof s.activeMs !== 'number') return undefined
80
81    return { usd: s.usd, scenes: s.scenes, activeMs: s.activeMs }
82  }
83  for (const [key, v] of Object.entries(r.buddy ?? {})) {
84    const s = spend(v)
85    if (s) stats.buddy[key] = s
86  }
87  stats.main = spend(r.main) ?? stats.main
88
89  return stats
90}
91
92// Everything the buddy keeps for the session.
93type Buddy = {
94  model: Model
95  pace: Pace
96  thread: ReturnType<typeof createThread>
97  // The log of what happened since the last scene, and when the turn began.
98  pending: string[]
99  startedAt: number
100  lastCall: number
101  loggedAt: number
102  isTurn: boolean
103  isAsking: boolean
104  isAnimating: boolean
105  // The last trouble toasted, so the same again is not.
106  error?: string
107  // The scene playing and when it arrived, and the one dissolving out of
108  // the picture and when that one arrived (its clock keeps running).
109  scene: Script
110  sceneAt: number
111  previous?: Script
112  previousAt: number
113  // Whether stock scenes are dealt, what Claude is doing by the log and the
114  // thing it last touched (to label a stock scene), the phase the scene
115  // playing was dealt for, when the director was last asked, the concepts
116  // of recent stock scenes, and how many were dealt this session.
117  isStock: boolean
118  // Ready-made only: the director is never asked, and nothing is spent.
119  isFree: boolean
120  source: Source
121  dealer: Dealer
122  doing: Phase
123  what?: string
124  scenePhase?: Phase
125  // Whether the scene playing is the director's, and the tools Claude has
126  // started this turn (thinking after the first is part of the work in hand).
127  isLiveScene: boolean
128  turnTools: number
129  liveAt: number
130  sessionStock: number
131  // The spinner being drawn on, its width, when it showed, and the rows the
132  // band was last drawn with.
133  spinner?: string
134  cols: number
135  shownAt: number
136  drawnRows: number
137  // Whether the session's login refused these requests: no more are made.
138  isRefused: boolean
139  // Tool calls running now with when each began, and the spinner's phase.
140  running: Map<number, { what: string; at: number }>
141  callCount: number
142  phase: string
143  phaseAt: number
144  // No request before this time: a rate limit's or a failure's wait, and how
145  // many requests in a row have failed.
146  calmUntil: number
147  failures: number
148  // Whether the cartoons show (kept across sessions), and what they and
149  // Claude's own work have spent, for the settings pane's estimates.
150  isShown: boolean
151  stats: Stats
152  sessionUsd: number
153  sessionScenes: number
154  isSubscription: boolean
155  windows: Window[]
156  // The last scene code error reported to the narrator, for the settings pane.
157  lastTrouble?: string
158  // The turn's start and the session's cost then, and when working time with
159  // the cartoons showing was last added up.
160  turnAt: number
161  turnCostAt: number
162  accruedAt: number
163}
164
165function createBuddy(model: Model, pace: Pace, isThinking: boolean, styles: StyleSet, source: Source): Buddy {
166  return {
167    source,
168    isStock: source !== 'fresh only',
169    isFree: source === 'ready-made only',
170    dealer: createDealer(STYLE_SETS[styles]),
171    doing: 'thinking',
172    isLiveScene: false,
173    turnTools: 0,
174    liveAt: 0,
175    sessionStock: 0,
176    model,
177    pace,
178    thread: createThread(model, { isThinking, styles }),
179    pending: [],
180    startedAt: 0,
181    lastCall: 0,
182    loggedAt: 0,
183    isTurn: false,
184    isAsking: false,
185    isAnimating: false,
186    scene: FIRST,
187    sceneAt: 0,
188    previousAt: 0,
189    cols: 60,
190    shownAt: 0,
191    drawnRows: 1,
192    isRefused: false,
193    running: new Map(),
194    callCount: 0,
195    phase: '',
196    phaseAt: 0,
197    calmUntil: 0,
198    failures: 0,
199    isShown: true,
200    stats: emptyStats(),
201    sessionUsd: 0,
202    sessionScenes: 0,
203    isSubscription: false,
204    windows: [],
205    turnAt: 0,
206    turnCostAt: 0,
207    accruedAt: 0,
208  }
209}
210
211const rowsAt = (b: Buddy, at: number) => {
212  const grown = clamp01((at - b.shownAt) / GROW_MS)
213
214  return Math.max(1, Math.ceil(ROWS * (1 - Math.pow(1 - grown, 3))))
215}
216
217const frameAt = (b: Buddy, at: number, rows: number) =>
218  stage({
219    cols: b.cols,
220    rows,
221    t: at / 1000,
222    script: b.scene,
223    previous: b.previous,
224    since: at - b.sceneAt,
225    previousSince: at - b.previousAt,
226    reveal: clamp01((at - b.shownAt) / (GROW_MS * 1.6)),
227  })
228
229const stamp = (b: Buddy, at: number) => `+${Math.round((at - b.startedAt) / 1000)}s`
230
231// Logs a line for the next scene; while the cartoons are hidden nothing is
232// logged, so nothing is sent.
233function note(b: Buddy, at: number, line: string) {
234  if (!b.isShown) return
235  b.pending.push(`${stamp(b, at)} ${line}`)
236  b.loggedAt = at
237  const doing = phaseOf(line)
238  // Thinking between tool calls is part of the work in hand, not a new
239  // phase; only thinking before the turn's first tool is.
240  if (doing && !(doing === 'thinking' && b.turnTools > 0)) b.doing = doing
241  const what = whatOf(line)
242  if (what) b.what = what
243}
244
245// The scene playing gives way to another.
246function show(b: Buddy, script: Script, at: number) {
247  b.previous = b.scene
248  b.previousAt = b.sceneAt
249  b.scene = script
250  b.sceneAt = at
251  b.scenePhase = b.doing
252  b.error = undefined
253}
254
255// A stock scene for what Claude is doing, if there is one it can draw.
256function dealStock(b: Buddy, at: number) {
257  const got = deal(b.dealer, b.doing, b.what)
258  const script = got && cleanScript(got.raw)
259  if (!got || !script) return false
260  // A stock scene that breaks is not the director's to fix.
261  if (script.code) script.code.isReported = true
262  b.sessionStock += 1
263  show(b, script, at)
264  b.isLiveScene = false
265
266  return true
267}
268
269const bucket = (b: Buddy) => (b.stats.buddy[spendKey(b.model, b.pace)] ??= { usd: 0, scenes: 0, activeMs: 0 })
270
271// Adds up Claude's working time with the cartoons showing.
272function accrue(b: Buddy, now: number) {
273  if (b.isTurn && b.isShown && b.accruedAt > 0) bucket(b).activeMs += Math.max(0, now - b.accruedAt)
274  b.accruedAt = now
275}
276
277// One scene request, on the session's own credential and nothing else: on a
278// subscription it counts toward the plan's limits, never a separate bill. If
279// the login refuses (no credential, 401, 403), the cartoons stop asking for
280// the rest of the session rather than looking for another way to pay.
281async function requestScene($: EngineInterface, b: Buddy): Promise<Narration> {
282  const auth = await $.session.authorize()
283  let response: { ok: boolean; status: number; text: string; headers: Record<string, string> } | undefined
284  if (auth) {
285    response = await $.http.fetch(URL, { method: 'POST', auth: auth.handle, ...b.thread.request(auth.kind) })
286    if (b.thread.isFallbackRefused(response.status, response.text)) {
287      response = await $.http.fetch(URL, { method: 'POST', auth: auth.handle, ...b.thread.request(auth.kind) })
288    }
289  }
290  if (!response || response.status === 401 || response.status === 403) {
291    b.thread.abandon()
292    b.isRefused = true
293
294    return { error: "this session's login can't make the requests the cartoons need, so they are off for this session" }
295  }
296  if (response.ok) {
297    b.failures = 0
298
299    return b.thread.accept(response.text)
300  }
301  b.pending.unshift(b.thread.abandon().replace(/^\[strip [^\]]*\]\n/, ''))
302  // A request refused as malformed would be refused again with the same
303  // history: the conversation begins again.
304  if (response.status === 400) b.thread.reset()
305  // A rate limit waits as long as it asks; any other failure waits a little
306  // longer each time it repeats.
307  b.failures += 1
308  const wait = response.status === 429 ? Number(response.headers['retry-after'] ?? 20) : Math.min(120, 5 * 2 ** (b.failures - 1))
309  b.calmUntil = (await $.clock.now()) + Math.max(5, Math.min(120, wait || 20)) * 1000
310
311  return { error: `API ${response.status}: ${response.text.replace(/\s+/g, ' ').slice(0, 140)}` }
312}
313
314// Asks for a scene whenever there is news, at most once per the pace's gap,
315// while a turn runs and the cartoons show.
316function ask($: EngineInterface, b: Buddy) {
317  if (b.isAsking) return
318  b.isAsking = true
319  const gapMs = GAP_MS[b.pace]
320  void (async () => {
321    while (b.isTurn && b.isShown && !b.isRefused) {
322      const now = await $.clock.now()
323      // A quiet stretch mid-turn is news too, once the scene has run its
324      // course (or its code broke): what is still going on, for its next
325      // beat. Before that the scene keeps playing on its own, unasked.
326      const quiet = Math.max(QUIET_MS, gapMs * 2)
327      const isSpent = Boolean(b.scene.code?.error) || now - b.sceneAt >= BEAT_MS
328      if (b.pending.length === 0 && now - b.loggedAt >= quiet && now - b.lastCall >= quiet) {
329        const oldest = [...b.running.values()].sort((x, y) => x.at - y.at)[0]
330        if (!isSpent) b.loggedAt = now
331        else if (b.isStock && !b.scene.code?.error && dealStock(b, now)) {
332          b.loggedAt = now
333          $.ui.invalidate('ui.render')
334        } else if (oldest) note(b, now, `still running ${oldest.what} (${Math.round((now - oldest.at) / 1000)}s so far)`)
335        else if (b.phase) note(b, now, `still ${b.phase} (${Math.round((now - b.phaseAt) / 1000)}s so far)`)
336        else b.loggedAt = now
337      }
338      // With the stock on, the pace does not apply: stock scenes are dealt by
339      // isStockDue, and the director is asked at most once per LIVE_MS.
340      if (b.pending.length > 0 && now - b.lastCall >= (b.isStock ? 0 : gapMs)) {
341        // A scene whose code broke is news for the narrator, once.
342        const broken = b.scene.code?.error && !b.scene.code.isReported ? b.scene.code : undefined
343        // With the stock on, routine news deals a stock scene once the scene
344        // playing is due for replacing, and the log waits for the director's
345        // next turn: news worth it, once LIVE_MS have passed since its last.
346        const isNews = Boolean(broken) || b.pending.some(isInteresting)
347        // The waking-up scene gives way at once.
348        const isDue = isStockDue({ age: b.scene === FIRST ? Infinity : now - b.sceneAt, isBroken: Boolean(b.scene.code?.error), isLive: b.isLiveScene, isNewPhase: b.doing !== b.scenePhase })
349        // The stock has nothing for this phase in the styles allowed: the
350        // director draws it, at the same spacing.
351        const isUnstocked = b.isStock && isDue && !SCENES.some(s => s.phase === b.doing && b.dealer.styles.includes(s.style))
352        const isLive = !b.isStock || (!b.isFree && (isNews || isUnstocked) && now - b.liveAt >= LIVE_MS)
353        if (!isLive || now < b.calmUntil) {
354          if (b.isStock && isDue && dealStock(b, now)) $.ui.invalidate('ui.render')
355          // The log keeps its task line and its latest lines.
356          if (b.pending.length > 60) b.pending = [...b.pending.filter(line => line.startsWith('[task] ')).slice(-1), ...b.pending.slice(-50)]
357          await $.clock.sleep(400)
358          continue
359        }
360        b.lastCall = now
361        b.liveAt = now
362        if (broken) {
363          broken.isReported = true
364          b.lastTrouble = broken.error
365        }
366        const activity = [...(broken ? [`[your last scene's code stopped: ${broken.error?.slice(0, 200)}]`] : []), ...b.pending].join('\n')
367        b.pending = []
368        b.thread.ask(`[strip ${b.cols}x${ROWS}]\n${activity}`)
369        const told = await requestScene($, b)
370        if (told.spent) {
371          const usd = costOf(b.model, told.spent)
372          const spend = bucket(b)
373          spend.usd += usd
374          addTokens(spend, told.spent)
375          b.sessionUsd += usd
376          spend.scenes += 1
377          b.sessionScenes += 1
378          // The working time so far goes with the money, so the saved rate holds
379          // if the session ends mid-turn.
380          accrue(b, await $.clock.now())
381          await $.store.set('stats', b.stats).catch(() => {})
382        }
383        if (told.script) {
384          show(b, told.script, await $.clock.now())
385          b.isLiveScene = true
386        } else {
387          if (told.error && told.error !== b.error) $.ui.toast(`toons: ${told.error}`)
388          b.error = told.error
389        }
390        $.ui.invalidate('ui.render')
391      }
392      await $.clock.sleep(400)
393    }
394    b.isAsking = false
395  })().catch(() => {
396    // The module unloaded mid-wait (a reload): the next load asks afresh.
397    b.isAsking = false
398  })
399}
400
401// Shows or hides the cartoons; hidden, nothing is asked for or drawn.
402async function toggle($: EngineInterface, b: Buddy, show: boolean) {
403  const now = await $.clock.now()
404  accrue(b, now)
405  b.isShown = show
406  await $.store.set('isShown', show).catch(() => {})
407  if (show && b.isTurn) {
408    // The narrator missed what happened while hidden: it starts fresh.
409    note(b, now, '[the developer switched the cartoons on]')
410    ask($, b)
411  }
412  if (!show) b.pending = []
413  $.ui.invalidate('ui.render')
414}
415
416// Reads the session's cost and its plan's windows, if it has any.
417async function readPlan($: EngineInterface, b: Buddy) {
418  const usage = await $.session.usage()
419  b.isSubscription = usage.rateLimits.length > 0
420  b.windows = usage.rateLimits
421
422  return usage.cost?.usd ?? 0
423}
424
425// Changes one of the plugin's settings as /config would; the engine then
426// reloads the plugin with it. The row's key is the plugin's name (or its
427// name@inline when loaded from a folder), a dot, and the field.
428async function setOption($: EngineInterface, field: string, value: string) {
429  try {
430    // The row is found by its label rather than the plugin's name, which the
431    // engine may still hold from an older manifest until it restarts.
432    const rows = await $.config.list()
433    const row = rows.find(r => r.key.endsWith(`.${field}`) && (r.key.startsWith(`${PLUGIN}.`) || r.label.startsWith('Toons:')))
434    if (!row) throw new Error('no such setting; restart Claude Code if the plugin was renamed')
435    const { deny } = await $.config.set({ key: row.key, value })
436    if (deny) throw new Error(deny)
437  } catch (e) {
438    $.ui.toast(`toons: could not change ${field}: ${e instanceof Error ? e.message : String(e)}`)
439  }
440}
441
442// Paints the scene at about 20 frames a second while the spinner shows, each
443// frame once the last one landed.
444function animate($: EngineInterface, b: Buddy) {
445  if (b.isAnimating) return
446  b.isAnimating = true
447  void (async () => {
448    while (b.spinner && b.isShown) {
449      const at = await $.clock.now()
450      // While it grows, each new height is a redraw; frames keep painting at
451      // the height drawn until it lands.
452      if (rowsAt(b, at) !== b.drawnRows) $.ui.invalidate('ui.render')
453      const frame = frameAt(b, at, b.drawnRows)
454      await $.ui.blit({ requestId: b.spinner, key: 'oracle', cells: frame, columns: b.cols, rows: b.drawnRows }).catch(() => {})
455      await $.clock.sleep(50)
456    }
457    b.isAnimating = false
458  })().catch(() => {
459    b.isAnimating = false
460  })
461}
462
463export const register: Register = (on, options) => {
464  const settings = options as Record<string, unknown>
465  const b = createBuddy(
466    isModel(settings.model) ? settings.model : 'claude-sonnet-5-5',
467    isPace(settings.pace) ? settings.pace : 'every 15 seconds',
468    settings.thinking === 'on',
469    isStyleSet(settings.styles) ? settings.styles : 'mix',
470    sourceOf(settings.library),
471  )
472
473  on('session.start', async ($, e, next) => {
474    b.isShown = (await $.store.get('isShown')) !== false
475    b.stats = cleanStats(await $.store.get('stats'))
476    await $.command.register({
477      name: COMMAND,
478      description: 'Show or hide the cartoons under the spinner; "/toons settings" for the model, pace and cost',
479      argumentHint: '[on|off|settings]',
480      immediate: true,
481    })
482
483    return next(e)
484  })
485
486  on('command.run', { command: COMMAND }, async ($, e) => {
487    const arg = e.args.trim().toLowerCase()
488    if (arg === 'settings') {
489      await $.ui.open({ id: PANE, title: 'Toons', focus: true, closeOnEscape: true, rows: 22 })
490
491      return { text: 'Toons settings opened.' }
492    }
493    await toggle($, b, arg === 'on' ? true : arg === 'off' ? false : !b.isShown)
494
495    return {
496      text: b.isShown
497        ? `Cartoons on (${MODEL_NAMES[b.model]}, a new scene ${b.pace}). /toons settings shows what they cost.`
498        : 'Cartoons off: no scenes are requested or drawn until you turn them back on with /toons.',
499    }
500  })
501
502  on('prompt.submit', async ($, e, next) => {
503    const now = await $.clock.now()
504    b.startedAt = now
505    if (b.isShown) b.pending.push(`[task] ${e.text.replace(/\s+/g, ' ').slice(0, 400)}`)
506    b.loggedAt = now
507    b.phase = ''
508    b.running.clear()
509    b.turnTools = 0
510    b.isTurn = true
511    b.turnAt = now
512    b.accruedAt = now
513    b.turnCostAt = await readPlan($, b)
514    if (b.isShown) ask($, b)
515
516    return next(e)
517  })
518
519  on('tool.call', async ($, e, next) => {
520    const what = `${e.tool}${describe(e as unknown as Record<string, unknown>)}`
521    const id = (b.callCount += 1)
522    if (b.isTurn) b.turnTools += 1
523    const began = await $.clock.now()
524    if (b.isTurn) {
525      b.running.set(id, { what, at: began })
526      note(b, began, `-> started ${what}`)
527    }
528    let ran: Awaited<ReturnType<typeof next>>
529    try {
530      ran = await next(e)
531    } finally {
532      b.running.delete(id)
533    }
534    if (b.isTurn) {
535      const at = await $.clock.now()
536      const took = Math.round((at - began) / 1000)
537      const how = ran.deny !== undefined ? 'denied' : ran.isError ? `failed: ${(ran.text ?? '').replace(/\s+/g, ' ').slice(0, 120)}` : 'done'
538      note(b, at, `<- ${how} after ${took}s: ${what}`)
539    }
540
541    return ran
542  })
543
544  on('turn.complete', async ($, e, next) => {
545    const now = await $.clock.now()
546    accrue(b, now)
547    b.isTurn = false
548    b.spinner = undefined
549    if (b.isShown) b.pending.push('[turn finished]')
550    // What Claude's own work cost this turn, against its working time: the
551    // yardstick the settings pane measures the cartoons by.
552    if (b.turnAt > 0) {
553      const cost = await readPlan($, b)
554      b.stats.main.usd += Math.max(0, cost - b.turnCostAt)
555      b.stats.main.activeMs += now - b.turnAt
556      b.stats.main.scenes += 1
557      b.turnAt = 0
558      await $.store.set('stats', b.stats).catch(() => {})
559    }
560
561    return next(e)
562  })
563
564  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
565    await readPlan($, b)
566    const cost = summarizeCost({ stats: b.stats, model: b.model, pace: b.pace, windows: b.windows })
567    if (e.surface === 'mobile') {
568      const { Text } = $.ui.resolve(e)
569
570      return <Text>Cartoons cost about {money(cost.hour.usd)} per hour of Claude working. Change settings in the terminal.</Text>
571    }
572    const { Box, Text, Select } = $.ui.resolve(e)
573    // A label and its value on one line, the labels in one column.
574    const row = (label: string, value: string, note?: string) => (
575      <Box flexDirection="row">
576        <Box width={18}>
577          <Text dimColor>{label}</Text>
578        </Box>
579        <Text>{value}</Text>
580        {note && <Text dimColor>{`  ${note}`}</Text>}
581      </Box>
582    )
583    const heading = (text: string) => (
584      <Box marginTop={1}>
585        <Text bold color="#d97757">
586          {text}
587        </Text>
588      </Box>
589    )
590    const cheapest = Math.min(...cost.models.map(m => m.usd))
591
592    return (
593      <Box flexDirection="column" paddingX={1}>
594        <Select
595          key="shown"
596          label="Cartoons          "
597          options={[{ value: 'shown' }, { value: 'hidden' }]}
598          value={b.isShown ? 'shown' : 'hidden'}
599          autoFocus
600          onSelect={(value: string) => void toggle($, b, value === 'shown')}
601        />
602        <Select
603          key="model"
604          label="Director model    "
605          options={MODELS.map(m => ({ value: m, label: MODEL_NAMES[m] }))}
606          value={b.model}
607          onSelect={(value: string) => void setOption($, 'model', value)}
608        />
609        <Select
610          key="pace"
611          label="New scene         "
612          options={PACES.map(p => ({ value: p }))}
613          value={b.pace}
614          onSelect={(value: string) => void setOption($, 'pace', value)}
615        />
616        <Select
617          key="thinking"
618          label="Director thinks   "
619          options={[{ value: 'off', label: 'off (cheaper)' }, { value: 'on' }]}
620          value={b.thread.isThinking ? 'on' : 'off'}
621          onSelect={(value: string) => void setOption($, 'thinking', value)}
622        />
623        <Select
624          key="library"
625          label="Scenes            "
626          options={[
627            { value: 'ready-made only', label: 'ready-made only: free, no tokens used' },
628            { value: 'mix', label: 'mix: ready-made, fresh ones for news (~1-3% extra)' },
629            { value: 'fresh only', label: 'fresh only: every scene drawn new, uses far more tokens' },
630          ]}
631          value={b.source}
632          onSelect={(value: string) => void setOption($, 'library', value)}
633        />
634        <Select
635          key="styles"
636          label="Scene styles      "
637          options={Object.keys(STYLE_SETS).map(value => ({ value, label: value === 'mix' ? 'mix (3D, pixel art, text art)' : value }))}
638          value={b.thread.styles}
639          onSelect={(value: string) => void setOption($, 'styles', value)}
640        />
641        <Text dimColor>Toggle any time with /toons, even while Claude works</Text>
642
643        {b.isFree ? (
644          <Box flexDirection="column">
645            {heading('Cost')}
646            {row('Cartoons', 'free', 'ready-made scenes only: no requests, no tokens')}
647          </Box>
648        ) : (
649          <Box flexDirection="column">
650            {heading('Per hour of Claude working')}
651            {cost.claude ? row("Claude's own work", `~${money(cost.claude.usd)}`, 'measured') : row("Claude's own work", 'not measured yet', 'after ~10 min of Claude working')}
652            {row(
653              'Cartoons',
654              `~${money(cost.hour.usd)}`,
655              `~${Math.round(cost.hour.scenes)} scenes, ${cost.hour.measuredMinutes !== undefined ? `measured over ${cost.hour.measuredMinutes} min` : 'estimate'}`,
656            )}
657            {cost.claude &&
658              row('One scene', money(cost.hour.usd / Math.max(1, cost.hour.scenes)), `costs about ${cost.claude.sceneSeconds < 1 ? 'a second' : `${Math.round(cost.claude.sceneSeconds)} seconds`} of Claude's own work`)}
659            {cost.perScene &&
660              row(
661                'Per scene',
662                money(cost.perScene.usd),
663                `${Math.round(cost.perScene.outputShare * 100)}% is the scene itself (~${Math.round(cost.perScene.output)} tokens); the rest is the cached history (~${Math.round(cost.perScene.cacheRead / 1000)}k tokens, mostly cache reads)`,
664              )}
665
666            {heading('Each model at this pace')}
667            {cost.models.map(m => (
668              <Box flexDirection="row">
669                <Box width={18}>
670                  <Text bold={m.model === b.model} dimColor={m.model !== b.model}>
671                    {m.model === b.model ? `> ${m.name}` : `  ${m.name}`}
672                  </Text>
673                </Box>
674                <Text bold={m.model === b.model} dimColor={m.model !== b.model}>{`~${money(m.usd)}/hr`}</Text>
675                <Text dimColor>{`  ~${Math.round(m.scenes)} scenes${m.usd === cheapest ? ', cheapest' : ''}`}</Text>
676              </Box>
677            ))}
678          </Box>
679        )}
680
681        {heading('Usage')}
682        {cost.windows.length > 0 && row('Your plan', cost.windows.map(w => `${w.label} ${w.percent}%`).join(' · '))}
683        {b.isFree
684          ? row('This session', `${b.sessionStock} ready-made scenes`, 'free')
685          : row('This session', `${b.sessionScenes} scenes · ${money(b.sessionUsd)}`, b.sessionStock > 0 ? `and ${b.sessionStock} ready-made, free` : undefined)}
686        {b.lastTrouble && row('Last scene error', b.lastTrouble.slice(0, 70), 'sent back to the director to fix')}
687
688        <Box marginTop={1}>
689          <Text dimColor>
690            {b.isSubscription
691              ? "On a subscription neither line is charged separately: both count toward the same usage limits, each in proportion to its cost."
692              : "Billed to this session's API key."}{' '}
693            Hidden or idle: nothing.
694          </Text>
695        </Box>
696      </Box>
697    )
698  })
699
700  on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
701    if (e.surface !== 'terminal') return next(e)
702    const spinnerLine = await next(e)
703    if (!b.isShown) {
704      b.spinner = undefined
705
706      return spinnerLine
707    }
708    const { Box, Raster } = $.ui.resolve(e)
709    // The spinner knows when Claude turns to thinking or to writing its reply.
710    const nextPhase = e.props.mode === 'thinking' ? 'thinking' : e.props.mode === 'responding' ? 'writing the reply' : ''
711    if (b.isTurn && nextPhase && nextPhase !== b.phase) {
712      const at = await $.clock.now()
713      b.phase = nextPhase
714      b.phaseAt = at
715      note(b, at, `[Claude is ${nextPhase}]`)
716    } else if (!nextPhase) {
717      b.phase = ''
718    }
719    const now = await $.clock.now()
720    if (b.spinner !== e.requestId) b.shownAt = now
721    b.spinner = e.requestId
722    // A little narrower than the screen, so the spinner's indent never clips it.
723    b.cols = Math.max(20, Math.min(220, (e.viewport?.columns ?? 80) - 6))
724    b.drawnRows = rowsAt(b, now)
725    const cells = frameAt(b, now, b.drawnRows)
726    if (b.sceneAt === 0) b.sceneAt = now
727    animate($, b)
728
729    return (
730      <Box flexDirection="column">
731        {spinnerLine}
732        <Raster key="oracle" columns={b.cols} rows={b.drawnRows} cells={cells} />
733      </Box>
734    )
735  })
736}
737
hooks/cost.ts 147 lines
1// What the cartoons cost: API prices per model, what a scene typically
2// spends, and an hour of Claude working with cartoons on, put next to what
3// Claude's own work costs so the number means something on a subscription.
4
5export const MODELS = ['claude-haiku-4-5', 'claude-sonnet-5-5', 'claude-opus-5-5'] as const
6export type Model = (typeof MODELS)[number]
7
8export const MODEL_NAMES: Record<Model, string> = {
9  'claude-haiku-4-5': 'Haiku 4.5',
10  'claude-sonnet-5-5': 'Sonnet 5.5',
11  'claude-opus-5-5': 'Opus 5.5',
12}
13
14export const PACES = ['as fast as possible', 'every 15 seconds', 'every 30 seconds', 'every minute'] as const
15export type Pace = (typeof PACES)[number]
16
17// The least time between two scene requests, per pace.
18export const GAP_MS: Record<Pace, number> = {
19  'as fast as possible': 1500,
20  'every 15 seconds': 15_000,
21  'every 30 seconds': 30_000,
22  'every minute': 60_000,
23}
24
25export const isModel = (v: unknown): v is Model => MODELS.includes(v as Model)
26export const isPace = (v: unknown): v is Pace => PACES.includes(v as Pace)
27
28export type CallUsage = { input: number; output: number; cacheRead: number; cacheWrite: number }
29
30// US dollars per million tokens, at Anthropic's API list prices.
31const PRICES: Record<Model, CallUsage> = {
32  'claude-haiku-4-5': { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
33  'claude-sonnet-5-5': { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 },
34  'claude-opus-5-5': { input: 4, output: 20, cacheRead: 0.2, cacheWrite: 5 },
35}
36
37export const costOf = (model: Model, u: CallUsage) => {
38  const p = PRICES[model]
39
40  return (u.input * p.input + u.output * p.output + u.cacheRead * p.cacheRead + u.cacheWrite * p.cacheWrite) / 1e6
41}
42
43// Seconds a scene takes to come back, per model, Sonnet's measured at about
44// 9 (a request waits for the last one, so "as fast as possible" is bounded by
45// it). A faster model draws more scenes an hour at that pace, so Haiku's
46// hourly cost there lands near Sonnet's despite costing half per scene.
47const LATENCY_S: Record<Model, number> = { 'claude-haiku-4-5': 5, 'claude-sonnet-5-5': 9, 'claude-opus-5-5': 14 }
48
49// A typical scene's tokens on Sonnet 5.5 (measured at about 1.7 cents a
50// scene): the cached conversation read back, what each exchange adds to it,
51// and the scene itself. The scene's own tokens are most of the cost.
52const TYPICAL: CallUsage = { input: 300, output: 1100, cacheRead: 30_000, cacheWrite: 1500 }
53
54export const scenesPerHour = (model: Model, pace: Pace) => 3600 / Math.max(GAP_MS[pace] / 1000, LATENCY_S[model])
55
56// What one model and pace has spent, and over how much of Claude's working
57// time with the cartoons showing.
58export type Spend = { usd: number; scenes: number; activeMs: number; tokens?: CallUsage }
59
60export const addTokens = (spend: Spend, u: CallUsage) => {
61  const t = (spend.tokens ??= { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 })
62  t.input += u.input
63  t.output += u.output
64  t.cacheRead += u.cacheRead
65  t.cacheWrite += u.cacheWrite
66}
67export type Stats = { buddy: Record<string, Spend>; main: Spend }
68
69export const emptyStats = (): Stats => ({ buddy: {}, main: { usd: 0, scenes: 0, activeMs: 0 } })
70export const spendKey = (model: Model, pace: Pace) => `${model}|${pace}`
71
72// Measured figures count once they cover this much working time.
73const ENOUGH_MS = 10 * 60_000
74
75// Dollars per hour of Claude working, measured where there is enough of it.
76const hourly = (spend: Spend | undefined) => (spend && spend.activeMs >= ENOUGH_MS ? spend.usd / (spend.activeMs / 3_600_000) : undefined)
77
78export function estimate(stats: Stats, model: Model, pace: Pace) {
79  const measured = hourly(stats.buddy[spendKey(model, pace)])
80  const spend = stats.buddy[spendKey(model, pace)]
81
82  return {
83    usd: measured ?? scenesPerHour(model, pace) * costOf(model, TYPICAL),
84    scenes: measured !== undefined && spend ? spend.scenes / (spend.activeMs / 3_600_000) : scenesPerHour(model, pace),
85    measuredMinutes: measured !== undefined && spend ? Math.round(spend.activeMs / 60_000) : undefined,
86  }
87}
88
89// Dollars as a person reads them: cents under a dollar, then fewer digits.
90export const money = (usd: number) => (usd < 0.995 ? `$${usd.toFixed(2)}` : `$${usd.toFixed(usd < 9.95 ? 2 : 0)}`)
91
92export type Window = { kind: string; percentUsed: number }
93
94export type CostSummary = {
95  // An hour of Claude working with the cartoons on, at this model and pace.
96  hour: { usd: number; scenes: number; measuredMinutes?: number }
97  // Claude's own work, once measured: what an hour of it costs, and how many
98  // seconds of it cost as much as one scene, so the two read in one unit.
99  claude?: { usd: number; sceneSeconds: number }
100  // What a scene spends, from the tokens measured so far at this model (any
101  // pace), with the share of its cost that is the scene itself (output).
102  perScene?: { usd: number; output: number; cacheRead: number; outputShare: number }
103  // An hour at this pace on each model.
104  models: { model: Model; name: string; usd: number; scenes: number }[]
105  // The plan's windows, where the session has any.
106  windows: { label: string; percent: number }[]
107}
108
109export function summarizeCost(args: { stats: Stats; model: Model; pace: Pace; windows: Window[] }): CostSummary {
110  const { stats, model, pace, windows } = args
111  const hour = estimate(stats, model, pace)
112  const claudeUsd = hourly(stats.main)
113  const share = claudeUsd ? hour.usd / claudeUsd : undefined
114  // Every scene on this model, whatever the pace.
115  const mine = Object.entries(stats.buddy)
116    .filter(([key, s]) => key.startsWith(`${model}|`) && s.tokens)
117    .map(([, s]) => s)
118  const scenes = mine.reduce((n, s) => n + s.scenes, 0)
119  const tokens = mine.reduce(
120    (sum, s) => ({
121      input: sum.input + (s.tokens?.input ?? 0),
122      output: sum.output + (s.tokens?.output ?? 0),
123      cacheRead: sum.cacheRead + (s.tokens?.cacheRead ?? 0),
124      cacheWrite: sum.cacheWrite + (s.tokens?.cacheWrite ?? 0),
125    }),
126    { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
127  )
128  const usd = costOf(model, tokens)
129  const outputUsd = costOf(model, { ...tokens, input: 0, cacheRead: 0, cacheWrite: 0 })
130
131  return {
132    hour,
133    perScene:
134      scenes >= 5
135        ? { usd: usd / scenes, output: tokens.output / scenes, cacheRead: tokens.cacheRead / scenes, outputShare: usd > 0 ? outputUsd / usd : 0 }
136        : undefined,
137    claude:
138      claudeUsd && share !== undefined
139        ? { usd: claudeUsd, sceneSeconds: (hour.usd / Math.max(1, hour.scenes)) / (claudeUsd / 3600) }
140        : undefined,
141    models: MODELS.map(m => ({ model: m, name: MODEL_NAMES[m], usd: estimate(stats, m, pace).usd, scenes: estimate(stats, m, pace).scenes })),
142    windows: windows
143      .filter(w => w.kind === 'five_hour' || w.kind === 'seven_day')
144      .map(w => ({ label: w.kind === 'five_hour' ? '5-hour' : 'weekly', percent: w.percentUsed })),
145  }
146}
147
hooks/library.ts 127 lines
1// A library of ready-made scenes, one for each kind of thing Claude does,
2// dealt for free while the work is routine, so the director is asked only
3// when something worth a new picture happens.
4
5import { SCENES, type Stock } from './scenes'
6
7export const PHASES = ['thinking', 'searching', 'reading', 'editing', 'testing', 'building', 'running', 'git', 'web', 'agents', 'writing'] as const
8export type Phase = (typeof PHASES)[number]
9export const isPhase = (v: unknown): v is Phase => PHASES.includes(v as Phase)
10
11// What a scene of each phase is about, for whoever generates one.
12export const ABOUT: Record<Phase, string> = {
13  thinking: 'Claude is thinking: working out what to do next, weighing options, puzzling over a problem',
14  searching: 'Claude is searching the codebase: grepping for a pattern, globbing for files, hunting for where something is defined',
15  reading: 'Claude is reading code: going through files to understand how they work',
16  editing: 'Claude is editing code: writing and changing files, making the fix',
17  testing: 'Claude is running the tests and waiting to see if they pass',
18  building: 'Claude is building or installing: a compile, a bundle, a package install that takes a while',
19  running: 'Claude is running a command in the shell and waiting for it',
20  git: 'Claude is working with git: committing, branching, looking at history or a diff',
21  web: 'Claude is fetching a web page or searching the web for an answer',
22  agents: 'Claude has sent out subagents to work on parts of the task in parallel and is waiting for them',
23  writing: 'Claude is writing its reply to the developer, the work done',
24}
25
26// The phase a line of the log belongs to, if it names one: a tool starting,
27// or the spinner turning to thinking or writing.
28export function phaseOf(line: string): Phase | undefined {
29  if (/\[Claude is thinking\]/.test(line)) return 'thinking'
30  if (/\[Claude is writing the reply\]/.test(line)) return 'writing'
31  const m = /-> started (\w+)(?: `([^`]*)`)?/.exec(line)
32  if (!m) return undefined
33  const tool = m[1] ?? ''
34  const args = m[2] ?? ''
35  if (/^(Read|NotebookRead)$/.test(tool)) return 'reading'
36  if (/^(Grep|Glob|LS|Explore)$/.test(tool)) return 'searching'
37  if (/^(Edit|Write|MultiEdit|NotebookEdit)$/.test(tool)) return 'editing'
38  if (/^(WebFetch|WebSearch)$/.test(tool) || /^mcp__/.test(tool)) return 'web'
39  if (/^(Agent|Task|Workflow)$/.test(tool)) return 'agents'
40  if (tool === 'Bash') {
41    if (/\b(test|tests|jest|vitest|pytest|mocha|spec|rspec|go test|cargo test)\b/.test(args)) return 'testing'
42    if (/\b(build|tsc|make|cargo (build|check)|compile|webpack|vite|bundle|install|npm ci|yarn|pnpm|pip|poetry|gradle|mvn|xcodebuild)\b/.test(args)) return 'building'
43    if (/\b(git|gh)\b/.test(args)) return 'git'
44
45    return 'running'
46  }
47
48  return undefined
49}
50
51// How long a scene keeps the strip before a stock scene may replace it: at
52// least DWELL_MS (a fresh scene from the director, LIVE_DWELL_MS, since it
53// was paid for and is about this moment), then when the work turns to a new
54// phase, or after STOCK_MS in any case. A scene whose code broke goes at once.
55export const DWELL_MS = 20_000
56export const LIVE_DWELL_MS = 45_000
57export const STOCK_MS = 60_000
58
59export function isStockDue(scene: { age: number; isBroken: boolean; isLive: boolean; isNewPhase: boolean }) {
60  if (scene.isBroken) return true
61  if (scene.age < (scene.isLive ? LIVE_DWELL_MS : DWELL_MS)) return false
62
63  return scene.isNewPhase || scene.age >= STOCK_MS
64}
65
66// Whether a line of the log is news worth a scene of its own from the
67// director: the task, a failure, the end of the turn, a scene that broke.
68export const isInteresting = (line: string) => /^\[task\] |^\+\d+s <- (failed|denied)|^\[turn finished\]|^\[your last scene's code/.test(line)
69
70// The thing Claude last touched, short enough to label a prop: a file's
71// name, or the head of a command.
72export function whatOf(line: string): string | undefined {
73  const m = /-> started \w+ `([^`|]*)/.exec(line)
74  if (!m) return undefined
75  const arg = m[1]?.trim() ?? ''
76  if (!arg) return undefined
77  const name = /^[\w./-]+$/.test(arg) && arg.includes('/') ? arg.slice(arg.lastIndexOf('/') + 1) : arg.split(' ').slice(0, 2).join(' ')
78
79  return name.replace(/["\\]/g, '').slice(0, 20) || undefined
80}
81
82export const STYLES = ['3D', 'pixel art', 'text art'] as const
83export type Style = (typeof STYLES)[number]
84
85// How a scene is really drawn, by what its code calls: the style it was
86// dealt is a request the model may not have honored.
87export function styleOf(scene: unknown): Style {
88  const code = JSON.stringify(scene)
89  if (/\b(camera|mesh3d|clawd3d|line3d)\(/.test(code)) return '3D'
90  if (/\bpixels?\(/.test(code)) return 'pixel art'
91
92  return 'text art'
93}
94
95// The stock, dealt like cards: each phase has a shuffled deck of its scenes
96// in the styles allowed, and a scene comes around again only once the rest
97// of its deck has been dealt.
98export type Dealer = { styles: readonly string[]; decks: Map<Phase, Stock[]> }
99
100export const createDealer = (styles: readonly string[]): Dealer => ({ styles, decks: new Map() })
101
102// A scene for a phase, as the raw object the director would have answered,
103// with "{what}" in it filled in. Nothing when the stock has none for the
104// phase in the styles allowed.
105export function deal(dealer: Dealer, phase: Phase, what: string | undefined, random: () => number = Math.random): { concept: string; raw: unknown } | undefined {
106  let deck = dealer.decks.get(phase)
107  if (!deck || deck.length === 0) {
108    deck = SCENES.filter(s => s.phase === phase && dealer.styles.includes(s.style)).sort(() => random() - 0.5)
109    dealer.decks.set(phase, deck)
110  }
111  const pick = deck.pop()
112  if (!pick) return undefined
113  const filled = JSON.stringify(pick.scene).split('{what}').join(JSON.stringify(what ?? 'the code').slice(1, -1))
114
115  return { concept: pick.concept, raw: JSON.parse(filled) }
116}
117
118// The brief a generator adds to the director's prompt for one stock scene.
119export function brief(phase: Phase, world: string, cols: number) {
120  return [
121    `[strip ${cols}x9]`,
122    `[task] (unknown: this is a ready-made scene)`,
123    `This scene goes into a library of ready-made scenes, dealt whenever ${ABOUT[phase]}. No more news will come while it plays, for a minute or two, so it has to tell its own story: something happens, and keeps happening, in this world, about this kind of work. Clawd has a voice here; what it says and when is yours. The real file or command is not known: write {what} wherever a label or a line would name it, and it is filled in when the scene is dealt (up to 20 characters).`,
124    `[world: ${world}]`,
125  ].join('\n')
126}
127
hooks/narrator.ts 461 lines
1// The buddy's one conversation with Sonnet: append-only for the session, so
2// each request reads the earlier ones from the prompt cache. Every update is a
3// user message holding what the main agent did since the last; every reply is
4// kept exactly as returned, thinking blocks included.
5
6import type { CallUsage, Model } from './cost'
7import { EFFECTS } from './effects'
8import { cleanScript, type Script } from './script'
9
10
11// A request on the session's own subscription login opens its system prompt
12// as Claude Code's own requests do.
13const IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude."
14
15// The director's prompt, built from only what the styles allowed need: a
16// model never sees the parts of the drawing language it may not use, so it
17// cannot reach for them.
18const HEAD = `You direct a tiny animated cartoon that plays inside Claude Code's spinner. Claude Code is a terminal app where Claude, an AI coding agent, works on a developer's code. While Claude works, a strip of the terminal under the "thinking" spinner is yours: 9 rows tall and as wide as the terminal. Every few seconds you get a log of what Claude just did and answer with a new scene for the developer to watch while they wait.
19
20Each user message starts with the strip's size, "[strip 120x9]" (columns x rows), then the log of what happened since your last scene, each line stamped with seconds since the task began:
21- "[task] ..." is what the developer asked for.
22- "-> started Bash \`npm test\`" means Claude just launched a tool; "<- done after 12s: Bash \`npm test\`" or "<- failed: 3 tests failing ... after 12s: Bash \`npm test\`" means it finished.
23- "[Claude is thinking]" or "[Claude is writing the reply]" means Claude turned to thinking or to writing its answer.
24- "still running Bash \`npm test\` (25s so far)" or "still thinking (15s so far)" means nothing new has happened for a while and the scene playing has run its course. Answer the next beat of the same scene: keep its world, set and cast and move the action along (the countdown reaches 1, the crane lifts the next block, night falls on the siege), never start over.
25- "[turn finished]" means Claude stopped.
26- "[your last scene's code stopped: ...]" means the code you wrote hit an error or ran out of time, and the error. Fix that mistake from now on.
27- "[recent scenes: ...]" lists the concepts of your last few scenes. Do not repeat any of them, or anything close.
28- "[world: ...]" is the setting this scene must live in (deep sea, wild west, cooking show...). Translate the real work into that world with wit: in a cooking show a failing test is a fallen soufflé, in the wild west a bug is an outlaw on a wanted poster. Fresh worlds keep the cartoon from going stale, so commit to it.`
29const STYLE_LINES: Record<string, string> = {
30  '3D': '"3D" means the scene is a 3D world drawn with camera, mesh3d, clawd3d and friends (a 2D Clawd or labels may sit on top)',
31  'pixel art': '"pixel art" means the set and props are drawn with pixels() on the pixel grid, in several colors, Clawd dressed with pixel art at its anchors',
32  'text art': '"text art" means sprites and text',
33}
34const DRAWING = `You see the whole session in this one conversation.
35
36Tell the story visually. The picture carries the meaning: what Claude is doing, how it is going, what just broke or got fixed. There is no caption, and nothing should read like a status line. Text belongs in the picture only as labels on things, with the real names from the log: the file name on a crate or a book spine, the test name on a banner, the command on a little terminal.
37
38What makes a scene good:
39- Show what the work means, not its mechanics. "Claude reads a file" is how; the story is why and how it is going. A bug hunt is a safari stalking a beetle, a stakeout, a bomb squad, a mole-whacking arcade; tracing a config is following a river to its source; a refactor is moving house. A long run of reads and searches is one investigation with an arc (setting out, a lead, a dead end, closing in, the reveal), and each new scene is its next chapter in a new place, never the same shot again.
40- Banned, because it is the scene you reach for by default and the developer is tired of it: Clawd walking past or around files, documents, papers, scrolls, books or folders, with or without a magnifying glass. Files may appear only as labels on things that belong to the world (a crate, a fish, a planet, a wanted poster).
41- Vary Clawd's role and the shot, not just the props: Clawd rides, pilots, swims, digs, cooks, conducts, hides, sleeps, is chased, is tiny in a huge set, watches from a corner while the world does the work, or is mid-leap. Walking left to right is one option of many and rarely the best.
42- Some starting points, not a menu: searching is sonar sweeping, a metal detector on a beach, a sniffer dog; editing is a crane placing blocks, a tailor stitching, a surgeon; running tests is a racetrack, an exam hall, a castle siege where failures are bugs storming the walls; builds and installs are a factory conveyor or a rocket on the pad; git is trains on branching tracks; web requests are a fishing boat or a carrier pigeon; subagents are helpers hatching; waiting on a long command is a campfire at night; success is a parade, fireworks, a podium.
43- Clawd, the Claude Code mascot, stands in for Claude and is in nearly every scene: it carries the file, drives the crane, flees the bugs, climbs the podium. It may also make an entrance late, peek in from the edge, or be inside something (a submarine porthole, a cockpit). In code, Clawd can be dressed, recolored, scaled and posed (see clawd below), so give it a costume and a role that fit the world: a helmet on the construction site, a cape for the hero, a chef's hat on the cooking show, green when seasick, two Clawds of different colors as rivals. Add Clawd as an actor with kind "clawd" and frames [] (the stage draws Clawd itself in pixel art, 14 columns wide and 4 rows tall, in its own orange). Clawd animates on its own: it bobs gently and looks the way it goes whenever its x changes, and blinks when it stands still. So give Clawd motion through x and y, and put its props beside it as separate sprite actors. Every other actor has kind "sprite".
44- Everything around Clawd changes when the work changes: for new work, never reuse the metaphor, the set, or the supporting characters of any of your last five scenes. (A "still ..." beat is the exception: it is the next beat of the scene that is playing.)
45- Play with the frame itself: big set pieces that fill the strip, a skyline, a parallax background scrolling slower than the foreground, a split screen, a giant thing only partly in view, a tiny world seen from far away, the whole scene scrolling past like a camera pan, a silhouette against a sunset. The strip is wide; use it.
46- Motion with intent, timed like a cartoon. You are free to choose the pace the moment calls for: a slow stakeout, a frantic chase, a rocket blasting off, an explosion, a pratfall, a leap, a long fall, a zoom across the strip. Use anticipation and payoff: a beat of stillness, then the action; things ease in and out (smoothstep) rather than starting and stopping dead. A scene can be a little story of several beats: with "show" things appear, vanish, get swapped (the closed chest becomes the open one at t=3, the bug pops when the hammer lands), and with "frame" a sprite's animation follows the action rather than looping. The one thing to avoid is aimless noise: jitter, constant wobble, everything moving at once with no focus.
47- A composed set, not a figure on an empty strip: still set pieces (fps 0) such as shelves, buildings, servers, trees, a terminal window, a track, waves, a horizon line, drawn with box drawing (─│┌┐└┘├┤┬┴┼═║╔╗╚╝), blocks (█▓▒░▀▄▌▐), braille (⣿⣶⣤⡇) and ASCII; then characters and props moving and interacting in front of them.
48- Clean layout. Place things on purpose with the strip size in hand: assign every element its own column range before writing its x. Actors are solid: the space inside each line of a sprite, from its first character to its last, hides whatever is behind it, and later actors are drawn in front. List set pieces first and characters after. No character standing on a sign, no text running into a box, no two sprites sharing cells unless one is deliberately passing in front of the other.
49- Color with intent: each sprite is one color, so split a multicolored thing into several actors (you have up to 20). All colors must be clearly visible on a dark background.
50- These are guides, not a cage. Surprise the developer: an unexpected angle, a sight gag, a callback to an earlier scene, a tiny running subplot in the corner.
51
52Speech: Clawd can say one short line to the developer in a speech bubble. Make it fun to read: jokes, puns and a bit of personality are welcome. But it must be about what is actually happening and make sense at a glance to someone who has not read the log: name the real thing (the file, the test, the error) and say something about it a person would get immediately. "3 tests vs. one undefined password. undefined is winning." works; "the scrolls whisper of absence" does not. No riddles, nothing cryptic, nothing that only makes sense if you already know the metaphor. Skip speech when there is nothing worth saying: leave say empty.
53
54You have two ways to draw, and can mix them in one scene:
551. Declarative: actors, particles and a background, each moved by a math expression of time (below). Quick for simple staging.
562. Code: a real program in the "code" field, for anything the declarative parts cannot do: physics and simulations (gravity, bouncing, flocks, fluids, cellular automata, growing plants, sand piling up), transformations (morphing one shape into another, zooming, ripples and distortion, a scrolling world, a camera pan, text that shatters or assembles), per-cell shaders (fire, water, clouds, plasma painted with background colors), procedural sets, characters that react to each other, state that builds up over time. Reach for code whenever the idea is more than things sliding around: it is how the best scenes are made.
57
58The code language is JavaScript, interpreted: let/const/var, functions and arrow functions (closures), if/else, switch, for, for-of, for-in, while, do-while, break/continue/return, ternaries, template literals, arrays and plain objects (with destructuring and ...spread), the usual operators, Math.*, Array.from, Object.keys/values/entries, and array methods (push pop shift unshift slice splice concat indexOf includes join reverse fill map filter forEach some every find findIndex reduce sort flat at) and string methods (slice substring split repeat padStart padEnd toUpperCase toLowerCase trim replace includes startsWith endsWith charAt charCodeAt at). No classes, no regex, no "this", no async. The top level runs once when the scene starts: set up state there. Then define function frame(t, dt), called about 20 times a second with t (seconds since the scene started) and dt (seconds since the last frame); it redraws the whole picture each time on a fresh strip, and its variables at the top level persist between frames. The globals w and h are the strip's columns and rows, and t and dt are also globals.
59Drawing (x is the column, y the row, 0,0 top-left; anything off the strip is clipped; colors are "#rrggbb" or numbers from rgb/hsl/mix):
60- put(x, y, ch, color, bg?) one character; with bg it also paints the cell's background, so put(x, y, ' ', null, bg) paints a solid colored cell, and ▀▄ with a color and a bg give two pixels per cell
61- text(x, y, str, color, bg?) a line of text
62- sprite(x, y, art, color, bg?) multi-line art (lines split on \\n), solid inside its outline
63- fill(x, y, w, h, ch, color, bg?), line(x0, y0, x1, y1, ch, color), circle(cx, cy, r, ch, color) (r in rows; drawn twice as wide so it looks round), disc(cx, cy, r, ch, color, bg?)
64- clawd(x, y, options) draws Clawd, 14 wide and 4 rows tall at scale 1, x, y its top-left (y may end in .5 to sit half a row lower). Options: pose ("stand", "walk", "jump", "sit"), stride (0 to 3, the walk frame: use floor(x/1.5)%4 so its feet match its travel), facing (-1 left, 0 ahead, 1 right), eyes ("open", "closed", "wide"), look ({x, y} in pixels, a glance), blink (true/false), color (any color: Clawd can be painted, sunburnt, frozen blue, camouflaged), eyeColor, scale (1 to 4; 2 is 28 wide and 8 rows tall), arms ("up", "out" or "down" for both, or {left, right}: change it over time to wave, point, dig, cheer, carry; the left and right anchors follow the claw tips, so a prop drawn at c.right moves with the claw). The short form clawd(x, y, facing, stride, blink, color) still works. It returns where things attach: {x, py, row, w, h, top, left, right, feet, eyes}, each anchor {x, py, row} with py the pixel row (for pixel and pixels) and row the text row (for text, sprite and say), so a hat goes at pixels(c.top.x - 2, c.top.py - 3, ...), a prop in its hand at c.right, a shadow under c.feet, and a bubble at say(line, c.top.x, c.row). Dress Clawd for the scene: a hard hat, a crown, a snorkel, a cape, sunglasses over c.eyes, a lantern at c.left, and recolor it when the story calls for it.`
65const PIXELS = `- pixel(x, py, color) and pixels(x, py, art, palette) draw at pixel resolution: pixel rows are twice as fine as text rows (py 0 to 2h-1; pixel row py is in text row floor(py/2)), the same grid Clawd is drawn on, so pixel art sits flush with it. In pixels(), art is lines of characters and palette maps each character to a color ({"#": "#fc0", "o": "#000"}); "." and " " are transparent. Pixels and text can mix in one cell. Use this for anything that should look drawn rather than typed: props, creatures, vehicles, a whole pixel-art set.`
66const MID = `- say(text, x, y) a speech bubble pointing at x, y (Clawd's head is about x+7, y)
67- rgb(r, g, b), hsl(hue 0-360, sat 0-1, light 0-1), mix(c1, c2, k) make colors
68- clamp(v, lo, hi), lerp(a, b, k), smoothstep(a, b, v), fract(v), mod(a, b), rand(n) (a fixed random number per n), noise(x) and noise2(x, y) (smooth noise in [0,1)), and Math.random()`
69const WORLD3D = `3D: the strip can also be a window into a world. Surfaces are lit smoothly (diffuse plus a highlight) and drawn as solid cells in their lit color with block-glyph edges, clean and flat; a mesh given ascii: true is drawn instead as glyphs as dense as its brightness (" .,:;-=+*#%@"), the classic terminal look, good for a planet, a donut, a curved hero object, not for floors and walls. Give surfaces mid-to-light colors (dark ones vanish into the fog), and keep the camera and light steady or moving smoothly. camera(ex, ey, ez, tx, ty, tz, fov) sets the eye, what it looks at, and the angle it sees across (default eye 0,2.5,9 looking at 0,1,0, fov 90: at the origin that shows about 20 units across and 3 units tall, so the world is wide and low, like the strip; build things about 1 to 2 units tall and spread them left to right, and move the camera to fly, orbit or dolly). light(dx, dy, dz, ambient) is the direction toward the light; fog(near, far, color) fades things with distance (default 8 to 40 into black: that is the depth cue, keep it). Shapes: box(w, h, d) (centered), sphere(r, segments), cylinder(r, h, segments) and cone(r, h, segments) (standing on y 0), plane(w, d) (flat on the ground), or your own {verts: [x,y,z,...], faces: [[i,j,k,...], ...]}. mesh3d(mesh, {x, y, z, rx, ry, rz, scale, color, wire, unlit, ascii}) draws one, lit smoothly; wire draws its edges; unlit skips the lighting; ascii uses the glyph ramp. line3d(x0,y0,z0, x1,y1,z1, color) and point3d(x, y, z, color) for rails, rain, stars. clawd3d(x, y, z, options) puts Clawd standing at a world point as a solid lit like any mesh, its eyes flat on its face, sized by distance (options as clawd, plus size: its height in units, 0.9 by default: about as tall as a crate, a third of the strip at the camera's target; it is never drawn taller than most of the strip, and one whose feet would fall below the strip stands on its bottom edge instead), so it can walk down a road into the distance; it returns the same anchors, or null behind the camera. project(x, y, z) gives {x, y, px, py, depth, scale} for placing text, a bubble or 2D art at a world point. Everything 3D in a frame shares one depth buffer, so later draws go behind nearer ones. The camera, light and fog keep between frames. Good 3D scenes: a road or rails vanishing to a point with things passing, a planet with a moon orbiting, a city of boxes at night, a tunnel flying through, a chessboard, a spinning gear, a crane lifting a crate, an orbiting camera around one hero object. Keep it to a few dozen shapes; big flat-shaded shapes read better than detail at this size. Mix freely with 2D: a 3D set behind a 2D Clawd, or text labels placed with project().`
70const CODING = `Write the code compactly: no comments, no blank lines, short names, nothing decorative, and keep it under about 60 lines; every token of it is paid for. The code draws over the background effect and particles and under the actors. Keep each frame light: a few thousand steps is fine (a loop over every cell of the strip with a little math each is fine), heavy nested loops are not, and a frame that runs too long or throws stops the code for the rest of the scene. When the code draws everything, leave actors and particles empty and set the background's intensity to 0. When the code draws Clawd and its speech, do not also add a "clawd" actor. "" for no code.
71
72A code example, for the shape of it (do not copy the idea):
73let drops = Array.from({length: 40}, (_, i) => ({x: rand(i) * w, y: rand(i + 50) * h, v: 4 + rand(i + 99) * 6}))
74let cx = -14
75function frame(t, dt) {
76  for (let x = 0; x < w; x++) put(x, h - 1, x % 7 == 0 ? '┴' : '─', '#5a6b7c')
77  for (const d of drops) {
78    d.y += d.v * dt
79    if (d.y > h - 1) { d.y = 0; d.x = rand(t + d.x) * w }
80    put(d.x, d.y, '│', '#6fa8dc')
81  }
82  cx = Math.min(w / 2 - 7, cx + 5 * dt)
83  const walking = cx < w / 2 - 7
84  clawd(cx, h - 5, walking ? 1 : 0, walking ? Math.floor(cx / 1.25) % 4 : -1, !walking && fract(t * 0.4) < 0.05)
85  sprite(cx + 2, h - 8, ' ▄███▄ \\n▀▀▀█▀▀▀\\n   │   ', '#e06c75')
86  if (!walking) say('npm test in the rain. 3 failing, umbrella holding.', cx + 7, h - 8)
87}
88`
89const EXAMPLE3D = `A 3D example, for the shape of it (do not copy the idea): a road into the distance with crates passing and Clawd on it.
90const crate = box(1.2, 1.2, 1.2)
91const post = cylinder(0.08, 2, 6)
92const crates = Array.from({length: 6}, (_, i) => ({x: i % 2 ? 3 : -3, z: -4 - i * 6, spin: rand(i) * 3}))
93function frame(t, dt) {
94  camera(0, 2.2, 8 - t * 1.5, 0, 1, -30, 90)
95  fog(10, 45, "#000")
96  for (let i = -2; i < 12; i++) {
97    const z = -i * 6 + mod(t * 1.5, 6)
98    mesh3d(post, {x: -5, z, color: "#8a8"})
99    mesh3d(post, {x: 5, z, color: "#8a8"})
100    line3d(-5, 2, z, 5, 2, z, "#575")
101  }
102  line3d(-5, 0, 10, -5, 0, -80, "#aaa")
103  line3d(5, 0, 10, 5, 0, -80, "#aaa")
104  let nearest = null
105  for (const c of crates) {
106    c.z += dt * 2.5
107    if (c.z > 10) c.z -= 40
108    mesh3d(crate, {x: c.x, y: 0.6, z: c.z, ry: c.spin + t, color: "#e0a060"})
109    if (c.z < 6 && (!nearest || c.z > nearest.z)) nearest = c
110  }
111  // One label, on the nearest crate, placed with project().
112  const p = nearest && project(nearest.x, 1.5, nearest.z)
113  if (p) text(p.x - 3, p.y - 1, "auth.ts", "#ffd")
114  const me = clawd3d(0, 0, -1 - t * 1.5, {facing: 0, stride: Math.floor(t * 6) % 4})
115  if (me && t > 1) say("Six crates, one road, zero tests passing yet.", me.top.x, me.row)
116}`
117const TAIL = `
118Reply with one scene as JSON matching the schema:
119- concept: one short line naming the scene's world, metaphor and shot, e.g. "wild west: Clawd as sheriff nailing a wanted poster for auth.ts to a saloon wall". Decide it first.
120- code: the scene's program, or "".
121- actors: up to 20, each with kind ("clawd" or "sprite"), frames (for a sprite: 1 to 12 frames, each a string with lines separated by \\n, at most 9 lines and 80 characters wide, every frame the same size; for Clawd: []), fps (0 for a still), frame (an expression picking the frame index, wrapped to the frame count, or "" to cycle by fps), show (an expression: the actor is drawn only while it is above 0, or "" for always), x and y (expressions for the top-left cell; columns 0 to w-1, rows 0 to h-1, and anything off the strip is simply clipped), color (hex; Clawd's is "#d97757"), say ("" for nothing, else at most 60 characters), and sayAt (seconds into the scene when it starts talking).
122- particles: up to 6 swarms (rain, sparks, dust, bytes, bugs, confetti, bubbles, zzz, snow, stars, smoke, leaves, fish, birds, anything) with glyphs (characters to cycle, up to 16), count (up to 120), x and y expressions where k is the particle's index from 0 to n-1, and a color. Particles are drawn behind actors.
123- background: a faint ambient effect behind everything: effect (plasma, fire, starfield, rain, tunnel, lava, glitch, aurora, waves, pulse, fireworks), palette (3 to 5 hex colors, all visible on near-black), speed (0.3 to 2.5), intensity (0 to 1, keep it low when the set is busy).
124
125Single-width characters only: ASCII, Latin-1, box drawing, blocks, braille, arrows (←↑→↓↖↗↘↙⇐⇒), geometric shapes (■□▪▲△▼▽◆◇○●◐◑◢◣◤◥), and ★☆♥♦♣♠♪♫☺☻✓✗✦✧. No emoji.
126
127Expressions are small math formulas. Variables: t (seconds since this scene started), w and h (the strip's width and height), k and n (a particle's index and count, or an actor's index and the actor count). Operators: + - * / % ^ and parentheses. Functions: sin cos abs min max floor ceil round sqrt pow exp sign mod(a,b) fract(x) clamp(x,lo,hi) lerp(a,b,k) tan tri(x) (0 to 1 and back over each unit) rand(x) (a fixed random number in [0,1) per x) noise(x) (smooth wandering value in [0,1)) step(edge,x) between(x,lo,hi) (1 while lo <= x < hi) smoothstep(a,b,x) (eases 0 to 1 as x goes from a to b) atan2(y,x) hypot(a,b), and pi. Anything else evaluates to 0.
128
129Motions that work:
130- walk across and wrap: x = "mod(t*5, w+20) - 20"
131- pace back and forth: x = "10 + tri(t*0.05)*(w-30)"
132- walk to a spot and stop: x = "min(40, 5 + t*5)"
133- walk in from the right and stop: x = "max(w*0.6, w - t*5)"
134- follow another actor with a gap: reuse its x expression minus the gap
135- drop in and land: y = "min(4, -6 + t*10)"
136- falling particles: x = "rand(k)*w", y = "mod(t*(3+rand(k+9)*4) + rand(k+3)*h, h)"
137- orbit: x = "40 + cos(t*0.8 + k*2*pi/n)*12", y = "3 + sin(t*0.8 + k*2*pi/n)*2"
138- burst of sparks: x = "30 + cos(k*2.4)*fract(t)*14", y = "3 + sin(k*2.4)*fract(t)*3"
139- ease from one spot to another between t=1 and t=3: x = "lerp(5, 60, smoothstep(1, 3, t))"
140- jump: y = "4 - 3*sin(pi*clamp(t-2, 0, 1))"
141- appear at t=2.5 and leave at t=6: show = "between(t, 2.5, 6)"
142- explosion frames once at t=3, then hold the last: frame = "clamp(floor((t-3)*8), 0, 4)", show = "step(3, t)"
143- blink on and off: show = "step(0.5, fract(t*2))"
144- parallax pan: far hills x = "-mod(t*2, 60)", near trees x = "-mod(t*6, 60)" (draw them wider than the strip so the wrap is unseen)
145- wander like a fish or a fly: x = "20 + noise(t*0.5 + k)*40", y = "1 + noise(t*0.7 + k*3)*6"
146
147Keep it charming, take creative risks, and make every scene look different from the last.`
148
149export function systemFor(styles: readonly string[]) {
150  const allowed = [...new Set(styles)]
151  const isOne = allowed.length === 1
152  const has = (style: string) => allowed.includes(style)
153  // With one style there is nothing to deal: every scene is drawn in it.
154  const style = isOne
155    ? `Every scene is drawn as ${allowed[0]}: ${STYLE_LINES[allowed[0] ?? '']?.replace(/^"[^"]+" means /, '')}.`
156    : `- "[style: ...]" is how this scene is drawn, and it is not optional: ${allowed.map(s => STYLE_LINES[s]).join('; ')}. Each has its own look, and the developer wants to see all of them over a session.`
157
158  return [
159    HEAD,
160    style,
161    DRAWING,
162    ...(has('pixel art') || has('3D') ? [PIXELS] : []),
163    MID,
164    ...(has('3D') ? [WORLD3D] : []),
165    CODING,
166    ...(has('3D') ? [EXAMPLE3D, ''] : []),
167    TAIL,
168  ].join('\n')
169}
170
171// Settings dealt one per new scene, so a run of similar work (a dozen reads in
172// a bug hunt) still plays out across very different worlds.
173export const WORLDS = [
174  'deep sea', 'outer space', 'wild west', 'medieval castle', 'jungle safari', 'cooking show',
175  'bank heist', 'sports broadcast', 'film noir city at night', 'fairy tale forest', 'weather report',
176  'nature documentary', 'cyberpunk street', 'circus', 'haunted house', 'pirate ship', 'farm',
177  'construction site', 'orchestra concert', 'retro arcade game', 'ski slope', 'mad science lab',
178  'train station', 'volcano island', 'arctic expedition', 'desert caravan', 'theater stage',
179  'vegetable garden', 'airport runway', 'dinosaur era', 'ant colony up close', 'rush hour traffic',
180  'mountain climb', 'submarine', 'carnival midway', 'beehive', 'lighthouse in a storm', 'ancient tomb',
181  'skate park', 'fishing pond', 'mail room', 'rocket launch pad', 'museum after hours', 'bakery',
182  'bowling alley', 'snow globe', 'underground mine', 'race track pit stop', 'ufo abduction',
183  'zen rock garden',
184]
185
186// How many past concepts each new request lists.
187const RECENT = 6
188
189// The drawing styles dealt with the world, per the "styles" setting; the
190// prompt holds only these styles' drawing calls, and a style is named in
191// each request only when there is more than one to choose from.
192export const STYLE_SETS = {
193  mix: ['3D', '3D', 'pixel art', 'text art'],
194  '3D': ['3D'],
195  'pixel art': ['pixel art'],
196  'text art': ['text art'],
197  'no 3D': ['pixel art', 'text art'],
198} as const
199export type StyleSet = keyof typeof STYLE_SETS
200export const isStyleSet = (v: unknown): v is StyleSet => typeof v === 'string' && v in STYLE_SETS
201
202// The thread is cut back to KEEP messages once it reaches MOST, so a long
203// session never outgrows the context window; cutting in one go, not a little
204// each time, keeps the cached prefix good between cuts.
205const MOST = 60
206const KEEP = 30
207
208// Older scenes are kept only as their concept line: the last FULL replies
209// stay whole (a "still" beat is the next beat of the latest, and a callback
210// may reach one back), and once BATCH more have piled up behind them they are
211// collapsed together, so the cached prefix is rebuilt once per batch rather
212// than on every request. Scenes are not repeated thanks to the concept lines,
213// not the whole replies, so few need keeping.
214const FULL = 2
215const BATCH = 6
216
217// A line of the log that only says something is still going.
218const isQuiet = (line: string) => /^\+\d+s still /.test(line)
219
220export const SCHEMA = {
221  type: 'object',
222  properties: {
223    concept: { type: 'string' },
224    code: { type: 'string' },
225    actors: {
226      type: 'array',
227      items: {
228        type: 'object',
229        properties: {
230          kind: { type: 'string', enum: ['clawd', 'sprite'] },
231          frames: { type: 'array', items: { type: 'string' } },
232          fps: { type: 'number' },
233          frame: { type: 'string' },
234          show: { type: 'string' },
235          x: { type: 'string' },
236          y: { type: 'string' },
237          color: { type: 'string' },
238          say: { type: 'string' },
239          sayAt: { type: 'number' },
240        },
241        required: ['kind', 'frames', 'fps', 'frame', 'show', 'x', 'y', 'color', 'say', 'sayAt'],
242        additionalProperties: false,
243      },
244    },
245    particles: {
246      type: 'array',
247      items: {
248        type: 'object',
249        properties: {
250          glyphs: { type: 'string' },
251          count: { type: 'number' },
252          x: { type: 'string' },
253          y: { type: 'string' },
254          color: { type: 'string' },
255        },
256        required: ['glyphs', 'count', 'x', 'y', 'color'],
257        additionalProperties: false,
258      },
259    },
260    background: {
261      type: 'object',
262      properties: {
263        effect: { type: 'string', enum: [...EFFECTS] },
264        palette: { type: 'array', items: { type: 'string' } },
265        speed: { type: 'number' },
266        intensity: { type: 'number' },
267      },
268      required: ['effect', 'palette', 'speed', 'intensity'],
269      additionalProperties: false,
270    },
271  },
272  required: ['concept', 'code', 'actors', 'particles', 'background'],
273  additionalProperties: false,
274}
275
276type Block = Record<string, unknown>
277type Message = { role: 'user' | 'assistant'; content: string | Block[] }
278
279// A scene, or why there is none, and what the call spent when one was made.
280export type Narration = { script?: Script; error?: string; spent?: CallUsage }
281
282export const URL = 'https://api.anthropic.com/v1/messages'
283
284// The thread itself: the hooks send what it builds and hand back what came.
285export function createThread(model: Model, options: { isThinking?: boolean; styles?: StyleSet } = {}) {
286  const styles: readonly string[] = STYLE_SETS[options.styles ?? 'mix']
287  const system = systemFor(styles)
288  const messages: Message[] = []
289  // Off once the API refuses the server-side fallback option.
290  let canFallBack = true
291  // The concepts of the scenes so far, and the worlds not yet dealt.
292  const concepts: string[] = []
293  // The developer's latest request.
294  let task = ''
295  let deck: string[] = []
296  const deal = (random: () => number) => {
297    if (deck.length === 0) deck = [...WORLDS].sort(() => random() - 0.5)
298
299    return deck.pop() as string
300  }
301
302  // Opens the next exchange with what happened since the last scene; new work
303  // also gets the recent concepts to steer clear of and a fresh world.
304  const ask = (activityIn: string, random: () => number = Math.random) => {
305    let activity = activityIn
306    const lines = activity.split('\n').filter(line => !line.startsWith('[strip '))
307    task = lines.find(line => line.startsWith('[task] ')) ?? task
308    // A scene whose code broke has no next beat: a quiet beat after it asks
309    // for a new scene.
310    const isBroken = lines.some(line => line.startsWith("[your last scene's code"))
311    if (messages.length >= MOST) {
312      const dropped = messages.slice(0, messages.length - KEEP).filter(m => m.role === 'assistant').length
313      messages.splice(0, messages.length - KEEP)
314      collapsed = Math.max(0, collapsed - dropped)
315      stripThinking()
316    }
317    // The task the developer gave may have been cut, or the thread begun
318    // again: the first message says it again.
319    const first = messages[0]
320    if (first && typeof first.content === 'string' && task && !first.content.includes(task)) first.content = `${task}\n${first.content}`
321    else if (!first && task && !lines.includes(task)) activity = `${task}\n${activity}`
322    const recent = concepts.slice(-RECENT)
323    const steer = lines.length > 0 && !isBroken && lines.every(isQuiet)
324      ? [`[playing: ${recent[recent.length - 1] ?? 'the first scene'}; it has run its course: answer its next beat]`]
325      : [...(recent.length > 0 ? [`[recent scenes: ${recent.join(' / ')}]`] : []), `[world: ${deal(random)}]`, ...(new Set(styles).size > 1 ? [`[style: ${styles[Math.floor(random() * styles.length)]}]`] : [])]
326    messages.push({ role: 'user', content: [activity, ...steer].join('\n') })
327  }
328
329  // The request for the open exchange, for a credential of `kind`.
330  const request = (kind: 'bearer' | 'api-key') => {
331    const betas = [kind === 'bearer' ? 'oauth-2025-04-20' : '', canFallBack ? 'server-side-fallback-2026-07-01' : '']
332    const body = {
333      model,
334      // Sonnet can skip thinking, which is billed as output; Opus cannot, and
335      // Haiku does not think unless asked.
336      ...(model === 'claude-sonnet-5-5' && !options.isThinking ? { thinking: { type: 'between_tools' } } : {}),
337      max_tokens: 16000,
338      system: [
339        ...(kind === 'bearer' ? [{ type: 'text', text: IDENTITY }] : []),
340        { type: 'text', text: system, cache_control: { type: 'ephemeral' } },
341      ],
342      cache_control: { type: 'ephemeral' },
343      // Haiku 4.5 takes no effort setting.
344      output_config: {
345        ...(model === 'claude-haiku-4-5' ? {} : { effort: 'low' }),
346        format: { type: 'json_schema', schema: SCHEMA },
347      },
348      messages,
349      ...(canFallBack ? { fallbacks: 'default' } : {}),
350    }
351
352    return {
353      headers: {
354        'content-type': 'application/json',
355        'anthropic-version': '2023-06-01',
356        'anthropic-beta': betas.filter(Boolean).join(','),
357      },
358      body: JSON.stringify(body),
359    }
360  }
361
362  // True when the API refused the fallback option: send the request again.
363  const isFallbackRefused = (status: number, text: string) => {
364    if (status !== 400 || !canFallBack || !text.includes('fallback')) return false
365    canFallBack = false
366
367    return true
368  }
369
370  // How many replies from the start have been collapsed to their concept.
371  let collapsed = 0
372
373  // A thinking block is bound to the history it was made in: once that is
374  // edited (a cut, a collapse), replaying one is refused, so every edit
375  // drops them all. The model reads nothing from past thinking anyway.
376  const isThought = (block: Block) => block.type === 'thinking' || block.type === 'redacted_thinking'
377  const stripThinking = () => {
378    for (const m of messages) {
379      if (m.role === 'assistant' && Array.isArray(m.content) && m.content.some(isThought)) m.content = m.content.filter(block => !isThought(block))
380    }
381  }
382
383  // Collapses the oldest whole replies to their concept lines, a batch at a
384  // time.
385  const collapse = () => {
386    const replies = messages.filter(m => m.role === 'assistant')
387    const whole = replies.length - collapsed
388    if (whole < FULL + BATCH) return
389    const batch = replies.slice(collapsed, replies.length - FULL)
390    for (const m of batch) {
391      const concept = conceptOf(m)
392      m.content = [{ type: 'text', text: JSON.stringify({ concept, note: 'an earlier scene, kept only as its concept' }) }]
393    }
394    collapsed += batch.length
395    stripThinking()
396  }
397
398  // Begins the conversation again, after a request the API would not take
399  // however it is retried: the next exchange says the task again.
400  const reset = () => {
401    messages.length = 0
402    collapsed = 0
403  }
404
405  // A reply's concept line, from its JSON.
406  const conceptOf = (m: Message) => {
407    if (typeof m.content === 'string') return ''
408    const text = m.content.find(block => block.type === 'text')?.text
409    try {
410      const raw = JSON.parse(typeof text === 'string' ? text : '') as { concept?: unknown }
411
412      return typeof raw.concept === 'string' ? raw.concept.slice(0, 140) : ''
413    } catch {
414      return ''
415    }
416  }
417
418  // Closes the exchange unanswered: its activity goes with the next one.
419  const abandon = () => {
420    const open = messages.pop()
421
422    const content = typeof open?.content === 'string' ? open.content : ''
423
424    return content.split('\n').filter(line => !/^\[(recent scenes|world|style|playing): /.test(line)).join('\n')
425  }
426
427  // Closes the exchange with the reply, kept whole, and reads its scene.
428  const accept = (text: string): Narration => {
429    let reply: { content: Block[]; stop_reason: string; usage?: Record<string, number> }
430    try {
431      reply = JSON.parse(text) as typeof reply
432      if (!Array.isArray(reply.content)) throw new Error('no content')
433    } catch {
434      abandon()
435
436      return { error: 'the API answered something unreadable' }
437    }
438    messages.push({ role: 'assistant', content: reply.content })
439    collapse()
440    const spent: CallUsage = {
441      input: reply.usage?.input_tokens ?? 0,
442      output: reply.usage?.output_tokens ?? 0,
443      cacheRead: reply.usage?.cache_read_input_tokens ?? 0,
444      cacheWrite: reply.usage?.cache_creation_input_tokens ?? 0,
445    }
446    if (reply.stop_reason === 'refusal') return { error: 'the buddy declined to draw that one', spent }
447    const answer = reply.content.find(block => block.type === 'text')?.text
448    try {
449      const raw = JSON.parse(typeof answer === 'string' ? answer : '') as { concept?: unknown }
450      if (typeof raw?.concept === 'string' && raw.concept.trim()) concepts.push(raw.concept.trim().slice(0, 140))
451      const script = cleanScript(raw)
452
453      return script ? { script, spent } : { error: 'the buddy answered a scene it cannot draw', spent }
454    } catch {
455      return { error: 'the buddy answered something other than a scene', spent }
456    }
457  }
458
459  return { ask, request, isFallbackRefused, abandon, accept, reset, isThinking: Boolean(options.isThinking), styles: options.styles ?? 'mix' }
460}
461
hooks/scenes.ts 148 lines
1// The stock of ready-made scenes, written by scripts/library.ts. Do not edit
2// by hand: run the script to add scenes.
3
4import type { Phase } from './library'
5
6export type Stock = { phase: Phase; world: string; style: string; concept: string; source: string; scene: unknown }
7
8export const SCENES: Stock[] = [
9  {"phase":"testing","world":"pirate ship","style":"pixel art","concept":"rush hour traffic: Clawd as a traffic cop at a toll gate waving test-cars through, green ones pass, red ones get a ticket","source":"claude-opus-5-5","scene":{"concept":"rush hour traffic: Clawd as a traffic cop at a toll gate waving test-cars through, green ones pass, red ones get a ticket","code":"let cars=[],P=0,F=0,nx=0;const G=66;\nconst art=\"..######..\\n.#ww#ww#..\\n##########\\n.oo....oo.\";\nfunction frame(t,dt){for(let x=0;x<w;x++){const b=3+Math.floor(noise(x*0.3)*6);for(let p=b;p<10;p++)pixel(x,p,(x+p)%7==0&&p>b+1?'#ffd36b':'#2a3550');}\nfill(0,5,w,1,'▀','#777','#333');fill(0,6,w,3,' ',null,'#333');for(let x=0;x<w;x+=6)put(x-Math.floor(t*0)%6,8,'═','#ccc','#333');\nif(t>=nx){cars.push({x:-12,f:rand(cars.length*7.3)<0.25,s:0,c:hsl(rand(cars.length)*360,0.7,0.6)});nx=t+1.6+rand(t)*1.5;}\nlet near=null;for(let i=0;i<cars.length;i++){const c=cars[i];let lim=i?cars[i-1].x-13:999;if(c.s<1&&c.x+10>=G-1&&c.x<G){c.s+=dt;if(c.s>=1){c.f?F++:P++;}near=c;}else{c.x=Math.min(c.x+14*dt,lim);}\nif(c.x+10>=G-3&&c.x<G)near=c;pixels(Math.floor(c.x),12,art,{'#':c.c,w:'#bfe8ff',o:'#111'});if(c.s>=1&&c.f&&c.x<G+40)text(Math.floor(c.x)+3,5,'✗',\"#ff5555\");if(c.s>=1&&!c.f&&c.x<G+30)text(Math.floor(c.x)+3,5,'✓','#5f5');}\ncars=cars.filter(c=>c.x<w+5);const open=near&&near.s>=1;\nfill(G+1,2,6,4,' ',null,'#556');text(G+1,2,'TOLL',\"#fff\",'#556');put(G+3,3,'●',near&&near.s<1?'#fc3':open?(near.f?'#f44':'#4f4'):'#444','#556');\nif(open)line(G,1,G,5,'│','#fff');else text(G-8,5,'▬▬▬▬▬▬▬▬','#f55');\nconst cc=clawd(G+10,1,{facing:-1,arms:open?{left:'up',right:'down'}:'out',blink:fract(t*0.3)<0.05});pixels(cc.top.x-3,cc.top.py-2,'..###..\\n#######',{'#':'#3366ff'});\ntext(G+27,1,'{what}','#ffd36b');text(G+27,2,'✓ '+P,'#5f5');text(G+27,3,'✗ '+F,'#f66');\nconst lines=['Rush hour on {what}. Keep it moving!','Green light, you pass. Next!','That one failed. Pull over, buddy.','Bumper to bumper assertions.'];const k=Math.floor(t/8)%4;if(fract(t/8)<0.55)say(lines[k],cc.top.x,cc.row);}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#1a2340","#2a3550","#3a4560"],"speed":0.5,"intensity":0}}},
10  {"phase":"searching","world":"bank heist","style":"3D","concept":"deep sea: Clawd in a lit submarine sweeping sonar across a sunken vault field, hunting the one chest labeled {what}","source":"claude-opus-5-5","scene":{"concept":"deep sea: Clawd in a lit submarine sweeping sonar across a sunken vault field, hunting the one chest labeled {what}","code":"const rock=box(1.6,0.8,1.4),chest=box(1,0.7,0.7),sub=sphere(0.7,10),fin=cone(0.3,0.6,6);const ch=Array.from({length:9},(_,i)=>({x:-14+i*3.5,z:-3-rand(i)*6}));const hit=6;function frame(t,dt){const L=20;const ph=mod(t,L);camera(0,3,9,0,0.8,-4,90);fog(6,30,'#012');light(0.3,1,0.4,0.35);mesh3d(plane(60,40),{y:0,z:-8,color:'#3a6a7a'});for(let i=0;i<9;i++){const c=ch[i];mesh3d(rock,{x:c.x+1.2,y:0.4,z:c.z-1.5,ry:i,color:'#6f8890'});const f=i==hit&&ph>12;mesh3d(chest,{x:c.x,y:0.35,z:c.z,color:f?'#ffd060':'#a07040'});}const sx=lerp(-14,ch[hit].x,smoothstep(0,12,ph))+Math.sin(t*0.7)*0.3;const sy=2.6+Math.sin(t*1.3)*0.15;mesh3d(sub,{x:sx,y:sy,z:-2,scale:1,color:'#e8c050'});mesh3d(fin,{x:sx-0.9,y:sy-0.3,z:-2,rz:1.57,color:'#d0a040'});const p=project(sx,sy,-2);if(p){const r=fract(t*0.6)*30;for(let a=0;a<40;a++){const an=a/40*6.28;const x=p.x+Math.cos(an)*r,y=p.y+Math.sin(an)*r*0.25+3;put(x,y,'·','#5ff0d0');}clawd(p.x-4,p.y-1.5,{scale:1,facing:1,eyes:ph>12?'wide':'open',color:'#d97757'});if(ph>12){const q=project(ch[hit].x,1,ch[hit].z);if(q){text(q.x-4,q.y-1,'{what}','#ffe9a0');}say('Found {what}! Down here the whole time.',p.x+3,p.row||p.y-2);}else if(ph>3&&ph<9)say('Pinging every crate for {what}...',p.x+3,p.y-2);}for(let i=0;i<14;i++){const by=mod(-t*1.5-rand(i)*9,10);put(rand(i+7)*w,by,'o','#8fd8ff');}}","actors":[],"particles":[],"background":{"effect":"waves","palette":["#023","#035","#047"],"speed":0.5,"intensity":0}}},
11  {"phase":"reading","world":"deep sea","style":"3D","concept":"theater stage in the deep sea: Clawd as a diver-actor reads {what} aloud under a spotlight on a sunken stage while fish audience drifts and ","source":"claude-opus-5-5","scene":{"concept":"theater stage in the deep sea: Clawd as a diver-actor reads {what} aloud under a spotlight on a sunken stage while fish audience drifts and the curtain rises","code":"const flr=box(16,0.3,5),cur=box(8,3.2,0.2),col=cylinder(0.25,3,8),fish=sphere(0.25,6),scr=box(2.4,1.4,0.1);\nconst F=Array.from({length:7},(_,i)=>({x:-9+i*3,z:4+rand(i)*2,p:rand(i+5)*6}));\nconst B=Array.from({length:30},(_,i)=>({x:rand(i)*24-12,y:rand(i+40)*4,z:rand(i+80)*-6}));\nfunction frame(t,dt){\nconst a=t*0.08;camera(Math.sin(a)*3,2.4,9,0,1,0,80);light(0.3,1,0.6,0.35);fog(8,30,'#021826');\nmesh3d(flr,{y:-0.15,color:'#b98a5a'});\nconst o=smoothstep(0,4,t)*5;\nmesh3d(cur,{x:-4-o,y:0.2,z:-1.5,color:'#c0405a'});mesh3d(cur,{x:4+o,y:0.2,z:-1.5,color:'#c0405a'});\nmesh3d(col,{x:-7.5,z:-2,color:'#9ab'});mesh3d(col,{x:7.5,z:-2,color:'#9ab'});\nmesh3d(scr,{x:0,y:2,z:-2.2,rx:0.05,color:'#e8e0c0'});\nconst sp=project(0,2.3,-2.1);if(sp)text(sp.x-Math.floor('{what}'.length/2),sp.y,'{what}','#203040');\nconst ph=Math.floor(t/8)%4;const cx=Math.sin(t*0.4)*2.5;\nconst c=clawd3d(cx,0,0.5,{facing:Math.cos(t*0.4)>0?1:-1,stride:Math.floor(t*4)%4,arms:ph==1?'up':ph==2?{left:'down',right:'out'}:'out'});\nfor(const f of F){const y=0.6+Math.sin(t+f.p)*0.2;mesh3d(fish,{x:f.x+Math.sin(t*0.3+f.p)*0.5,y,z:f.z,color:hsl(30+f.p*40,0.7,0.6)});}\nfor(const b of B){b.y+=dt*0.5;if(b.y>4)b.y=0;point3d(b.x+Math.sin(t+b.y)*0.2,b.y,b.z,'#8fd8ff');}\nif(c&&t>4){const L=['Act I: {what}, line by line.','Plot twist: it imports half the ocean.','Aha! So that is who calls this function.','Encore! Reading {what} once more.'];say(L[ph],c.top.x,c.row);}\n}","actors":[],"particles":[],"background":{"effect":"waves","palette":["#03304a","#064a6a","#0a6a8a"],"speed":0.4,"intensity":0}}},
12  {"phase":"thinking","world":"sports broadcast","style":"text art","concept":"race track pit stop: Clawd as crew chief in a headset, mulling strategy while the {what} car gets tires swapped and rivals scream past","source":"claude-opus-5-5","scene":{"concept":"race track pit stop: Clawd as crew chief in a headset, mulling strategy while the {what} car gets tires swapped and rivals scream past","code":"const L=['Box box box! New tires on {what}, then we decide.','Undercut or overcut? Thinking it through...','Fresh rubber. Plan for {what} almost locked in.','Go go go! I have a strategy now. Mostly.'];\nfunction car(x,y,c,lab){text(x,y,'   ▄▄███▄▄▄▄▄   ',c);text(x,y+1,'▐█▀▀▀▀▀▀▀▀▀▀▀██▶',c);if(lab)text(x+3,y+1,lab.slice(0,10),'#fff',c)}\nfunction frame(t,dt){for(let x=0;x<w;x++){put(x,0,'·▪•'[(x*7+Math.floor(t*3))%3],hsl((x*37)%360,.6,.6));put(x,3,x%4<2?'▀':'▀',x%8<4?'#e33':'#eee');put(x,8,'═','#777')}\nfor(let i=0;i<3;i++){const cx=mod(t*60+i*55,w+60)-30;text(cx,1,'▄▄██▄▄▶',['#4af','#fd3','#5e5'][i]);text(cx+1,2,'○   ○','#aaa')}\nconst p=mod(t,12),cy=Math.floor(t/12);let x=p<2?lerp(-30,40,smoothstep(0,2,p)):p<8?40:lerp(40,w+40,smoothstep(8,10,p));\ntext(38,4,'┌─PIT─┐','#888');car(x,5,'#d97757','{what}');\nconst st=p>=2&&p<8;const wo=st&&p>3&&p<6;text(x+1,7,wo?'     ':'(◎)','#ccc');text(x+12,7,wo?'   ':'(◎)','#ccc');\nif(st){const k=Math.floor(t*6)%2;text(x-4,6,k?'╪':'┼','#fd3');text(x+18,6,k?'╪':'┼','#fd3');if(wo){text(x-9,7,'(◎)','#666');text(x+20,7,'(◎)','#666')}}\nconst cl=clawd(78,4.5,{facing:-1,arms:st?{left:'out',right:'down'}:'up',blink:fract(t*.3)<.05});\ntext(cl.top.x-3,cl.top.row-1>=0?cl.top.row:4,'','#fff');put(cl.left.x-1,cl.left.row,'▮','#ccc');\nsprite(96,4,'┌──────────┐\\n│ LAP '+String(12+cy).padStart(2,'0')+'   │\\n│ P'+(3-Math.min(2,cy))+'  +'+(p<8?'0.4':'0.1')+'s │\\n└────┬─────┘','#5e5');text(101,8,'┴','#5e5');\nif(p>1.5&&p<11)say(L[cy%4],cl.top.x,cl.row)}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#222","#333","#2a2a2a"],"speed":0.5,"intensity":0}}},
13  {"phase":"editing","world":"bowling alley","style":"3D","concept":"weather report: Clawd the TV meteorologist points at a 3D forecast map as edits blow the storm away from {what}","source":"claude-opus-5-5","scene":{"concept":"weather report: Clawd the TV meteorologist points at a 3D forecast map as edits blow the storm away from {what}","code":"const gr=plane(34,12),cl=sphere(1,8),sn=sphere(1,10),bx=box(1,1,1);const cs=[[-6,1.4],[-3,0.8],[0,1.8],[3,1],[6,1.3]];\nfunction frame(t,dt){camera(2,4,9,2,0,-2,80);light(0.3,1,0.6,0.35);fog(8,40,'#000');const k=fract(t/24),s=1-smoothstep(0.35,0.75,k);mesh3d(gr,{z:-2,color:'#5a9a6a'});cs.forEach((c,i)=>mesh3d(bx,{x:c[0]+2,y:c[1]/2,z:-2,scale:1,color:i==2?'#e8c070':'#9ab0c8',ry:0.3}));const cx=2+Math.sin(t*0.6)*1.2+(1-s)*9;if(s>0.05){for(let j=0;j<4;j++)mesh3d(cl,{x:cx+j*0.9-1.3,y:3+Math.sin(j*2)*0.2,z:-2,scale:s*(0.8+j%2*0.3),color:'#b8bcc8'});for(let i=0;i<12;i++){const rx=cx-1.6+i*0.3,ry=mod(3-t*4-i*0.7,3);line3d(rx,ry,-2,rx,ry-0.4,-2,'#7fb8ff')}}mesh3d(sn,{x:9,y:lerp(-1.5,3.5,1-s),z:-5,scale:1.1,color:'#ffd84a',unlit:true});const p=project(2,2.3,-2);if(p)text(p.x-5,p.y,'{what}','#fff');text(w-22,0,s>0.5?'STORM WARNING':'CLEAR SKIES',s>0.5?'#ff6b6b':'#ffd84a');const temp=Math.round(lerp(12,27,1-s));text(w-22,1,temp+'° and editing','#cde');const c=clawd(3,4.5,{facing:1,arms:{left:'down',right:'out'},blink:fract(t*0.3)<0.04});line(c.right.x,c.right.row,c.right.x+8,c.right.row-2,'-','#ddd');pixels(c.top.x-3,c.top.py-2,'.######.\\n########','#'.length?{'#':'#335'}:{});if(k<0.3)say('Heavy bugs over {what}. Patching now.',c.top.x,c.row);else if(k>0.8)say('Forecast for {what}: sunny, zero errors.',c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#203050","#304870","#405890"],"speed":0.4,"intensity":0}}},
14  {"phase":"running","world":"vegetable garden","style":"3D","concept":"cyberpunk rooftop garden: Clawd waters neon vegetables in a 3D street of glowing towers while {what} runs, a carrot rising slowly like a pro","source":"claude-opus-5-5","scene":{"concept":"cyberpunk rooftop garden: Clawd waters neon vegetables in a 3D street of glowing towers while {what} runs, a carrot rising slowly like a progress bar","code":"const bx=box(1,1,1),cy=cylinder(0.25,1,8),sp=sphere(0.5,8),cn=cone(0.3,1,8);\nconst tw=Array.from({length:14},(_,i)=>({x:(i%2?1:-1)*(7+rand(i)*4),z:-4-i*4,h:3+rand(i+9)*6,c:i%3?'#7a6cff':'#40d0e0'}));\nfunction frame(t,dt){camera(Math.sin(t*0.1)*2,2.4,8,0,1,-6,90);light(0.4,1,0.6,0.35);fog(8,45,'#05010a');\nfor(const b of tw)mesh3d(bx,{x:b.x,y:b.h/2,z:b.z,scale:1,color:b.c,rx:0,ry:0,rz:0});\nfor(const b of tw){const p=project(b.x,b.h,b.z);if(p&&b.z>-20)text(p.x-1,p.y,'▓▓',b.c);}\nfor(let i=0;i<6;i++){mesh3d(box(1.6,0.4,1),{x:-5+i*2,y:0.2,z:0,color:'#8a5a3a'});}\nconst g=Math.min(1,t/60);\nfor(let i=0;i<6;i++){const s=0.3+Math.min(1,g*1.4+rand(i)*0.3)*0.9;const x=-5+i*2;\nif(i%2)mesh3d(sp,{x,y:0.4+s*0.4,z:0,scale:s,color:i%4==1?'#ff4fa0':'#c060ff'});else mesh3d(cn,{x,y:0.4,z:0,scale:s,color:'#ff9a30'});\nmesh3d(cn,{x,y:0.4+s,z:0,scale:s*0.5,color:'#50ff90'});}\nconst cx=-6+((t*0.8)%13);const c=clawd3d(cx,0,1.5,{facing:1,stride:Math.floor(t*6)%4,arms:{left:'down',right:'out'}});\nif(c){for(let k=0;k<4;k++){const f=fract(t*2+k/4);put(c.right.x+1+k,c.right.row+Math.floor(f*3),'.','#60e0ff');}\nconst lab=project(0,1.8,0);if(lab)text(lab.x-4,lab.y,'{what}','#ff4fa0');\nconst m=Math.floor(t/12)%4;const L=['Watering {what}. Code grows slower than kale.','Neon radishes ripening. {what} still chugging.','Pro tip: compiles taste best organic.','Almost harvest. Carrots ahead of {what}.'];\nif(fract(t/12)<0.7)say(L[m],c.top.x,c.row);}\nfor(let i=0;i<20;i++){const x=(rand(i)*w+t*(4+rand(i+5)*6))%w;put(x,Math.floor(rand(i+3)*3),'-','#ff4fa0');}}","actors":[],"particles":[],"background":{"effect":"rain","palette":["#3040a0","#6050c0","#40a0d0"],"speed":1,"intensity":0.3}}},
15  {"phase":"building","world":"train station","style":"pixel art","concept":"snow globe: a tiny train station inside a snow globe where a freight train hauls {what} round and round; Clawd shakes the globe whenever the","source":"claude-opus-5-5","scene":{"concept":"snow globe: a tiny train station inside a snow globe where a freight train hauls {what} round and round; Clawd shakes the globe whenever the build drags","code":"const cx=Math.floor(w/2),rx=34,ry=8;let sn=Array.from({length:70},(_,i)=>({x:cx-30+rand(i)*60,y:rand(i+9)*12,v:3}));const ins=(x,y)=>{const a=(x-cx)/rx,b=(y-8)/ry;return a*a+b*b<0.92};const lines=['Building {what}... one lap at a time.','Shake it! Compilers love a blizzard.','Still loading cargo. The snow is not helping.','Next stop: a finished {what}. Probably.'];function frame(t,dt){const T=mod(t,12),sh=T<1.2,ox=sh?Math.round(Math.sin(t*35)*1.5):0;for(let x=cx-28;x<cx+28;x++){pixel(x+ox,13,'#eef');pixel(x+ox,14,'#cde');}for(let x=cx-27;x<cx+27;x+=2)pixel(x+ox,12,'#888');pixels(cx+8+ox,4,' rrrrrrrr \nrrrrrrrrrr\n wwwwwwww \n wdwwwwdw \n wdwwbbdw \n wwwwbbww \n wwwwbbww ',{r:'#c33',w:'#ec9',d:'#69c',b:'#753'});text(cx+9+ox,2,'{what}'.slice(0,8),'#fe8');const tx=cx-30+mod(t*7,60);const tr=' kk         \nyyyy gggg gg\nyyyy gggg gg\n o o  o o  o';for(let i=0;i<tr.length;i++){}pixels(Math.round(tx)+ox,7,' s          \n s          \nyyyyy gggg gggg\nyyyyy gggg gggg\n o o   o o  o o',{y:'#fc3',g:'#4b7',o:'#444',s:'#bbb'});for(const s of sn){if(sh)s.v=-8-rand(s.x)*14;s.v=lerp(s.v,3,dt*1.2);s.y+=s.v*dt;s.x+=Math.sin(t+s.y)*dt*2;if(s.y>12){s.y=1;s.x=cx-30+Math.random()*60}if(s.y<1)s.y=1;if(ins(s.x,s.y))pixel(Math.round(s.x)+ox,Math.round(s.y),'#fff')}for(let a=0;a<6.28;a+=0.02){const y=8+Math.sin(a)*ry;if(y<15.5)pixel(Math.round(cx+ox+Math.cos(a)*rx),Math.round(y),'#8bf')}fill(cx-26+ox,8,52,1,'▄','#a63','#000');text(cx-4+ox,8,'{what}'.slice(0,8),'#fd9','#a63');const c=clawd(cx-rx-17,4.5,{facing:1,arms:sh?'up':{left:'down',right:'out'},blink:fract(t*0.3)<0.04,eyes:sh?'wide':'open'});pixels(c.top.x-3,c.top.py-3,' rrrr \nwwwwww',{r:'#c33',w:'#fff'});if(T>2&&T<9)say(lines[Math.floor(t/12)%4],c.top.x,c.row);}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#335","#557","#88a"],"speed":0.4,"intensity":0.2}}},
16  {"phase":"git","world":"desert caravan","style":"3D","concept":"museum after hours: Clawd as night guard patrolling a 3D gallery of commit exhibits on pedestals, the newest exhibit {what} rising into plac","source":"claude-opus-5-5","scene":{"concept":"museum after hours: Clawd as night guard patrolling a 3D gallery of commit exhibits on pedestals, the newest exhibit {what} rising into place","code":"const ped=box(1.2,1.2,1.2),gem=sphere(0.45,10),fl=plane(80,14),col=cylinder(0.25,3.5,8);\nconst hs=['a1f3c9','7be204','c0ffee','3d9a1b','e45f70','91bd2c','f00d42'];\nfunction frame(t,dt){const N=hs.length,end=N*4;const cx=Math.min(t*0.45,end-1);\ncamera(cx,2.4,7,cx+1,1,-2,80);fog(6,30,'#000');light(0.3,1,0.6,0.35);\nmesh3d(fl,{x:20,y:0,z:-3,color:'#8a7a66'});\nfor(let i=0;i<=N;i++){const x=i*4;mesh3d(col,{x:x+2,z:-5,color:'#d8d0c0'});\nconst last=i==N;const r=last?smoothstep(end-6,end-2,cx):1;const y=last?-1.2+r*1.2:0;\nmesh3d(ped,{x,y:y+0.6,z:-2,color:last?'#e8c070':'#c8c8d0'});\nmesh3d(gem,{x,y:y+1.7,z:-2,ry:t,color:last?'#ffd060':mix('#80c0ff','#c090ff',i/N),ascii:last});\nconst p=project(x,y+2.5,-2);const lb=last?'{what}':hs[i];if(p&&r>0.5)text(p.x-lb.length/2,p.y,lb,last?'#ffe080':'#ccd');}\nconst wk=cx<end-1;const me=clawd3d(cx+1,0,0.5,{facing:wk?1:0,stride:wk?Math.floor(t*5)%4:0,arms:wk?'down':{left:'down',right:'up'},color:'#d97757'});\nif(me){for(let k=0;k<6;k++)put(me.right.x+1+k,me.right.row-1+Math.floor(k/3),'░',mix('#fff6a0','#554',k/6));\nconst q=fract(t/12);let s='';if(q>0.1&&q<0.35)s='Night shift: guarding every commit since the first.';\nif(!wk)s='New exhibit: {what}. Nobody touch the history.';if(s)say(s,me.top.x,me.row);}}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#778"],"speed":0.3,"intensity":0}}},
17  {"phase":"agents","world":"haunted house","style":"3D","concept":"rocket launch pad: Clawd as flight director at a row of four pads, sending subagent rockets up one after another, each refilled and launched","source":"claude-opus-5-5","scene":{"concept":"rocket launch pad: Clawd as flight director at a row of four pads, sending subagent rockets up one after another, each refilled and launched again","code":"const bd=cylinder(0.3,1.6,10),ns=cone(0.3,0.6,10),pd=box(1.8,0.2,1.8),tw=box(0.25,3,0.25),gr=plane(40,24);\nconst cols=['#e8e8f0','#9fd0ff','#ffd27a','#b8f0a0'],L=['Four agents fueled. Liftoff on {what} in 3...','Agent two is up. Nobody touch the big red button.','They report back when they land. I wait. I sip.','Another one cleared the tower. Parallel is nice.'];\nfunction frame(t,dt){camera(0,2.6,9,0,1.6,0,90);light(0.4,1,0.6,0.35);fog(10,40,'#000');\nmesh3d(gr,{y:0,z:-2,color:'#556070'});let up=false;\nfor(let i=0;i<4;i++){const x=-6+i*4,z=-2,ph=mod(t+i*3,12);\nmesh3d(pd,{x,y:0.1,z,color:'#8a8f99'});mesh3d(tw,{x:x+1.1,y:1.5,z,color:'#c06040'});\nconst y=ph<4?0.2:0.2+Math.pow(ph-4,2)*0.6;if(ph>=4&&ph<6)up=true;\nif(y<25){mesh3d(bd,{x,y,z,color:cols[i]});mesh3d(ns,{x,y:y+1.6,z,color:'#e05050'});\nif(ph>3.4)for(let k=0;k<6;k++)point3d(x+(Math.random()-0.5)*0.4,y-0.2-Math.random()*1.2,z,k%2?'#ffb030':'#ff5020');}\nconst p=project(x,3.2,z);if(p&&ph<4)text(p.x-1,p.y,'T-'+Math.ceil(4-ph),'#ffe070');\nif(p&&i==0)text(p.x-4,p.y+3,'{what}','#ffffff');}\nconst c=clawd3d(0,0,3,{facing:0,arms:up?'up':'down',blink:fract(t*0.3)<0.04});\nif(c)say(L[Math.floor(t/7)%4],c.top.x,c.row);}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#ffffff","#9fb8ff","#ffe0a0"],"speed":0.4,"intensity":0.3}}},
18  {"phase":"writing","world":"fairy tale forest","style":"3D","concept":"orchestra concert: Clawd conducts a 3D orchestra through the grand finale of the reply, {what} on the marquee","source":"claude-opus-5-5","scene":{"concept":"orchestra concert: Clawd conducts a 3D orchestra through the grand finale of the reply, {what} on the marquee","code":"const fl=plane(26,12),bd=box(28,5,.3),body=cylinder(.32,.9,8),head=sphere(.24,8),horn=cone(.18,.6,8),st=box(.5,.05,.4);\nconst P=Array.from({length:11},(_,i)=>{const a=(i/10-.5)*2.4;return{x:Math.sin(a)*7,z:-3-Math.cos(a)*3,c:hsl(i*33,.5,.65)}});\nconst L=['Final movement: {what}. Strings, gently...','Brass! Give me the summary in fortissimo!','Woodwinds, softer, it is only a code block.','And now... the closing paragraph. Hold it...','Bravo! Encore? No? Fine, I will keep typing.'];\nfunction frame(t,dt){camera(Math.sin(t*.12)*3,2.4,8.5,0,1,-3,80);light(.3,1,.7,.35);fog(8,30,'#000');\nmesh3d(bd,{y:2.5,z:-8,color:'#a03a4a'});mesh3d(fl,{z:-3,color:'#b07a40'});\nconst beat=Math.abs(Math.sin(t*3.2));\nP.forEach((p,i)=>{const b=Math.max(0,Math.sin(t*3.2+i*.6))*.12;mesh3d(body,{x:p.x,z:p.z,color:p.c});mesh3d(head,{x:p.x,y:1.15+b,z:p.z,color:'#f0d0b0'});mesh3d(st,{x:p.x,y:.9,z:p.z+.6,color:'#ddd'});if(i%3==1)mesh3d(horn,{x:p.x+.3,y:1,z:p.z+.3,rx:1.4+b,color:'#ffd060'});});\nconst m=project(0,4.2,-7.8);if(m)text(Math.round(m.x-6),Math.max(0,m.y),'♪ '+'{what}'+' ♪','#ffe8a0');\nconst up=fract(t*1.6)<.5;const c=clawd3d(0,0,2.5,{facing:0,arms:up?{left:'out',right:'up'}:{left:'up',right:'out'},size:1});\nif(c){text(c.right.x+1,c.right.row-1,up?'/':'─','#fff');}\nfor(let k=0;k<8;k++){const q=fract(t*.3+k/8);put(fract(k*.37+t*.02)*w,h-1-q*h,'♪♫'[k%2],hsl(40+k*30,.7,.7));}\nif(c&&fract(t/7)<.75)say(L[Math.floor(t/7)%L.length],c.top.x,c.row);}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#3a1020","#602030","#804050"],"speed":0.6,"intensity":0}}},
19  {"phase":"web","world":"ufo abduction","style":"3D","concept":"ufo abduction: Clawd pilots a saucer over a moonlit farm, tractor-beaming up a crate labeled {what} again and again","source":"claude-opus-5-5","scene":{"concept":"ufo abduction: Clawd pilots a saucer over a moonlit farm, tractor-beaming up a crate labeled {what} again and again","code":"const disk=sphere(1,12),dome=sphere(0.6,10),cr=box(0.8,0.8,0.8),barn=box(2,1.6,1.6),roof=cone(1.4,0.8,4),fence=box(0.1,0.5,0.1);\nconst st=Array.from({length:40},(_,i)=>[rand(i)*40-20,2+rand(i+9)*5,-20]);\nconst lines=['Beaming up {what}. Hold still, little crate.','Got it! The internet never sees us coming.','Another one for the mothership archive.','Tractor beam: the original download button.'];\nfunction frame(t,dt){camera(0,2,14,0,2,0,90);light(0.4,1,0.6,0.35);fog(10,40,'#000');\nfor(const s of st)point3d(s[0],s[1],s[2],'#ccd');\nmesh3d(plane(60,30),{y:0,color:'#4a7a44'});\nmesh3d(barn,{x:-8,y:0.8,z:-3,color:'#c05050'});mesh3d(roof,{x:-8,y:1.6,z:-3,ry:0.78,color:'#8a3a3a'});\nfor(let i=0;i<8;i++)mesh3d(fence,{x:4+i*1.2,y:0.25,z:-2,color:'#b9a27a'});\nconst c=Math.floor(t/10),p=t%10;\nconst ux=p<3?lerp(-16,0,smoothstep(0,3,p)):p<7?0:lerp(0,16,smoothstep(7,10,p)),uy=3.7+Math.sin(t*2)*0.1;\nconst k=smoothstep(3.5,6.5,p),cy=0.4+k*2.6;\nif(p>3&&p<7){for(let a=0;a<8;a++){const an=a/8*6.28+t;line3d(ux,uy-0.2,0,Math.cos(an)*1.2,0,Math.sin(an)*1.2,'#9fffb0')}}\nif(p<6.6){mesh3d(cr,{x:0,y:cy,z:0,ry:k*4,rx:k*2,color:'#e0a060'});const q=project(0,cy+0.6,0);if(q&&p<3.5)text(q.x-5,q.y-1,'{what}','#ffd')}\nmesh3d(disk,{x:ux,y:uy,z:0,scale:1,color:'#b8c4d0',sx:1});\nmesh3d(disk,{x:ux,y:uy,z:0,ry:t,scale:1,color:'#b8c4d0'});\nconst me=clawd3d(ux,uy+0.5,0.3,{size:0.6,facing:0,arms:p>3&&p<7?'up':'down'});\nmesh3d(dome,{x:ux,y:uy+0.6,z:0,wire:true,color:'#8fe8ff'});\nfor(let a=0;a<6;a++){const an=a/6*6.28+t*3;point3d(ux+Math.cos(an)*1.6,uy-0.1,Math.sin(an)*0.6,a%2?'#ffe066':'#ff6b9a')}\nif(me&&p>1.5&&p<8.5)say(lines[c%4],me.top.x,me.row)}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#668","#aac"],"speed":0.4,"intensity":0}}},
20  {"phase":"testing","world":"mountain climb","style":"3D","concept":"fishing pond 3D: Clawd on a dock waiting for bites while {what} runs, each passing test a leaping fish, the odd failure an old boot","source":"claude-opus-5-5","scene":{"concept":"fishing pond 3D: Clawd on a dock waiting for bites while {what} runs, each passing test a leaping fish, the odd failure an old boot","code":"const pl=plane(40,30),dk=box(4,0.3,2.5),tr=cone(1.2,3,8),tk=cylinder(0.2,1,6),bb=sphere(0.18,8),fs=sphere(0.35,8),bt=box(0.5,0.4,0.3),bk=cylinder(0.4,0.5,8),sg=box(1.8,0.8,0.1),ps=cylinder(0.06,1.5,5);\nfunction frame(t,dt){camera(0,2.6,9,0,0.8,-2,80);light(0.4,1,0.6,0.35);fog(10,40,'#000');\nmesh3d(pl,{y:0,z:-6,color:'#3f7fb0'});\nfor(let i=0;i<6;i++){const x=-10+i*4.2,z=-13-rand(i)*4;mesh3d(tk,{x,z,color:'#8a6a4a'});mesh3d(tr,{x,y:1,z,color:i%2?'#5fb06a':'#7cc47a'});}\nmesh3d(dk,{x:-6,y:0.3,z:2,color:'#c09060'});mesh3d(ps,{x:6,z:0,color:'#b08a60'});mesh3d(sg,{x:6,y:1.6,z:0,color:'#e8d8a8'});\nconst sp=project(6,1.6,0);if(sp)text(sp.x-5,sp.y,'{what}'.slice(0,12),'#ffe');\nmesh3d(bk,{x:-4.6,y:0.45,z:2.6,color:'#9aa6b2'});\nconst c=mod(t,10),n=floor(t/10),boot=n%4==3;let by=0.05+sin(t*2)*0.04;if(c>5&&c<7)by=0.05-abs(sin(t*18))*0.15;\nconst bx=2,bz=-2;\nconst me=clawd3d(-6.5,0.45,2,{facing:1,arms:{left:'down',right:c>7&&c<8.5?'up':'out'},eyes:c>5&&c<7?'wide':'open'});\nconst tip=project(-3.5,2.4,1),bp=project(bx,by+0.15,bz);if(tip&&bp&&!(c>7&&c<8.5))line(tip.x,tip.y,bp.x,bp.y,'.','#ddd');\nif(me&&tip)line(me.right.x,me.right.row,tip.x,tip.y,'/','#a87a4a');\nif(!(c>7&&c<8.5))mesh3d(bb,{x:bx,y:by,z:bz,color:'#ff5050'});\nif(c>7&&c<8.5){const k=(c-7)/1.5,x=lerp(bx,-4.6,k),y=Math.sin(Math.PI*k)*2.5+0.6,z=lerp(bz,2.6,k);if(boot)mesh3d(bt,{x,y,z,rz:t*6,color:'#a07850'});else mesh3d(fs,{x,y,z,color:'#f0c040',scale:1});if(tip){const fp=project(x,y,z);if(fp)line(tip.x,tip.y,fp.x,fp.y,'.','#ddd');}}\nconst f=n-floor((n+1)/4);for(let i=0;i<Math.min(f,5);i++)mesh3d(sphere(0.15,6),{x:-4.8+i*0.12,y:1,z:2.6,color:'#f0c040'});\nif(c>5&&c<6.8)for(let i=0;i<5;i++){const p=project(bx+Math.cos(i*1.3+t)*0.6*(c-5),0.05,bz+Math.sin(i*1.3)*0.3);if(p)put(p.x,p.y,'~','#bfe8ff');}\nif(me){let s='';if(c<4&&c>1)s=n==0?'Line cast into {what}. Now we wait.':'Patience. The tests bite when they bite.';else if(c>5&&c<7)s='Nibble! Something in {what} is moving!';else if(c>7.5&&c<10)s=boot?'An old boot. One failing test, recast.':'Caught a passing test! Into the bucket.';if(s)say(s,me.top.x,me.row);}}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#203a5a","#2e5a7a","#4a7a9a"],"speed":0.4,"intensity":0}}},
21  {"phase":"thinking","world":"nature documentary","style":"3D","concept":"carnival midway at night: Clawd riding a slowly turning 3D ferris wheel, pondering {what} from the top while booth lights blink below","source":"claude-opus-5-5","scene":{"concept":"carnival midway at night: Clawd riding a slowly turning 3D ferris wheel, pondering {what} from the top while booth lights blink below","code":"const cab=box(0.9,0.7,0.7),hub=cylinder(0.3,0.4,10),booth=box(1.6,1.4,1.2),roof=cone(1.1,0.8,8),post=cylinder(0.06,4,6);\nconst lines=['Thinking about {what}. Best view is at the top.','Round and round, like my ideas on {what}.','Ooh, the top! I can almost see the answer.','One more lap and I will have it figured out.'];\nfunction frame(t,dt){camera(Math.sin(t*0.1)*2,2.6,10,0,2.2,0,90);light(0.4,0.8,0.6,0.35);fog(10,40,'#05030a');\nconst cx=0,cy=4.2,R=3.2,a=t*0.25;\nfor(let i=0;i<8;i++){const b=a+i*Math.PI/4,x=cx+Math.cos(b)*R,y=cy+Math.sin(b)*R;line3d(cx,cy,0,x,y,0,'#ffd27a');const b2=b+Math.PI/4;line3d(x,y,0,cx+Math.cos(b2)*R,cy+Math.sin(b2)*R,0,'#ff8fc0');if(i)mesh3d(cab,{x,y:y-0.6,z:0.3,color:hsl(i*45,0.7,0.6)});}\nmesh3d(hub,{x:cx,y:cy-0.2,z:0,rx:1.57,color:'#ccc'});line3d(-2,0,0,cx,cy,0,'#aab');line3d(2,0,0,cx,cy,0,'#aab');\nconst me={x:cx+Math.cos(a)*R,y:cy+Math.sin(a)*R};mesh3d(cab,{x:me.x,y:me.y-0.6,z:0.3,color:'#ffcf5a'});\nconst c=clawd3d(me.x,me.y-0.5,0.75,{size:0.6,facing:0,blink:fract(t*0.3)<0.05,arms:Math.sin(a)>0.9?'up':'down'});\nfor(let j=0;j<4;j++){const bx=-9+j*(j<2?3:3)+(j>1?10:0);mesh3d(booth,{x:bx,y:0.7,z:2,color:j%2?'#e06666':'#f4f4f4'});mesh3d(roof,{x:bx,y:1.4,z:2,color:j%2?'#f4f4f4':'#e06666'});for(let k=0;k<5;k++){const p=project(bx-0.7+k*0.35,2.3,2);if(p)put(p.x,p.y,'●',fract(t*2+k*0.2+j*0.5)<0.5?'#ffe066':'#ff6fa8');}}\nconst lp=project(6,1.0,2.6);if(lp)text(lp.x-4,lp.y,'{what}','#fff');\nif(c){const s=Math.sin(a);const i=Math.floor(t/7)%lines.length;if(fract(t/7)<0.75)say(lines[i],c.top.x,c.row);}}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#668","#aab"],"speed":0.4,"intensity":0.25}}},
22  {"phase":"thinking","world":"desert caravan","style":"3D","concept":"mail room in 3D: Clawd sorting thought-parcels on a conveyor, weighing which pigeonhole {what} belongs in","source":"claude-opus-5-5","scene":{"concept":"mail room in 3D: Clawd sorting thought-parcels on a conveyor, weighing which pigeonhole {what} belongs in","code":"const bx=box(0.9,0.7,0.9),sh=box(0.9,0.9,0.1),cv=box(14,0.2,1.6);\nfunction frame(t,dt){camera(Math.sin(t*0.15)*1.5,2.6,8,0,1,0,90);light(0.4,1,0.6,0.35);fog(8,35,'#000');\nmesh3d(cv,{x:0,y:0.4,z:0,color:'#8890a0'});\nfor(let i=0;i<8;i++){const x=mod(t*1.2+i*2,16)-8;mesh3d(bx,{x,y:0.85,z:0,ry:i,color:i%3?'#d8a868':'#c89858'});}\nfor(let c=0;c<9;c++)for(let r=0;r<3;r++){const x=-8+c*2,y=1+r*1.05,z=-5;const lit=floor(t*0.7)%27==c*3+r;mesh3d(sh,{x,y,z,color:lit?'#ffe080':'#a07850'});}\nconst k=fract(t/6),ph=floor(t/6);const hx=lerp(2,-6+(ph*5)%13,smoothstep(0.4,0.9,k)),hy=lerp(1.4,3,smoothstep(0.4,0.9,k)),hz=lerp(0.8,-4.6,smoothstep(0.4,0.9,k));\nconst me=clawd3d(2.5,0,1.6,{facing:k<0.4?0:-1,arms:k<0.4?'out':'up'});\nmesh3d(bx,{x:hx,y:hy,z:hz,scale:0.7,ry:t,color:'#f0c070'});\nconst p=project(hx,hy+0.6,hz);if(p)text(p.x-4,p.y,'{what}','#fff');\nif(me){const L=['Which pigeonhole does {what} go in?','Not this one. Hmm. Maybe that one.','Return to sender: me, five seconds ago.','Sorting thoughts, one parcel at a time.'];say(L[ph%4],me.top.x,me.row);}}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#221a10","#332818","#443320"],"speed":0.4,"intensity":0}}},
23  {"phase":"thinking","world":"ancient tomb","style":"3D","concept":"film noir city at night: Clawd as trench-coat detective under a streetlamp on a rainy 3D street, puzzling over a case labeled {what} while t","source":"claude-opus-5-5","scene":{"concept":"film noir city at night: Clawd as trench-coat detective under a streetlamp on a rainy 3D street, puzzling over a case labeled {what} while the camera slowly dollies past dark buildings","code":"const bld=box(1,1,1);const lamp=cylinder(0.06,3,6);const head=sphere(0.25,8);const B=Array.from({length:14},(_,i)=>({x:(i%2?1:-1)*(5+rand(i)*2),z:-i*4+4,hh:2+rand(i+9)*5,c:mix('#8890a0','#a0a8b8',rand(i+3))}));const R=Array.from({length:60},(_,i)=>({x:(rand(i)-0.5)*16,y:rand(i+70)*6,z:-rand(i+40)*20+4}));const lines=['Case file: {what}. Something here does not add up.','The trail goes cold right about... here.','Every clue points back to {what}.','Wait. What if it was the config all along?','Got it. Time to make some moves.'];function frame(t,dt){const a=t*0.05;camera(Math.sin(a)*3,2.4,8,0,1,-4,85);light(-0.4,0.8,0.5,0.25);fog(6,34,'#000');for(const b of B){const z=b.z+mod(t*0.4,4)-2;mesh3d(bld,{x:b.x,y:b.hh/2,z:b.z,rx:0,ry:0,scale:1,color:b.c});mesh3d(box(2,b.hh,2),{x:b.x,y:b.hh/2,z:b.z,color:b.c});for(let k=0;k<3;k++){if(rand(k+b.z*7+floor(t*0.3))>0.6){const p=project(b.x+(b.x>0?-1.01:1.01),1+k*1.3,b.z);if(p)put(p.x,p.y,'▪','#ffd27a')}}}line3d(-3,0,8,-3,0,-50,'#556');line3d(3,0,8,3,0,-50,'#556');mesh3d(lamp,{x:2.2,z:-1,color:'#b0b0b0'});mesh3d(head,{x:2.2,y:3,z:-1,color:'#fff0b0',unlit:true});for(const r of R){r.y-=dt*9;if(r.y<0)r.y=6;const p=project(r.x,r.y,r.z);if(p)put(p.x,p.y,'╎','#6a7a9a')}const think=fract(t/6)<0.5;const c=clawd3d(0.6,0,-1,{facing:0,size:1.2,color:'#b06a50',arms:think?{left:'down',right:'up'}:'down',eyes:fract(t*0.3)<0.04?'closed':'open'});if(c){pixels(c.top.x-6,c.top.py-3,'....######....\\n..##########..\\n##############','#'==='#'?{'#':'#5a4a3a'}:{});const s=project(0.6,1.6,0.2);const i=floor(t/9)%lines.length;if(fract(t/9)<0.75)say(lines[i],c.top.x,c.row);const q=project(-0.6,0.9,-0.6);if(q)text(q.x-4,q.y,'{what}','#ffe9a8');if(think&&c.right)put(c.right.x+1,c.right.row-1,fract(t)<0.5?'?':'¿','#ffd27a')}}","actors":[],"particles":[],"background":{"effect":"rain","palette":["#334","#445","#556"],"speed":1,"intensity":0}}},
24  {"phase":"thinking","world":"ufo abduction","style":"pixel art","concept":"jungle safari: pith-helmeted Clawd crouches in tall grass with binoculars, stalking a beetle labeled {what} that keeps peeking out and ducki","source":"claude-opus-5-5","scene":{"concept":"jungle safari: pith-helmeted Clawd crouches in tall grass with binoculars, stalking a beetle labeled {what} that keeps peeking out and ducking away","code":"const P={g:'#3c8a3a',G:'#6cc04a',b:'#7a5230',k:'#2a1a0a',y:'#e8c860',r:'#c0402a',w:'#ffffff'};\nconst lines=['Something in {what} just rustled.','Hold still... it thinks I can not see it.','There! No. That was a leaf.','Patience. Bugs always come back for a snack.','Got a lead on {what}. Quietly now.'];\nfunction frame(t,dt){for(let x=0;x<w;x++){for(let py=0;py<4;py++){const n=noise2(x*0.15,py*0.5+t*0.05);if(n>0.35+py*0.08)pixel(x,py,n>0.6?P.G:P.g)}}\nfor(const tx of [8,30,88,108]){fill(tx,1,3,h-1,' ',null,'#5a3a20');put(tx+3,2+(tx%3),'~','#4a9a3a')}\nfor(let x=0;x<w;x+=9)line(x+4,0,x+4,1+Math.floor(noise(x+t*0.3)*3),'|','#3a7a2a');\nconst cy=t%12,bx=72+Math.sin(t*0.4)*14;const up=cy>3&&cy<6||cy>8.5&&cy<10;\nconst c=clawd(46,h-5,{pose:'stand',facing:bx>53?1:-1,blink:fract(t*0.3)<0.04,eyes:up?'wide':'open',arms:{left:'down',right:'up'}});\npixels(c.top.x-5,c.top.py-3,'..yyyyyy..\\n.yyyyyyyy.\\nyyyyyyyyyy\\n..kkkkkk..',P);\npixels(c.right.x-1,c.right.py-2,'kk.kk\\nkk.kk',P);\nif(up){const by=h*2-6-Math.min(1,(cy%3))*2;pixels(bx,by,'.k.k.\\nrrrrr\\nrwrwr\\n.rrr.',P);text(bx-2,Math.floor(by/2)-1,'{what}','#ffd27a')}\nfor(let x=0;x<w;x++){const s=Math.sin(x*1.3+t*1.5)*0.6;pixel(x,h*2-3+Math.round(s),P.G);pixel(x,h*2-2,P.g);pixel(x,h*2-1,P.g)}\nconst i=Math.floor(t/7)%lines.length;if(fract(t/7)<0.7)say(lines[i],c.top.x,c.row)}","actors":[],"particles":[{"glyphs":"·*","count":8,"x":"20+noise(t*0.3+k)*80","y":"2+noise(t*0.4+k*3)*4","color":"#e8f070"}],"background":{"effect":"aurora","palette":["#0a3a1a","#1a5a2a","#2a6a3a"],"speed":0.3,"intensity":0.2}}},
25  {"phase":"thinking","world":"medieval castle","style":"3D","concept":"medieval train station: Clawd as a knight-stationmaster on a stone platform, pondering which track to send the royal steam engine down while","source":"claude-opus-5-5","scene":{"concept":"medieval train station: Clawd as a knight-stationmaster on a stone platform, pondering which track to send the royal steam engine down while trains arrive from the misty distance","code":"const plat=box(4,0.6,30),pil=cylinder(0.25,2.6,8),tow=box(1.4,3,1.4),cap=cone(1,1,8),eng=box(1.6,1.4,3),chim=cylinder(0.25,0.7,8),car=box(1.6,1.2,3);\nfunction frame(t,dt){camera(6+Math.sin(t*0.1)*1.5,2.6,8,0,1,-12,90);light(0.4,1,0.6,0.35);fog(10,50,'#000');\nmesh3d(plat,{x:-2.5,y:0.3,z:-10,color:'#a89c88'});\nfor(let i=0;i<6;i++){const z=4-i*6;mesh3d(pil,{x:-4,y:0.6,z,color:'#c8bca8'});mesh3d(pil,{x:-1,y:0.6,z,color:'#c8bca8'});line3d(-4,3.2,z,-1,3.2,z,'#998870');}\nmesh3d(tow,{x:-7,y:1.5,z:-20,color:'#b0a490'});mesh3d(cap,{x:-7,y:3,z:-20,color:'#c05050'});mesh3d(tow,{x:9,y:1.5,z:-26,color:'#b0a490'});mesh3d(cap,{x:9,y:3,z:-26,color:'#5070c0'});\nfor(const r of [0.6,2.4,4.4,6.2])line3d(r,0,10,r,0,-60,'#8a8a9a');\nfor(let z=8;z>-60;z-=2){line3d(0.3,0,z,2.7,0,z,'#6a5040');line3d(4.1,0,z,6.5,0,z,'#6a5040');}\nconst cyc=mod(t,22),tz=-60+smoothstep(0,9,cyc)*55+smoothstep(14,22,cyc)*60,tx=cyc<11?1.5:5.3;\nmesh3d(eng,{x:tx,y:0.9,z:tz,color:'#d0b040'});mesh3d(chim,{x:tx,y:1.6,z:tz-1,color:'#806040'});\nfor(let c=1;c<3;c++)mesh3d(car,{x:tx,y:0.8,z:tz+c*3.3,color:c%2?'#c06a50':'#6a9a70'});\nfor(let s=0;s<5;s++){const p=project(tx,2.4+s*0.4+fract(t)*0.4,tz-1-s*0.3);if(p)put(p.x+s,p.y,s%2?'○':'°','#ddd');}\nconst lp=project(tx,1.7,tz+0.5);if(lp&&tz>-30)text(lp.x-5,lp.y,'{what}','#ffe');\nconst sw=project(3.4,0.2,0);if(sw)text(sw.x-1,sw.y,cyc<11?'◄╱':'╲►','#fc6');\nconst me=clawd3d(-2.5,0.6,1,{facing:1,arms:cyc>9&&cyc<12?'up':{left:'down',right:'out'},size:1});\nif(me){pixels(me.top.x-3,me.top.py-3,'.ggggg.\\nggggggg\\n.g...g.','{\"g\":\"#c0c8d0\"}'.length?{g:'#c0c8d0'}:{});\nconst L=cyc<5?'Which track for {what}? Hmm, left feels royal.':cyc<10?'Thinking... the north line or the east line.':cyc<13?'Throw the lever! East it is!':cyc<18?'Train departs on time. Chivalry lives.':'Next one is due. More thinking, sire.';say(L,me.top.x,me.row);}}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#203040","#405070","#604060"],"speed":0.4,"intensity":0}}},
26  {"phase":"thinking","world":"pirate ship","style":"pixel art","concept":"pirate ship meets train station: Clawd as pirate captain at the wheel of a ship-train steaming along rails over the sea, scanning with a spy","source":"claude-opus-5-5","scene":{"concept":"pirate ship meets train station: Clawd as pirate captain at the wheel of a ship-train steaming along rails over the sea, scanning with a spyglass for the answer","code":"function frame(t,dt){for(let y=0;y<h;y++)for(let x=0;x<w;x++){if(y<6){put(x,y,' ',null,mix('#1a2a4a','#3a4a7a',y/6))}else put(x,y,'~',y==6?'#7ab':'#48a',y==6?'#2a5a8a':'#1d4470')}for(let x=0;x<w;x++)put(x,6,(x+Math.floor(t*8))%4==0?'┼':'═','#a98')\nfor(let i=0;i<5;i++){const sx=mod(i*31-t*3,w+20)-10;pixels(sx,2+i%2,'..##..\\n.####.\\n######',{'#':'#cdd'})}\nconst bx=30;pixels(bx,6,'..oooooooooooooooooooooooooooooooooo..\\n.oooooooooooooooooooooooooooooooooooo.\\n..o..o.....o..o.....o..o.....o..o.....',{o:'#8a5a2a'});fill(bx+2,4,34,1,'▄','#6b3f1a');fill(bx+18,0,1,4,'│','#cba');pixels(bx+13,0,'wwwwwwwwwww\\nwwwwwwwwwww\\nwwwwwwwwwww\\nwwwwwwwwwww\\nwwwwwwwwwww',{w:'#eee'});text(bx+14,1,'{what}'.slice(0,9),'#222','#eee');pixels(bx+30,0,'.k.\\nkkk\\nkkk\\nkkk\\nkkk\\nkkk',{k:'#333'});for(let i=0;i<4;i++){const p=fract(t*0.7+i/4);put(bx+31-p*25,-p*2,'○',mix('#ddd','#556',p))}\nconst c=clawd(bx+2,1,{facing:1,arms:{left:'down',right:'up'},look:{x:2,y:-1}});pixels(c.top.x-4,c.top.py-3,'..kkkk..\\nkkkkkkkk\\n.kwkkwk.',{k:'#222',w:'#fff'});pixels(c.right.x,c.right.py-1,'yyyyyy',{y:'#da4'});const st=Math.floor(t/12)%4;const L=['Full steam ahead on {what}!','Spyglass says: answer at next stop.','Arr, rails or waves, we keep thinking.','Land ho! Er... platform ho!'];if(fract(t/12)<0.7)say(L[st],c.top.x,c.row)\nconst px=w-mod(t*6,w+40)+20;pixels(px,8,'gggggggggggggg',{g:'#998'});pixels(px+5,4,'.p.\\nppp\\n.p.\\n.p.',{p:'#cc8'});text(px+2,3,'STATION','#ffd')}","actors":[],"particles":[],"background":{"effect":"waves","palette":["#1a2a4a","#2a4a7a","#3a6a9a"],"speed":0.5,"intensity":0}}},
27  {"phase":"thinking","world":"construction site","style":"text art","concept":"snow globe construction site: Clawd in a hard hat inside a glass globe, thinking while snow falls, a tiny crane slowly stacking blocks label","source":"claude-opus-5-5","scene":{"concept":"snow globe construction site: Clawd in a hard hat inside a glass globe, thinking while snow falls, a tiny crane slowly stacking blocks labeled {what}; every so often the globe gets shaken and the snow swirls up","code":"let fl=Array.from({length:70},(_,i)=>({x:rand(i),y:rand(i+40)*9,v:0.6+rand(i+80)}))\nfunction frame(t,dt){const cx=w/2,R=28;const sh=mod(t,24)<1.2;const off=sh?Math.sin(t*40)*1.5:0;for(let y=0;y<8;y++){const dy=(y-4.5)/4.6;const hw=Math.sqrt(Math.max(0,1-dy*dy))*R;put(cx-hw+off,y,'(','#9fc8e8');put(cx+hw+off,y,')','#9fc8e8')}text(cx-R*0.85+off,7,'▄'.repeat(Math.floor(R*1.7)),'#dfe8f0');fill(cx-24,8,48,1,'█','#7a4a2a');text(cx-4,8,'{what}'.slice(0,8),'#ffd27a','#7a4a2a');const bx=cx+6+off;line(bx,1,bx,6,'║','#f0c040');line(bx,1,bx+12,1,'═','#f0c040');const p=fract(t/8);const hx=bx+lerp(11,3,smoothstep(0.2,0.6,p));const hy=lerp(2,5,smoothstep(0.6,0.9,p))*(p<0.9?1:0)+(p>=0.9?2:0);const n=Math.min(4,Math.floor(t/8));line(hx,2,hx,hy,'│','#aaa');if(p<0.9)text(hx-1,hy+1,'[■]','#e08040');for(let i=0;i<n;i++)text(bx+2,6-i,'[■]','#e08040');for(const f of fl){f.y+=(sh?-6:f.v)*dt;if(f.y>7)f.y=0.5;if(f.y<0)f.y=6.5;const x=cx+(f.x-0.5)*2*R*Math.sqrt(Math.max(0,1-((f.y-4.5)/4.6)**2))*0.9+off;put(x+Math.sin(t+f.x*9),f.y,sh?'*':'·','#ffffff')}const c=clawd(cx-20+off,3,{facing:1,eyes:sh?'wide':'open',blink:fract(t*0.3)<0.04,arms:sh?'up':{left:'down',right:'out'}});text(c.top.x-3,c.top.row-1,'▄███▄','#ffcc22');const m=mod(t,24);if(m>2&&m<7)say('Thinking about {what}. One block at a time.',c.top.x,c.row-1);else if(m>13&&m<18)say('Who keeps shaking the site?!',c.top.x,c.row-1)}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334466","#556688","#8899bb"],"speed":0.3,"intensity":0.2}}},
28  {"phase":"thinking","world":"weather report","style":"3D","concept":"arctic expedition: Clawd in a parka plants flags across a 3D ice field, pondering beside an igloo under aurora while a weather balloon label","source":"claude-opus-5-5","scene":{"concept":"arctic expedition: Clawd in a parka plants flags across a 3D ice field, pondering beside an igloo under aurora while a weather balloon labeled {what} drifts up","code":"const ice=box(1.6,1,1.6),ig=sphere(1.2,10),fl=cylinder(0.04,1.4,5),bg=cone(0.5,1.6,6);const flags=[];let nx=2;\nfunction frame(t,dt){camera(Math.sin(t*0.1)*2,2.6,9,0,1,0,90);light(0.4,0.8,0.5,0.35);fog(8,40,'#04101a');for(let i=0;i<w;i+=3){const a=Math.sin(i*0.05+t*0.6)*0.5+0.5;put(i,0,'░',hsl(140+a*60,0.7,0.5));put(i+1,1,'▒',hsl(160+a*80,0.6,0.35));}\nfor(let i=-4;i<5;i++)for(let j=0;j<3;j++)mesh3d(ice,{x:i*3+(j%2)*1.5,y:-0.5,z:-j*4,color:'#cfe8f5'});mesh3d(ig,{x:-6,y:0,z:-2,color:'#e8f4ff'});mesh3d(box(0.6,0.7,0.6),{x:-6,y:0.35,z:-0.8,color:'#335'});\nif(t>nx&&flags.length<6){flags.push(-2+flags.length*1.8);nx+=7;}const cyc=t%7,px=flags.length?flags[flags.length-1]:-2;const cx=lerp(px-1.8,px-0.6,smoothstep(0,3,cyc));\nfor(const f of flags){mesh3d(fl,{x:f,y:0,z:0.5,color:'#999'});const p=project(f,1.4,0.5);if(p)text(p.x,p.y,'▶',f==flags[flags.length-1]?'#ff5050':'#ffcc33');}\nconst by=1+t*0.04;mesh3d(sphere(0.35,8),{x:5,y:by+1.6,z:-3,color:'#ff8866'});line3d(5,by+1.25,-3,5,by,-3,'#ccc');const bp=project(5,by+2.3,-3);if(bp)text(bp.x-4,bp.y,'{what}','#fff');\nconst c=clawd3d(cx,0,0.5,{facing:cyc<3?1:0,stride:cyc<3?Math.floor(t*6)%4:0,arms:cyc>3?{left:'down',right:'up'}:'down',eyes:cyc>5?'closed':'open'});\nif(c){pixels(c.top.x-4,c.top.py-2,'.wwwwwwww.\\nwbbbbbbbbw',{w:'#fff',b:'#3a6fd8'});if(cyc>3.5)say(['Flag here: {what} is solid ice.','Hmm, thin ice past this line.','Mapping {what} one step at a time.','Brr. Thinking keeps me warm.'][flags.length%4],c.top.x,c.row);}\nfor(let k=0;k<30;k++)put(mod(rand(k)*w+t*3,w),mod(rand(k+5)*h+t*(1+rand(k)*2),h),'*','#eef');}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#1fae7a","#3fd0c0","#6a5acd"],"speed":0.5,"intensity":0}}},
29  {"phase":"searching","world":"ant colony up close","style":"3D","concept":"desert caravan in 3D: Clawd rides a camel train across dunes toward an oasis marked {what}, scouting tracks in the sand","source":"claude-opus-5-5","scene":{"concept":"desert caravan in 3D: Clawd rides a camel train across dunes toward an oasis marked {what}, scouting tracks in the sand","code":"const dune=sphere(3,10),cam=box(1.4,0.8,0.5),leg=cylinder(0.08,0.7,4),neck=box(0.25,0.8,0.25),palm=cylinder(0.12,2,5),leaf=cone(1,0.5,6),pool=cylinder(1.6,0.05,12);\nfunction frame(t,dt){camera(0,2.6,9,0,1,-6,90);light(0.4,0.8,0.5,0.35);fog(10,45,'#1a0f08');\nfor(let x=0;x<w;x++)for(let y=0;y<3;y++)put(x,y,' ',null,mix('#2a1640','#a0502a',y/3));\ndisc(w-18,1,1,' ',null,'#ffd27a');\nfor(let i=0;i<8;i++){const x=mod(i*7-t*1.2,56)-28;mesh3d(dune,{x,y:-2.4,z:-14-(i%3)*5,color:'#d9a860'})}\nconst ox=mod(30-t*1.2,70)-30;mesh3d(pool,{x:ox,z:-12,color:'#5ab8e0'});mesh3d(palm,{x:ox+1.5,z:-12.5,color:'#b08050'});mesh3d(leaf,{x:ox+1.5,y:2,z:-12.5,color:'#6fc060'});\nconst p=project(ox,2.6,-12);if(p)text(p.x-5,p.y,'{what}','#fff4c0');\nlet me=null;for(let k=0;k<3;k++){const x=-6+k*3.2,b=Math.abs(Math.sin(t*4+k))*0.15;mesh3d(cam,{x,y:1+b,z:0,color:'#c89060'});mesh3d(neck,{x:x+0.8,y:1.6+b,z:0,rz:-0.3,color:'#c89060'});for(const d of[-0.5,0.5]){mesh3d(leg,{x:x+d+Math.sin(t*6+k+d)*0.1,y:0.3,z:0.15,color:'#a87048'})}\nif(k==2)me=clawd3d(x,1.4+b,0,{size:0.7,facing:1,arms:fract(t/8)>0.6?{left:'down',right:'up'}:'down'})}\nfor(let i=0;i<14;i++){const tx=mod(i*1.5+t*1.2,20)-10;mesh3d(box(0.3,0.02,0.15),{x:tx,z:1.5,color:'#8a5a30'})}\nif(me){const s=fract(t/16);if(s>0.15&&s<0.45)say('Tracks lead to {what}. The grep is warm.',me.top.x,me.row);else if(s>0.6&&s<0.9)say('Mirage or match? Only one way to find out.',me.top.x,me.row)}}","actors":[],"particles":[],"background":{"effect":"waves","palette":["#3a2010","#6a3a18","#a0602a"],"speed":0.5,"intensity":0}}},
30  {"phase":"searching","world":"medieval castle","style":"3D","concept":"skate park at dusk: Clawd grinds a row of ramps, kickflipping over each crate to check its label, hunting for {what}","source":"claude-opus-5-5","scene":{"concept":"skate park at dusk: Clawd grinds a row of ramps, kickflipping over each crate to check its label, hunting for {what}","code":"const pl=plane(34,12),rb=box(3,0.6,2.4),cr=box(0.8,0.8,0.8),bd=box(1.1,0.08,0.45),pole=cylinder(0.06,3,6),rx=[-9,-3,3,9];\nfunction frame(t,dt){camera(0,3.2,10,0,1,0,90);light(0.4,1,0.6,0.35);fog(10,40,'#000');\nmesh3d(pl,{y:0,z:-1,color:'#8a8f9a'});for(const x of [-14,14])mesh3d(pole,{x,z:-4,color:'#cc8'});\nconst c=Math.floor(t/12)%4,ph=mod(t,12);\nfor(let i=0;i<4;i++){mesh3d(rb,{x:rx[i],y:0.2,z:-2,rz:i%2?0.25:-0.25,color:'#d0b080'});const hit=i==c&&ph>8;mesh3d(cr,{x:rx[i],y:0.9+(hit?0.3*Math.abs(Math.sin(t*6)):0),z:-3,ry:t*0.5+i,color:hit?'#ffd23f':'#b07850'});const p=project(rx[i],1.9,-3);if(p)text(p.x-4,p.y,i==c?'{what}':['utils/','lib/','src/','test/'][i],hit?'#ffe066':'#aab');}\nconst tx=ph<8?Math.sin(t*0.9)*11:lerp(Math.sin(t*0.9)*11,rx[c],smoothstep(8,9,ph)),jy=ph<8?Math.max(0,Math.sin(t*3))*0.9:0;\nmesh3d(bd,{x:tx,y:jy+0.1,z:0.5,rz:jy>0.3?Math.sin(t*12)*0.6:0,color:'#e05a5a'});\nconst me=clawd3d(tx,jy+0.15,0.5,{facing:Math.cos(t*0.9)>0?1:-1,arms:jy>0.3?'up':'out',size:0.9});\nif(me){if(ph>8.5)say('Landed it! {what} was right under this ramp.',me.top.x,me.row);else if(ph>1&&ph<4)say('Ollie over every crate till I find {what}.',me.top.x,me.row);}\nif(ph>8)for(let k=0;k<12;k++){const p=project(rx[c]+Math.cos(k*2.4+t)*1.5,1.2+Math.sin(k*1.7+t*2)*0.8,-3);if(p)put(p.x,p.y,'✦',hsl(k*30,0.9,0.7));}}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#3a2a5a","#6a3a7a","#c86a4a"],"speed":0.5,"intensity":0.2}}},
31  {"phase":"searching","world":"arctic expedition","style":"3D","concept":"vegetable garden 3D: Clawd in a straw hat pulls up carrots row by row hunting the one labeled {what}","source":"claude-opus-5-5","scene":{"concept":"vegetable garden 3D: Clawd in a straw hat pulls up carrots row by row hunting the one labeled {what}","code":"const bed=box(1.6,0.3,1.2),leaf=cone(0.25,0.7,5),root=cone(0.18,0.8,6),post=cylinder(0.06,1.2,5);\nconst rows=[];for(let i=0;i<8;i++)rows.push({x:-9+i*2.6,z:-1});\nfunction frame(t,dt){camera(0,2.6,8,0,0.8,-2,90);light(0.4,1,0.6,0.35);fog(8,40,'#000');\nmesh3d(plane(40,30),{y:0,z:-6,color:'#7a5a3a'});\nfor(let j=0;j<5;j++)mesh3d(post,{x:-12+j*6,z:-8,color:'#c9a070'});line3d(-12,1,-8,12,1,-8,'#c9a070');\nconst cyc=4,k=Math.floor(t/cyc)%8,p=fract(t/cyc);const tgt=(Math.floor(t/cyc)%3==2)?k:-1;\nlet cx=-9;\nfor(let i=0;i<8;i++){const r=rows[i];mesh3d(bed,{x:r.x,y:0.15,z:r.z,color:'#8b6a48'});\nlet up=0;if(i<k)up=-1;if(i==k)up=smoothstep(0.35,0.6,p)*1.4;\nif(up>=0){const y=0.3+up;mesh3d(leaf,{x:r.x,y:y,z:r.z,color:'#6fd36f'});if(up>0.1)mesh3d(root,{x:r.x,y:y,z:r.z,rx:Math.PI,color:i==tgt?'#ffd24a':'#f08a30'});}\nelse mesh3d(root,{x:r.x,y:0.4,z:r.z+0.8,rz:1.5,color:'#f08a30'});}\ncx=lerp(k==0?-11:rows[Math.max(0,k-1)].x,rows[k].x-1.2,smoothstep(0,0.3,p));\nconst c=clawd3d(cx,0,0,{facing:p<0.3?1:0,stride:Math.floor(t*6)%4,arms:p>0.35&&p<0.7?'up':'down',size:1});\nif(c){pixels(c.top.x-5,c.top.py-3,'..yyyy..\\nyyyyyyyyyy\\n',{y:'#e8d27a'});}\nconst lp=project(rows[k].x,2.3,rows[k].z);if(lp&&p>0.5)text(lp.x-4,lp.y,i0(tgt==k),tgt==k?'#ffd24a':'#ccc');\nif(c&&p>0.6){say(tgt==k?'Found {what}! Golden root!':'Just a carrot. Next row.',c.top.x,c.row);}}\nfunction i0(b){return b?'{what}':'nope'}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#203a20","#2f5a3a","#4a7a4a"],"speed":0.4,"intensity":0}}},
32  {"phase":"searching","world":"race track pit stop","style":"3D","concept":"pirate ship: Clawd in a crow's nest on a 3D galleon sweeping a spyglass over a sea of buoys, hunting the one labeled {what}","source":"claude-opus-5-5","scene":{"concept":"pirate ship: Clawd in a crow's nest on a 3D galleon sweeping a spyglass over a sea of buoys, hunting the one labeled {what}","code":"const hull=box(6,1.2,2),mast=cylinder(0.12,4,6),sail=box(0.1,1.8,2.4),nest=cylinder(0.5,0.4,8),buoy=cone(0.35,0.9,6);\nconst bs=Array.from({length:9},(_,i)=>({x:-14+i*3.5+rand(i)*2,z:-6-rand(i+9)*14}));\nconst tgt=6;\nfunction frame(t,dt){\nconst ang=Math.sin(t*0.25)*0.5;\ncamera(Math.sin(t*0.1)*3,3,10,0,1.5,-6,90);light(0.4,0.8,0.5,0.35);fog(8,40,'#001018');\nfor(let x=-24;x<=24;x+=2)for(let z=-28;z<=6;z+=3){const y=Math.sin(x*0.5+t*1.5+z*0.3)*0.12;line3d(x,y,z,x+1.6,y,z,'#2a6a9a');}\nconst roll=Math.sin(t*0.9)*0.05,by=Math.sin(t*0.9)*0.1;\nmesh3d(hull,{x:-5,y:0.3+by,z:2,rz:roll,color:'#a8743c'});\nmesh3d(mast,{x:-5,y:0.9+by,z:2,color:'#c9a070'});\nmesh3d(sail,{x:-5,y:2.6+by,z:2,ry:1.57,color:'#eee6cc'});\nmesh3d(nest,{x:-5,y:4.3+by,z:2,color:'#8a5a30'});\nlet found=t>40;\nbs.forEach((b,i)=>{const hit=found&&i==tgt;const s=hit?1+0.2*Math.sin(t*8):1;mesh3d(buoy,{x:b.x,y:Math.sin(t+i)*0.15,z:b.z,scale:s,color:hit?'#ffd23c':(i%2?'#d05050':'#e0e0e0')});});\nconst me=clawd3d(-5,4.6+by,2,{size:0.8,facing:1,arms:{left:'down',right:'out'}});\nif(me){const sx=me.right.x;text(sx,me.right.row,'═◎','#e8c060');\nconst ax=Math.round(w/2+ang*60);line(sx+2,me.right.row,ax,5,'·',found?'#ffd23c':'#88aacc');\npixels(me.top.x-3,me.top.py-3,'.KKKKK.\\nKKwKwKK','{\"K\":\"#222\",\"w\":\"#fff\"}'.length?{K:'#222',w:'#fff'}:{});\nconst lines=['Sweepin\\' the seas for {what}...','Nothin\\' but bilge in this grep.','Every buoy\\'s a decoy, arr.','LAND HO! {what} off the port bow!'];\nconst li=found?3:Math.floor(t/12)%3;if(fract(t/12)<0.6||found)say(lines[li],me.top.x,me.row);}\nconst tb=bs[tgt],p=project(tb.x,1.4,tb.z);if(p&&found)text(p.x-4,p.y-1,'{what}','#ffd23c');\nif(found)for(let i=0;i<12;i++)put(w*0.6+Math.cos(i+t*3)*(t%2)*14,4+Math.sin(i*2.1)*(t%2)*3,'✦',hsl(i*30,0.8,0.65));}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#335577","#6688aa","#aaccee"],"speed":0.3,"intensity":0.3}}},
33  {"phase":"searching","world":"ancient tomb","style":"3D","concept":"lighthouse in a storm: Clawd as keeper atop a 3D lighthouse, the rotating beam sweeping the dark sea searching for a drifting crate labeled ","source":"claude-opus-5-5","scene":{"concept":"lighthouse in a storm: Clawd as keeper atop a 3D lighthouse, the rotating beam sweeping the dark sea searching for a drifting crate labeled {what}","code":"const tw=cylinder(0.9,4,10),top=cylinder(1.1,0.8,10),cap=cone(1.2,0.8,10),rk=box(3,0.6,2),cr=box(0.7,0.7,0.7);const sea=plane(60,40);function frame(t,dt){camera(0,3,11,0,2.2,0,90);light(0.3,1,0.6,0.35);fog(8,40,'#000');mesh3d(sea,{y:-0.1,z:-8,color:'#3a5a7a'});for(let i=0;i<14;i++){const x=-14+i*2.2,y=0.05+Math.sin(t*2+i)*0.15;line3d(x,y,-2-i%3*3,x+1.2,y,-2-i%3*3,'#9cd')}mesh3d(rk,{x:-3,y:0.3,z:0,color:'#8a8a80'});mesh3d(tw,{x:-3,y:0.5,z:0,color:'#e8e0d0'});mesh3d(top,{x:-3,y:4.5,z:0,color:'#c04040'});mesh3d(cap,{x:-3,y:5.3,z:0,color:'#c04040'});const a=t*0.9,lx=-3,ly=5,lz=0;for(let k=0;k<3;k++){const aa=a+k*0.04;line3d(lx,ly,lz,lx+Math.cos(aa)*18,0.5,lz-Math.abs(Math.sin(aa))*14-2,'#ffeb99')}const cxz=6+Math.sin(t*0.3)*3,cy=0.3+Math.sin(t*1.7)*0.2;const found=Math.cos(a%(2*Math.PI))>0.8&&Math.sin(a%(2*Math.PI))<0;mesh3d(cr,{x:cxz,y:cy,z:-6,ry:t*0.5,rz:Math.sin(t)*0.3,color:found?'#ffd070':'#a07040'});const p=project(cxz,1.2,-6);if(p)text(p.x-4,p.y-1,'{what}',found?'#fff6a0':'#c8a070');const me=clawd3d(-3,4.5,1.2,{size:0.7,facing:1,arms:found?'up':{left:'down',right:'out'}});if(me){const s=fract(t/14);if(s<0.3)say('Sweeping the dark for {what}...',me.top.x,me.row);else if(found)say('THERE! {what}, dead ahead!',me.top.x,me.row)}if(fract(t*0.13)<0.03){for(let x=0;x<w;x+=1)put(x,0,' ',null,'#334');line(70,0,64,4,'/','#fff')}}","actors":[],"particles":[{"glyphs":"/","count":80,"x":"mod(rand(k)*w - t*12, w)","y":"mod(t*(10+rand(k+9)*5)+rand(k+3)*h,h)","color":"#7a9cc0"}],"background":{"effect":"rain","palette":["#223344","#334455","#445566"],"speed":1.5,"intensity":0.2}}},
34  {"phase":"searching","world":"fairy tale forest","style":"3D","concept":"construction site in a fairy-tale forest, 3D: Clawd in a hard hat drives an excavator down a forest road, digging test pits one by one until","source":"claude-opus-5-5","scene":{"concept":"construction site in a fairy-tale forest, 3D: Clawd in a hard hat drives an excavator down a forest road, digging test pits one by one until the last pit strikes a glowing crate labeled {what}","code":"const tr=cone(0.9,2.6,7),tk=cylinder(0.15,0.8,6),cab=box(1.2,0.9,1),trk=box(1.6,0.35,1.2),arm=box(0.15,0.15,1.6),pit=cylinder(0.6,0.05,10),cr=box(0.8,0.8,0.8),cn=cone(0.25,0.6,6);\nconst trees=Array.from({length:16},(_,i)=>({x:(i%2?1:-1)*(5+rand(i)*4),z:-i*3+4}));\nfunction frame(t,dt){const P=14,cyc=t%P,seg=Math.floor(t/P),X=-6+cyc*0.9,dig=fract(cyc/2.8),dz=0;\ncamera(0,3,9,0,0.8,-4,90);light(0.4,1,0.6,0.35);fog(8,40,'#0b1410');\nmesh3d(plane(40,40),{y:0,z:-10,color:'#5a7a45'});\nfor(const q of trees){mesh3d(tk,{x:q.x,z:q.z,color:'#8a6a4a'});mesh3d(tr,{x:q.x,y:0.7,z:q.z,color:'#4fa86a'});}\nfor(let i=0;i<5;i++){const px=-6+i*2.8*0.9*1.0+1.5;if(px<X){mesh3d(pit,{x:px,z:1,color:'#7a5a3a'});}}\nfor(let i=-1;i<2;i+=2)mesh3d(cn,{x:i*7,z:1.5,color:'#ff9a40'});\nconst found=cyc>11.5;\nif(found){mesh3d(cr,{x:6.5,y:0.4+Math.sin(t*3)*0.1,z:1,ry:t,color:'#ffd060'});const p=project(6.5,1.3,1);if(p)text(p.x-4,p.y-1,'{what}','#fff2a0');}\nmesh3d(trk,{x:X,y:0.2,z:-1,color:'#777'});mesh3d(cab,{x:X,y:0.85,z:-1,color:'#f2c230'});\nconst a=Math.sin(dig*Math.PI)*0.7;mesh3d(arm,{x:X+0.8,y:1+a*0.5,z:-0.2,rx:0.6-a,color:'#e0a020'});\nconst c=clawd3d(X-0.2,1.3,-1,{size:0.6,facing:1});\nif(c){pixels(c.top.x-3,c.top.py-2,'..yyyy..\\nyyyyyyyy','{\"y\":\"#ffd23a\"}'.length?{y:'#ffd23a'}:{});\nif(found)say('Struck gold: {what} was buried right here!',c.top.x,c.row);else if(cyc>1&&cyc<5)say('Not under this oak. Next pit!',c.top.x,c.row);else if(cyc>6&&cyc<9)say('Digging for {what}... the forest hides it well.',c.top.x,c.row);}\nfor(let i=0;i<3;i++){const sx=X+1.5+Math.cos(i*2+t*4)*0.6,sy=0.3+fract(t*1.5+i/3)*0.8;point3d(sx,sy,0.2,'#c8a070');}\nfor(let i=0;i<6;i++){const p=project(-8+noise(t*0.3+i)*16,1.5+noise(t*0.4+i*5)*1.5,-3);if(p)put(p.x,p.y,'*','#bfffd0');}}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#3a5a4a","#6a8a7a","#a0c0b0"],"speed":0.3,"intensity":0}}},
35  {"phase":"reading","world":"underground mine","style":"text art","concept":"bakery: Clawd as pastry inspector slicing a giant layered cake labeled {what} to study each layer inside","source":"claude-opus-5-5","scene":{"concept":"bakery: Clawd as pastry inspector slicing a giant layered cake labeled {what} to study each layer inside","code":"const L=[['sponge','#f3d27a'],['jam','#e0566b'],['cream','#fff3e0'],['sponge','#f3d27a'],['ganache','#a8653a']];\nfunction frame(t,dt){\nfor(let x=0;x<w;x++)put(x,h-1,'═','#8a6a4a');\ntext(2,1,'┌──────┐',  '#c0c0c0');text(2,2,'│ OVEN │','#c0c0c0');text(2,3,'│▒▒▒▒▒▒│',mix('#a04020','#ffa040',0.5+0.5*Math.sin(t*2)));text(2,4,'│▒▒▒▒▒▒│','#ff8030');text(2,5,'└──────┘','#c0c0c0');\nconst cx=62,cw=46;\nconst cut=Math.min(cw-2,Math.floor(t*0.8)%(cw));\nfor(let i=0;i<5;i++){const y=2+i;for(let x=0;x<cw;x++){const open=Math.abs(x-cut)<2;if(open&&x==cut-1){text(cx+x,y,'▌',L[i][1]);continue}if(open)continue;put(cx+x,y,i==2?'░':i==1?'▒':'█',L[i][1])}}\ntext(cx,1,'~'.repeat(cw),'#fff3e0');put(cx+20,0,'♥','#e0566b');\ntext(cx+4,7,'{what}','#ffd27a');\nconst li=Math.floor(t/4)%5;text(cx+cw+2,2+li,'← '+L[li][0],'#9fe0a0');\nconst kx=cx+cut-15;const c=clawd(Math.min(kx,cx-16),3.5,1,-1,fract(t*0.3)<0.04);\ntext(c.top.x-2,c.row-1,'▄█▄','#ffffff');\ntext(c.right.x+1,c.right.row,'━━╾','#d0d0d0');\nconst S=['Reading {what} one layer at a time.','Jam layer calls the cream layer. Interesting.','Every import is another tier of sponge.','Who baked this? Taking notes.'];\nif(fract(t/7)<0.6)say(S[Math.floor(t/7)%4],c.top.x,c.row)}","actors":[],"particles":[{"glyphs":"·∙.","count":20,"x":"60+rand(k)*50","y":"mod(-t*0.5+rand(k+3)*9,9)","color":"#e8d8b0"}],"background":{"effect":"pulse","palette":["#3a2418","#4a3020","#2a1a10"],"speed":0.4,"intensity":0.2}}},
36  {"phase":"searching","world":"underground mine","style":"3D","concept":"desert caravan: Clawd riding the lead camel past rolling dunes, chasing an oasis labeled {what} that first turns out to be a mirage","source":"claude-opus-5-5","scene":{"concept":"desert caravan: Clawd riding the lead camel past rolling dunes, chasing an oasis labeled {what} that first turns out to be a mirage","code":"const dune=cone(3,1.4,8),body=box(1.4,0.6,0.5),leg=box(0.15,0.8,0.15),hump=sphere(0.35,6),head=box(0.3,0.3,0.3),trunk=cylinder(0.12,2.2,6),leaf=cone(1,0.4,6),pool=cylinder(1.6,0.05,10)\nfunction frame(t,dt){camera(-5,2.8,6,1,1,-4,80);light(0.4,1,0.6,0.35);fog(12,45,'#2a1408')\nfor(let i=0;i<14;i++){const x=mod(i*7-t*2,98)-49;mesh3d(dune,{x,z:-6-rand(i)*22,scale:0.8+rand(i+5)*0.6,color:'#e0b070'})}\nfor(let i=0;i<3;i++){const cx=-i*2.4,b=Math.abs(Math.sin(t*3+i))*0.12;mesh3d(body,{x:cx,y:1.1+b,color:'#c89060'});mesh3d(hump,{x:cx,y:1.45+b,color:'#b07a50'});mesh3d(head,{x:cx+0.9,y:1.6+b,color:'#c89060'});for(const d of[-0.5,0.5])mesh3d(leg,{x:cx+d,y:0.4,rz:Math.sin(t*6+i+d)*0.3,color:'#a87048'})}\nconst b0=Math.abs(Math.sin(t*3))*0.12,me=clawd3d(-0.1,1.55+b0,0,{size:0.7,facing:1,stride:-1,arms:mod(t,20)>13?'up':'down'})\nconst p=mod(t,20),cyc=Math.floor(t/20)%2,ox=26-p*1.6,real=cyc==1||p<11\nif(real&&ox>-20){const sh=cyc==0&&p>8?(Math.sin(t*20)>0?'#80c0ff':'#ffe0a0'):null;mesh3d(pool,{x:ox,z:-7,color:sh||'#60a8e0'});mesh3d(trunk,{x:ox,z:-7,color:sh||'#a07040'});mesh3d(leaf,{x:ox,y:2.1,z:-7,color:sh||'#60c060'});const q=project(ox,3,-7);if(q)text(q.x-5,q.y-1,'{what}','#fff2c0')}\nif(me){const s=cyc==0?(p<8?'Oasis {what} dead ahead. Water, finally!':p<13?'It is... shimmering. Bad sign.':'Mirage. Wrong match, keep grepping.'):(p<12?'Second lead on {what}. Fingers crossed.':'Real palms! Found where it lives.');say(s,me.top.x,me.row)}}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#ff9a4a","#ffcf70","#c06040"],"speed":0.4,"intensity":0}}},
37  {"phase":"reading","world":"circus","style":"3D","concept":"ufo abduction over a circus tent at night: Clawd's flying saucer beams up crates labeled {what} one by one to read them, 3D","source":"claude-opus-5-5","scene":{"concept":"ufo abduction over a circus tent at night: Clawd's flying saucer beams up crates labeled {what} one by one to read them, 3D","code":"const tent=cone(3,2.5,10),pole=cylinder(0.06,3.4,5),saucer=sphere(1,12),dome=sphere(0.5,10),crate=box(0.7,0.7,0.7),ground=plane(40,20);\nfunction frame(t,dt){camera(0,2.2,9,0,1.6,0,80);light(0.3,1,0.5,0.35);fog(8,35,'#000');\nfor(let i=0;i<40;i++)point3d(rand(i)*40-20,3+rand(i+7)*5,-12,'#aab');\nmesh3d(ground,{y:0,z:-3,color:'#4a6a3a'});\nmesh3d(tent,{x:-5,z:-3,color:'#e05a5a'});mesh3d(pole,{x:-5,y:2.4,z:-3,color:'#ddd'});\nmesh3d(tent,{x:6,z:-4,scale:0.7,color:'#e0c050'});\nconst sx=Math.sin(t*0.3)*1.5,sy=3.3+Math.sin(t*1.3)*0.1;\nconst cyc=t%6,k=Math.floor(t/6);\nconst lift=smoothstep(1,4.5,cyc);\nif(cyc>0.8&&cyc<5){for(let i=0;i<14;i++){const a=i/14*6.28,r=0.25+(1-(i%2))*0.4;line3d(sx,sy-0.2,0,sx+Math.cos(a)*1.1,0,Math.sin(a)*0.6,i%2?'#9fe8a0':'#cfffb0');}}\nconst cy=lerp(0.35,sy-0.5,lift);\nif(cyc<5)mesh3d(crate,{x:sx*lift,y:cy,z:0,ry:t*1.5*lift,color:'#d9a066'});\nfor(let j=0;j<3;j++)mesh3d(crate,{x:2.5+j*0.8,y:0.35,z:0.5,color:'#c08850'});\nmesh3d(saucer,{x:sx,y:sy,z:0,scale:1,color:'#b0c0d0'});\nmesh3d(dome,{x:sx,y:sy+0.5,z:0,color:'#90e0ff'});\nfor(let i=0;i<6;i++){const a=t*2+i;const p=project(sx+Math.cos(a)*1.6,sy-0.1,Math.sin(a)*0.5);if(p)put(p.x,p.y,'●',hsl((i*60+t*90)%360,0.9,0.6));}\nconst p=project(sx*lift,cy+0.6,0);if(p&&cyc<5)text(p.x-4,p.y,'{what}','#ffe9a0');\nconst c=project(sx,sy+0.6,0);if(c){clawd(c.x-7,c.y-3,{scale:1,arms:'up',look:{x:0,y:2}});}\nconst lines=['Beam it up. {what} goes in for study.','Probing {what}... fascinating logic.','Next specimen: more {what}!'];\nif(c&&cyc>1.5&&cyc<4.5)say(lines[k%3],c.x,c.y-3);}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#99a"],"speed":0.3,"intensity":0}}},
38  {"phase":"reading","world":"mail room","style":"text art","concept":"ufo abduction, text art: a saucer hovers over a moonlit field beaming crates labeled {what} up one by one to study them, while Clawd in an a","source":"claude-opus-5-5","scene":{"concept":"ufo abduction, text art: a saucer hovers over a moonlit field beaming crates labeled {what} up one by one to study them, while Clawd in an antenna headband steers it from the ground","code":"const cx=[8,30,52,74];function frame(t,dt){for(let i=0;i<40;i++)put(rand(i)*w,rand(i+7)*4,fract(t*0.3+rand(i+3))<0.1?'+':'.','#8899cc');for(let x=0;x<w;x++)put(x,8,x%5?'▒':'░','#3a6b3a');const P=8,ph=mod(t,P),i=Math.floor(t/P),j=i%4,pj=(i+3)%4;const ux=lerp(cx[pj],cx[j],smoothstep(0,2,ph))-2;for(let k=0;k<4;k++){if(k==j&&ph>3)continue;sprite(cx[k],6,'┌───┐\\n└───┘','#c9a26b');}if(ph>2&&ph<6.5){for(let y=3;y<8;y++)for(let x=-y+3;x<=y-3+4;x++)if(fract(x*0.37+y*0.21+t*2)<0.5)put(ux+4+x-2+1,y,'░','#7fffb0');}if(ph>3&&ph<6.2){const cy=6-smoothstep(3,6,ph)*3.5;sprite(cx[j],cy,'┌───┐\\n└───┘','#ffd27a');text(cx[j]-1,cy-1,'{what}','#ffffff');}else if(ph<=3)text(cx[j]-1,5,'{what}','#ffffff');sprite(ux,0,' .-===-. \\n(=o=o=o=)\\n \\'-----\\' ','#b0c4ff');for(let k=0;k<3;k++)put(ux+2+k*2,1,'o',fract(t*3+k/3)<0.5?'#ff6bd6':'#6bffe0');if(ph>6.2&&ph<8)text(ux+10,1,'scan '+'▮'.repeat(Math.floor((ph-6.2)*4)),'#7fffb0');const c=clawd(w-22,4,{facing:-1,arms:ph>2&&ph<6.5?'up':'down'});put(c.top.x-2,c.top.row-1,'╲','#9fff9f');put(c.top.x+2,c.top.row-1,'╱','#9fff9f');put(c.top.x-3,c.top.row-1,'●','#ff6bd6');put(c.top.x+3,c.top.row-1,'●','#ff6bd6');if(mod(t,24)>3&&mod(t,24)<9)say('Take me to your {what}. I want to read it.',c.top.x,c.row-1);else if(mod(t,24)>14&&mod(t,24)<20)say('Probing {what}. Painless, just a few functions.',c.top.x,c.row-1);}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#557","#99b"],"speed":0.4,"intensity":0}}},
39  {"phase":"reading","world":"medieval castle","style":"3D","concept":"construction site 3D: hard-hatted Clawd inspects a swinging crate labeled {what} as a crane hauls it past a steel skeleton, reading every be","source":"claude-opus-5-5","scene":{"concept":"construction site 3D: hard-hatted Clawd inspects a swinging crate labeled {what} as a crane hauls it past a steel skeleton, reading every beam","code":"const col=box(0.25,3,0.25),bm=box(3,0.2,0.25),cr=box(1,0.8,0.8),tw=box(0.35,3.6,0.35),jb=box(11,0.25,0.25),gr=plane(40,16);\nconst L=['Hoisting {what} up for a closer look.','Load-bearing function spotted. Do not remove.','Checking every rivet in {what}.','Ah, so THAT holds up the second floor.'];\nfunction frame(t,dt){camera(Math.sin(t*0.1)*1.5,2.4,9,0,1.4,0,90);fog(10,40,'#000');light(0.5,1,0.6,0.35);\nmesh3d(gr,{y:0,color:'#8a7a60'});\nfor(const x of [-9,-6,-3])mesh3d(col,{x,y:1.5,z:-1,color:'#d0a070'});\nfor(let f=1;f<4;f++)for(const x of [-7.5,-4.5]){if(f==3&&x==-4.5&&fract(t*0.05)<0.5)continue;mesh3d(bm,{x,y:f,z:-1,color:'#c08850'});}\nmesh3d(tw,{x:6,y:1.8,z:-1,color:'#f0c040'});mesh3d(jb,{x:3,y:3.6,z:-1,color:'#f0c040'});\nconst hx=3+Math.sin(t*0.3)*4.5,hy=2+Math.sin(t*0.7)*0.4;\nline3d(hx,3.5,-1,hx,hy+0.4,-1,'#ccc');mesh3d(cr,{x:hx,y:hy,z:-1,ry:Math.sin(t*0.9)*0.4,color:'#e09060'});\nconst p=project(hx,hy,-1);if(p)text(Math.round(p.x-5),p.y,'{what}','#fff8d0');\nconst cx=-1+Math.sin(t*0.3)*2.5;const c=clawd3d(cx,0,2,{facing:Math.cos(t*0.3)>0?1:-1,stride:Math.floor(t*5)%4,arms:fract(t*0.2)<0.5?'up':'down'});\nif(c){pixels(c.top.x-4,c.top.py-3,' yyyyyy \\nyyyyyyyy\\nYYYYYYYYYY',{y:'#ffd23f',Y:'#e0a800'});if(fract(t/8)<0.7)say(L[Math.floor(t/8)%4],c.top.x,c.row);}\n}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#778"],"speed":0.3,"intensity":0}}},
40  {"phase":"reading","world":"weather report","style":"3D","concept":"weather report: Clawd as TV weatherman pointing at a slowly orbiting 3D map where a cloud front labeled {what} rolls in, rains, then clears ","source":"claude-opus-5-5","scene":{"concept":"weather report: Clawd as TV weatherman pointing at a slowly orbiting 3D map where a cloud front labeled {what} rolls in, rains, then clears to sunshine","code":"const ground=plane(16,7),cl=sphere(0.7,10),sun=sphere(0.8,12),hill=cone(0.9,1.2,8),tw=cylinder(0.15,1.6,6);\nconst cs=Array.from({length:5},(_,i)=>({o:i*1.3,z:-1.5+rand(i)*3,s:0.8+rand(i+7)*0.6}));\nfunction frame(t,dt){const a=Math.sin(t*0.15)*0.4;camera(Math.sin(a)*10,4.5,Math.cos(a)*10,1,0,0,70);light(0.4,1,0.5,0.35);fog(8,30,'#000');\nfor(let x=0;x<w;x++)put(x,h-1,'▀','#335','#112');\nmesh3d(ground,{color:'#6a9'});mesh3d(hill,{x:-4,z:-1,color:'#9b7'});mesh3d(hill,{x:4.5,z:1,color:'#9b7',scale:0.8});mesh3d(tw,{x:1,z:0.5,color:'#ccd'});\nconst ph=mod(t,40),k=smoothstep(28,36,ph);const fx=lerp(-9,1.5,smoothstep(0,10,ph))+k*12;\nconst sx=5.5,sh=smoothstep(26,34,ph);mesh3d(sun,{x:sx,y:1.5+sh*1.5,z:-2,color:'#fd5',unlit:true});\nfor(const c of cs){const x=fx+c.o-2.6;mesh3d(cl,{x,y:2.2,z:c.z,scale:c.s,color:k>0.5?'#eef':'#aab'});\nif(ph>10&&ph<30)for(let j=0;j<3;j++){const yy=2-fract(t*1.5+j*0.33+c.o)*2;line3d(x+j*0.3-0.3,yy,c.z,x+j*0.3-0.4,yy-0.4,c.z,'#7af')}}\nconst p=project(fx,3.4,0);if(p)text(p.x-6,Math.max(0,p.y),'{what} front','#fff');\nfill(0,0,19,h-1,' ',null,'#1a2238');text(1,0,'CLAWD WX 7',ph>10&&ph<30?'#7af':'#fd5');\nconst arm=Math.sin(t*2)>0?'out':'up';const c=clawd(3,3,{facing:1,arms:{left:'down',right:arm}});\nconst msg=ph<10?'High-pressure {what} moving in. Reading it closely.':ph<20?'Heavy logic showers over {what}. Bring a debugger.':ph<30?'Scattered callbacks, 80% chance of insight.':'Clearing up! {what} finally makes sense.';\nif(mod(ph,10)>1)say(msg,c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#223355","#334477","#225566"],"speed":0.4,"intensity":0}}},
41  {"phase":"reading","world":"bowling alley","style":"3D","concept":"submarine: Clawd rides atop a sub sweeping sonar pings across the seabed, closing in on a sunken wreck stamped {what}","source":"claude-opus-5-5","scene":{"concept":"submarine: Clawd rides atop a sub sweeping sonar pings across the seabed, closing in on a sunken wreck stamped {what}","code":"const hull=cylinder(0.7,3,12),tw=box(0.7,0.7,0.9),rk=sphere(1,6),wr=box(4,1.2,1.6),ms=cylinder(0.08,2.6,5),fl=plane(80,80)\nconst rocks=Array.from({length:7},(_,i)=>({x:-12+i*4+rand(i)*2,z:-2-rand(i+9)*10,s:0.4+rand(i+5)*0.6}))\nconst lines=['Diving into {what}. Lights on.','Ping... something big down there.','Every function has a shipwreck story.','Ah, so THAT is where the data goes.','Mapping the hull, one call at a time.']\nfunction frame(t,dt){fill(0,0,w,h,' ',null,'#021626')\ncamera(Math.sin(t*0.1)*1.5,2.6,9,1,1,-2,90)\nfog(6,32,'#021626')\nlight(0.3,1,0.5,0.35)\nmesh3d(fl,{y:0,color:'#c9b48a'})\nfor(const r of rocks)mesh3d(rk,{x:r.x,y:0,z:r.z,scale:r.s,color:'#8a9a8a'})\nmesh3d(wr,{x:6,y:0.5,z:-4,ry:0.3,rz:0.12,color:'#b08a66'})\nmesh3d(ms,{x:6,y:1,z:-4,rz:0.45,color:'#9a7a5a'})\nconst sx=lerp(-9,1.5,smoothstep(0,25,t)),sy=2+Math.sin(t*0.8)*0.15\nmesh3d(hull,{x:sx+1.5,y:sy,z:0,rz:Math.PI/2,color:'#e8c84a'})\nmesh3d(tw,{x:sx,y:sy+0.9,z:0,color:'#d9b83a'})\nconst me=clawd3d(sx,sy+1.25,0,{size:0.6,facing:1,arms:fract(t*0.2)<0.3?'out':'down'})\nconst pr=fract(t*0.35)*12\nfor(let i=0;i<28;i++){const a=i/28*6.283;point3d(sx+1.6+Math.cos(a)*pr,sy,Math.sin(a)*pr,mix('#7fffd4','#021626',pr/12))}\nfor(let i=0;i<6;i++)point3d(sx-1.8-rand(i)*0.5,sy+fract(t*0.5+rand(i+3))*3,0,'#bfefff')\nconst hit=Math.abs(pr-5)<1.2\nline3d(sx+3,sy,0,6,0.8,-4,'#fff3a0')\nconst p=project(6,2,-4)\nif(p)text(p.x-4,p.y-1,'{what}',hit?'#ffffff':'#ffd27f')\nif(me&&t>2)say(lines[Math.floor((t-2)/9)%lines.length],me.top.x,me.row)}","actors":[],"particles":[],"background":{"effect":"waves","palette":["#0a3a5a","#0f5a7a","#1a7a9a"],"speed":0.5,"intensity":0}}},
42  {"phase":"reading","world":"fairy tale forest","style":"3D","concept":"race track pit stop: Clawd as pit chief reading the engine of a 3D race car labeled {what}, lap after lap of cars streaking past","source":"claude-opus-5-5","scene":{"concept":"race track pit stop: Clawd as pit chief reading the engine of a 3D race car labeled {what}, lap after lap of cars streaking past","code":"const car=box(3,0.6,1.4),cab=box(1.2,0.5,1),wh=cylinder(0.35,0.3,10),crate=box(0.8,0.8,0.8),post=cylinder(0.1,2.5,6);\nfunction frame(t,dt){camera(Math.sin(t*0.15)*2,2.6,9,0,0.8,0,80);light(0.4,1,0.6,0.35);fog(10,45,'#000');\nfor(let i=-6;i<=6;i++){line3d(i*4,0,-3,i*4+2,0,-3,'#ddd');}\nline3d(-30,0,-1.5,30,0,-1.5,'#888');line3d(-30,0,-8,30,0,-8,'#888');\nfor(let i=0;i<3;i++){const x=mod(t*30+i*25,90)-45;mesh3d(car,{x,y:0.4,z:-5,color:['#e55','#5ae','#ec4'][i]});mesh3d(cab,{x:x-0.3,y:0.9,z:-5,color:'#ddd'});}\nconst lift=0.3+0.15*smoothstep(1,2,t);mesh3d(car,{x:-2,y:lift,z:1,color:'#d43'});mesh3d(cab,{x:-2.4,y:lift+0.5,z:1,color:'#eee'});\nfor(const[a,b]of[[-3,0.3],[-1,0.3],[-3,1.7],[-1,1.7]])mesh3d(wh,{x:-2+a+2-1,y:lift-0.3,z:b,rx:1.57,ry:0,rz:0,color:'#666'});\nconst hood=Math.sin(Math.min(1,smoothstep(2,3,t))*1.2);mesh3d(box(1,0.06,1.2),{x:-0.8,y:lift+0.35+hood*0.4,z:1,rz:-hood,color:'#f86'});\nfor(let i=0;i<3;i++)mesh3d(crate,{x:5+i*1.1,y:0.4,z:0,color:'#c94'});mesh3d(post,{x:8,z:-1,color:'#aaa'});\nconst p=project(-2,lift+0.3,1.8);if(p)text(p.x-4,p.y,'{what}','#fff');\nconst cx=0.6+Math.sin(t*0.6)*0.3;const c=clawd3d(cx,0,1.8,{facing:-1,arms:fract(t*0.5)<0.5?'out':{left:'up',right:'down'},size:1});\nif(c){pixels(c.top.x-3,c.top.py-2,'.######.\\n########','#ccc'==0?{}:{'#':'#3af'});\nconst L=['Popping the hood on {what}...','Ooh, the gears mesh here.','Tracing that fuel line back.','Every lap I understand more.'];say(L[Math.floor(t/7)%4],c.top.x,c.row);}\nconst fl=Math.floor(t*4)%2;text(w-10,0,fl?'▓░▓░▓░':'░▓░▓░▓','#eee');}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#222","#333","#422"],"speed":0.5,"intensity":0}}},
43  {"phase":"reading","world":"fishing pond","style":"pixel art","concept":"bowling alley: Clawd bowls frame after frame down a lane named {what}, each strike knocking over pins and adding a mark to a growing scorebo","source":"claude-opus-5-5","scene":{"concept":"bowling alley: Clawd bowls frame after frame down a lane named {what}, each strike knocking over pins and adding a mark to a growing scoreboard of understood lines","code":"const P=12;let pins=[];for(let r=0;r<4;r++)for(let j=0;j<=r;j++)pins.push({r,j});\nconst ball=[' ## ','####','####',' ## '],pin=['.w.','www','.r.','www','www'];\nfunction frame(t,dt){const cy=Math.floor(t/P),ph=t%P,L=20,R=w-14;\nfor(let x=0;x<w;x++){put(x,8,'▀','#c8914a','#7a4f24');put(x,5,'─','#4a3a2a');}\nfor(let x=L;x<w;x+=8)put(x,7,'▲','#e8c070');\ntext(Math.floor(w/2)-12,0,'╔═ LANE: {what} ═╗','#9fd0ff');\nlet sc='';for(let i=0;i<10;i++)sc+=i<cy?'[X]':(i==cy&&ph>4.2?'[X]':'[ ]');text(Math.floor(w/2)-15,1,sc,'#ffd27a');\nconst roll=smoothstep(1.5,4.2,ph),bx=lerp(L,R-6,roll),hit=ph>4.2,set=ph>8;\nconst px=R,base=10;\nfor(const p of pins){let x=px+p.r*3,y=base-p.j*2-1+p.r;let dx=0,dy=0;if(hit&&!set){const k=Math.min(1,(ph-4.2)*2);dx=(p.r+1)*3*k*(rand(p.r*7+p.j+cy)-0.3);dy=-4*k*rand(p.j+p.r*3)+k*3}\nif(!set||ph>9.5)pixels(Math.round(x+dx),Math.round(y+dy),pin.join('\\n'),{w:'#f4f4f4',r:'#e04848'});}\nif(set){const d=ph<9.5?smoothstep(8,9,ph)*6:6-smoothstep(9.5,11,ph)*6;fill(R-2,0,12,Math.floor(d/2)+1,'▓','#6a6a7a');}\nif(ph>1.2&&!hit)pixels(Math.round(bx),11,ball.join('\\n'),{'#':hsl(220+cy*40,0.7,0.6)});\nconst wind=ph<1.5?smoothstep(0.3,1.5,ph):0;const c=clawd(4-wind*2+(ph<1.8&&ph>1.4?3:0),4,{pose:'stand',facing:1,arms:ph<1.5?{left:'down',right:wind>0.5?'down':'out'}:{left:'down',right:'out'},eyes:hit&&ph<5.5?'wide':'open'});\nif(ph<1.3)pixels(c.right.x,c.right.py-1,ball.join('\\n'),{'#':hsl(220+cy*40,0.7,0.6)});\nconst lines=['Lining up on {what}. Reading the lane first.','Strike! That function finally makes sense.','Pinsetter resets. Next block of {what}...','Another one down. The logic is clicking.'];\nif(ph>4.5&&ph<7.8)say(lines[cy%4],c.top.x,c.row);}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#2a1a3a","#3a2050","#1a2a4a"],"speed":0.5,"intensity":0.2}}},
44  {"phase":"editing","world":"zen rock garden","style":"3D","concept":"ant colony up close, 3D: a column of ants hauls code crumbs into a growing anthill labeled {what} while foreman Clawd directs the build","source":"claude-opus-5-5","scene":{"concept":"ant colony up close, 3D: a column of ants hauls code crumbs into a growing anthill labeled {what} while foreman Clawd directs the build","code":"const hill=cone(2.2,1.6,10),body=sphere(0.18,6),crumb=box(0.3,0.25,0.3),peb=sphere(0.4,6);\nconst ants=Array.from({length:9},(_,i)=>({o:i/9}));\nfunction frame(t,dt){camera(Math.sin(t*0.1)*1.5,1.4,7,0,0.6,-2,80);light(0.4,1,0.6,0.35);fog(8,35,'#000');\nmesh3d(plane(40,30),{z:-6,color:'#a08060'});\nfor(let i=0;i<7;i++)mesh3d(peb,{x:-9+i*3.2+rand(i)*1.5,y:0.2,z:-5-rand(i+5)*6,color:'#b0a898'});\nconst g=1+Math.min(t,90)*0.006;mesh3d(hill,{x:-3,z:-2,scale:g,color:'#c89a5a'});\nmesh3d(sphere(0.35,8),{x:-3,y:1.6*g,z:-1.8,color:'#3a2a1a',unlit:true});\nfor(const a of ants){const k=fract(a.o+t*0.07),x=lerp(9,-2.4,k),z=-1+Math.sin(k*9)*0.4,b=Math.abs(Math.sin(t*12+a.o*20))*0.05;\nmesh3d(body,{x,y:0.18+b,z,color:'#c0503a'});mesh3d(body,{x:x-0.3,y:0.2+b,z,scale:0.8,color:'#d0604a'});mesh3d(body,{x:x+0.32,y:0.18+b,z,scale:1.1,color:'#c0503a'});\nmesh3d(crumb,{x,y:0.5+b,z,ry:t+a.o*5,color:'#f0d070'});}\nconst p=project(-3,0.9*g,-1);if(p)text(p.x-4,p.y,'{what}','#fff4c0');\nconst c=clawd3d(1.5,0,0.8,{facing:1,arms:fract(t*0.5)<0.5?{left:'up',right:'out'}:'out',size:0.9});\nif(c){pixels(c.top.x-3,c.top.py-3,'.yyyyy.\\nyyyyyyy\\nooooooo','#'===''?{}:{y:'#ffd23a',o:'#e0a000'});\nif(t>2&&t<9)say('Every crumb a line of {what}. Hup, hup!',c.top.x,c.row);\nif(t>20&&t<28)say('Tunnel holds. Next ant, next edit.',c.top.x,c.row);\nif(t>45&&t<53)say('{what} is getting tall. Ants unionizing.',c.top.x,c.row);}}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#3a2a1a","#4a3520","#2a1e10"],"speed":0.5,"intensity":0}}},
45  {"phase":"editing","world":"cyberpunk street","style":"pixel art","concept":"race track pit stop: Clawd in a helmet as pit crew, swapping a worn tire on the {what} car before it screams back out, lap after lap","source":"claude-opus-5-5","scene":{"concept":"race track pit stop: Clawd in a helmet as pit crew, swapping a worn tire on the {what} car before it screams back out, lap after lap","code":"const car=['..........rrrrrrr...........','.........rrbbbbbrr........rr','rrrrrrrrrrrrrrrrrrrrrrrrrrrr','rryyyyyyyyyyyyyyyyyyyyyyyyrr','rrrrrrrrrrrrrrrrrrrrrrrrrrrr'];const pal={r:'#e04040',b:'#80c8ff',y:'#ffd040'};const tire=['.kkkk.','kkggkk','kkggkk','.kkkk.'];function frame(t,dt){const u=t%10,lap=Math.floor(t/10)+1;for(let x=0;x<w;x++){put(x,0,'▓',hsl((x*37)%360,0.5,0.55));put(x,1,x%2?'o':'∩',hsl((x*53+t*40)%360,0.6,0.6));put(x,2,'═','#888');put(x,8,(x+Math.floor(t*0))%8<4?'▀':' ','#ddd','#333')}fill(0,3,w,5,' ',null,'#1a1a22');for(let x=0;x<w;x+=12)text(x,3,'│ PIT │','#555');sprite(100,4,'┌──────┐\\n│LAP '+String(lap).padStart(2)+'│\\n└──┬───┘\\n   │',  '#ffd040');let cx=u<2?lerp(-40,60,smoothstep(0,2,u)):u<7.5?60:lerp(60,160,smoothstep(7.5,9,u));pixels(cx,10,car.join('\\n'),pal);text(cx+9,6,'{what}'.slice(0,16),'#201000','#ffd040');const fx=cx+21,rx=cx+1;pixels(fx,14,tire.join('\\n'),{k:'#222',g:'#aaa'});let ox=rx,oc='#777';if(u>=3&&u<7.5){ox=rx-(u-3)*14;oc='#666'}if(!(u>=4.5&&u<5.5))pixels(ox,14,tire.join('\\n'),{k:u>=3&&u<7.5?'#555':'#222',g:oc});if(u>=5.5&&u<7.5||u<3||u>=7.5)if(u>=5.5&&u<7.5)pixels(rx,14,tire.join('\\n'),{k:'#222',g:'#ff0'});const px=u<2?40:u<7.5?44:40;const c=clawd(px,4,{facing:1,arms:u>3&&u<6?(Math.floor(u*6)%2?'out':'down'):(u>6&&u<7.5?'up':'down'),stride:-1,blink:fract(t*0.3)<0.05});pixels(c.top.x-4,c.top.py-3,'..wwwww..\\n.wwrrrww.\\nwwwwwwwww',{w:'#f0f0f0',r:'#e04040'});if(u>3&&u<6)for(let i=0;i<3;i++)put(px+15+i,5+Math.floor(rand(i+Math.floor(t*10))*2),'*','#ffa040');if(u>6&&u<7.5)say(lap%2?'Fresh rubber on {what}. GO GO GO!':'Patched {what} in 4.2 seconds. New record!',c.top.x,c.row);if(u>7.5)for(let i=0;i<6;i++)put(cx-2-i*3,6+(i%2),'~','#999')}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#331111","#442222","#333333"],"speed":0.5,"intensity":0}}},
46  {"phase":"editing","world":"wild west","style":"pixel art","concept":"train station: Clawd as rail mechanic riding atop a stop-and-go freight line, welding each rusty {what} car green before it rolls out","source":"claude-opus-5-5","scene":{"concept":"train station: Clawd as rail mechanic riding atop a stop-and-go freight line, welding each rusty {what} car green before it rolls out","code":"const P=8,S=26,L=['Next car: {what}. Hold still, this will sting.','Rust out, patch in. {what} is good to go.','All aboard the {what} fix express!']\nfunction frame(t){const n=Math.floor(t/P),ph=t%P,m=smoothstep(5.5,8,ph);for(let x=0;x<w;x++){put(x,8,x%3?'═':'╪','#b8865a');}for(let k=0;k<5;k++){const lx=mod(k*31-t*0,w);put(lx,0,'●','#ffd060');put(lx,1,'│','#889');put(lx,2,'│','#889');put(lx,3,'│','#889');}text(2,1,'PLATFORM 9','#9cf');for(let i=n-2;i<=n+4;i++){const x=Math.round(w/2-12+(i-n)*S-S*m),f=i<n||(i==n&&ph>4.5),c=f?'#4fb87a':'#b5653d',d=f?'#2f7a50':'#7a3e22';fill(x,4,24,3,' ',null,c);fill(x,4,24,1,'▀',d,c);const lb='{what}';text(x+12-Math.floor(lb.length/2),5,lb,'#fff',c);text(x+2,7,'(●)',  '#ccc');text(x+19,7,'(●)','#ccc');text(x+24,6,'══','#999');}const cx=w/2-7,wd=ph>1.5&&ph<4.5;const c=clawd(cx,0,{facing:0,arms:wd?(Math.floor(t*6)%2?'out':'down'):'down',eyes:wd?'closed':'open'});pixels(c.top.x-3,c.top.py-2,'.yyyyy.\\nyyyyyyy',{y:'#ffcc33'});if(wd){for(let s=0;s<6;s++)put(c.right.x+1+Math.random()*4,3+Math.random()*1.5,'*·✦'[s%3],s%2?'#ffee88':'#ff9933');put(c.right.x,c.right.row,'╲','#ccc');}if(ph>4.6&&ph<7.5)say(L[n%3],c.top.x,c.row);}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#1a2a4a","#2a3a6a","#3a2a5a"],"speed":0.4,"intensity":0}}},
47  {"phase":"editing","world":"race track pit stop","style":"3D","concept":"dinosaur era, 3D: a stegosaurus thunders into a stone-age pit stop and Clawd hot-swaps its cracked back plate (stamped {what}) for a fresh o","source":"claude-opus-5-5","scene":{"concept":"dinosaur era, 3D: a stegosaurus thunders into a stone-age pit stop and Clawd hot-swaps its cracked back plate (stamped {what}) for a fresh one before it roars off, lap after lap","code":"const B=box(4,1.4,1.6),L=cylinder(0.3,1.1,6),P=box(0.12,0.9,0.8),N=box(1.6,0.5,0.5),Hd=box(0.9,0.6,0.6),T=box(2.6,0.4,0.4),G=plane(70,30),V=cone(3,5,8),S=box(1.2,0.6,1.2);\nconst ss=(a,b,v)=>smoothstep(a,b,v);\nfunction frame(t,dt){camera(Math.sin(t*0.1)*1.5,3,9,0,1.3,0,80);light(0.4,1,0.6,0.35);fog(10,40,'#000');\nmesh3d(G,{color:'#8a7650'});mesh3d(V,{x:-12,z:-20,color:'#b0654a'});mesh3d(V,{x:14,z:-24,scale:0.7,color:'#9a6a5a'});\nmesh3d(S,{x:-5,y:0.3,z:2,color:'#a9a9a0'});mesh3d(S,{x:5,y:0.3,z:2,color:'#a9a9a0'});\nconst c=t%12,lap=Math.floor(t/12);const dx=c<9?lerp(-26,0,ss(0,3,c)):lerp(0,26,ss(9,12,c));\nconst col='#7fa86a';mesh3d(B,{x:dx,y:1.7,color:col});\nfor(const a of [-1.3,1.3])for(const z of [-0.5,0.5])mesh3d(L,{x:dx+a,z,color:'#6a9258'});\nmesh3d(N,{x:dx+2.5,y:2.4,rz:0.6,color:col});mesh3d(Hd,{x:dx+3.3,y:3,color:'#8fb87a'});mesh3d(T,{x:dx-3.2,y:1.5,rz:0.3,color:col});\nfor(let i=0;i<5;i++){let y=3,cl='#d8b070',x=dx-1.6+i*0.8;if(i==2){if(c<4.5){y=3+ss(3,4.5,c)*5;x+=ss(3,4.5,c)*-4;cl='#d06a5a'}else{y=3+(1-ss(5,7,c))*5;cl='#9be07a'}}mesh3d(P,{x,y,color:cl});if(i==2){const p=project(x,y+0.8,0);if(p)text(p.x-3,p.y-1,'{what}',c<4.5?'#ffb0a0':'#d0ffc0')}}\nconst sw=c>2.8&&c<8;const me=clawd3d(0.6,0,2.4,{facing:sw?0:(dx<0?-1:1),arms:sw?'up':'down',eyes:c>7&&c<9?'wide':'open',size:1});\nif(me){if(c>3&&c<5)say('Old {what} plate: cracked. Swapping it!',me.top.x,me.row);else if(c>7&&c<9.5)say(lap%2?'Patched in 4 seconds. Pit record!':'Fresh plate, bolted on. GO GO GO!',me.top.x,me.row)}}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#553","#664","#775"],"speed":0.3,"intensity":0}}},
48  {"phase":"editing","world":"bakery","style":"3D","concept":"circus big top in 3D: Clawd as tightrope walker crossing the high wire, dropping each edited block of {what} onto a growing acrobat stack in","source":"claude-opus-5-5","scene":{"concept":"circus big top in 3D: Clawd as tightrope walker crossing the high wire, dropping each edited block of {what} onto a growing acrobat stack in the ring","code":"const bx=box(0.9,0.6,0.9),pole=cylinder(0.1,3,6),ring=cylinder(5,0.2,24),sb=box(0.5,0.35,0.5);\nconst lines=['Steady... one edit to {what} on the wire.','Drop! Lands right on the stack.','No net. Just tests.','Hold my claws, adding another change.'];\nfunction frame(t,dt){camera(Math.sin(t*0.2)*2,2.2,10,0,1.4,0,90);light(0.4,1,0.6,0.3);fog(10,40,'#000');\nconst P=6,c=Math.floor(t/P),ph=fract(t/P),n=c%4;\nmesh3d(ring,{color:'#d04848'});mesh3d(pole,{x:-6,color:'#eeeeee'});mesh3d(pole,{x:6,color:'#eeeeee'});line3d(-6,2.6,0,6,2.6,0,'#ffdd88');\nfor(let i=0;i<n;i++)mesh3d(bx,{y:0.5+i*0.6,ry:i*0.3,color:hsl(40+i*90,0.7,0.6)});\nconst cx=lerp(-5.5,5.5,ph);\nconst me=clawd3d(cx,2.6,0,{facing:1,stride:Math.floor(t*6)%4,size:0.6,arms:ph<0.5?'up':'out'});\nconst tg=0.5+n*0.6,k=smoothstep(0.5,0.65,ph),bxx=ph<0.5?cx:0,by=ph<0.5?3.5:lerp(3.5,tg,k*k);\nif(n<3)mesh3d(sb,{x:bxx,y:by,ry:t,color:hsl(40+n*90,0.7,0.65)});\nconst p=project(bxx,by+0.4,0);if(p&&n<3)text(p.x-4,p.y,'{what}','#ffffcc');\nif(me&&ph>0.1&&ph<0.45)say(lines[c%4],me.top.x,me.row);}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#401020","#602030","#803040"],"speed":0.5,"intensity":0}}},
49  {"phase":"editing","world":"pirate ship","style":"3D","concept":"outer space pirate ship: Clawd as space-pirate shipwright welding new hull plates onto a 3D galleon drifting past a ringed planet","source":"claude-opus-5-5","scene":{"concept":"outer space pirate ship: Clawd as space-pirate shipwright welding new hull plates onto a 3D galleon drifting past a ringed planet","code":"const hull=box(6,1,1.6),mast=cylinder(0.08,2.4,6),sail=box(1.6,1.2,0.05),plate=box(0.7,0.5,0.1),pl=sphere(3,16),ring=cylinder(4.6,0.02,24);\nlet n=0;\nfunction frame(t,dt){\ncamera(Math.sin(t*0.1)*2,2.2,9,0,1,0,80);light(0.5,1,0.6,0.35);fog(10,50,'#000');\nfor(let i=0;i<70;i++)put(Math.floor(rand(i)*w),Math.floor(rand(i+9)*h),rand(i+3)<0.3?'✦':'.',rand(i+5)<0.5?'#99a':'#668');\nmesh3d(pl,{x:-10,y:1,z:-26,color:'#c98ad8',ascii:true,ry:t*0.2});\nmesh3d(ring,{x:-10,y:1,z:-26,rx:0.4,rz:0.2,color:'#e8c070',wire:true});\nconst by=Math.sin(t*0.8)*0.15;\nmesh3d(hull,{x:1,y:0.2+by,z:0,color:'#b08050'});\nfor(const mx of [-0.5,2])mesh3d(mast,{x:mx,y:0.7+by,z:0,color:'#ddc'});\nfor(const mx of [-0.5,2])mesh3d(sail,{x:mx,y:2+by,z:0.1,color:'#eee',ry:Math.sin(t)*0.1});\nn=Math.min(6,Math.floor(t/4));\nfor(let i=0;i<6;i++){const done=i<n;const k=done?1:(i==n?fract(t/4):0);if(i>n)continue;\nmesh3d(plate,{x:-1.5+i*0.9,y:0.25+by+(1-k)*2.5,z:0.85,color:done?'#9ad0ff':mix('#ffd060','#9ad0ff',k)});}\nconst p=project(1,1.4+by,0.9);if(p)text(p.x-5,p.y-3,'☠ {what}','#ffcc66');\nconst cx=-1.5+Math.min(n,5)*0.9;\nconst c=clawd3d(cx,1.3+by,1.4,{facing:0,arms:fract(t)<0.5?'up':'out',size:0.8});\nif(c){if(fract(t*2)<0.5){put(c.right.x+1,c.right.row,'*','#fff6a0');put(c.right.x+2,c.right.row+1,'·','#ffb040');}\npixels(c.top.x-3,c.top.py-3,'..###..\\n.#####.\\n#######','{\"#\":\"#222\"}');\nconst s=['Patching {what} plank by plank. Arr!','Hull breach sealed. Space is very drafty.','No bugs aboard. Well, fewer bugs.'];\nif(t>2)say(s[Math.floor(t/9)%3],c.top.x,c.row);}\n}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#99a"],"speed":0.4,"intensity":0}}},
50  {"phase":"editing","world":"ant colony up close","style":"3D","concept":"pirate ship in 3D: Clawd as ship's carpenter patching the hull mid-voyage, planks labeled {what} flying into place while the ship rolls","source":"claude-opus-5-5","scene":{"concept":"pirate ship in 3D: Clawd as ship's carpenter patching the hull mid-voyage, planks labeled {what} flying into place while the ship rolls","code":"const hull=box(9,1.4,3),deck=box(9.4,0.15,3.2),mast=cylinder(0.12,4.5,6),sail=box(3,2,0.08),plank=box(1.4,0.18,0.5),crate=box(0.8,0.8,0.8);\nfunction frame(t,dt){camera(0,2.6,9,0,1.2,0,80);light(0.4,1,0.6,0.35);fog(9,40,'#000');\nfor(let i=0;i<7;i++){const z=4-i*5;for(let x=-24;x<24;x+=2){const a=Math.sin(x*0.5+t*2+i)*0.15,b=Math.sin(x*0.5+1+t*2+i)*0.15;line3d(x,a,z,x+2,b,z,i%2?'#3a7ac0':'#5aa0e0')}}\nconst y=0.2+Math.sin(t*1.1)*0.15,r=Math.sin(t*1.1)*0.04;\nmesh3d(hull,{y:y+0.2,rz:r,color:'#a0683a'});mesh3d(deck,{y:y+0.95,rz:r,color:'#d8a060'});mesh3d(mast,{x:-1,y:y+1,color:'#c08850'});mesh3d(sail,{x:-1,y:y+3.4,ry:Math.sin(t*0.7)*0.2,color:'#f0e8d0'});\nfor(let i=0;i<3;i++)mesh3d(crate,{x:3+i*0.9,y:y+1.4,color:'#e0b070'});\nconst cyc=4,k=Math.floor(t/cyc),p=fract(t/cyc),n=Math.min(k,6);\nfor(let i=0;i<n;i++)mesh3d(plank,{x:-3.6+i*1.45,y:y+1.15,z:1.7,color:'#f0c080'});\nconst tx=-3.6+n*1.45,e=smoothstep(0,0.5,p),px=lerp(3,tx,e),py=y+1.4+Math.sin(Math.PI*e)*1.5;\nif(k<7||1){mesh3d(plank,{x:k<7?px:3,y:k<7?py:y+1.4,z:1.7,ry:(1-e)*3,color:'#ffd090'});const q=project(k<7?px:3,(k<7?py:y+1.4)+0.4,1.7);if(q)text(q.x-5,q.y,'{what}','#ffe8a0')}\nconst hit=p>0.55&&fract(t*3)<0.5;const c=clawd3d(Math.min(tx+1.2,4),y+1,1.5,{facing:-1,arms:hit?{left:'up',right:'down'}:'out',size:1});\nif(p>0.55&&fract(t*3)<0.2){const s=project(tx,y+1.3,1.7);if(s){put(s.x-1,s.y,'*','#ff0');put(s.x+1,s.y-1,'✦','#fc6')}}\nif(c&&p>0.6&&p<0.95)say(k%2?'Patching {what}. No leaks on my watch!':'Another plank, another fix. Hold steady!',c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#99a"],"speed":0.3,"intensity":0}}},
51  {"phase":"testing","world":"volcano island","style":"pixel art","concept":"theater stage: opening night of {what}, Clawd as Hamlet in a roving spotlight while the curtain rises and falls and the audience waits for t","source":"claude-opus-5-5","scene":{"concept":"theater stage: opening night of {what}, Clawd as Hamlet in a roving spotlight while the curtain rises and falls and the audience waits for the verdict","code":"const P=14;function frame(t){const c=t%P,op=smoothstep(0,2.5,c)*(1-smoothstep(P-2,P,c));fill(0,0,w,h,' ',null,'#1c1024');fill(0,6,w,1,'▀','#a0682f','#4a2c12');for(let x=4;x<w;x+=10)put(x,6,'◆','#ffd860','#4a2c12');const cx=w/2-7+Math.sin(t*0.45)*18;for(let y=1;y<6;y++){const r=3+y*1.6;for(let x=Math.floor(cx+7-r);x<cx+7+r;x++)put(x,y,' ',null,mix('#1c1024','#5a4a30',0.4+y*0.08))}const a=clawd(cx,2,{facing:Math.cos(t*0.45)>0?1:-1,arms:{left:'down',right:c%4<2?'up':'out'},blink:fract(t*0.3)<0.04});pixels(a.top.x-3,a.top.py-3,'..yyyyy..\\n.yyyyyyy.\\nkkkkkkkkk','{\"y\":\"#2a2a40\",\"k\":\"#e05050\"}'.length?{y:'#6a5acd',k:'#ffd860'}:{});pixels(a.right.x,a.right.py-4,'.www.\\nwkwkw\\nwwwww\\n.w.w.',{w:'#eeeedd',k:'#333'});const hw=Math.floor(w/2*(1-op*0.88));fill(0,0,hw,6,'│','#d04a60','#8e1a30');fill(w-hw,0,hw,6,'│','#d04a60','#8e1a30');fill(0,0,w,1,'▼','#ffd27a','#6e1022');const lb='~ {what} ~';text(Math.floor(w/2-lb.length/2),0,lb,'#fff2b0','#6e1022');const clap=c>8&&c<11.5;for(let i=0;i<w;i+=5){const b=clap&&Math.sin(t*14+i)>0?1:0;put(i+2,7-b,'●','#7a6a9a');fill(i,8,5,1,'▀','#5a4a7a')}if(clap)for(let k=0;k<14;k++){const f=fract(t*0.8+rand(k));text(Math.floor(rand(k+3)*w),Math.floor(7-f*5),k%2?'♥':'✓',k%2?'#ff7090':'#7ae08a')}if(c>3&&c<7.5)say('To pass, or not to pass: {what} takes the stage.',a.top.x,a.row);else if(c>8.5&&c<11.5)say('Hold for applause... critics still reading the logs.',a.top.x,a.row)}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#2a1030","#3a1840","#201028"],"speed":0.5,"intensity":0}}},
52  {"phase":"testing","world":"train station","style":"3D","concept":"theater stage: a train-station play where Clawd, as stationmaster, waits on the platform while test trains roll on stage carrying pass/fail ","source":"claude-opus-5-5","scene":{"concept":"theater stage: a train-station play where Clawd, as stationmaster, waits on the platform while test trains roll on stage carrying pass/fail cars","code":"const fl=box(16,0.3,6),cur=box(2,4.5,0.3),val=box(18,0.8,0.3),bd=box(16,4,0.2),eng=box(2.2,1.1,1),car=box(2,0.9,1),lamp=cylinder(0.06,2.2,6)\nfunction frame(t){camera(0,2.2,8,0,1.1,0,80);light(0.3,1,0.7,0.35);fog(10,40,'#000')\nmesh3d(bd,{y:2,z:-3,color:'#7090b0'});mesh3d(fl,{y:-0.15,color:'#b08858'})\nline3d(-8,0.05,-1.6,8,0.05,-1.6,'#ccc');line3d(-8,0.05,-0.8,8,0.05,-0.8,'#ccc')\nmesh3d(lamp,{x:-4,z:0.8,color:'#999'});mesh3d(lamp,{x:4,z:0.8,color:'#999'})\nconst s=fract(t/8),r=floor(t/8),x=s<0.4?lerp(-15,0,smoothstep(0,0.4,s)):s<0.65?0:lerp(0,15,smoothstep(0.65,1,s)),dw=s>0.42&&s<0.65,bad=r%3==1\nmesh3d(eng,{x:x+2.4,y:0.65,z:-1.2,color:'#e0c060'})\nfor(let i=0;i<3;i++){mesh3d(car,{x:x-i*2.3,y:0.55,z:-1.2,color:'#90c8e8'});const p=project(x-i*2.3,1.4,-1.2);if(dw&&p)text(p.x,p.y,bad&&i==1?'✗':'✓',bad&&i==1?'#ff5050':'#60ff80')}\nconst q=project(x+2.4,1.5,-1.2);if(q)text(q.x-4,q.y-1,'{what}','#fff')\nmesh3d(cur,{x:-7.5,y:0,z:1.2,color:'#c03040'});mesh3d(cur,{x:7.5,y:0,z:1.2,color:'#c03040'});mesh3d(val,{y:3.6,z:1.2,color:'#c03040'})\nconst c=clawd3d(0,0,2.2,{facing:0,arms:dw?'up':'down',eyes:dw&&bad?'wide':'open',size:0.8})\nif(c){const ln=s<0.4?'Now arriving: {what}. Mind the gap.':dw?(bad?'One car derailed! Hold the curtain!':'All cars green. Applause, please!'):'Next train in eight seconds...';say(ln,c.top.x,c.row)}}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#302030","#402838","#203040"],"speed":0.5,"intensity":0}}},
53  {"phase":"testing","world":"mad science lab","style":"pixel art","concept":"mad science lab after hours in a museum: Clawd in goggles throws the lever, lightning zaps a jar labeled {what}, tests bubble green one by o","source":"claude-opus-5-5","scene":{"concept":"mad science lab after hours in a museum: Clawd in goggles throws the lever, lightning zaps a jar labeled {what}, tests bubble green one by one","code":"const N=8;let res=Array.from({length:N},(_,i)=>rand(i*7)<0.85);\nfunction frame(t,dt){\nfill(0,8,w,1,'▀','#5a4a6a');\nfor(let x=0;x<w;x+=30){pixels(x+2,0,'##.##\\n#....#\\n#....#\\n######','#'==='#'?{'#':'#8a6a3a'}:{});}\nconst cyc=mod(t,12),z=cyc>2&&cyc<2.6;\nconst jx=60;pixels(jx,2,'..####..\\n.#....#.\\n#......#\\n#......#\\n#......#\\n#......#\\n.######.\\n..#..#..',{'#':'#9ab'});\nconst lv=Math.floor(clamp((cyc-2)*3,0,N));\nfor(let i=0;i<N;i++){const y=13-Math.floor(i*1.1);const k=i<lv;pixel(jx+3+(i%2),y,k?(res[i]?'#4f4':'#f44'):'#335');}\ntext(jx-1,8,'{what}'.slice(0,12),'#ffd');\nline(jx+4,0,jx+4,0,'┬','#aaa');\nif(z){for(let i=0;i<5;i++)put(jx+2+Math.floor(rand(t*50+i)*6),Math.floor(rand(t*30+i)*1.5),'ϟ'.length?'⚡'.length==1?'*':'*':'*','#ff6');for(let y=0;y<2;y++)put(jx+4+(y%2?1:-1),y,'/','#ffef55');}\nconst lever=cyc>1.6&&cyc<6;\nconst lx=40;text(lx,4,'┌──┐','#888');text(lx,5,'│'+(lever?'▼ ':'▲ ')+'│','#ccc');text(lx,6,'└──┘','#888');\nconst c=clawd(24,4,{facing:1,arms:lever?{right:'down',left:'up'}:{right:'up',left:'down'},blink:fract(t*0.3)<0.04});\npixels(c.eyes.x-1,c.eyes.py,'gggggggggg',{g:'#4cf'});\nconst g=lv>=N&&cyc>5;if(g)for(let i=0;i<10;i++)put(jx+1+Math.floor(rand(i+Math.floor(t*4))*8),Math.floor(rand(i+3+Math.floor(t*4))*2),'○','#7f7');\nconst fails=res.filter(r=>!r).length;\nif(cyc>5.5&&cyc<11)say(fails?'IT LIVES! mostly. '+fails+' tests twitching in {what}.':'IT LIVES! {what} passed every jolt!',c.top.x,c.row);\nif(cyc>11.8)res=res.map((r,i)=>r||rand(t+i)<0.6);\nconst gx=w-20+Math.sin(t*0.3)*8;pixels(gx,10,'.##.\\n####\\n#..#',{'#':'#ddc'});pixel(gx+1,11,'#000');\n}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#201030","#302048","#183028"],"speed":0.5,"intensity":0.2}}},
54  {"phase":"testing","world":"sports broadcast","style":"text art","concept":"sports broadcast x mountain climb: live TV coverage of Clawd scaling the {what} north face, checkpoint flags for passing tests, a slip at th","source":"claude-opus-5-5","scene":{"concept":"sports broadcast x mountain climb: live TV coverage of Clawd scaling the {what} north face, checkpoint flags for passing tests, a slip at the crux, summit flag","code":"const P=38;function sy(x){return x<20?8:x<=95?8-(x-20)*4/75:4+(x-95)*4/23}\nfunction pr(tt){if(tt<14)return tt/14*0.6;if(tt<17)return 0.6-smoothstep(14,15.5,tt)*0.15;if(tt<30)return 0.45+(tt-17)/13*0.55;return 1}\nfunction frame(t,dt){const tt=t%P,p=pr(tt);\nfor(let x=20;x<119;x++){const top=Math.ceil(sy(x));for(let y=top;y<9;y++){const sn=y<6&&x>78&&x<108;put(x,y,sn?'▒':(x+y)%5?'▓':'░',sn?'#e8f0ff':'#7d8590')}}\nsprite(92,0,'  ▲  \\n ▟█▙ \\n▟███▙','#e8f0ff');\nfor(const c of [0.25,0.5,0.75]){const fx=20+75*c,ok=p>c+0.03;put(fx,Math.ceil(sy(fx))-1,ok?'▶':'▷',ok?'#5fd75f':'#888')}\nconst cx=20+75*p-7,stride=Math.floor(cx/1.5)%4,mv=tt<30&&!(tt>16&&tt<17);\nconst slip=tt>=14&&tt<16;\nconst c=clawd(cx,sy(cx+7)-4,{pose:tt>=30?'jump':'walk',stride:mv?stride:-1,facing:slip?-1:1,eyes:slip?'wide':'open',arms:tt>=30?'up':'out'});\nsprite(c.top.x-2,c.top.row,'▄█▄','#ffd23f');\nline(c.right.x,c.right.row,c.right.x+3,c.right.row+1,'─','#c8a060');\nif(tt>=30){sprite(97,0,'│▀▀\\n│  ','#ff5f5f');for(let i=0;i<14;i++)put(96+Math.cos(i*2.3)*fract(tt)*14,2+Math.sin(i*2.3)*fract(tt)*3,'✦',hsl(i*40,0.9,0.65))}\nfill(0,0,19,3,' ',null,'#1a1a3a');text(1,0,'● LIVE',fract(t)<0.6?'#ff4040':'#802020');text(1,1,'{what}','#ffffff');text(1,2,'ALT '+String(Math.round(800+p*4000)).padStart(4)+'m','#ffd23f');\nsprite(1,5,'┌──────────┐\\n│ ◉  ◉ ┬   │\\n└──────────┘','#6fa8dc');text(3,8,'COMMENTARY','#6fa8dc');\nlet s='';if(tt>1&&tt<6)s='Live from {what}: base camp to summit!';else if(tt>8&&tt<13)s='First checkpoints green. Ropes holding.';else if(tt>=14&&tt<19)s='Whoa! One test slipped. Re-roping!';else if(tt>21&&tt<27)s='Retry looks clean. Air is thin up here.';else if(tt>=30&&tt<37)s='SUMMIT! Every test planted a flag!';\nif(s)say(s,c.top.x,c.row)}","actors":[],"particles":[{"glyphs":"·*","count":20,"x":"rand(k)*w","y":"mod(t*(1+rand(k+9)) + rand(k+3)*h, h)","color":"#c0d0e8"}],"background":{"effect":"aurora","palette":["#203060","#305080","#406090"],"speed":0.4,"intensity":0.2}}},
55  {"phase":"testing","world":"arctic expedition","style":"pixel art","concept":"snow globe arctic: tiny Clawd in a parka inside a snow globe, waiting by a flag labeled {what} while snow settles; each settled flake is a p","source":"claude-opus-5-5","scene":{"concept":"snow globe arctic: tiny Clawd in a parka inside a snow globe, waiting by a flag labeled {what} while snow settles; each settled flake is a passing test","code":"let fl=Array.from({length:50},(_,i)=>({x:rand(i)*44,y:rand(i+7)*16,v:1+rand(i+3)*2}));let piled=0;\nfunction frame(t,dt){const cx=Math.floor(w/2),R=22;\nfor(let py=0;py<18;py++)for(let x=cx-R;x<=cx+R;x++){const dx=(x-cx)/R,dy=(py-9)/9;const d=dx*dx+dy*dy;if(d<1&&d>0.85)pixel(x,py,'#9fd8ff');}\nfor(let x=cx-18;x<=cx+18;x++){const hh=Math.min(3,1+Math.floor(piled/40+noise(x*0.3)*1.5));for(let k=0;k<hh;k++)pixel(x,15-k,'#eef6ff');}\npixels(cx-16,16,'..................................\\n.##################################.','{\"#\":\"#8a5a3a\"}');\nfill(cx-17,8,35,1,'▀','#6b3f22');\nconst sh=t%14<1.2;const ox=sh?Math.round(Math.sin(t*40)*2):0;\nfor(const f of fl){f.y+=f.v*dt*(sh?-6:1);f.x+=Math.sin(t+f.y)*dt*2;if(f.y>14){f.y=0;f.x=rand(t*9+f.x)*44;piled++;}if(f.y<0)f.y=14;const px=cx-21+f.x+ox,dx=(px-cx)/R,dy=(f.y-9)/9;if(dx*dx+dy*dy<0.8)pixel(Math.round(px),Math.round(f.y),'#ffffff');}\npixels(cx+8+ox,4,'#....\\n#ooo.\\n#ooo.\\n#....\\n#\\n#\\n#\\n#\\n#','{\"#\":\"#c0c0c0\",\"o\":\"#e05050\"}');text(cx+10+ox,1,'{what}','#ffd27a');\nconst c=clawd(cx-9+ox,3.5,{pose:sh?'jump':'stand',facing:sh?0:1,eyes:sh?'wide':'open',color:'#d97757',arms:sh?'up':'down',blink:fract(t*0.3)<0.04});\npixels(c.top.x-4,c.top.py-2,'..wwwwww..\\n.wbbbbbbw.\\nwbbbbbbbbw','{\"w\":\"#ffffff\",\"b\":\"#4a7fd0\"}');\nconst lines=['Snug in here. Tests running on {what}.','Whoa! Who shook the globe?!','Each flake that lands is a passing test.','Snowdrift rising. Good sign.'];const i=sh?1:[0,2,3][Math.floor(t/5)%3];if(t%5>1.5||sh)say(lines[i],c.top.x+20,c.row);\ntext(cx-R-14,7,'❄ expedition','#7ab8e0');for(let x=0;x<w;x++)if(Math.abs(x-cx)>R+1)put(x,8,'▁','#3a5a7a');}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#1a4a6a","#2a8a7a","#5a6ad0"],"speed":0.4,"intensity":0.3}}},
56  {"phase":"testing","world":"theater stage","style":"3D","concept":"arctic expedition on a theater stage: Clawd in a parka waits by an ice-fishing hole while test flags pop up green across a 3D snowfield unde","source":"claude-opus-5-5","scene":{"concept":"arctic expedition on a theater stage: Clawd in a parka waits by an ice-fishing hole while test flags pop up green across a 3D snowfield under spotlights","code":"const ice=plane(40,14),flag=cylinder(0.05,1.4,5),ban=box(0.6,0.35,0.05),berg=cone(1.6,2.4,6),hole=cylinder(0.7,0.05,12),cur=box(1.5,4,0.3);\nconst N=8;\nfunction frame(t,dt){\nconst a=Math.sin(t*0.1)*0.4;\ncamera(Math.sin(a)*9,2.6,Math.cos(a)*9,0,0.8,0,90);\nlight(0.3,1,0.5,0.35);fog(10,40,'#05080f');\nmesh3d(ice,{color:'#cfe6f5'});\nmesh3d(berg,{x:-7,z:-5,color:'#a8d4ef'});mesh3d(berg,{x:7.5,z:-6,scale:1.3,color:'#b8ddf2'});\nfor(let i=0;i<6;i++){mesh3d(cur,{x:-10+i*0.4,y:2,z:-7+i,color:'#b03040'});mesh3d(cur,{x:10-i*0.4,y:2,z:-7+i,color:'#b03040'});}\nmesh3d(hole,{x:1.5,z:1,color:'#1a3a6a',unlit:true});\nconst cyc=t%20;\nfor(let i=0;i<N;i++){const fx=-5+i*1.4,fz=-2.5;const up=cyc>1+i*2;if(!up)continue;\nconst bad=rand(i+Math.floor(t/20)*9)<0.12;\nmesh3d(flag,{x:fx,z:fz,color:'#ddd'});mesh3d(ban,{x:fx+0.3,y:1.25,z:fz,color:bad?'#e05050':'#50d070'});}\nconst bob=Math.sin(t*3)*0.05;\nline3d(1.2,1.1,1,1.5,0.05+bob,1,'#ffffff');\nconst c=clawd3d(0,0,1,{facing:0,arms:{left:'down',right:'out'},blink:fract(t*0.3)<0.04,color:'#d97757'});\nif(c){pixels(c.top.x-4,c.top.py-2,'..wwwwwwww..\\nwwwwwwwwwwww','{\"w\":\"#f4f4f4\"}'.length?{w:'#f4f4f4'}:{});}\nconst p=project(-5,0.2,-2.5);if(p)text(p.x-1,p.y,'{what}','#ffe9a0');\nfor(let i=0;i<30;i++){put(mod(rand(i)*w+t*2,w),mod(rand(i+5)*h+t*(1+rand(i+3)),h),'*','#eef6ff');}\nif(c){if(cyc>3&&cyc<8)say('Ice fishing for passing tests in {what}.',c.top.x,c.row);else if(cyc>12&&cyc<17)say('Green flags so far. Toes: frozen.',c.top.x,c.row);}\n}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#40e0a0","#3080ff","#a060ff"],"speed":0.5,"intensity":0.3}}},
57  {"phase":"testing","world":"bakery","style":"pixel art","concept":"construction site, pixel art: Clawd in a hard hat directs a tower crane stacking test blocks for {what}; every third block comes up red and ","source":"claude-opus-5-5","scene":{"concept":"construction site, pixel art: Clawd in a hard hat directs a tower crane stacking test blocks for {what}; every third block comes up red and gets fixed","code":"function frame(t,dt){const H=2*h;for(let x=0;x<w;x++){pixel(x,H-1,'#7a5a3a');pixel(x,H-2,'#9a7a4a')}for(let y=4;y<H-2;y++){pixel(20,y,'#f0c020');pixel(23,y,'#f0c020');if(y%2)pixel(21+(y%4>1?1:0),y,'#c09010')}for(let x=12;x<72;x++){pixel(x,3,'#f0c020');if(x%3==0)pixel(x,4,'#c09010')}fill(12,1,6,1,' ',null,'#888');pixels(19,0,'######\\n#oooo#','{\"#\":\"#f0c020\",\"o\":\"#6cf\"}');const c=Math.floor(t/6),p=fract(t/6),n=c%5,red=c%3==2;for(let i=0;i<n;i++){fill(56,h-2-i,10,1,'▒','#4c8',  '#2a6')}text(57,h-2-Math.max(0,n-1)-(n?1:0),'','#fff');for(let i=0;i<3;i++)fill(26+i*2,h-2,9,1,'▓','#a76','#643');let bx,by;const ty=h-2-n;if(p<0.15){bx=28;by=lerp(h-3,2,p/0.15)}else if(p<0.55){bx=lerp(28,56,smoothstep(0.15,0.55,p));by=2}else if(p<0.8){bx=56;by=lerp(2,ty,smoothstep(0.55,0.8,p))}else{bx=56;by=ty}const fix=red&&p>0.85;const col=red&&!fix?'#e44':'#4c8';const sh=red&&p>0.8&&!fix?Math.round(Math.sin(t*30)):0;line(bx+5,2,bx+5,by-1,'│','#ccc');fill(bx+sh,by,10,1,'▒',col,red&&!fix?'#822':'#2a6');text(bx+1+sh,by,'{what}'.slice(0,8),'#fff');const cx=p>0.8&&red?lerp(84,68,smoothstep(0.8,0.85,p)):84;const k=clawd(cx,h-5.5,{facing:-1,arms:p<0.8?{left:'up',right:'out'}:'up',stride:Math.floor(cx/1.5)%4});pixels(k.top.x-4,k.top.py-2,'..####..\\n########','{\"#\":\"#fd2\"}');if(fix)put(k.left.x-1,k.left.row,'✓','#4f8');const L=['Easy now... {what} going up.','Red block! One test failing.','Patched. Tower grows!','Floor by floor, test by test.'];const s=red?(fix?L[2]:p>0.75?L[1]:''):(p>0.3&&p<0.7?L[c%2?3:0]:'');if(s)say(s,k.top.x,k.row);fill(96,h-3,18,2,'▓','#555','#333');text(98,h-3,'SITE: TESTS','#fd2');for(let i=0;i<9;i++)put(96+i*2,h-1,i%2?'▲':'▲',i%2?'#f80':'#fff')}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#203048","#304860","#405870"],"speed":0.3,"intensity":0}}},
58  {"phase":"testing","world":"ant colony up close","style":"pixel art","concept":"lighthouse in a storm: keeper Clawd at the rocks while the sweeping beam guides test ships past the reef, each one lit up and checked as it ","source":"claude-opus-5-5","scene":{"concept":"lighthouse in a storm: keeper Clawd at the rocks while the sweeping beam guides test ships past the reef, each one lit up and checked as it passes","code":"let sh=Array.from({length:4},(_,i)=>({x:-12-i*24,ok:0}));let n=0;\nconst LH='..rr..\\n.yyyy.\\n.rrrr.\\nwwwwww\\nrrrrrr\\nwwwwww\\nrrrrrr\\nwwwwww\\nrrrrrr\\nwwwwww\\nrrrrrr\\nkkkkkkk';\nconst B='...m....\\n..sm....\\n.ssm....\\nhhhhhhhh\\n.hhhhhh.';\nfunction frame(t,dt){const fl=fract(t*0.13)<0.03;\nif(fl)fill(0,0,w,h,' ',null,'#2a2a44');\nfor(let x=0;x<w;x++){const y=7+Math.sin(x*0.3+t*2)*0.6;put(x,Math.round(y),'~','#4a7fc0');put(x,8,'≈','#2e5a99');}\nfor(let x=66;x<104;x++)put(x,8,'▓','#6b6560');fill(84,7,20,1,'▓','#6b6560');\nconst lx=w>110?92:w-24;pixels(lx,2,LH,{r:'#e04848',w:'#eeeeee',y:'#ffe066',k:'#555555'});\nconst a=Math.sin(t*0.9)*0.5+Math.PI;const bx=lx+3,by=1.6;\nfor(let r=2;r<90;r++){const px=bx+Math.cos(a)*r,py=by-Math.sin(a)*r*0.12+r*0.035;put(px,py,r%2?'░':'▒','#ffe98a');}\nconst beamX=bx+Math.cos(a)*60;\nfor(const s of sh){s.x+=dt*5;if(s.x>66){s.x-=96;s.ok=0;}if(Math.abs(s.x+4-beamX)<4&&!s.ok){s.ok=1;n++;}\nconst bob=Math.sin(t*2+s.x)*1;pixels(s.x,8+Math.round(bob),B,{m:'#a07040',s:'#f0f0f0',h:s.ok?'#7ad67a':'#c08850'});\nif(s.ok)text(s.x+3,3+Math.round(bob/2),'✓','#7ad67a');}\ntext(Math.max(0,sh.reduce((m,s)=>s.x>m.x?s:m).x-4),2,'{what}','#ffffff');\nconst c=clawd(70,3.5,{facing:-1,arms:{left:'up',right:'down'},blink:fract(t*0.3)<0.05});\npixels(c.top.x-3,c.top.py-2,'yyyyyy\\n.yyyy.',{y:'#ffd23f'});\npixels(c.left.x-1,c.left.py-2,'.k.\\nkok\\n.k.',{k:'#888',o:'#ffcc33'});\nconst q=['Steady... {what} sailing in.','Lit '+n+' ships so far. No wrecks!','Rough seas, green hulls.'];if(fract(t/24)<0.6)say(q[Math.floor(t/8)%3],c.top.x,c.row);}","actors":[],"particles":[{"glyphs":"/'","count":70,"x":"mod(rand(k)*w - t*8, w)","y":"mod(t*(9+rand(k+9)*5)+rand(k+3)*h, h)","color":"#7f9fcf"}],"background":{"effect":"rain","palette":["#1b2a44","#2c3e66","#41588a"],"speed":1.2,"intensity":0.25}}},
59  {"phase":"building","world":"mail room","style":"3D","concept":"zen rock garden: Clawd rakes sand rings around stones as packages of {what} settle into place, mail-room style, one by one onto stone pedest","source":"claude-opus-5-5","scene":{"concept":"zen rock garden: Clawd rakes sand rings around stones as packages of {what} settle into place, mail-room style, one by one onto stone pedestals","code":"const stone=sphere(0.7,10),ped=cylinder(0.9,0.3,10),pkg=box(0.8,0.6,0.8),gnd=plane(40,20);\nconst N=5,xs=[-7,-3.5,0,3.5,7];\nfunction frame(t,dt){\ncamera(Math.sin(t*0.05)*2,3,9,0,0.6,0,80);light(0.4,1,0.6,0.35);fog(8,35,'#000');\nmesh3d(gnd,{color:'#c8bfa0'});\nfor(let r=1;r<5;r++)for(let i=0;i<N;i++){const a=t*0.3;for(let k=0;k<16;k++){const q=k/16*6.283;point3d(xs[i]+Math.cos(q)*r*0.5,0.02,-2+Math.sin(q)*r*0.4,'#a89e80')}}\nconst cyc=4,done=Math.floor(t/cyc),ph=fract(t/cyc);\nfor(let i=0;i<N;i++){mesh3d(ped,{x:xs[i],z:-2,color:'#8c8c84'});\nconst slot=i<=done%(N+1)-1;\nif(slot)mesh3d(pkg,{x:xs[i],y:0.6,z:-2,color:'#d8a060'});\nelse if(i==done%(N+1)){const y=lerp(5,0.6,smoothstep(0,0.8,ph));mesh3d(pkg,{x:xs[i],y,z:-2,ry:(1-ph)*3,color:'#e8b070'});const p=project(xs[i],y+0.7,-2);if(p)text(p.x-4,p.y,'{what}'.slice(0,10),'#ffe')}\nelse mesh3d(stone,{x:xs[i],y:0.5,z:-2,scale:0.6,color:'#9a9a90'})}\nconst cx=Math.sin(t*0.4)*6;\nconst c=clawd3d(cx,0,1.5,{facing:Math.cos(t*0.4)>0?1:-1,stride:Math.floor(t*4)%4,arms:'down'});\nline3d(cx,0.4,1.5,cx-0.6,0,2.2,'#b08850');\nif(c){const m=Math.floor(t/12)%3;say(['Raking {what}. Patience is a dependency.','One package settles. Then another.','The build is complete when it is complete.'][m],c.top.x,c.row)}\n}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#2a4a3a","#3a5a4a","#4a6a5a"],"speed":0.3,"intensity":0}}},
60  {"phase":"building","world":"arctic expedition","style":"3D","concept":"arctic submarine: Clawd's sub surfaces through ice, loads {what} crates from a crane onto deck, dives and resurfaces in a loop","source":"claude-opus-5-5","scene":{"concept":"arctic submarine: Clawd's sub surfaces through ice, loads {what} crates from a crane onto deck, dives and resurfaces in a loop","code":"const hull=sphere(1,12),tow=box(0.8,0.9,1.2),crate=box(0.6,0.6,0.6),ice=box(3,0.3,2.5),berg=cone(1.6,2.5,6);let n=0;function frame(t,dt){camera(0,2.6,9,0,0.8,0,90);light(0.4,1,0.6,0.35);fog(10,40,'#001018');const c=t%16,u=smoothstep(0,3,c)*(1-smoothstep(13,16,c)),sy=lerp(-2.2,0,u);if(c<0.05)n=0;for(let i=0;i<7;i++){const x=-12+i*4+(i%2)*0.7;if(Math.abs(x)>2.5)mesh3d(ice,{x,y:-0.15,z:-1-(i%3),color:'#cfe8f5'})}mesh3d(berg,{x:-8,z:-9,color:'#b8dcef'});mesh3d(berg,{x:9,z:-12,scale:1.4,color:'#a9d0e8'});for(let x=-14;x<=14;x+=2)line3d(x,0,3,x+1,0,3,'#3a7ab0');mesh3d(hull,{y:sy,rz:0,scale:1,color:'#e8b04a',rx:0,ry:0,});mesh3d(hull,{x:-1.3,y:sy,scale:0.8,color:'#e8b04a'});mesh3d(hull,{x:1.3,y:sy,scale:0.8,color:'#e8b04a'});mesh3d(tow,{x:0,y:sy+1.1,color:'#d09a3a'});const me=clawd3d(0,sy+1.55,0,{size:0.6,arms:c>5&&c<11?'up':'down'});const k=c-4,ph=fract(k/2);if(c>4&&c<12){const cx=lerp(5,0.9,smoothstep(0,0.6,ph)),cy=lerp(3,sy+0.9,smoothstep(0.4,0.9,ph));line3d(5,3.5,0,cx,3.5,0,'#999');line3d(cx,3.5,0,cx,cy+0.3,0,'#bbb');mesh3d(crate,{x:cx,y:cy,color:'#c0703a'});const p=project(cx,cy+0.5,0);if(p)text(p.x-4,p.y,'{what}','#fff2c0');if(ph>0.9&&n<Math.floor(k/2)+1)n++}for(let i=0;i<Math.min(n,4);i++)mesh3d(crate,{x:-1.8+i*0.5,y:sy+0.95,z:0.3,color:'#c0703a'});line3d(5,0,0,5,3.6,0,'#ccc');if(me&&c>4&&c<12)say(n<2?'Loading {what} aboard. Ice fishing for deps!':'Hold full soon. Then we dive!',me.top.x,me.row);else if(me&&c>12)say('Diving! Back up with the next batch.',me.top.x,me.row)}","actors":[],"particles":[{"glyphs":"*·.","count":40,"x":"rand(k)*w","y":"mod(t*(1+rand(k+5))+rand(k+2)*h,h)","color":"#e8f4ff"}],"background":{"effect":"aurora","palette":["#1a6b5a","#2fa37a","#4a7bd0"],"speed":0.4,"intensity":0.3}}},
61  {"phase":"building","world":"orchestra concert","style":"pixel art","concept":"bank heist: masked Clawd cracks a spinning vault dial while gold bars stamped {what} fly into a growing loot pile, ducking a guard's sweepin","source":"claude-opus-5-5","scene":{"concept":"bank heist: masked Clawd cracks a spinning vault dial while gold bars stamped {what} fly into a growing loot pile, ducking a guard's sweeping flashlight","code":"function frame(t,dt){fill(0,0,w,h,' ',null,'#15151f');for(let y=0;y<8;y++)for(let x=(y%2)*4;x<w;x+=8)put(x,y,'│','#2a2a3a');fill(0,8,w,1,'▀','#5a5a6a');const vx=w-22;disc(vx,4,4,' ',null,'#5c6274');circle(vx,4,4,'●','#a0a8b8');const a=t*0.7+Math.sin(t*3)*0.2;for(let i=0;i<3;i++){const b=a+i*2.094;line(vx,4,vx+Math.cos(b)*7,4+Math.sin(b)*3.4,'▪','#dde')}put(vx,4,'◆','#fc3');fill(vx+10,0,2,8,'█','#3a3f4c');const per=2.5,k=Math.floor(t/per)%20,p=fract(t/per);for(let i=0;i<k;i++)pixels(6+(i%5)*5,15-Math.floor(i/5)*2,'yyyy',{y:i%2?'#fc3':'#e8b020'});text(7,1,'{what}','#ffcc66');text(7,2,'$$ LOOT $$','#8a7a3a');const tx=6+(k%5)*5,ty=15-Math.floor(k/5)*2;const bx=lerp(vx-6,tx,p),by=lerp(10,ty,p)-Math.sin(p*Math.PI)*10;pixels(bx,by,'yyyy',{y:'#ffe066'});const gm=mod(t,14),guard=gm>10;if(guard){const s=lerp(-30,w,smoothstep(10,14,gm));for(let y=0;y<8;y++)for(let x=0;x<16;x++)put(s+x+y,y,' ',null,'#3a3820')}const cx=vx-34;const c=clawd(cx,4,{facing:guard?0:1,eyes:guard?'wide':'open',arms:guard?'down':{left:'down',right:'out'},blink:!guard&&fract(t*0.3)<0.04});pixels(c.top.x-6,c.eyes.py-1,'kkkkkkkkkkkkk',{k:'#0a0a0a'});pixels(c.right.x,c.right.py-1,'ss\\n s\\n  s',{s:'#9ab'});const lines=['Every compiled byte of {what}, straight into the bag.','Tumblers clicking. {what} is almost cracked.','Shh. The compiler hears everything.','Another bar of {what}. This heist is going swimmingly.'];if(guard)say('Freeze! Pretend to be a progress bar.',c.top.x,c.row);else if(mod(t,14)<6)say(lines[Math.floor(t/14)%4],c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#202030","#2a2a40","#302a20"],"speed":0.5,"intensity":0}}},
62  {"phase":"building","world":"sports broadcast","style":"3D","concept":"lighthouse in a storm: Clawd keeper on the rocks guides a cargo ship labeled {what} into harbor through rain, beam sweeping, 3D","source":"claude-opus-5-5","scene":{"concept":"lighthouse in a storm: Clawd keeper on the rocks guides a cargo ship labeled {what} into harbor through rain, beam sweeping, 3D","code":"const tw=cylinder(0.9,4,12),cap=cone(1.1,0.8,12),lamp=sphere(0.5,8),rock=sphere(1.6,6),hull=box(2.4,0.6,1),cab=box(0.8,0.6,0.7),mast=cylinder(0.05,1.2,4);\nconst rn=Array.from({length:90},(_,i)=>({x:rand(i)*w,y:rand(i+40)*h,v:12+rand(i+7)*8}));\nfunction frame(t,dt){\ncamera(0,2.4,9,0,1.2,0,90);light(-0.4,0.8,0.6,0.35);fog(8,40,'#05080c');\nconst fl=fract(t/7)<0.04;\nfor(let z=-30;z<6;z+=3)for(let x=-18;x<18;x+=2){const y=Math.sin(x*0.6+t*2+z*0.4)*0.15;line3d(x,y,z,x+1.6,Math.sin((x+1.6)*0.6+t*2+z*0.4)*0.15,z,fl?'#bde':'#3a6a8a')}\nmesh3d(rock,{x:6,y:-0.9,z:-1,color:'#7a7468'});\nmesh3d(tw,{x:6,y:0.4,z:-1,color:'#e8e2d8'});\nmesh3d(box(1.85,0.5,1.85),{x:6,y:2.6,z:-1,color:'#c94a3a'});\nmesh3d(lamp,{x:6,y:4.7,z:-1,color:'#ffe680',unlit:true});\nmesh3d(cap,{x:6,y:5.1,z:-1,color:'#c94a3a'});\nconst a=t*1.3;\nfor(let i=0;i<3;i++){const s=a+i*0.04;line3d(6,4.7,-1,6+Math.cos(s)*22,4.2,-1+Math.sin(s)*22,'#fff4a8')}\nconst k=smoothstep(0,80,t%90),sx=lerp(-16,-1,k),sz=lerp(-24,-1,k),bob=Math.sin(t*1.7)*0.2,rl=Math.sin(t*1.3)*0.12;\nmesh3d(hull,{x:sx,y:0.2+bob,z:sz,rz:rl,color:'#d0a050'});\nmesh3d(cab,{x:sx+0.5,y:0.75+bob,z:sz,rz:rl,color:'#e0e0e0'});\nmesh3d(mast,{x:sx-0.5,y:0.5+bob,z:sz,color:'#ccc'});\nconst p=project(sx,1.6+bob,sz);if(p)text(p.x-5,p.y-1,'{what}','#ffd27a');\nfor(const r of rn){r.y+=r.v*dt;r.x-=r.v*dt*0.3;if(r.y>h){r.y=0;r.x=rand(t+r.x)*w*1.2}put(r.x,r.y,'/','#6c8cb0')}\nif(fl)for(let x=0;x<w;x+=1)if(rand(x+floor(t))<0.04)put(x,0,'▼','#ffffff');\nconst c=clawd3d(3.4,0,0.5,{facing:-1,arms:{left:'up',right:'down'},size:0.9});\nif(c){pixels(c.left.x-1,c.left.py-3,'.y.\\nyYy\\n.y.',{y:'#ffd040',Y:'#fff8c0'});\nconst ph=t%90,l=ph<20?'Hold steady, {what}. Follow the light!':ph<45?'Big wave! Cargo still strapped down.':ph<75?'Almost to the dock. Keep compiling!':'{what} has landed. Lighthouse: 1, storm: 0';\nif(fract(ph/20)<0.6)say(l,c.top.x,c.row)}\n}","actors":[],"particles":[],"background":{"effect":"rain","palette":["#1a2a3a","#2a4058","#3a5070"],"speed":1.2,"intensity":0}}},
63  {"phase":"building","world":"haunted house","style":"3D","concept":"nature documentary: Clawd crouched in the grass narrating as a termite colony hauls crumbs to raise a growing mound labeled {what}, slow 3D ","source":"claude-opus-5-5","scene":{"concept":"nature documentary: Clawd crouched in the grass narrating as a termite colony hauls crumbs to raise a growing mound labeled {what}, slow 3D orbit","code":"const md=cone(1.3,1,12),ant=sphere(0.13,5),cr=box(0.15,0.15,0.15),gr=plane(50,24),bl=cone(0.06,0.7,4);\nconst ants=Array.from({length:14},(_,i)=>({a:i*2.39,o:rand(i)}));\nconst gs=Array.from({length:30},(_,i)=>({x:(rand(i+5)-0.5)*24,z:(rand(i+40)-0.5)*10}));\nconst L=['Here we observe the wild {what}, building.','Each worker carries one crumb. Patience.','The mound grows. Nobody knows why it takes so long.','Shh. A dependency is being installed.','Remarkable. It has eaten another megabyte.','Nature is beautiful. Also slow.'];\nfunction frame(t,dt){\ncamera(Math.sin(t*0.06)*5,2.3,8,0,0.9,0,80);light(0.5,1,0.4,0.35);fog(8,32,'#000');\nmesh3d(gr,{color:'#6f9a4c'});\nfor(const g of gs)mesh3d(bl,{x:g.x,z:g.z,color:'#9ccc5a'});\nconst H=0.7+2*smoothstep(0,100,t)+0.05*Math.sin(t);\nmesh3d(md,{scale:H,color:'#c8925a'});\nfor(const a of ants){const p=fract(t*0.12+a.o),r=7*(1-p)+1.3*H*0.8,x=Math.cos(a.a)*r,z=Math.sin(a.a)*r*0.6,y=p>0.9?(p-0.9)*10*H*0.7:0;\nmesh3d(ant,{x,y:y+0.13,z,color:'#e0dccc'});mesh3d(cr,{x,y:y+0.35,z,ry:t,color:'#ffd27a'});}\nconst q=project(0,H+0.4,0);if(q)text(q.x-5,q.y-1,'{what}','#fff4c0');\nconst c=clawd3d(-6,0,3,{facing:1,pose:'sit',size:0.8,blink:fract(t*0.3)<0.05});\nif(c){pixels(c.eyes.x-3,c.eyes.py-1,'##.##\\n##.##',{'#':'#444'});if(t>2)say(L[Math.floor((t-2)/11)%L.length],c.top.x,c.row);}\n}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#2a4a3a","#3c6a4a","#5a7a3a"],"speed":0.4,"intensity":0}}},
64  {"phase":"building","world":"medieval castle","style":"pixel art","concept":"dinosaur era: Clawd rides a brontosaurus whose long neck works as a crane, hauling stone blocks from a quarry to build the {what} tower whil","source":"claude-opus-5-5","scene":{"concept":"dinosaur era: Clawd rides a brontosaurus whose long neck works as a crane, hauling stone blocks from a quarry to build the {what} tower while a volcano smokes behind","code":"function frame(t,dt){const G=16;for(let x=0;x<w;x++){pixel(x,G,'#6a8a3a');pixel(x,G+1,'#4a5a2a')}for(let i=0;i<14;i++){const y=4+i;for(let x=100-i*1.4;x<100+i*1.4;x++)pixel(x,y,i<2?'#ff6a2a':'#8a5a4a')}for(let k=0;k<6;k++){const f=fract(t*0.3+k/6);put(99+Math.sin(f*6+k)*3,1-f*2+1,'○','#aaa')}for(let k=0;k<3;k++)pixels(4+k*5,G-2-(k%2)*0,'####\\n####',{'#':'#b0a898'});const P=4,c=Math.floor(t/P),p=fract(t/P),n=c%8,s=p<0.5?smoothstep(0,0.5,p):1-smoothstep(0.5,1,p),tx=66,top=G-n*2;for(let i=0;i<n;i++)pixels(tx+(i%2),G-2-i*2,'######\\n######',{'#':i%2?'#c8c0a8':'#a8a090'});const hx=lerp(8,tx+2,s),hy=lerp(G-5,top-6,s)-6*Math.sin(Math.PI*s);for(let a=0;a<=1;a+=0.03){const x=lerp(40,hx,a),y=lerp(10,hy,a)-Math.sin(a*Math.PI)*3;pixel(x,y,'#5fb05f');pixel(x,y+1,'#5fb05f')}pixels(hx-1,hy-1,'###o\\n#####',{'#':'#5fb05f','o':'#000'});const carry=p<0.5&&p>0.05;if(carry){line(0,0,0,0,'','#000');pixels(hx-1,hy+2,'####\\n####',{'#':'#c8c0a8'})}for(let y=10;y<15;y++){const r=Math.floor(9*Math.sqrt(1-((y-12)/3)**2));for(let x=30-r;x<30+r;x++)pixel(x,y,'#5fb05f')}for(let k=0;k<4;k++){const lx=23+k*4;pixel(lx,15,'#4a904a');pixel(lx+1,15,'#4a904a')}for(let a=0;a<1;a+=0.08)pixel(21-a*8,12+a*3,'#5fb05f');const cl=clawd(23,1.5,{arms:{left:'down',right:s>0.1&&s<0.9?'up':'out'},facing:1,blink:fract(t*0.3)<0.04});pixels(cl.right.x,cl.right.py-3,'o\\n#\\n#\\no',{'o':'#eee','#':'#ddd'});pixels(cl.top.x-5,cl.top.py-1,'.#.#.#.#.\\n#########',{'#':'#c89040','.':'#c89040'});text(tx+9,8,'{what}','#ffd27a');text(tx+8,7,'▲','#ffd27a');const lines=['Bronto-crane, block '+(n+1)+' of {what}!','Hand-laid, hand-compiled. Well, neck-laid.','No cranes for 65 million years. Improvising.','{what} tower rising, one dino-load at a time.'];if(p<0.6)say(lines[c%4],cl.top.x,cl.row)}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#3a2a5a","#7a3a3a","#c86a3a"],"speed":0.4,"intensity":0.25}}},
65  {"phase":"building","world":"museum after hours","style":"text art","concept":"museum after hours meets construction: Clawd as night-shift builder assembling a dinosaur skeleton exhibit bone by bone with a ceiling crane","source":"claude-opus-5-5","scene":{"concept":"museum after hours meets construction: Clawd as night-shift builder assembling a dinosaur skeleton exhibit bone by bone with a ceiling crane, flashlight guard passing","code":"let bones=['  ▄▄','▄███▄','═╦═╦═','║ ║ ║','╱╲','◢◣']\nfunction frame(t,dt){\nfor(let x=0;x<w;x++){put(x,h-1,x%10==0?'┴':'═','#8a7a6a');put(x,0,x%6==0?'┬':'─','#6a6a8a')}\nfor(let i=0;i<4;i++){const px=8+i*30;sprite(px,1,'╔═══╗\\n║░░░║\\n║░░░║\\n╚═══╝','#5a6a9a')}\nconst cy=t%12,n=Math.min(8,Math.floor(t/12)+3)\nconst bx=60,by=h-2\nsprite(bx-12,by,'▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀','#a08868')\nconst parts=[[0,-1,'══════════'],[10,-2,'╦╦╦╦'],[-4,-1,'═══╗'],[14,-3,'▄▀▀▄'],[14,-2,'▀▄▄▀'],[2,-2,'╦═╦═╦═╦'],[3,0,'║'],[9,0,'║']]\nfor(let i=0;i<n;i++){const p=parts[i];text(bx+p[0],by+p[1]-0,p[2],'#e8dcc0')}\nconst tx=bx+parts[n%8][0],ph=smoothstep(0,5,cy),dn=smoothstep(5,9,cy)\nconst hx=lerp(105,tx,ph),hy=lerp(1,by+parts[n%8][1]-1,dn)\ntext(0,0,'','#000')\nline(hx,1,hx,hy,'│','#c8c8c8')\nput(hx,1,'▼','#f5c542')\nif(cy<9)text(hx,hy+1,parts[n%8][2],'#e8dcc0')\ntext(bx-2,by+1>h-1?h-1:by+1,'','#fff')\ntext(bx+18,h-4,'['+'{what}'+']','#f5c542')\nconst gx=mod(t*4,w+40)-20\nsprite(gx,h-4,' o \\n/█>','#7aa0d8')\nfor(let k=1;k<8;k++)put(gx+3+k,h-3,'░','#ffee88')\nconst c=clawd(100,h-5,{facing:-1,arms:{left:'up',right:'down'},blink:fract(t*0.3)<0.05})\ntext(c.top.x-2,c.top.row-1,'▄██▄','#f5c542')\nif(t%20<5)say(t<20?'Building {what}, one bone at a time.':'Shh. The guard thinks it\\'s a real dinosaur.',c.top.x,c.row)\n}","actors":[],"particles":[{"glyphs":"·","count":15,"x":"rand(k)*w","y":"1+mod(t*0.5+rand(k+4)*7,7)","color":"#8a8070"}],"background":{"effect":"pulse","palette":["#101830","#182040","#202850"],"speed":0.3,"intensity":0.15}}},
66  {"phase":"building","world":"skate park","style":"pixel art","concept":"skate park at night, noir-lit: Clawd skates a halfpipe carrying build crates labeled {what}, dropping them into a bin that fills up","source":"claude-opus-5-5","scene":{"concept":"skate park at night, noir-lit: Clawd skates a halfpipe carrying build crates labeled {what}, dropping them into a bin that fills up","code":"let cr=0;function frame(t,dt){const H=2*h;for(let x=0;x<w;x+=1){const n=noise(x*0.07)*4;for(let y=0;y<H;y++){if(y>H-6-n&&x%9<6&&y<H-4)pixel(x,y,'#2a3348')}}for(let x=0;x<w;x+=11)pixel(x,3+(x%3),'#ffe9a8');pixels(w-8,2,'.oo.\\noooo\\n.oo.','{\"o\":\"#f4e7b0\"}'.length?{o:'#f4e7b0'}:{});const L=20,R=w-34;for(let x=L;x<=R;x++){const u=(x-L)/(R-L);const y=Math.round(H-3-Math.pow(Math.abs(u*2-1),3)*10);pixel(x,y,'#9ad4ff');for(let yy=y+1;yy<H;yy++)pixel(x,yy,'#3d4a66')}const p=t*0.9;const u=0.5+0.5*Math.sin(p);const cx=L+u*(R-L);const cy=H-3-Math.pow(Math.abs(u*2-1),3)*10;const f=Math.cos(p)>0?1:-1;const c=clawd(cx-7,(cy-10)/2,{pose:'stand',facing:f,arms:'out'});pixels(Math.round(cx-6),Math.round(cy-2),'############\\n.o........o.',{'#':'#ff6b9a','o':'#ddd'});const bx=w-24;pixels(bx,H-9,'#........#\\n#........#\\n#........#\\n#........#\\n##########',{'#':'#7ae0a0'});const ph=Math.floor(p/(2*Math.PI));cr=Math.min(4,ph);for(let i=0;i<cr;i++)pixels(bx+1+(i%2)*4,H-6-Math.floor(i/2)*2,'####\\n####',{'#':'#e0a060'});if(Math.sin(p)>0.8)pixels(c.right.x,c.right.py-2,'###\\n###',{'#':'#e0a060'});text(bx-1,h-1,'{what}'.slice(0,20),'#ffe9a8');if(t>3&&t<9)say('Kickflipping {what} into the bin, one crate per run.',c.top.x,c.row);if(t>40&&t<48)say('Bin full soon. Still landing every trick.',c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"rain","palette":["#334","#446","#557"],"speed":0.6,"intensity":0.3}}},
67  {"phase":"building","world":"retro arcade game","style":"3D","concept":"ancient tomb as 3D arcade: Clawd lowers sandstone blocks onto a growing pyramid labelled {what} while torches flicker, score ticking up","source":"claude-opus-5-5","scene":{"concept":"ancient tomb as 3D arcade: Clawd lowers sandstone blocks onto a growing pyramid labelled {what} while torches flicker, score ticking up","code":"const blk=box(1.6,0.8,1.6);const col=cylinder(0.3,3,8);let placed=0;const total=15;const lay=[];for(let r=0;r<5;r++)for(let i=0;i<5-r;i++)lay.push({x:(i-(4-r)/2)*1.7,y:0.4+r*0.85});function frame(t,dt){camera(Math.sin(t*0.15)*2,3,10,0,1.5,0,80);light(0.4,0.8,0.5,0.35);fog(8,35,'#000');mesh3d(plane(40,20),{y:0,z:-3,color:'#a08050'});for(const s of[-1,1]){mesh3d(col,{x:s*8,z:-2,color:'#c9a86a'});const fl=0.6+0.4*noise(t*6+s);const p=project(s*8,3.3,-2);if(p){put(p.x,p.y,'▲',mix('#ff6020','#ffe060',fl));put(p.x,p.y+1,'█','#6a4a2a')}}const cyc=t%2.2,done=Math.floor(t/2.2)%total;for(let i=0;i<done;i++){const b=lay[i];mesh3d(blk,{x:b.x,y:b.y,color:i%2?'#d8b878':'#c8a060'})}const b=lay[done];const k=smoothstep(0,1.6,cyc);const by=lerp(4.2,b.y,k);mesh3d(blk,{x:b.x,y:by,color:'#f0d090'});line3d(b.x,by+0.4,0,b.x,5,0,'#998866');const c=clawd3d(b.x,4.4,0,{facing:0,arms:'down',size:0.8});const tp=project(0,4.6,0);if(tp)text(tp.x-Math.floor('{what}'.length/2),0,'{what}','#ffe08a');text(1,0,'SCORE '+String(Math.floor(t*37)*10).padStart(6,'0'),'#7cf');text(w-10,0,'1UP ♥♥♥','#f77');if(done==total-1&&cyc>1.6)text(w/2-5,h-1,'LEVEL UP!','#ff0');if(c&&t%30>2&&t%30<7)say('Stacking {what} one block at a time.',c.top.x,c.row);else if(c&&t%30>16&&t%30<21)say('Pharaohs waited centuries. You get a progress bar.',c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#887"],"speed":0.4,"intensity":0}}},
68  {"phase":"running","world":"fairy tale forest","style":"3D","concept":"fairy tale forest launch pad: Clawd as a gnome mission controller watching a toadstool rocket labeled {what} count down and lift off among 3","source":"claude-opus-5-5","scene":{"concept":"fairy tale forest launch pad: Clawd as a gnome mission controller watching a toadstool rocket labeled {what} count down and lift off among 3D pine trees","code":"const tr=cone(0.9,2.4,8),tk=cylinder(0.15,0.6,6),body=cylinder(0.45,2.2,10),cap=sphere(0.9,10),pad=cylinder(1.6,0.2,12);\nconst trees=Array.from({length:14},(_,i)=>({x:(i%7-3)*3.4+(rand(i)-0.5),z:-3-Math.floor(i/7)*4-rand(i+9)*2}));\nfunction frame(t,dt){const c=mod(t,24),L=smoothstep(10,20,c),ry=c>10?Math.pow(c-10,2)*0.12:0;camera(0,2.2+L*1.5,9,0,1+L*2,-2,90);light(0.4,1,0.6,0.35);fog(10,40,'#000');\nmesh3d(plane(40,30),{z:-6,color:'#4a7a4a'});for(const q of trees){mesh3d(tk,{x:q.x,z:q.z,color:'#9a6a40'});mesh3d(tr,{x:q.x,y:0.6,z:q.z,color:'#5fae6a'})}\nmesh3d(pad,{x:2,z:-1,color:'#b0a090'});const sh=c<10&&c>7?Math.sin(t*60)*0.04:0;mesh3d(body,{x:2+sh,y:0.2+ry,z:-1,color:'#f4ead0'});mesh3d(cap,{x:2+sh,y:2.4+ry,z:-1,scale:1,color:'#e05050'});\nconst p=project(2,1.3+ry,-1);if(p)text(p.x-5,p.y,'{what}'.slice(0,12),'#523');\nif(c>9){for(let i=0;i<14;i++){const f=fract(t*3+i*0.13);point3d(2+(rand(i)-0.5)*f*1.5,0.2+ry-f*1.5,-1,i%2?'#ffd060':'#ff8040')}}\nfor(let i=0;i<6;i++)point3d(-6+i*2.4+Math.sin(t+i),1.5+Math.sin(t*1.7+i)*0.5,0,'#ffffaa');\nconst me=clawd3d(-3,0,1,{facing:1,arms:c>10?'up':{left:'down',right:'out'},size:1});if(me){pixels(me.top.x-3,me.top.py-5,'...r...\\n..rrr..\\n.rrrrr.\\nrrrrrrr','{\"r\":\"#c03030\"}'.length?{r:'#c03030'}:{});\nconst n=Math.ceil(10-c);if(c<10)say(n>0?`{what} countdown: ${n}... wish upon a star.`:'Ignition!',me.top.x,me.row);else if(c<18)say('Up she goes! The mushroom has left the forest.',me.top.x,me.row);else say('Waiting for {what} to land somewhere magical.',me.top.x,me.row)}}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#ffffcc","#aaccff","#ffd0f0"],"speed":0.5,"intensity":0.3}}},
69  {"phase":"running","world":"airport runway","style":"pixel art","concept":"underground mine: hard-hatted Clawd pushes a minecart labeled {what} down a lantern-lit tunnel as gems pile up in the cart","source":"claude-opus-5-5","scene":{"concept":"underground mine: hard-hatted Clawd pushes a minecart labeled {what} down a lantern-lit tunnel as gems pile up in the cart","code":"let ore=[];function frame(t,dt){const s=t*6;for(let y=0;y<h;y++)for(let x=0;x<w;x++){const n=noise2((x+s*0.5)*0.15,y*0.4);const tun=y>=2&&y<=7;let c=tun?mix('#1a1410','#2a2018',n):mix('#3a2a1e','#6a4a30',n);const d=Math.abs(x-60)/60;if(tun)c=mix(c,'#000',d*0.6);put(x,y,' ',null,c);if(!tun&&rand(Math.floor(x+s*0.5)*7+y)>0.97)put(x,y,'◆',hsl((x*37)%360,0.8,0.65),c)}for(let i=-1;i<6;i++){const bx=Math.round(i*28-mod(s,28));fill(bx,2,2,6,'█','#7a5230');fill(bx-1,1,4,1,'▀','#9a6a3a');put(bx+3,3,'●','#ffd060');}for(let x=0;x<w;x++)put(x,8,mod(x+Math.floor(s),4)==0?'╪':'═','#888');const c=clawd(48,4,{pose:'walk',stride:Math.floor(s/1.5)%4,facing:1,arms:'out'});pixels(c.top.x-4,c.top.py-3,'..yyyy..\n.yyyyyy.\nyyyyyyyy','#'.length?{y:'#f0c020'}:{});pixels(c.top.x-1,c.top.py-3,'w','#'.length?{w:'#fff8c0'}:{});const cx=63;pixels(cx,9,'bbbbbbbbbbbbbbbb\n.bggggggggggggb.\n.bggggggggggggb.\n.bbbbbbbbbbbbbb.\n..oo........oo..','#'.length?{b:'#8a8a9a',g:'#5a5a6a',o:'#333'}:{});text(cx+3,5,'{what}'.slice(0,10),'#ffe080','#5a5a6a');const f=Math.min(12,Math.floor(t/4));const pal=['#5ef','#f5a','#fd4','#6f6'];for(let i=0;i<f;i++)pixel(cx+2+(i*5)%12,8-Math.floor(i/6),pal[i%4]);if(fract(t/4)<0.15)put(cx+8+Math.floor(rand(Math.floor(t/4))*4),3,'✦','#fff');const L=['Every shovel of {what} is a gem. Probably.','Still digging. The tunnel goes deeper than the docs.','Cart is filling up! Output looks shiny.','Mind the beams. And the exit code.'];if(t>2)say(L[Math.floor(t/12)%4],c.top.x,c.row);}","actors":[],"particles":[{"glyphs":".·","count":20,"x":"rand(k)*w","y":"2+mod(t*(1+rand(k+5))+rand(k+3)*6,6)","color":"#a08060"}],"background":{"effect":"plasma","palette":["#2a1a10","#3a2818","#4a3020"],"speed":0.3,"intensity":0}}},
70  {"phase":"running","world":"ant colony up close","style":"3D","concept":"airport runway: Clawd as ground marshal waving wands while the {what} jet taxis, roars down the runway and lifts off, again and again","source":"claude-opus-5-5","scene":{"concept":"airport runway: Clawd as ground marshal waving wands while the {what} jet taxis, roars down the runway and lifts off, again and again","code":"const fus=box(4,0.7,0.7),wing=box(1.2,0.12,4.4),tail=box(0.6,1,0.12),strip=plane(60,5),lt=box(0.15,0.15,0.15),tw=box(1.2,3,1.2),cab=box(1.8,0.8,1.8);\nconst lines=['{what} cleared for takeoff. Wheels up!','Ground crew to tower: {what} is rolling.','Another one airborne. I love this job.','Fasten seatbelts, {what} is going up.'];\nfunction frame(t,dt){camera(0,2.6,11,0,1,0,90);light(0.4,1,0.6,0.35);fog(10,45,'#000');\nmesh3d(strip,{y:0,color:'#7a7f8a'});\nfor(let i=-15;i<16;i++){mesh3d(box(1.2,0.02,0.15),{x:i*2+mod(0,2),y:0.02,color:'#eee'});const on=fract(t*0.8-i*0.05)<0.3;mesh3d(lt,{x:i*2,y:0.1,z:2.6,color:on?'#ffd23f':'#806a20',unlit:true});mesh3d(lt,{x:i*2,y:0.1,z:-2.6,color:on?'#ffd23f':'#806a20',unlit:true});}\nmesh3d(tw,{x:10,y:1.5,z:-8,color:'#b8c4d6'});mesh3d(cab,{x:10,y:3.4,z:-8,color:'#6fc3ff'});\nconst P=16,c=Math.floor(t/P),p=mod(t,P);let x,y=0.6,rz=0;\nif(p<5)x=lerp(-28,-12,smoothstep(0,5,p));else if(p<8)x=-12;else{const q=p-8;x=-12+q*q*1.2;y=0.6+Math.max(0,x-2)*0.22;rz=Math.min(0.25,Math.max(0,x-2)*0.05);}\nconst o={x,y,z:0,rz,color:'#e8ecf2'};mesh3d(fus,o);mesh3d(wing,{...o,x:x-0.2,color:'#c9d3e0'});mesh3d(tail,{...o,x:x-1.8,y:y+0.6+rz*1.8,color:'#d97757'});\nconst pp=project(x,y+1.4,0);if(pp)text(pp.x-4,pp.y,'{what}','#ffe08a');\nif(p>8&&p<14)for(let k=0;k<6;k++){const e=project(x-2.2-k*0.6,y-0.1-rz*2,0);if(e)put(e.x,e.y,k<2?'▒':'░',k<2?'#ffb347':'#888');}\nconst wave=p<8?Math.floor(t*2)%2:0;const me=clawd3d(-3,0,4,{facing:0,arms:wave?{left:'up',right:'out'}:{left:'out',right:'up'},size:0.9});\nif(me){put(me.left.x,me.left.row-1,'▮','#ff5a36');put(me.right.x,me.right.row-1,'▮','#ff5a36');pixels(me.top.x-3,me.top.py-2,'..####..\\n########','{\"#\":\"#ffd23f\"}');if(p>8.5&&p<13)say(lines[c%lines.length],me.top.x,me.row);}}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#557","#99b"],"speed":0.4,"intensity":0.3}}},
71  {"phase":"running","world":"fishing pond","style":"text art","concept":"mad science lab: goggled Clawd at the console while Tesla coils arc and a huge flask labeled {what} slowly fills and froths over","source":"claude-opus-5-5","scene":{"concept":"mad science lab: goggled Clawd at the console while Tesla coils arc and a huge flask labeled {what} slowly fills and froths over","code":"function frame(t,dt){fill(0,8,w,1,'═','#667');const coil=' (O) \\n  ║  \\n ═╬═ \\n  ║  \\n ═╬═ \\n ▐█▌ ';sprite(6,2,coil,'#c8a');sprite(30,2,coil,'#c8a');const zap=fract(t/3)<0.3;if(zap){for(let x=11;x<33;x++){const y=2+Math.round(Math.sin(x*1.7+t*40));put(x,y,Math.random()<0.5?'/':'\\\\','#aef')}put(8,2,'✦','#fff');put(32,2,'✦','#fff')}const c=clawd(42,4,{facing:1,arms:zap?'up':{left:'down',right:'out'},eyes:zap?'wide':'open'});text(c.eyes.x-1,c.eyes.row,'◎-◎','#8ff');text(43,3,'▄▄▄','#ddd');for(let x=58;x<81;x++)put(x,6,'═','#889');const bx=58+mod(t*8,23);put(Math.floor(bx),6,'●','#7f7');sprite(80,2,'   ┌──┐   \\n   │  │   \\n  ╱    ╲  \\n ╱      ╲ \\n│        │\\n│        │\\n╰────────╯','#9cf');const R={3:[84,85],4:[83,86],5:[82,87],6:[81,88],7:[81,88]};const p=fract(t/15),lvl=p*5.5;for(let r=7;r>=3;r--){if(7-r<lvl){const[a,b]=R[r];for(let x=a;x<=b;x++)put(x,r,(x+r+Math.floor(t*3))%5?'▓':'o',hsl(110+20*Math.sin(t+x),0.8,0.5))}}if(p>0.85){for(let i=0;i<6;i++)put(83+((i*3+Math.floor(t*6))%5),1-((i+Math.floor(t*5))%2),'░','#7f7')}text(92,5,'◄ {what}','#fd6');fill(96,7,10,1,'▀','#556');text(97,6,'▯▯▯','#f6a');const s=mod(t,15);if(s>2&&s<6)say('Feeding {what} to the reactor. It lives!',c.top.x,c.row);if(s>11&&s<14.5)say('Almost done... or almost exploding.',c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#203040","#302050","#204030"],"speed":0.6,"intensity":0.2}}},
72  {"phase":"running","world":"orchestra concert","style":"3D","concept":"orchestra concert on a construction site: Clawd in a hard hat conducts tower cranes that lift girder-notes onto a rising building score whil","source":"claude-opus-5-5","scene":{"concept":"orchestra concert on a construction site: Clawd in a hard hat conducts tower cranes that lift girder-notes onto a rising building score while {what} runs","code":"const bx=box(1,1,1),cy=cylinder(0.12,5,6),gd=box(2.2,0.25,0.4);let fl=0;\nfunction frame(t,dt){camera(Math.sin(t*0.1)*2,2.6,9,0,1.6,0,90);light(0.4,1,0.6,0.35);fog(10,40,'#000');\nmesh3d(plane(40,20),{y:0,color:'#7a6a50'});\nconst n=Math.floor(t/4),p=fract(t/4);\nfor(let i=0;i<Math.min(n,12);i++){const c=i%4,r=Math.floor(i/4);mesh3d(bx,{x:-1.5+c,y:0.5+r,z:-2,color:['#e8b04a','#d0d4dc','#c98a5a','#a8c0e0'][c]})}\nfor(const s of [-1,1]){const cx=s*6;mesh3d(cy,{x:cx,z:-2,color:'#f0c040'});line3d(cx,5,-2,cx-s*5,5,-2,'#f0c040');line3d(cx,5,-2,cx+s*2,5,-2,'#f0c040')}\nconst i=n%12,c=i%4,r=Math.floor(i/4),s=n%2?1:-1,tx=-1.5+c,ty=0.5+Math.min(n,12)/4|0;\nconst k=Math.min(1,p*1.6),e=k*k*(3-2*k),gx=lerp(s*9,tx,e),gy=lerp(1,4.6,Math.sin(Math.PI*Math.min(1,e*1.3))*0.6)+ (e>0.9?lerp(4.6,0.5+r,(e-0.9)*10):0)*0;\nconst yy=e<0.7?lerp(0.3,4.2,e/0.7):lerp(4.2,0.5+(Math.min(n,12)/4|0),(e-0.7)/0.3);line3d(gx,5,-2,gx,yy+0.2,-2,'#ccc');mesh3d(gd,{x:gx,y:yy,z:-2,ry:Math.sin(t*2)*0.2,color:'#e06a4a'});\nconst q=project(gx,yy+0.5,-2);if(q)text(q.x-1,q.y-1,'♪','#ffe');\nconst arm=Math.sin(t*3)>0?'up':'out';const m=clawd3d(3.5,0,2,{facing:-1,arms:{left:arm,right:Math.sin(t*3+1.5)>0?'up':'down'}});\nif(m){pixels(m.top.x-4,m.top.py-3,'..yyyy..\\nyyyyyyyy','{\"y\":\"#ffd23a\"}'.length?{y:'#ffd23a'}:{});const lb=project(-3.5,0.3,1);if(lb)text(lb.x,lb.y,'[ '+'{what}'+' ]','#9fe');\nconst L=['Allegro, cranes! {what} in 4/4.','Brass section: more rivets.','Floor '+(Math.min(n,12)/4|0)+' and a key change.','Still building. Still bowing.'];if(t>2)say(L[Math.floor(t/9)%4],m.top.x,m.row)}}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#889"],"speed":0.3,"intensity":0.2}}},
73  {"phase":"running","world":"desert caravan","style":"3D","concept":"zen rock garden: Clawd as a monk slowly raking ripples around a stone labeled {what} while the command runs, 3D orbiting garden","source":"claude-opus-5-5","scene":{"concept":"zen rock garden: Clawd as a monk slowly raking ripples around a stone labeled {what} while the command runs, 3D orbiting garden","code":"const rock=sphere(1,10),stone=sphere(0.6,8),lan=box(0.5,1.4,0.5),cap=cone(0.7,0.5,6);\nfunction frame(t,dt){\nconst a=t*0.08;\ncamera(Math.sin(a)*9,3,Math.cos(a)*9,0,0.5,0,80);\nlight(0.4,1,0.5,0.35);fog(8,30,'#000');\nfor(let r=1.5;r<9;r+=0.7){let p=null;const s=Math.min(48,Math.floor(t*2)+10);for(let i=0;i<=s;i++){const q=i/48*Math.PI*2;const x=Math.cos(q)*r,z=Math.sin(q)*r*0.8;if(p)line3d(p[0],0,p[1],x,0,z,'#c8b890');p=[x,z];}}\nmesh3d(rock,{x:0,y:0.6,z:0,scale:1,color:'#9a9488'});\nmesh3d(stone,{x:4,y:0.3,z:-2,color:'#8a8478'});\nmesh3d(stone,{x:-5,y:0.25,z:1.5,scale:0.7,color:'#a09a8a'});\nmesh3d(lan,{x:-3,y:0.7,z:-4,color:'#b0a898'});mesh3d(cap,{x:-3,y:1.4,z:-4,color:'#c0b8a8'});\nconst ang=t*0.25,cx=Math.cos(ang)*3.2,cz=Math.sin(ang)*2.6;\nconst c=clawd3d(cx,0,cz,{stride:Math.floor(t*3)%4,facing:Math.sin(ang+a)>0?-1:1,size:0.8});\nconst p=project(0,1.9,0);if(p)text(p.x-4,p.y,'{what}','#ffe9b0');\nif(c){text(c.left.x-2,c.left.row,'⊥','#d0b070');const m=t%30;\nif(m>4&&m<11)say('Raking circles around {what}. Patience.',c.top.x,c.row);\nelse if(m>17&&m<24)say('The command finishes when it finishes.',c.top.x,c.row);}\nconst lf=fract(t*0.07);put(10+lf*90,lf*8,'✦','#f0a0b0');\n}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#2a3a2a","#3a4a3a","#4a5a4a"],"speed":0.3,"intensity":0}}},
74  {"phase":"running","world":"bank heist","style":"pixel art","concept":"dinosaur-era bank heist: Clawd in a striped mask cracks a stone-age vault while a brontosaurus slowly drags it open, labeled {what}","source":"claude-opus-5-5","scene":{"concept":"dinosaur-era bank heist: Clawd in a striped mask cracks a stone-age vault while a brontosaurus slowly drags it open, labeled {what}","code":"let gold=[];function frame(t,dt){for(let x=0;x<w;x++){for(let py=0;py<18;py++){pixel(x,py,py>15?'#6b4a2a':py<10?mix('#2a1f4a','#7a3a5a',py/10):'#3a2a3a')}}pixels(4,4,'..##.....\\n.####....\\n######...\\n..##.....\\n..##.....\\n..##.....',{'#':'#3f8a3a'});pixels(108,6,'.###.\\n#####\\n..#..\\n..#..',{'#':'#3f8a3a'});pixels(10,3,'....###\\n...####\\n.....##',{'#':'#ff9a3a'});const vx=60;for(let py=4;py<16;py++)for(let x=vx;x<vx+26;x++)pixel(x,py,(x+py)%5==0?'#8a8a8a':'#a8a8a8');const o=smoothstep(2,30,mod(t,40));const dx=Math.round(o*16);for(let py=6;py<16;py++)for(let x=vx+4;x<vx+22;x++)pixel(x,py,'#2a1a10');for(let py=6;py<16;py++)for(let x=vx+4+dx;x<vx+22+dx;x++)pixel(x,py,'#7a7a7a');circle(vx+13+dx,5.5,1,'●','#ddd');text(vx+5+dx,4,'{what}'.slice(0,12),'#ffd');line(vx+22+dx,5,96,5,'─','#c9a');const bx=96+dx*0.3;pixels(Math.round(bx),2,'.........####\\n........##o##\\n........###..\\n.......###...\\n#############.\\n###############\\n###############\\n.##..##..##..#\\n.##..##..##..#',{'#':'#7aa85a','o':'#000'});if(o>0.3&&Math.random()<o*0.4)gold.push({x:vx+8+Math.random()*10,y:12,vx:-4-Math.random()*6,vy:-8});for(const g of gold){g.vy+=20*dt;g.x+=g.vx*dt;g.y+=g.vy*dt;if(g.y>15){g.y=15;g.vx=0;g.vy=0}pixel(Math.round(g.x),Math.round(g.y),'#ffd23a')}if(gold.length>150)gold.splice(0,1);const cx=lerp(-14,36,smoothstep(0,3,t));const c=clawd(cx,4,{pose:cx<36?'walk':'stand',stride:Math.floor(cx/1.5)%4,facing:1,arms:o>0.5?'up':'out'});fill(c.eyes.x-6,c.eyes.row,13,1,'▀','#222');pixels(c.right.x,c.right.py-2,'.##.\\n####\\n####',{'#':'#c9b07a'});const ph=Math.floor(t/12)%4;const L=['Tiny arms? I brought a bronto. Cracking {what}.','Vault door: limestone. Patience: also limestone.','Gold coming out. {what} still churning.','Meteor-proof vault. Not bronto-proof.'];if(t>3)say(L[ph],c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#3a2a5a","#7a3a5a","#c96a3a"],"speed":0.5,"intensity":0}}},
75  {"phase":"running","world":"wild west","style":"text art","concept":"submarine: Clawd pilots a little sub through the deep, sonar pinging into the dark while {what} runs below the waterline","source":"claude-opus-5-5","scene":{"concept":"submarine: Clawd pilots a little sub through the deep, sonar pinging into the dark while {what} runs below the waterline","code":"let bub=Array.from({length:25},(_,i)=>({x:rand(i)*w,y:rand(i+40)*h,v:0.8+rand(i+9)*1.5}));const lines=['Diving on {what}. Hold your breath.','Sonar says: still running. Very deep.','Ping... ping... nothing broke yet.','Depth gauge climbing. Output soon?','A fish just asked if {what} is done.'];function frame(t,dt){for(let y=0;y<h;y++)for(let x=0;x<w;x+=1)if((x+y*7)%23==0)put(x,y,'.','#1d4a66');for(let x=0;x<w;x++){const n=noise(x*0.12+t*0.6);put(x,h-1,n>0.6?'▲':n>0.4?'▄':'_','#4a7a5a');if(rand(floor(x+t*0.6*8))>0.93)put(x,h-2,'}','#3f9a5f')}for(const b of bub){b.y-=b.v*dt;if(b.y<0){b.y=h-1;b.x=rand(t+b.x)*w}put(b.x,b.y,b.v>1.6?'o':'°','#7fc8e8')}const sx=24,bo=Math.sin(t*1.2)*0.4,sy=Math.round(1+bo);const hc='#e8c34a';fill(sx+4,sy+1,44,5,'█',hc);text(sx+2,sy+2,'▐█',hc);text(sx+2,sy+4,'▐█',hc);text(sx+48,sy+2,'█▌',hc);text(sx+48,sy+4,'█▌',hc);text(sx+3,sy+3,'█',hc);text(sx+49,sy+3,'█',hc);text(sx+4,sy+6,'▀'.repeat(44),hc);text(sx+30,sy-1,'┌┐',hc);text(sx+30,sy,'██',hc);text(sx+32,sy-1,'═○','#cfd8dc');const pr='|/-\\\\'[floor(t*10)%4];text(sx,sy+3,pr+'═','#cfd8dc');fill(sx+15,sy+1,18,4,' ',null,'#0b2b3c');const c=clawd(sx+17,sy+1,{facing:1,blink:fract(t*0.3)<0.04,arms:fract(t*0.25)<0.5?'down':{left:'down',right:'out'}});text(sx+36,sy+2,'◐','#6fd3ff');text(sx+40,sy+2,'◐','#6fd3ff');text(sx+36,sy+4,'{what}','#5a3a10',hc);const g=Math.min(1,t/90);text(sx+6,sy+2,'DEPTH','#5a3a10',hc);text(sx+6,sy+3,'▓'.repeat(1+floor(g*6)).padEnd(7,'░'),'#8a2a10',hc);for(let i=0;i<3;i++){const f=fract(t*0.4+i/3);const px=sx+52+f*60;if(px<w)for(let r=-1;r<=1;r++)put(px+Math.abs(r),sy+3+r*2,r?(r<0?'/':'\\\\'):')',mix('#6fffd0','#0b2b3c',f))}const fx=w-mod(t*4,w+20);text(fx,6,'<°)))><','#ff9a6a');const li=floor(t/9)%lines.length;if(fract(t/9)<0.7)say(lines[li],c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"waves","palette":["#06243a","#0a3550","#0e4a6a"],"speed":0.5,"intensity":0.3}}},
76  {"phase":"git","world":"race track pit stop","style":"3D","concept":"bakery: Clawd the baker stacks a layer cake one commit-layer at a time, each tier dropping onto the stand, the newest labeled {what}","source":"claude-opus-5-5","scene":{"concept":"bakery: Clawd the baker stacks a layer cake one commit-layer at a time, each tier dropping onto the stand, the newest labeled {what}","code":"const L=cylinder(1.4,0.35,20),P=cylinder(1.9,0.08,20),C=box(16,0.5,4),cols=['#f4c2c2','#f0e0b0','#d8a070','#f8f0f0','#e8a8c8','#b8e0a8'];\nfunction frame(t,dt){camera(0,2.6,8,0,1.3,0,80);light(0.4,1,0.6,0.35);fog(8,40,'#000');\nmesh3d(C,{x:0,y:-0.25,z:0,color:'#b08860'});mesh3d(P,{x:1.5,y:0,z:0,color:'#dddddd'});\nconst cyc=t%28,n=Math.floor(cyc/4),k=fract(cyc/4);\nfor(let i=0;i<n&&i<6;i++)mesh3d(L,{x:1.5,y:0.08+i*0.35,z:0,color:cols[i]});\nlet top=0.08+n*0.35;\nif(n<6){const y=lerp(4,top,smoothstep(0.1,0.5,k));mesh3d(L,{x:1.5,y,z:0,color:cols[n],ry:t});const p=project(3.2,y+0.2,0);if(p)text(p.x+1,p.y,'{what}','#ffe9a0');}\nelse{const p=project(1.5,top+0.6,0);if(p){text(p.x,p.y,'★','#ffd84a');}}\nconst land=n<6&&k>0.5&&k<0.75;\nconst c=clawd3d(-3.2,0,0.5,{facing:1,arms:land?'up':'out',size:1.2});\nif(c){pixels(c.top.x-3,c.top.py-5,' ..... \\n.......\\n.......\\n ##### ','{\".\":\"#ffffff\",\"#\":\"#dddddd\"}'.length?{'.':'#ffffff','#':'#dddddd'}:{});\nconst s=n>=6?'Seven layers of history. Nobody rebase the cake.':n>=3?'Layer '+(n+1)+': {what}. Frosted, signed, committed.':'Fresh commit out of the oven: {what}';say(s,c.top.x,c.row);}}\n","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#3a2418","#4a2e20","#5a3828"],"speed":0.4,"intensity":0}}},
77  {"phase":"git","world":"train station","style":"pixel art","concept":"train station: Clawd as conductor on a platform as commit-cars couple onto a train that departs down a branching track","source":"claude-opus-5-5","scene":{"concept":"train station: Clawd as conductor on a platform as commit-cars couple onto a train that departs down a branching track","code":"let cars=[];let st=0;\nfunction frame(t,dt){\nfor(let x=0;x<w;x++){put(x,0,' ',null,'#1a1f33');}\nfor(let i=0;i<8;i++)pixels(4+i*16,0,'..##..\\n.####.','{}'.length?{'#':'#2c3550'}:{});\nfill(0,7,w,1,'▀','#8a6d4a','#3a2a1a');\nfor(let x=0;x<w;x+=3)put(x,8,'╧','#776');\nline(70,8,w,6,'═','#556');\nfill(0,6,44,1,'▄','#9aa','#000');\ntext(2,1,'PLATFORM main',\"#ffd166\");\nconst cyc=mod(t,14);\nconst n=Math.min(4,Math.floor(cyc/2));\nconst dep=cyc>10?Math.pow(cyc-10,2)*5:0;\nconst bx=48+dep;\nconst P={'r':'#e06c75','y':'#ffd166','k':'#222','w':'#cde','g':'#5c9'};\npixels(bx,8,'..yy........\\nrrrrrrrrrrr.\\nrwwrrwwrrrrr\\nrrrrrrrrrrrr\\nkk..kk..kk..',P);\nif(dep<1)for(let p=0;p<3;p++){const a=fract(t*0.7+p/3);put(bx+2+a*4,3-a*3,'○','#aab');}\nfor(let i=0;i<n;i++){const cx=bx+13+i*13-(i==n-1&&cyc<10?Math.max(0,8-(cyc%2)*16):0);pixels(cx,9,'gggggggggggg\\ngwwgwwgwwggg\\ngggggggggggg\\nkk......kk..',P);text(cx+1,5,i==0?'{what}'.slice(0,11):'commit'+(i+1),'#fff');}\nconst c=clawd(20,2.5,{facing:1,arms:cyc>9&&cyc<11?{left:'down',right:'up'}:'down',blink:fract(t*0.3)<0.05});\npixels(c.top.x-4,c.top.py-2,'.kkkkkk.\\nkkkkkkkkk',{k:'#24305e'});\nif(cyc>9&&cyc<11)put(c.right.x,c.right.row-1,'⚑','#e06c75');\nif(cyc<3)say('All aboard! {what} is boarding.',c.top.x,c.row);\nelse if(cyc>9&&cyc<12)say('Committed! Next stop: HEAD.',c.top.x,c.row);\nelse if(cyc>12)say('Clear the track, new branch incoming.',c.top.x,c.row);\n}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#99a"],"speed":0.3,"intensity":0}}},
78  {"phase":"git","world":"ski slope","style":"3D","concept":"airport runway in 3D: commits take off as planes from a branching taxiway while Clawd waves them out as ground marshal","source":"claude-opus-5-5","scene":{"concept":"airport runway in 3D: commits take off as planes from a branching taxiway while Clawd waves them out as ground marshal","code":"const fus=box(0.8,0.7,3),wg=box(4,0.12,0.9),tl=box(0.12,0.8,0.6),twr=cylinder(0.4,3,8),cab=box(1.4,0.8,1.4);\nconst lines=['Cleared for takeoff: {what}','Another commit wheels-up!','Branch traffic heavy today.','Tower to main: {what} inbound.'];\nfunction frame(t,dt){camera(0,2.6,10,0,1,-20,85);fog(10,50,'#000');light(0.4,1,0.5,0.35);\nmesh3d(plane(8,120),{z:-40,color:'#7c8794'});\nfor(let i=0;i<14;i++){const z=6-i*6;line3d(0,0.02,z,0,0.02,z-2.5,'#fff');point3d(-4,0.1,z,'#ffd166');point3d(4,0.1,z,'#ffd166');}\nline3d(4,0.02,6,14,0.02,-30,'#7fd1b9');line3d(6,0.02,6,16,0.02,-30,'#7fd1b9');\nmesh3d(twr,{x:-10,z:-14,color:'#c9d1d9'});mesh3d(cab,{x:-10,y:3.3,z:-14,color:'#8ecae6'});\nconst cy=Math.floor(t/7),p=mod(t,7);const z=8-Math.pow(p,1.8)*2.4,y=0.45+Math.max(0,-z-6)*0.18;const rx=z<-6?-0.18:0;\nmesh3d(fus,{y,z,rx,color:'#e8eef5'});mesh3d(wg,{y,z:z+0.2,rx,color:'#d97757'});mesh3d(tl,{y:y+0.6,z:z+1.3,rx,color:'#d97757'});\nconst q=project(0,y+1.2,z);if(q&&q.depth>0)text(q.x-6,q.y,'{what}'.slice(0,20),'#ffe08a');\nconst bz=-20+mod(t*3,30);mesh3d(fus,{x:9+(-20-bz)*-0.0,z:bz,y:0.45,ry:-0.27,scale:0.7,color:'#a8dadc'});\nconst me=clawd3d(-5.5,0,4,{facing:1,arms:fract(t*1.5)<0.5?'up':'out'});\nif(me){pixels(me.left.x-1,me.left.py-3,'o\\no\\no','{\"o\":\"#ff9f1c\"}'==''?{}:{o:'#ff9f1c'});pixels(me.right.x,me.right.py-3,'o\\no\\no',{o:'#ff9f1c'});pixels(me.top.x-3,me.top.py-2,'.yyyy.\\nyyyyyy',{y:'#ffd23f'});if(p>1&&p<5.5)say(lines[cy%4],me.top.x,me.row);}}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#99a"],"speed":0.4,"intensity":0}}},
79  {"phase":"git","world":"submarine","style":"3D","concept":"vegetable garden 3D: git as a garden, Clawd grafting a new branch onto a commit-vine, planting commit seeds along rows that fork into branch","source":"claude-opus-5-5","scene":{"concept":"vegetable garden 3D: git as a garden, Clawd grafting a new branch onto a commit-vine, planting commit seeds along rows that fork into branches","code":"const seed=sphere(0.25,8),stem=cylinder(0.06,1,6),leaf=cone(0.35,0.6,6),bed=box(26,0.3,1.4);\nlet planted=0;\nfunction frame(t,dt){\ncamera(Math.sin(t*0.1)*2,3.2,9,0,0.6,-3,80);light(0.4,1,0.6,0.35);fog(10,40,'#000');\nmesh3d(bed,{x:0,y:-0.15,z:0,color:'#a07050'});\nmesh3d(bed,{x:4,y:-0.15,z:-5,color:'#8a6040'});\nline3d(-12,0.05,0,12,0.05,0,'#6c4');\nconst bx=-2;line3d(bx,0.05,0,bx+3,0.05,-5,'#6c4');line3d(bx+3,0.05,-5,16,0.05,-5,'#6c4');\nconst cyc=mod(t,30),n=Math.floor(cyc/3);\nfor(let i=0;i<8;i++){const x=-11+i*1.3;if(x>bx)break;const g=1;mesh3d(stem,{x,z:0,scale:g,color:'#7d5'});mesh3d(leaf,{x,y:0.8,z:0,color:'#5c4'});mesh3d(seed,{x,y:1.2,z:0,color:'#e55'});}\nfor(let i=0;i<n;i++){const x=bx+3+i*1.4;const g=clamp((cyc-i*3)/2,0.1,1);mesh3d(stem,{x,z:-5,scale:g,color:'#7d5'});mesh3d(leaf,{x,y:0.8*g,z:-5,scale:g,color:'#5c4'});if(g>0.9)mesh3d(seed,{x,y:1.2,z:-5,color:'#fb3'});}\nconst cx=bx+3+n*1.4-0.7,ph=fract(cyc/3);\nconst me=clawd3d(cx-1+ph,0,-4,{facing:1,stride:Math.floor(t*6)%4,arms:ph>0.6?'down':'out'});\nconst p=project(-11,1.6,0);if(p)text(p.x,p.y,'main','#9e8');\nconst q=project(bx+3,1.6,-5);if(q)text(q.x,q.y,'{what}','#fd6');\nif(me){if(cyc<6)say('New branch sprouting off main. Watering {what}.',me.top.x,me.row);else if(cyc>21&&cyc<27)say('Every commit a carrot. Harvest = merge.',me.top.x,me.row);}\n}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#234","#253","#345"],"speed":0.3,"intensity":0}}},
80  {"phase":"git","world":"weather report","style":"3D","concept":"circus big top in 3D: Clawd tightrope-walks the main branch high above the ring, a side rope forks off and merges back, and every step leave","source":"claude-opus-5-5","scene":{"concept":"circus big top in 3D: Clawd tightrope-walks the main branch high above the ring, a side rope forks off and merges back, and every step leaves a glowing commit bead on the wire","code":"const fl=cylinder(9,0.1,24),pl=cylinder(0.12,3,6),bl=sphere(0.22,8);\nconst bz=u=>-2.2*Math.sin(Math.PI*u);\nfunction frame(t){camera(Math.sin(t*0.2)*3,3.2,9,0,2.6,-2,90);fog(9,40,'#000');light(0.3,1,0.6,0.35);\nmesh3d(fl,{z:-3,color:'#c04848'});\nfor(let i=0;i<=16;i++){const a=i/16*Math.PI;line3d(0,9,-5,Math.cos(a)*13,0,-4-Math.sin(a)*9,i%2?'#e05050':'#eeeeee')}\nmesh3d(pl,{x:-8,color:'#dddddd'});mesh3d(pl,{x:8,color:'#dddddd'});\nline3d(-8,3,0,8,3,0,'#ffe080');\nfor(let i=0;i<8;i++){const u0=i/8,u1=(i+1)/8;line3d(-4+u0*8,3,bz(u0),-4+u1*8,3,bz(u1),'#70d0ff')}\nconst cy=mod(t,22),p=clamp(cy*1.0-8,-8,8);\nfor(let x=-7;x<=7;x+=2)if(p>x)mesh3d(bl,{x,y:3.25,z:0,color:'#ffd040'});\nfor(let k=1;k<4;k++){const u=k/4;if(cy>6+k*1.5)mesh3d(bl,{x:-4+u*8,y:3.25,z:bz(u),color:'#60e0ff'})}\nconst c=clawd3d(p,3,0,{stride:p<8?Math.floor(t*6)%4:-1,facing:p<8?1:0,arms:p<8?'out':'up',size:0.9});\nconst b=project(0,6.5,-5);if(b)text(b.x-6,b.y,'★ '+'{what}'+' ★','#ffee99');\nif(c){const s=cy<5?'Ladies and gentlemen: {what}, no safety net!':cy<12?'Feature branch swinging out... and back!':cy<18?'Merge caught! Every commit stays on the wire.':'Ta-da! Clean history, take a bow.';say(s,c.top.x,c.row)}}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#ffe080","#ff8080","#80d0ff"],"speed":0.5,"intensity":0}}},
81  {"phase":"web","world":"train station","style":"pixel art","concept":"submarine meets train station: Clawd pilots a little pixel sub along a seabed rail line, stopping at an underwater station to collect a mess","source":"claude-opus-5-5","scene":{"concept":"submarine meets train station: Clawd pilots a little pixel sub along a seabed rail line, stopping at an underwater station to collect a message capsule labeled {what} from a pneumatic tube","code":"const P={'#':'#f2c14e','o':'#9fe3ff','d':'#b07a2a','p':'#888'};\nconst sub=['....##.....','....##.....','.#########.','###oo#oo####','############','.##########.'];\nlet bub=Array.from({length:25},(_,i)=>({x:rand(i)*w,y:rand(i+9)*h*2,v:2+rand(i+5)*3}));\nfunction frame(t,dt){\nfor(let y=0;y<h;y++)for(let x=0;x<w;x+=1){const k=y/h;put(x,y,' ',null,mix('#0a2a4a','#04121f',k));}\nfor(let x=0;x<w;x++){put(x,h-1,x%4==0?'┴':'═','#7a8a99');}\nfor(let x=0;x<w;x+=9)pixels(x,2*h-5,'.#.\\n###',{'#':'#2f6b3a'});\nconst sx=Math.floor(w*0.62);fill(sx,1,22,6,' ',null,'#1c3a55');sprite(sx,0,'╔════════════════════╗','#9ab');for(let y=1;y<7;y++){put(sx,y,'║','#9ab');put(sx+21,y,'║','#9ab');}\ntext(sx+3,1,'STATION: WEB DEPTHS','#ffd27a');line(sx+16,2,sx+16,6,'┃','#bbb');\nconst cyc=mod(t,16),go=smoothstep(0,5,cyc),leave=smoothstep(10,15,cyc);\nconst x=lerp(-16,sx+2,go)+leave*(w-sx+20);\nconst cy=cyc>3&&cyc<11?lerp(2,5,smoothstep(3,6,cyc))-(cyc>8?lerp(0,3,smoothstep(8,10,cyc)):0):-3;\nif(cy>-3){pixels((sx+15)*1,cy*2,'.dd.\\ndddd\\n.dd.',P);text(sx+11,cy,'{what}'.slice(0,20),'#fff');}\nfor(const b of bub){b.y-=b.v*dt;if(b.y<0){b.y=h*2;b.x=rand(t+b.x)*w}pixel(b.x,b.y,'#7fc8ff');}\nconst yy=2*h-9+Math.sin(t*2)*0.6;pixels(x-2,yy-2,sub.join('\\n'),P);\nconst c=clawd(x,h-5,{facing:cyc<10?1:1,stride:Math.floor(x/1.5)%4,scale:1});\npixels(c.top.x-3,c.top.py-3,'.ooo.\\nooooo',{o:'#9fe3ff'});\nif(cyc>5&&cyc<10)say(cyc<8?'Next stop: {what}. Mind the gap, and the squid.':'Capsule aboard! Surfacing with answers.',c.top.x,c.row);\n}","actors":[],"particles":[],"background":{"effect":"waves","palette":["#0a3a5a","#105070","#1a6a8a"],"speed":0.5,"intensity":0}}},
82  {"phase":"git","world":"circus","style":"pixel art","concept":"volcano island: lava flows branch and merge like git history while Clawd in a lei sets glowing commit-stones along them","source":"claude-opus-5-5","scene":{"concept":"volcano island: lava flows branch and merge like git history while Clawd in a lei sets glowing commit-stones along them","code":"const C=[[56,14],[66,10],[72,14],[76,10],[86,10],[88,14],[100,14],[112,14]];\nfunction frame(t,dt){for(let y=0;y<18;y++)for(let x=0;x<w;x+=1){if(y<16)continue;pixel(x,y,y==16&&(x+Math.floor(t*4))%6<2?'#9ce':'#2a5a9a')}\nfor(let y=3;y<16;y++){const hw=(y-3)*2.2;for(let x=Math.floor(28-hw);x<=28+hw;x++)pixel(x,y,y<5?'#f60':(noise2(x*.3,y*.3)>.6?'#7a4a2a':'#5a3a22'))}\nfor(let x=50;x<w;x++)pixel(x,15,'#3a2a1a');\nfor(let i=0;i<9;i++){const p=fract(t*.5+i/9);pixel(28+Math.sin(i*2)*p*12,3-p*3+p*p*2,mix('#ff0','#f40',p))}\nfor(let x=28;x<w;x++){const y=x<50?Math.floor(5+(x-28)*.42):14;pixel(x,y,mix('#f80','#f30',noise(x*.2-t)))}\nfor(let x=60;x<=94;x++){const y=x<64?14-(x-60):x>90?10+(x-90):10;pixel(x,y,mix('#fc0','#f60',noise(x*.2+t)))}\nconst n=Math.floor(t/2.2)%(C.length+2);for(let i=0;i<Math.min(n,C.length);i++){const[a,b]=C[i];pixels(a-1,b-1,'.#.\\n###\\n.#.',{'#':i==n-1?'#fff':'#ffd060'})}\nconst tg=C[Math.min(Math.max(n-1,0),C.length-1)];const cx=tg[0]-7,cy=(tg[1]==10?2:5)-4+1;\nconst c=clawd(cx,Math.max(cy,0),{facing:1,stride:Math.floor(t*6)%4,arms:fract(t/2.2)<.3?'up':'down'});\npixels(c.top.x-4,c.top.py+4,'o.y.o.y.o',{o:'#f6a',y:'#ff6'});\ntext(4,1,'┌'+'─'.repeat(22)+'┐','#c96');text(4,2,'│ '+'{what}'.padEnd(20)+' │','#ffd');text(4,3,'└'+'─'.repeat(22)+'┘','#c96');\nif(n>=C.length)say('Branch merged back to main. Lava cooled, history set in stone.',c.top.x,c.row);else if(n==2)say('Feature branch erupting off main. Keep it flowing!',c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#331a33","#4a2244","#552211"],"speed":0.4,"intensity":0}}},
83  {"phase":"web","world":"zen rock garden","style":"3D","concept":"skate park 3D: Clawd skates a halfpipe, each run fetching a glowing parcel labeled {what} from a far ramp while a zen raked-sand pit sits be","source":"claude-opus-5-5","scene":{"concept":"skate park 3D: Clawd skates a halfpipe, each run fetching a glowing parcel labeled {what} from a far ramp while a zen raked-sand pit sits beside","code":"const rp=box(1,0.3,3),pk=box(0.5,0.5,0.5),stone=sphere(0.5,8),rail=cylinder(0.05,1,6);let got=0;function frame(t,dt){camera(Math.sin(t*0.1)*2,3,9,0,1,0,90);light(0.4,1,0.6,0.35);fog(10,40,'#000');for(let i=-9;i<=9;i++){const y=0.08*i*i/9;mesh3d(rp,{x:i,y:y,z:0,rz:Math.atan(0.18*i/9*9/9)*1.6,color:'#9ab0c8'})}for(let z=-6;z>-14;z-=1)line3d(-8,0.01,z,8,0.01,z,'#c8b890');mesh3d(stone,{x:-5,y:0.3,z:-9,color:'#a0a090'});mesh3d(stone,{x:4,y:0.4,z:-11,scale:1.4,color:'#b0a898'});mesh3d(rail,{x:0,y:0,z:-3,color:'#ccc'});const s=Math.sin(t*1.2),x=s*8,y=0.08*x*x/9+0.15;const cyc=Math.floor((t*1.2+Math.PI/2)/Math.PI);const hold=cyc%2==1;mesh3d(pk,{x:9.5,y:1.6,z:0,ry:t,color:hold?'#555':'#ffcc55'});const c=clawd3d(x,y,0,{facing:Math.cos(t*1.2)>0?1:-1,arms:'out',stride:0});if(c){if(hold){const p=project(x+0.6,y+1,0);if(p)text(p.x,p.y,'■',' #ffcc55'.trim());}if(mod(t,12)<4)say(hold?'Got {what}! Back for more.':'Dropping in for {what}...',c.top.x,c.row)}const q=project(9.5,2.3,0);if(q&&!hold)text(q.x-4,q.y,'{what}','#ffe9a0')}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#557","#99a"],"speed":0.4,"intensity":0}}},
84  {"phase":"web","world":"outer space","style":"pixel art","concept":"carnival midway: Clawd works a claw-crane game, fishing a prize labeled {what} out of the pile and into the chute, again and again","source":"claude-opus-5-5","scene":{"concept":"carnival midway: Clawd works a claw-crane game, fishing a prize labeled {what} out of the pile and into the chute, again and again","code":"const P=[[50,'#e55'],[58,'#5be'],[66,'#fd4'],[74,'#6d6'],[54,'#d6e'],[70,'#f94']];let got=0;\nfunction frame(t,dt){const T=t%8;for(let x=0;x<w;x++)put(x,0,x%4<2?'▼':'▽',hsl((x*20+t*90)%360,.8,.6));\nfor(let x=38;x<84;x++){put(x,1,'═','#c9a');put(x,8,'═','#c9a')}for(let y=1;y<9;y++){put(37,y,'║','#c9a');put(84,y,'║','#c9a')}\nfor(let i=0;i<14;i++){put(38+i*3.3,1,'●',hsl((i*40+t*200)%360,.9,.6))}\nfill(39,7,3,1,'▓','#777');text(39,2,'OUT','#fff');\nfor(let i=0;i<P.length;i++){if(i==2&&T>3.5)continue;pixels(P[i][0],13+(i%2),'.##.\\n####\\n.##.',{'#':P[i][1]})}\nlet cx=50,cy=3;if(T<2)cx=lerp(50,66,smoothstep(0,2,T));else if(T<5.5){cx=66;cy=T<3.5?lerp(3,10,smoothstep(2,3.5,T)):T<4?10:lerp(10,3,smoothstep(4,5.5,T))}else{cx=lerp(66,40,smoothstep(5.5,7,T));cy=3}\nline(cx+2,2,cx+2,cy/2,'│','#aaa');const op=T>3.4&&T<7?0:1;pixels(cx,cy,op?'#..#\\n#..#':'.##.\\n#..#',{'#':'#ddd'});\nif(T>3.5){let py=T<7?cy+3:cy+3+(T-7)*14;if(py<16){pixels(cx+0,py,'.##.\\n####\\n.##.',{'#':'#fd4'});text(cx-3,Math.floor(py/2)+2,'{what}','#ffe')}}\nfor(let x=86;x<w;x++)put(x,8,'▀','#634');fill(88,6,4,2,'█','#555');put(89,5,'●',T<2||T>5.5?'#e44':'#4e4');\nconst c=clawd(93,4,{facing:-1,arms:T<7?{left:'out',right:'down'}:'up',eyes:T>7?'wide':'open',blink:fract(t*.3)<.04});\npixels(c.top.x-3,c.top.py-2,'.#####.\\n#######',{'#':'#e33'});\nif(t%16>7&&t%16<11)say(T>7?'Ding! Fetched {what}, one token well spent.':'Steady... grabbing {what} off the web.',c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#311","#413","#224"],"speed":0.6,"intensity":0}}},
85  {"phase":"git","world":"construction site","style":"pixel art","concept":"nature documentary: Clawd hides in a bush with binoculars as commit-birds lay eggs along a branching git tree, the feature branch arcing off","source":"claude-opus-5-5","scene":{"concept":"nature documentary: Clawd hides in a bush with binoculars as commit-birds lay eggs along a branching git tree, the feature branch arcing off and merging back into main","code":"const N=[[6,10],[16,10],[26,10],[40,4],[50,4],[36,10],[60,4],[48,10],[58,10],[80,10]];const L=['Shh. A rare commit nests on {what}.','The feature branch returns to main. Majestic.','Every egg a commit. Every one signed.','No merge conflicts this season. Beautiful.'];const P=2.4;function frame(t,dt){for(let x=0;x<w;x++){put(x,8,'▀','#3c7a3a','#244a22');if(rand(x)<0.3)put(x,7,rand(x+5)<0.5?',':'\"','#5fa856')}const B='#8a5a3a';for(let x=0;x<88;x++){pixel(x,10,B);pixel(x,11,'#6b432b')}for(let i=0;i<6;i++){pixel(30+i,10-i,B);pixel(66+i,4+i,B)}for(let x=36;x<66;x++)pixel(x,4,B);for(let i=0;i<40;i++){const x=Math.floor(rand(i)*86),y=Math.floor(rand(i+40)*3);pixel(x,y+(x>34&&x<66?1:7),i%2?'#4f9a48':'#72c060')}text(42,1,'{what}','#e8d48a');text(2,6,'main','#c9a27a');const C=N.length+2,c=Math.floor(t/P)%C,k=fract(t/P);for(let j=0;j<Math.min(c,N.length);j++){const[ex,ey]=N[j];pixels(ex-1,ey-2,'.o.\\nooo',{o:j==c-1?'#ffd166':'#f2efe6'})}if(c<N.length){const[nx,ny]=N[c],s=smoothstep(0,0.75,k),bx=lerp(118,nx-1,s),by=lerp(0,ny-5,s)+(s<1?Math.sin(t*12)*0.6:0);pixels(bx,by,Math.floor(t*8)%2?'#.#\\n.#.':'...\\n###',{'#':'#7ab8e8'})}const cl=clawd(98,4,{facing:-1,arms:{left:'up',right:'down'},blink:fract(t*0.3)<0.04});pixels(cl.left.x-3,cl.left.py-1,'##.##\\n#o#o#',{'#':'#333','o':'#9fd3ff'});pixels(94,13,'..gg.gGg..gg..gg..\\n.gGggGgggGgggGggg.\\ngggGgggggGgggggGgg',{g:'#3f8c3a',G:'#5fb050'});if(t>2)say(L[Math.floor((t-2)/9)%L.length],cl.top.x,cl.row)}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#1d3a4a","#2c5a4a","#3a6a5a"],"speed":0.4,"intensity":0.2}}},
86  {"phase":"web","world":"carnival midway","style":"3D","concept":"zen rock garden: Clawd rakes calm ripples toward a far boulder (the web), and an answer-pebble labeled {what} rolls back across the sand","source":"claude-opus-5-5","scene":{"concept":"zen rock garden: Clawd rakes calm ripples toward a far boulder (the web), and an answer-pebble labeled {what} rolls back across the sand","code":"const sand=plane(40,30),rk=sphere(1,10),pb=sphere(0.25,8);\nfunction ring(cx,cz,r,c){for(let i=0;i<24;i++){const a=i/24*6.283,b=(i+1)/24*6.283;line3d(cx+Math.cos(a)*r*1.4,0.02,cz+Math.sin(a)*r,cx+Math.cos(b)*r*1.4,0.02,cz+Math.sin(b)*r,c)}}\nfunction frame(t,dt){camera(Math.sin(t*0.1)*1.5,3,9,0,0.5,-2,80);light(0.4,1,0.6,0.35);fog(10,40,'#000');mesh3d(sand,{y:0,z:-4,color:'#d8cfb0'});\nfor(let z=-14;z<6;z+=1.2)line3d(-16,0.01,z,16,0.01,z,'#b5a985');\nfor(let j=0;j<4;j++)ring(5,-5,1.6+mod(t*0.8+j*1.3,5.2),'#8f8466');\nmesh3d(rk,{x:5,y:0.7,z:-5,scale:1.3,color:'#a0a098'});mesh3d(rk,{x:-1,y:0.4,z:-10,scale:0.8,color:'#8f9a88'});mesh3d(rk,{x:9,y:0.3,z:-1,scale:0.5,color:'#9a9488'});\nconst p=mod(t,10),k=smoothstep(4,8,p);\nif(p>3.5){const x=lerp(5,-3.6,k),z=lerp(-3.4,2.4,k),y=0.25+Math.abs(Math.sin(k*9))*0.4*(1-k);mesh3d(pb,{x,y,z,color:'#e8e2d0'});const q=project(x,y+0.6,z);if(q)text(q.x-5,q.y,'{what}','#fff7d6')}\nconst me=clawd3d(-5,0,2,{facing:1,arms:p>8?'up':'out',stride:p<3?Math.floor(t*4)%4:-1});\nline3d(-4.4,0.6,2,-3.2,0,2.6,'#8a5a30');line3d(-3.4,0,2.3,-3,0,2.9,'#8a5a30');\nif(me){if(p<3)say('One slow rake toward the web...',me.top.x,me.row);else if(p>8)say('{what} rolled right back to me.',me.top.x,me.row)}}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#2a3a4a","#3a5a5a","#4a4a6a"],"speed":0.3,"intensity":0}}},
87  {"phase":"web","world":"circus","style":"text art","concept":"beehive: Clawd as a worker bee flying out from the honeycomb to far meadow flowers, sipping nectar labeled {what} and hauling it home","source":"claude-opus-5-5","scene":{"concept":"beehive: Clawd as a worker bee flying out from the honeycomb to far meadow flowers, sipping nectar labeled {what} and hauling it home","code":"const fx=[0.45,0.65,0.85].map(k=>Math.floor(w*k));const fc=['#ff6fa8','#c58cff','#ffd34d'];let jars=0,last=-1;\nfunction frame(t,dt){const P=14,cyc=Math.floor(t/P),p=mod(t,P),i=cyc%3;if(cyc!=last){last=cyc;if(cyc>0)jars++;}\nfor(let x=0;x<w;x++)put(x,h-1,x%5==0?'\"':'_','#5a9a3a');\nconst hv=[' ╱⬡╲ ','╱⬡⬡⬡╲','⬡⬡⬡⬡⬡'];const hive=['  ▄███▄  ',' ▐▓▒▓▒▓▌ ','▐▒▓▒▓▒▓▒▌','▐▓▒▓▒▓▒▓▌','▐▒▓ ● ▓▒▌','▐▓▒▓▒▓▒▓▌',' ▀▀▀▀▀▀▀ '];sprite(3,1,hive.join('\\n'),'#e8a33a');text(4,8,'HONEY x'+jars,'#ffe08a');\nfor(let k=0;k<3;k++){const x=fx[k],sw=Math.round(Math.sin(t*1.5+k)*0.6);sprite(x+sw,h-5,' \\\\|/ \\n-(@)-\\n /|\\\\ ',fc[k]);for(let y=h-2;y>h-4;y--)put(x+2,y,'│','#5fbf4a');if(k==i)text(x+2-5,h-6,'{what}'.padStart(5),'#ffffff');}\nconst hx=12,tx=fx[i]-5,ty=h-8;let x,y;if(p<5){const e=smoothstep(0,5,p);x=lerp(hx,tx,e);y=2+Math.sin(e*Math.PI)*-1.5+e*(ty-2);}else if(p<9){x=tx;y=ty+Math.sin(p*3)*0.3;}else{const e=smoothstep(9,13.5,p);x=lerp(tx,hx,e);y=lerp(ty,2,e)-Math.sin(e*Math.PI)*1.5;}\ny=clamp(y,0,4);const dir=p<5?1:p<9?0:-1;const c=clawd(x,y,{facing:dir,color:'#e8b43a',arms:p>5&&p<9?'out':'down'});const wg=Math.floor(t*12)%2?'⌒ ⌒':'v v';text(c.top.x-1,Math.max(0,c.row-1),wg,'#cfefff');for(let s=1;s<13;s+=4)put(x+s,y+2,'▒','#3a2a10');\nif(p>9)put(c.left.x-1,c.left.row,'●','#ffcf40');\nif(p>5.5&&p<8.8)say(i==2?'Sweet! {what} has the good nectar.':'Sipping {what}... bit fizzy.',c.top.x,c.row);else if(p<3&&cyc==0)say('Off to the web meadow for {what}!',c.top.x,c.row);}","actors":[],"particles":[{"glyphs":"·*","count":8,"x":"8+noise(t*0.4+k)*20","y":"1+noise(t*0.6+k*3)*6","color":"#ffd34d"}],"background":{"effect":"aurora","palette":["#2a4a6a","#3a6a5a","#6a5a2a"],"speed":0.4,"intensity":0.2}}},
88  {"phase":"web","world":"desert caravan","style":"3D","concept":"deep sea: Clawd in a 3D bathysphere lowering a fishing line through glowing water to hook a lantern-fish carrying {what} back up","source":"claude-opus-5-5","scene":{"concept":"deep sea: Clawd in a 3D bathysphere lowering a fishing line through glowing water to hook a lantern-fish carrying {what} back up","code":"const sub=sphere(1,10),rock=cone(1.4,2,6),fish=sphere(0.35,6),kelp=cylinder(0.06,2.5,4);\nconst rocks=Array.from({length:10},(_,i)=>({x:-18+i*4+rand(i)*2,z:-4-rand(i+5)*10,s:0.6+rand(i+9)}));\nconst bub=Array.from({length:25},(_,i)=>({x:(rand(i)-0.5)*20,y:rand(i+3)*4,z:-rand(i+7)*8}));\nfunction frame(t,dt){camera(Math.sin(t*0.1)*2,2,9,0,1,0,90);light(0.3,1,0.5,0.35);fog(6,30,'#021428');\nfor(let x=0;x<w;x++)for(let y=0;y<h;y++)put(x,y,' ',null,mix('#03284a','#010812',y/h));\nmesh3d(plane(60,30),{y:-0.5,z:-5,color:'#7a6a4a'});\nfor(const r of rocks)mesh3d(rock,{x:r.x,y:-0.5,z:r.z,scale:r.s,color:'#8a8f9a'});\nfor(let i=0;i<8;i++)mesh3d(kelp,{x:-14+i*4,y:-0.5,z:-2-i%3,rz:Math.sin(t+i)*0.2,color:'#5fbf7a'});\nconst c=(t%20)/20,sy=3.2+Math.sin(t*0.8)*0.15;mesh3d(sub,{x:-3,y:sy,z:0,color:'#e8c070'});\nconst me=clawd3d(-3,sy-0.5,0.9,{size:0.7,facing:1,blink:fract(t*0.3)<0.05});\nconst ly=c<0.4?lerp(sy-0.2,0,c/0.4):c<0.6?0:lerp(0,sy-0.2,(c-0.6)/0.4);\nline3d(-2,sy,0.5,3,ly,0.5,'#ddd');\nconst hooked=c>=0.5;const fx=hooked?3:3+Math.cos(t*0.7)*6,fy=hooked?ly-0.3:0.6+Math.sin(t)*0.3;\nmesh3d(fish,{x:fx,y:fy,z:0.5,color:'#ff8fb0'});point3d(fx+0.4,fy+0.5,0.5,'#ffff80');\nconst p=project(fx,fy-0.6,0.5);if(p)text(p.x-5,p.y,'{what}','#ffe');\nfor(const b of bub){b.y+=dt*0.8;if(b.y>5)b.y=-0.5;point3d(b.x+Math.sin(t+b.z)*0.2,b.y,b.z,'#9fdfff');}\nif(me){const s=c<0.4?'Dropping a line for {what}...':c<0.6?'Nibble! Something with answers.':'Reeling it up. Fresh from the web!';say(s,me.top.x,me.row);}}","actors":[],"particles":[],"background":{"effect":"waves","palette":["#03284a","#0a4a7a","#1a6aa0"],"speed":0.5,"intensity":0}}},
89  {"phase":"web","world":"submarine","style":"3D","concept":"ancient tomb (3D): explorer Clawd treks down a pillared crypt to snatch the glowing {what} relic from its pedestal, then flees a rolling bou","source":"claude-opus-5-5","scene":{"concept":"ancient tomb (3D): explorer Clawd treks down a pillared crypt to snatch the glowing {what} relic from its pedestal, then flees a rolling boulder back toward the camera","code":"const pil=cylinder(0.4,3,8),ped=box(1,1,1),gem=sphere(0.35,8),bld=sphere(1.3,10),fl=plane(9,60),lin=box(9,0.4,0.6);\nfunction frame(t){const p=t%16;camera(0,2.6,7,0,1,-10,80);light(0.3,1,0.6,0.35);fog(6,32,'#000');mesh3d(fl,{z:-20,color:'#a08860'});\nfor(let i=0;i<8;i++){const z=2-i*4;mesh3d(pil,{x:-3.5,z,color:'#c8b080'});mesh3d(pil,{x:3.5,z,color:'#c8b080'});mesh3d(lin,{y:3.2,z,color:'#b09a70'});const q=project(-3,2.2,z+0.3);if(q)put(q.x,q.y,'♦',fract(t*3+i*0.37)<0.5?'#ffb030':'#ff6020')}\nmesh3d(ped,{y:0.5,z:-16,color:'#b8a890'});\nlet cz,arms='down',held=p>=10.5;if(p<10)cz=4-p*1.9;else if(p<11.5){cz=-15;arms='up'}else{cz=-15+(p-11.5)*5;arms='up'}\nconst gy=held?1.35:1.4,gz=held?cz:-16;mesh3d(gem,{y:gy,z:gz,color:'#ffe060',ry:t});\nif(p>12){const bz=-19+(p-12)*5;mesh3d(bld,{y:1.3,z:bz,rx:-bz,color:'#9a8a78'})}\nconst c=clawd3d(0,0,cz,{pose:'walk',facing:0,stride:Math.floor(t*(p>11.5?10:6))%4,arms});\nconst g=project(0,gy+0.6,gz);if(g&&!held)text(g.x-5,g.y,'{what}','#ffe9a0');\nif(c){if(p>2&&p<6)say('Fetching {what} from the deep archive...',c.top.x,c.row);else if(p>10.5&&p<12.2)say('Got {what}! ...why is the floor rumbling?',c.top.x,c.row);else if(p>12.4&&p<15)say('Response received! RUN!',c.top.x,c.row)}}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#3a2a10","#5a4020","#7a5a30"],"speed":0.5,"intensity":0}}},
90  {"phase":"agents","world":"bank heist","style":"3D","concept":"haunted house heist: Clawd waits in a moonlit hallway while ghost-helper subagents drift off through doors, returning with loot labeled {wha","source":"claude-opus-5-5","scene":{"concept":"haunted house heist: Clawd waits in a moonlit hallway while ghost-helper subagents drift off through doors, returning with loot labeled {what}","code":"const fl=plane(30,8),dr=box(1.2,2.2,0.2),bag=sphere(0.35,8),moon=sphere(1.2,12);\nconst gh=Array.from({length:4},(_,i)=>({x:-6+i*4,ph:rand(i)*6}));\nfunction frame(t,dt){camera(Math.sin(t*0.1)*2,2.2,8,0,1,-2,90);light(0.3,0.8,0.5,0.35);fog(8,30,'#000');\nmesh3d(fl,{y:0,z:-2,color:'#6a5a7a'});mesh3d(box(30,4,0.3),{y:2,z:-5,color:'#7a6a90'});\nmesh3d(moon,{x:8,y:5,z:-20,color:'#eeeedd',unlit:true});\nfor(const g of gh){mesh3d(dr,{x:g.x,y:1.1,z:-4.8,color:'#a07850'});\nconst c=fract(t*0.08+g.ph/6),k=c<0.5?c*2:2-c*2,z=-4.5+ (1-k)*4,y=1.2+Math.sin(t*2+g.ph)*0.2;\nmesh3d(sphere(0.5,10),{x:g.x,y:y,z:z,color:'#ddeeff',ascii:true});\nif(c>0.5){mesh3d(bag,{x:g.x+0.5,y:y-0.4,z:z,color:'#e0c050'});const p=project(g.x+0.5,y-1,z);if(p)text(p.x-3,p.y,'{what}','#ffd27a');}\nconst e=project(g.x,y+0.1,z);if(e)text(e.x-1,e.y,'o o','#222');}\nconst me=clawd3d(0,0,1.5,{facing:0,eyes:fract(t*0.3)<0.05?'closed':'wide',arms:Math.sin(t)>0.7?'up':'down'});\nif(me){const m=t%24;if(m>2&&m<8)say('Sent my ghosts through every door for {what}.',me.top.x,me.row);else if(m>13&&m<19)say('Boo-ty incoming. Still waiting on the slow ghost.',me.top.x,me.row);}}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#2a2050","#403070","#205050"],"speed":0.4,"intensity":0}}},
91  {"phase":"agents","world":"deep sea","style":"text art","concept":"film noir city at night: Clawd as trench-coat detective under a streetlamp, sending three shadowy informants into the rainy alleys and colle","source":"claude-opus-5-5","scene":{"concept":"film noir city at night: Clawd as trench-coat detective under a streetlamp, sending three shadowy informants into the rainy alleys and collecting their tips on {what}","code":"const B=Array.from({length:16},(_,i)=>({x:30+i*6+Math.floor(rand(i)*3),hh:3+Math.floor(rand(i+7)*5),ww:4+Math.floor(rand(i+3)*3)}));\nconst L=['Three tails on {what}. Nobody sleeps tonight.','Word on the street: {what} has a past.','Keep digging, kid. Every alley talks.','The rain never stops. Neither do my agents.'];\nfunction frame(t,dt){for(const b of B){for(let y=h-1-b.hh;y<h-1;y++)for(let x=0;x<b.ww;x++){const lit=rand(b.x*31+x*7+y*13+Math.floor(t*0.3+b.x)*0.0)>0.7&&(x+y)%2==0;put(b.x+x,y,lit?'▪':'█',lit?'#e8c860':'#1e2433');}}\nfor(let x=0;x<w;x++)put(x,h-1,x%9==0?'┴':'─','#55606e');\ntext(2,0,'┌─────────┐','#c0485a');text(2,1,'│ HOTEL ★ │','#ff5a7a');text(2,2,'└─────────┘','#c0485a');\nfor(let y=1;y<h-1;y++)put(22,y,'│','#8a8f99');text(20,0,'╒═╕','#8a8f99');put(21,1,'▼','#ffe9a0');\nfor(let y=2;y<h-1;y++)put(21+(y%2),y,'░','#6a6040');\nfor(let i=0;i<60;i++){const x=Math.floor(rand(i)*w),y=mod(t*(10+rand(i+9)*5)+rand(i+3)*h,h);put(x-Math.floor(y*0.3),y,'/','#4a6a9a');}\nfor(let i=0;i<3;i++){const p=mod(t+i*5,15)/15;const out=p<0.5;const d=Math.sin(Math.PI*p);const x=26+d*(w-46)*(0.6+i*0.2);const back=!out;const c=clawd(x,h-5,{facing:out?1:-1,stride:Math.floor(x/1.5)%4,color:['#6a7080','#7a6a80','#6a8078'][i]});text(c.top.x-3,c.top.row-1,'▄███▄','#3a3f4a');if(back&&p<0.95)text(c.left.x-2,c.left.row,'{what}','#f0e6c0');}\nconst c=clawd(6,h-5,{facing:1,arms:{left:'down',right:mod(t,15)<1?'up':'down'},eyes:'open'});text(c.top.x-4,c.top.row-1,'▄▄███▄▄','#5a4a3a');text(c.x,c.row+2,'▓▓▓▓▓▓▓▓▓▓▓▓▓▓','#6b5a42');\nif(mod(t,4)<0.6)put(c.right.x+1,c.right.row-1,'~','#999');put(c.right.x,c.right.row,'═','#ddd');\nconst k=Math.floor(t/7)%L.length;if(mod(t,7)>1.5)say(L[k],c.top.x,c.row);}","actors":[],"particles":[],"background":{"effect":"rain","palette":["#203048","#2a3a5a","#344868"],"speed":1,"intensity":0.2}}},
92  {"phase":"agents","world":"retro arcade game","style":"3D","concept":"bowling alley in 3D: Clawd bowls while helper Clawds in other lanes roll in parallel, pins labeled {what} crash and reset","source":"claude-opus-5-5","scene":{"concept":"bowling alley in 3D: Clawd bowls while helper Clawds in other lanes roll in parallel, pins labeled {what} crash and reset","code":"const pin=cylinder(0.12,0.7,8),ball=sphere(0.35,10),lane=box(2.2,0.1,30);const L=[-6,-2,2,6],cols=['#d97757','#6fa8dc','#8fd18f','#e0a0e0'];function frame(t,dt){camera(0,2.6,9,0,0.5,-12,80);light(0.3,1,0.6,0.35);fog(10,40,'#000');for(let i=0;i<4;i++){const x=L[i];mesh3d(lane,{x,y:-0.05,z:-8,color:'#c8a070'});line3d(x-1.2,0.05,7,x-1.2,0.05,-23,'#555');const per=4+i*0.7,ph=mod(t+i*1.3,per),k=ph/per,bz=lerp(4,-20,smoothstep(0.1,0.7,k)),hit=bz<-18.5;if(k<0.75)mesh3d(ball,{x:x+Math.sin(k*6+i)*0.3,y:0.35,z:bz,rx:-t*8,color:cols[i]});for(let r=0;r<4;r++)for(let c=0;c<=r;c++){const px=x+(c-r/2)*0.35,pz=-19-r*0.35,s=hit&&k<0.95?1:0,j=r*4+c;mesh3d(pin,{x:px+s*(rand(j+i)-0.5)*2*(k-0.7)*8,y:s*Math.max(0,(k-0.7)*3*rand(j)),z:pz,rz:s*(rand(j*3)-0.5)*3,color:'#f4f4f0'});}if(i>0)clawd3d(x,0,5.5,{size:0.6,color:cols[i],arms:k<0.15?'out':'down',facing:0});const p=project(x,1.4,-19);if(p)text(p.x-4,p.y,i==1?'{what}':'',cols[i]);}const me=clawd3d(-6,0,5.5,{size:0.7,arms:mod(t,4)<0.5?'up':'out',facing:0});const s=Math.floor(t/4);if(me&&mod(t,8)>4)say(s%2?'Every lane rolling at once. Strike on {what}!':'My helpers bowl, I keep score.',me.top.x,me.row);text(w-16,0,'FRAME '+(s%10+1)+' X',  '#ffd866');}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#331144","#552266","#223366"],"speed":0.5,"intensity":0.15}}},
93  {"phase":"agents","world":"farm","style":"3D","concept":"beehive farm 3D: Clawd beekeeper waits by a hive as worker bees fly out to flower fields and return with nectar, honey jar filling","source":"claude-opus-5-5","scene":{"concept":"beehive farm 3D: Clawd beekeeper waits by a hive as worker bees fly out to flower fields and return with nectar, honey jar filling","code":"const hive=cylinder(1,1.8,8),roof=cone(1.2,0.8,8),jar=cylinder(0.5,1,10),fl=sphere(0.35,6),st=cylinder(0.05,0.8,4),gr=plane(60,30);const fls=[[-8,-6],[-5,-10],[7,-7],[10,-11],[-11,-12],[4,-13]];let honey=0;function frame(t,dt){camera(Math.sin(t*0.1)*2,2.6,9,0,1,-3,90);light(0.4,1,0.6,0.35);fog(10,40,'#000');mesh3d(gr,{y:0,z:-5,color:'#6a9a4a'});mesh3d(hive,{x:0,z:-3,color:'#e8c070'});mesh3d(roof,{x:0,y:1.8,z:-3,color:'#c89050'});for(let i=0;i<fls.length;i++){const[f,z]=fls[i];mesh3d(st,{x:f,z,color:'#5c5'});mesh3d(fl,{x:f,y:0.9,z,color:['#f7a','#fd5','#aaf'][i%3]})}honey=Math.min(1,honey+dt*0.012);mesh3d(jar,{x:3,z:-1,scale:1,color:'#cce'});mesh3d(cylinder(0.48,Math.max(0.05,honey),10),{x:3,y:0,z:-1,color:'#f0a020'});const pj=project(3,1.4,-1);if(pj)text(pj.x-5,pj.y-1,'{what}','#ffd');for(let b=0;b<12;b++){const[f,z]=fls[b%6];const k=0.5-0.5*Math.cos((t*0.25+rand(b))*Math.PI*2);const x=lerp(0,f,k),zz=lerp(-3,z,k),y=1.2+Math.sin(k*3.14)*1.5+Math.sin(t*9+b)*0.1;const p=project(x,y,zz);if(p)put(p.x,p.y,k>0.5?'●':'*',k>0.5?'#fd3':'#fa0')}const c=clawd3d(-3,0,-1,{facing:1,arms:Math.sin(t)>0.6?'up':'down'});if(c){pixels(c.top.x-4,c.top.py-3,'.wwwwwww.\nwwwwwwwww','#eee'.length?{w:'#eee'}:{});const m=Math.floor(t/12)%4;const L=['Twelve workers out on {what}. I just hold the smoker.','Bees: parallel. Me: patiently sequential.','Honey jar filling. Each drop is a finished subtask.','Buzz buzz. That is agent for \"still on it.\"'];if(t%12>2)say(L[m],c.top.x,c.row)}}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#335","#446","#553"],"speed":0.3,"intensity":0}}},
94  {"phase":"agents","world":"construction site","style":"3D","concept":"snow globe construction site: Clawd foreman in a snow globe watches helper crews raise a tower crate by crate while snow swirls","source":"claude-opus-5-5","scene":{"concept":"snow globe construction site: Clawd foreman in a snow globe watches helper crews raise a tower crate by crate while snow swirls","code":"const bx=box(1,0.6,1),pole=cylinder(0.1,3.2,6),base=cylinder(3.4,0.4,16),dome=sphere(3.6,14);\nconst crews=[-2.2,0,2.2];\nconst sn=Array.from({length:60},(_,i)=>({a:rand(i)*6.28,r:rand(i+7)*3.2,y:rand(i+3)*4}));\nfunction frame(t,dt){\nconst a=t*0.15;camera(Math.sin(a)*8,2.6,Math.cos(a)*8,0,1.3,0,80);light(0.5,1,0.6,0.35);fog(8,30,'#000');\nmesh3d(base,{y:-0.4,color:'#b08060'});\nmesh3d(dome,{y:1.4,color:'#9cf',wire:true});\ncrews.forEach((x,i)=>{const cyc=8,ph=t+i*2.6,n=Math.floor(ph/cyc),f=fract(ph/cyc);const st=Math.min(5,n+1);for(let k=0;k<st;k++)mesh3d(bx,{x,y:0.3+k*0.6,z:-1,color:hsl(30+i*90,0.6,0.6)});\nmesh3d(pole,{x:x+0.8,z:-1,color:'#fc4'});line3d(x+0.8,3.2,-1,x-0.2,3.2,-1,'#fc4');\nconst ty=0.3+st*0.6,cy=lerp(3,ty,smoothstep(0.2,0.8,f));line3d(x,3.2,-1,x,cy+0.3,-1,'#ccc');mesh3d(bx,{x,y:cy,z:-1,color:hsl(30+i*90,0.6,0.75)});\nconst p=project(x,cy+0.6,-1);if(p&&i==1)text(p.x-5,p.y,'{what}','#fff');});\nfor(const s of sn){s.y-=dt*0.5;s.a+=dt*0.3;if(s.y<0)s.y=4;point3d(Math.cos(s.a)*s.r,s.y,Math.sin(s.a)*s.r,'#fff');}\nconst me=clawd3d(0,0,1.4,{facing:0,arms:fract(t/8)>0.85?'up':'down',size:0.8});\nif(me){pixels(me.top.x-3,me.top.py-3,'.yyyyy.\\nyyyyyyy','#fd3'.length?{y:'#fd3'}:{});\nconst L=['Three crews, one {what}. I just hold the clipboard.','Somebody shook the globe. Work continues.','Another floor up. Helpers never sleep.'];if(t>2)say(L[Math.floor(t/10)%3],me.top.x,me.row);}}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#335","#668","#aac"],"speed":0.3,"intensity":0.2}}},
95  {"phase":"agents","world":"ski slope","style":"3D","concept":"weather report: Clawd as TV weather presenter pointing at a 3D map where four subagent storm fronts spiral in toward a {what} tower","source":"claude-opus-5-5","scene":{"concept":"weather report: Clawd as TV weather presenter pointing at a 3D map where four subagent storm fronts spiral in toward a {what} tower","code":"const pl=plane(18,9),cl=sphere(0.5,8),tw=box(0.6,1.8,0.6),sun=sphere(0.4,8)\nlet cs=Array.from({length:4},(_,i)=>({a:i*1.57,r:4+i*0.8})),done=0,fl=0\nconst L=['Four agent fronts moving in on {what}.','Heavy scattered parallel work today.','Front landed! Clear skies over {what}.','Expect more patches by afternoon.']\nfunction frame(t,dt){camera(4+Math.sin(t*0.15)*1.5,4,7,4,0.5,-2,75);fog(8,30,'#000');light(0.4,1,0.6,0.35)\nmesh3d(pl,{x:4,z:-2,color:'#5f9a5a'});mesh3d(tw,{x:4,y:0,z:-2,color:'#c8d0e0'})\nconst tp=project(4,2.1,-2);if(tp)text(tp.x-5,tp.y-1,'{what}','#ffe08a')\ncs.forEach((c,i)=>{c.r-=dt*0.35;c.a+=dt*0.25;if(c.r<0.9){c.r=6;done++;fl=1}\nconst x=4+Math.cos(c.a)*c.r,z=-2+Math.sin(c.a)*c.r*0.6\nmesh3d(cl,{x,y:1.6,z,scale:1.4,color:'#e8eef8'});mesh3d(cl,{x:x+0.5,y:1.5,z,scale:1,color:'#cfd8e8'})\nconst p=project(x,2.5,z);if(p)text(p.x-3,p.y,'agent'+(i+1),hsl(200+i*40,0.7,0.7))})\nfl=Math.max(0,fl-dt*1.5);if(fl>0){mesh3d(sun,{x:4,y:2.6,z:-2,scale:1+fl,color:'#ffd040',unlit:true});if(tp)text(tp.x+3,tp.y-1,'\\u2713'+done,'#8f8')}\nfill(0,0,30,9,' ',null,'#101828');text(2,0,'WEATHER 7','#7cc8ff');text(22,8,'LIVE','#ff6060')\nconst c=clawd(4,4,{facing:1,arms:{left:'down',right:Math.sin(t*2)>0?'up':'out'},blink:fract(t*0.3)<0.04})\npixels(c.eyes.x-4,c.eyes.py+3,'..rr..\\n...r..',{r:'#d03040'})\nsay(L[Math.floor(t/6)%4],c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#778"],"speed":0.3,"intensity":0}}},
96  {"phase":"agents","world":"circus","style":"pixel art","concept":"cyberpunk street, pixel art: Clawd as a rain-soaked fixer under a neon {what} sign, dispatching courier drones that zip off into the megacit","source":"claude-opus-5-5","scene":{"concept":"cyberpunk street, pixel art: Clawd as a rain-soaked fixer under a neon {what} sign, dispatching courier drones that zip off into the megacity and return with data packets","code":"const B=Array.from({length:30},(_,i)=>({x:i*5,hh:3+Math.floor(rand(i)*12),c:rand(i+7)<0.5?'#2a2350':'#1d3048'}));\nconst R=Array.from({length:50},(_,i)=>({x:rand(i+3)*w,y:rand(i+9)*h,v:8+rand(i)*6}));\nconst D=[0,1,2].map(i=>({o:i*4,c:['#ff4fd8','#3ef0ff','#ffe14a'][i],yy:2+i*2}));\nconst L=['Three drones out on {what}. Fixer waits.','Runners are fast. Rain is faster.','Packet back! Two more in the wind.','Night shift: I dispatch, they deliver.'];\nfunction frame(t,dt){\nfor(const b of B){const top=2*h-2-b.hh;fill(b.x,Math.floor(top/2),5,h,' ',null,b.c);for(let py=top+2;py<2*h-3;py+=3)for(let k=1;k<4;k+=2)if(rand(b.x*7+py*3+k)<0.4)pixel(b.x+k,py,rand(py+b.x)<0.5?'#ff6ad5':'#5ff');}\nfill(0,h-1,w,1,'▀','#555a70','#151522');\nfor(let x=0;x<w;x+=9)put(x,h-1,'▄','#ff4fd8','#151522');\nfill(8,0,26,1,' ',null,'#401040');text(9,0,'NEON//'+'{what}'.slice(0,18),fract(t*0.7)<0.05?'#602060':'#ff7af0','#401040');\nfill(12,3,22,1,'▀','#6a3fa0');\nfor(const r of R){r.y+=r.v*dt;if(r.y>h-1){r.y=0;r.x=Math.random()*w;}put(r.x,r.y,'╎','#4a6fa8');}\nconst c=clawd(18,h-5,{facing:1,arms:fract(t/4)<0.15?{left:'down',right:'up'}:'down',blink:fract(t*0.3)<0.04,look:{x:2,y:0}});\npixels(c.eyes.x-1,c.eyes.py-1,'##########',{'#':'#3ef0ff'});\nfor(const d of D){const p=fract((t+d.o)/12),out=p<0.5,k=out?smoothstep(0,0.5,p):1-smoothstep(0.5,1,p);const x=35+k*(w-30),y=d.yy*2+Math.sin(t*3+d.o)*1;pixels(x,y,'.##.\\n####\\n#..#',{'#':d.c});if(!out)pixels(x+1,y+3,'oo',{'o':'#fff'});if(k<0.98)line(x+4,Math.floor(y/2),x+4+Math.min(6,k*20),Math.floor(y/2),'·',d.c);}\nconst li=Math.floor(t/7)%L.length;if(fract(t/7)<0.7)say(L[li],c.top.x,c.row);\n}","actors":[],"particles":[],"background":{"effect":"rain","palette":["#203060","#402060","#305070"],"speed":1,"intensity":0}}},
97  {"phase":"writing","world":"mad science lab","style":"3D","concept":"lighthouse in a storm: Clawd as keeper atop a 3D lighthouse, beam sweeping the sea, guiding the {what} ship safely to harbor as the reply is","source":"claude-opus-5-5","scene":{"concept":"lighthouse in a storm: Clawd as keeper atop a 3D lighthouse, beam sweeping the sea, guiding the {what} ship safely to harbor as the reply is written","code":"const tw=cylinder(0.9,4,10),cap=cone(1.1,0.8,10),lamp=cylinder(0.6,0.6,8),rock=sphere(1.6,8),hull=box(1.6,0.5,0.6),mast=box(0.08,1.2,0.08)\nfunction frame(t,dt){camera(0,3,11,0,2.5,0,90);fog(8,40,'#000');light(0.5,1,0.6,0.35)\nfor(let x=0;x<w;x++){const y=h-1-Math.round(noise2(x*0.08,t*0.6)*1.5);for(let yy=y;yy<h;yy++)put(x,yy,yy==y?'~':'≈',yy==y?'#9cd':'#3a6a9a')}\nfor(let i=0;i<40;i++){const rx=mod(rand(i)*w+t*14+i*3,w),ry=mod(rand(i+7)*h+t*18,h);put(rx,ry,'/','#7890b0')}\nif(fract(t*0.13)<0.03)fill(0,0,w,2,' ',null,'#334')\nmesh3d(rock,{x:-4,y:-1,z:0,color:'#889'});mesh3d(tw,{x:-4,y:0,z:0,color:'#e8e0d8'});mesh3d(box(1.85,0.5,1.85),{x:-4,y:1.6,z:0,color:'#c33'});mesh3d(lamp,{x:-4,y:4,z:0,color:'#ffd860',unlit:true});mesh3d(cap,{x:-4,y:4.6,z:0,color:'#c33'})\nconst a=t*0.9,p=project(-4,4.3,0);if(p){const dx=Math.cos(a),len=Math.abs(dx)*70;for(let i=3;i<len;i++){const x=p.x+Math.sign(dx)*i,y=p.y+i*0.03*Math.sin(a*2);put(x,y,'═',mix('#fff6a0','#665520',i/len))}}\nconst k=clamp(t/50,0,1),sx=lerp(9,-1,k),bob=Math.sin(t*1.7)*0.2;mesh3d(hull,{x:sx,y:0.1+bob,z:2,rz:bob*0.5,color:'#b07040'});mesh3d(mast,{x:sx,y:0.9+bob,z:2,color:'#ddd'});mesh3d(box(0.6,0.6,0.05),{x:sx+0.3,y:1.1+bob,z:2,color:'#eee'})\nconst q=project(sx,2,2);if(q)text(q.x-4,q.y-1,'{what}','#ffd')\nconst c=clawd(p?p.x+5:40,p?p.y-1:1,{facing:-1,arms:{left:'out',right:Math.sin(t*3)>0?'up':'out'},scale:1});pixels(c.top.x-3,c.top.py-3,'.yyyyy.\\nyyyyyyy','{\"y\":\"#ffcc33\"}'.length?{y:'#ffcc33'}:{})\nconst L=['Keep that light turning, {what} is almost home!','Storm says no. Beam says {what}, this way.','Writing up the logbook while the ship docks.'];if(t>2)say(L[Math.floor(t/12)%3],c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"rain","palette":["#334466","#556688","#223344"],"speed":1.5,"intensity":0.2}}},
98  {"phase":"writing","world":"wild west","style":"text art","concept":"wild west mail room: Clawd as Pony Express clerk sealing the reply letter, stamping it, and handing it to a galloping courier at sunset","source":"claude-opus-5-5","scene":{"concept":"wild west mail room: Clawd as Pony Express clerk sealing the reply letter, stamping it, and handing it to a galloping courier at sunset","code":"let p=0\nfunction frame(t,dt){\nfor(let x=0;x<w;x++){put(x,8,x%6==0?'┴':'═','#8a6a4a')}\nfor(let y=0;y<8;y++)for(let x=0;x<w;x+=3){if(noise2(x*0.05,y*0.3+t*0.05)>0.8)put(x,y,'.',mix('#ff9a50','#7a3a6a',y/8))}\ndisc(w-18,7,3,' ',null,'#c8552a')\nsprite(2,1,'╔══════════════╗\\n║ PONY EXPRESS ║\\n╠══╦══╦══╦══╦══╣\\n║▒▒║▓▓║▒▒║▓▓║▒▒║\\n║▓▓║▒▒║▓▓║▒▒║▓▓║\\n║▒▒║▓▓║▒▒║▓▓║▒▒║\\n╚══╩══╩══╩══╩══╝','#c08850')\nsprite(22,4,'┌──────────────────┐\\n│                  │\\n└┬────────────────┬┘\\n │                │','#a07040')\nconst c=t%14\nconst cx=26\nconst cl=clawd(cx,0.5,{facing:1,arms:c<4&&fract(t*2)<0.5?'up':'out'})\nsprite(cl.top.x-3,0,' ▄▄▄ \\n▀▀▀▀▀','#7a5030')\nconst sealed=c>4\nif(c<9)sprite(44,4,sealed?'┌─✦──┐\\n│ ★  │':'┌────┐\\n│\\\\  /│','#f4e6c0')\nif(c<9)text(43,6,'{what}'.slice(0,20),'#ffd27a')\nlet hx=lerp(w+10,-40,smoothstep(8,13,c))\nconst g=Math.floor(t*8)%2\nsprite(hx,3,g?'   ▄▄       \\n ▄███████▄▄ \\n▀ █▀    █▀  \\n  ▀     ▀   ':'   ▄▄       \\n ▄███████▄▄ \\n  █▀   ▀█   \\n ▀       ▀  ','#b07a50')\nsprite(hx+3,1,' ▄█▄\\n █▓█','#e0c090')\nif(c>9.5&&c<13)sprite(hx+1,3,'✉','#f4e6c0')\nfor(let i=0;i<5;i++)put(hx+12+i*2,7-((t*5+i)%2),'░','#9a7a5a')\nif(c>1&&c<4)say('Sealing up the reply on {what}...',cx+7,0)\nelse if(c>5&&c<8)say('Stamped! Rider\\'s due any minute.',cx+7,0)\nelse if(c>9&&c<12)say('Ride fast, partner!',cx+7,0)\n}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#5a2a3a","#a0502a","#d08040"],"speed":0.4,"intensity":0}}},
99  {"phase":"writing","world":"mad science lab","style":"text art","concept":"ufo abduction over a mad science lab: Clawd beams the finished {what} up into a saucer while a lab tesla coil sparks below","source":"claude-opus-5-5","scene":{"concept":"ufo abduction over a mad science lab: Clawd beams the finished {what} up into a saucer while a lab tesla coil sparks below","code":"let c0=0\nfunction frame(t,dt){\nfor(let x=0;x<w;x++)put(x,h-1,x%9==0?'┴':'─','#5a6b7c')\nfor(let i=0;i<30;i++)put(Math.floor(rand(i)*w),Math.floor(rand(i+7)*3),fract(t*0.5+rand(i+3))<0.1?'✦':'.','#8899cc')\nsprite(4,h-5,' ┌┴┐ \n │≈│ \n │≈│ \n═╧═╧═','#7fd1b9')\nif(fract(t*1.3)<0.3){line(7,h-6,10+rand(Math.floor(t*9))*6,h-8,'~','#bfe8ff')}\nconst sx=lerp(-30,w*0.5-12,smoothstep(0,4,t))+(t>4?Math.sin(t*0.6)*6:0)\nsprite(sx,0,'     ▄▄████▄▄     \\n◢████◆██◆██◆████◣','#9fe870')\nconst on=t>4.5\nconst cx=sx+2\nif(on){for(let y=2;y<h-1;y++){const s=(y-2)*0.6;text(Math.round(cx+1-s),y,'░'.repeat(Math.round(12+s*2)),y%2?'#e8ff9a':'#c8f070')}}\nconst p=fract((t-5)/6)\nconst cy=on&&t>5?lerp(h-6,2,smoothstep(0,0.8,p)):h-6\nconst crx=w*0.75-14\nconst ky=h-5\nclawd(crx,ky,{facing:-1,arms:on?'up':'out',eyes:on?'wide':'open'})\nif(on&&t>5&&p<0.85){text(Math.round(cx+3),Math.round(cy),'[{what}]','#ffd479')}\nif(!on||t<=5){text(Math.round(cx+3),h-2,'[{what}]','#ffd479')}\nif(t>2&&t<5)say('Ship it? They are literally shipping it.',crx+7,ky)\nif(t>7)say('Beam up {what}. Tell the mothership: tests pass.',crx+7,ky)\n}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#99a"],"speed":0.4,"intensity":0.2}}},
100  {"phase":"writing","world":"museum after hours","style":"3D","concept":"construction site at dusk: Clawd in a hard hat foreman-waving as a 3D crane stacks the final blocks of the {what} tower, topping out with a ","source":"claude-opus-5-5","scene":{"concept":"construction site at dusk: Clawd in a hard hat foreman-waving as a 3D crane stacks the final blocks of the {what} tower, topping out with a flag","code":"const bk=box(1.6,0.5,1.2),mast=cylinder(0.15,5,6),gr=plane(40,14),fl=box(0.05,0.6,0.05);\nfunction frame(t,dt){camera(0,3,12,0,2,0,70);light(0.4,1,0.6,0.35);fog(10,40,'#000');\nmesh3d(gr,{color:'#7a6a50'});mesh3d(mast,{x:7,color:'#e8c040'});line3d(7,5,0,-2,5,0,'#e8c040');line3d(7,5,0,9,4.6,0,'#e8c040');line3d(7,5.6,0,-2,5,0,'#c9a030');line3d(7,5.6,0,9,4.6,0,'#c9a030');mesh3d(box(0.8,0.6,0.6),{x:9,y:4.4,color:'#999'});\nconst cy=t%32,n=Math.floor(cy/4),p=(cy%4)/4;\nfor(let i=0;i<Math.min(n,7);i++)mesh3d(bk,{x:0,y:0.25+i*0.5,color:i%2?'#d0d4dc':'#b8c4d8'});\nlet bx=6,by=0.25,top=0.25+n*0.5;\nif(n<7){if(p<0.3)by=lerp(0.25,4.3,smoothstep(0,0.3,p));else if(p<0.65){by=4.3;bx=lerp(6,0,smoothstep(0.3,0.65,p))}else{bx=0;by=lerp(4.3,top,smoothstep(0.65,1,p))}\nline3d(bx,5,0,bx,by+0.25,0,'#ddd');mesh3d(bk,{x:bx,y:by,ry:Math.sin(t*2)*0.15*(p>0.3&&p<0.65),color:'#f0a050'});mesh3d(bk,{x:6,y:0.25,z:1.5,color:'#f0a050'})}\nelse{mesh3d(fl,{x:0,y:3.8,color:'#fff'});const f=project(0.3,4.0,0);if(f)text(f.x,f.y,'▶',hsl(t*90%360,0.8,0.6))}\nconst lp=project(0,Math.min(n,7)*0.5-0.1,0.7);if(lp&&n>0)text(lp.x-5,lp.y,'{what}'.slice(0,20),'#222');\nconst c=clawd3d(-4.5,0,1,{facing:1,arms:n>=7?(Math.floor(t*3)%2?'up':'out'):{left:'down',right:p>0.6?'up':'out'},size:1.1});\nif(c){pixels(c.top.x-4,c.top.py-3,'..yyyyy..\\n.yyyyyyy.\\nyyyyyyyyy','{\"y\":\"#ffd02a\"}'.length?{y:'#ffd02a'}:{});\nconst L=n>=7?'Topped out! {what} is built, flag and all.':n<2?'Laying the foundation for {what}...':n<5?'Easy... a little left... perfect.':'One more floor and {what} is done.';say(L,c.top.x,c.row-1)}}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#3a2a5a","#7a3a5a","#c06040"],"speed":0.4,"intensity":0.2}}},
101  {"phase":"writing","world":"museum after hours","style":"3D","concept":"museum heist after hours: Clawd lowered on a rope into a 3D gallery, laser beams sweeping, to swap {what} onto the pedestal as the finished ","source":"claude-opus-5-5","scene":{"concept":"museum heist after hours: Clawd lowered on a rope into a 3D gallery, laser beams sweeping, to swap {what} onto the pedestal as the finished exhibit","code":"const ped=box(1.4,1.2,1.4),gem=sphere(0.45,10),col=cylinder(0.35,3.2,8),fl=plane(40,20),fr=box(2.2,1.4,0.1);\nfunction frame(t,dt){camera(Math.sin(t*0.15)*3,2.6,9,0,1.2,0,85);light(0.4,1,0.6,0.35);fog(8,32,'#000');\nmesh3d(fl,{y:0,color:'#7a6a90'});\nfor(let i=-3;i<=3;i++){if(i==0)continue;mesh3d(col,{x:i*3.2,z:-3,color:'#c8c0b0'});}\nfor(let i=-2;i<=2;i+=4)mesh3d(fr,{x:i*2.4,y:2.2,z:-4,color:'#d4a84a'});\nmesh3d(ped,{y:0.6,color:'#e0dcd0'});\nconst cyc=t%24,low=smoothstep(2,6,cyc)-smoothstep(16,20,cyc);const cy=4.2-low*2.4;\nline3d(0,6,0.6,0,cy+0.9,0.6,'#bbb');\nconst placed=cyc>9&&cyc<22;\nmesh3d(gem,{y:placed?1.7:cy+0.2,z:placed?0:0.6,ry:t,color:'#6fe0ff',ascii:true});\nfor(let k=0;k<3;k++){const a=t*0.7+k*2.1,x=Math.sin(a)*7;line3d(x,0.2+k*0.5,3,-x,0.6+k*0.4,-3,'#ff3344');}\nconst me=clawd3d(0,cy,0.6,{arms:low>0.9?'down':'up',color:'#d97757',eyes:cyc>9&&cyc<14?'wide':'open'});\nconst p=project(0,1.3,0.75);if(p)text(p.x-5,p.y,'{what}'.slice(0,12),'#ffd27a');\nif(me){me.top&&pixels(me.top.x-3,me.top.py-2,'#######\\n#.o#o.#','#222'.length?{'#':'#222','o':'#fff'}:{});\nif(cyc>10&&cyc<16)say('{what} is on the pedestal. Nobody touch it.',me.top.x,me.row);else if(cyc>19)say('Lasers dodged. Exhibit installed. Smooth.',me.top.x,me.row);}}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#201030","#301040","#402050"],"speed":0.5,"intensity":0}}},
102  {"phase":"writing","world":"haunted house","style":"text art","concept":"dinosaur era: Clawd chisels the final reply onto a stone tablet while a long-necked dino peers over a volcano skyline","source":"claude-opus-5-5","scene":{"concept":"dinosaur era: Clawd chisels the final reply onto a stone tablet while a long-necked dino peers over a volcano skyline","code":"function frame(t,dt){for(let x=0;x<w;x++){put(x,8,x%5?'▀':'▄','#7a5a3a');}sprite(70,2,'    ▄▄▄\\n   ▟███▙\\n  ▟█████▙\\n ▟███████▙\\n▟█████████▙','#8a4a3a');let e=fract(t*0.4);for(let i=0;i<5;i++)put(74+i+Math.sin(t*3+i)*2,1-e*3+i*0.3,'*','#ff9a3a');const nx=95+Math.sin(t*0.3)*3;sprite(nx,0,'  ▄▄▖\\n ▐▀▀ \\n ▐   \\n ▐   \\n▄██▄▄▄\\n██████▄\\n▐▌  ▐▌ ','#7ec27e');fill(30,3,22,5,' ',null,'#6a6a72');text(31,3,'┌'+'─'.repeat(18)+'┐','#c8c8d0','#6a6a72');const n=Math.min(17,Math.floor(t*1.2));text(32,4,'{what}'.slice(0,n),'#fff6c0','#6a6a72');let lines=Math.floor(t/4)%3+1;for(let l=0;l<lines&&l<2;l++)text(32,5+l,'≈≈ ≈≈≈ ≈≈'.slice(0,Math.min(9,Math.floor(t*2)%12)),'#e0e0e8','#6a6a72');text(31,7,'└'+'─'.repeat(18)+'┘','#c8c8d0','#6a6a72');const hit=fract(t*2)<0.3;const c=clawd(12,4,{facing:1,arms:{left:'down',right:hit?'down':'up'}});text(c.right.x,c.right.row-1,hit?'⚒':' ⚒','#d0b080');if(hit)put(29,4+Math.floor(rand(Math.floor(t*2))*3),'✦','#ffe080');if(fract(t/12)<0.4)say('Carving the reply in stone. {what}, now fossil-proof.',c.top.x,c.row);else if(fract(t/12)>0.6&&fract(t/12)<0.85)say('Typing was invented later. Stay tuned.',c.top.x,c.row);}","actors":[],"particles":[{"glyphs":"·.","count":12,"x":"rand(k)*w","y":"mod(t*0.5+rand(k+3)*8,8)","color":"#a08060"}],"background":{"effect":"fire","palette":["#3a1a10","#6a2a10","#a04a20"],"speed":0.5,"intensity":0.15}}},
103  {"phase":"writing","world":"snow globe","style":"3D","concept":"underground mine inside a snow globe: Clawd in a hard hat pushes a minecart down glowing rails, hauling the finished {what} gem to the surfa","source":"claude-opus-5-5","scene":{"concept":"underground mine inside a snow globe: Clawd in a hard hat pushes a minecart down glowing rails, hauling the finished {what} gem to the surface while snow drifts down","code":"const cart=box(1.4,0.8,1),gem=sphere(0.35,6),beam=box(0.25,3,0.25),top=box(5,0.25,0.4),rock=sphere(1,5);\nconst fl=Array.from({length:50},(_,i)=>({x:rand(i)*20-10,y:rand(i+7)*4,z:-rand(i+3)*14}));\nfunction frame(t,dt){camera(0,2,8,0,1,-10,90);fog(6,30,'#000');light(0.3,1,0.6,0.35);\nconst s=t*2;for(let i=0;i<8;i++){const z=-i*5+mod(s,5)+2;mesh3d(beam,{x:-2.5,y:1.5,z,color:'#b08050'});mesh3d(beam,{x:2.5,y:1.5,z,color:'#b08050'});mesh3d(top,{x:0,y:3,z,color:'#a07040'});point3d(0,2.7,z,'#ffd070');}\nfor(let i=0;i<6;i++){mesh3d(rock,{x:i%2?-5:5,y:0.5,z:-i*6+mod(s,6),scale:1.5,color:'#8a7a6a'});}\nline3d(-0.6,0,10,-0.6,0,-40,'#ccc');line3d(0.6,0,10,0.6,0,-40,'#ccc');\nfor(let i=0;i<20;i++){const z=-i*2+mod(s,2)+4;line3d(-0.9,0,z,0.9,0,z,'#7a5a3a');}\nconst bob=Math.sin(t*8)*0.03;mesh3d(cart,{x:0,y:0.5+bob,z:1,color:'#9aa4b0'});\nmesh3d(gem,{x:0,y:1.1+bob,z:1,ry:t*2,color:hsl(mod(t*40,360),0.7,0.65)});\nconst p=project(0,1.7,1);if(p)text(p.x-5,p.y-1,'{what}','#fff4c0');\nconst c=clawd3d(0,0,3.2,{facing:0,stride:Math.floor(t*6)%4,arms:'out',size:0.9});\nif(c){pixels(c.top.x-3,c.top.py-3,'.yyyyy.\\nyyyyyyy\\n.y#yyy.',{y:'#ffcc33','#':'#fff'});\nconst m=mod(t,24);if(m>2&&m<8)say('Hauling {what} up. Mined clean!',c.top.x,c.row);else if(m>13&&m<19)say('Snowing in a mine? Must be a snow globe.',c.top.x,c.row);}\nfor(const f of fl){f.y-=dt*0.6;if(f.y<0)f.y=4;const q=project(f.x,f.y,f.z);if(q)put(q.x,q.y,'*','#eef6ff');}\nfor(let x=0;x<w;x++){const e=Math.abs(x-w/2)/(w/2);if(e>0.92)put(x,0,'░','#aaccff');}}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#778"],"speed":0.3,"intensity":0}}},
104  {"phase":"writing","world":"desert caravan","style":"3D","concept":"airport runway: Clawd as ground marshaller waving off a 3D jet carrying the {what} reply, taking off again and again into the dusk","source":"claude-opus-5-5","scene":{"concept":"airport runway: Clawd as ground marshaller waving off a 3D jet carrying the {what} reply, taking off again and again into the dusk","code":"const fus=box(1.1,1.1,6),wg=box(7,0.2,1.6),tl=box(0.2,1.4,1.2),rw=plane(8,140),tw=box(1,4,1),cab=box(2,1,2);\nconst L=['Reply to {what}: cleared for takeoff!','Wheels up. Words on board.','Next flight boarding: more {what} notes.','Smooth climb. No turbulence in the diff.'];\nfunction frame(t,dt){camera(0,2.2,9,0,1.2,-20,90);fog(10,70,'#120a1a');light(0.3,1,0.6,0.35);\nconst T=mod(t,14),n=Math.floor(t/14);\nmesh3d(rw,{x:0,y:0,z:-60,color:'#8a8a94'});\nfor(let i=0;i<24;i++){const z=6-i*6;line3d(0,0.02,z,0,0.02,z-3,'#ffffff');point3d(-4.2,0.1,z,i%2?'#ffd060':'#60c0ff');point3d(4.2,0.1,z,i%2?'#ffd060':'#60c0ff');}\nmesh3d(tw,{x:9,y:2,z:-25,color:'#b0b8c8'});mesh3d(cab,{x:9,y:4.5,z:-25,color:'#80d0e0'});\nconst a=smoothstep(1.5,10,T),z=4-a*a*90,y=Math.max(0,(-z-18)*0.25),p=-Math.min(0.25,Math.max(0,(-z-18)*0.02));\nif(T<11){mesh3d(fus,{x:0,y:0.9+y,z,rx:p,color:'#e8eef4'});mesh3d(wg,{x:0,y:0.9+y,z:z+0.3,rx:p,color:'#c8d0dc'});mesh3d(tl,{x:0,y:1.9+y,z:z+2.6,rx:p,color:'#e07a50'});\nconst q=project(0,1.8+y,z);if(q&&z>-30)text(q.x-4,q.y-1,'{what}','#ffe8a0');}\nconst up=fract(t*1.2)<0.5;const c=clawd3d(-5.5,0,3,{facing:1,arms:T<2?'out':(up?'up':'out')});\nif(c){put(c.left.x,c.left.row,'●','#ff5030');put(c.right.x,c.right.row,'●','#ff5030');\nif(T>0.5&&T<6)say(L[n%4],c.top.x,c.row);}}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#3a1a4a","#7a3050","#d0704a","#2a3060"],"speed":0.4,"intensity":0.3}}},
105  {"phase":"thinking","world":"retro arcade game","style":"text art","concept":"underground mine: Clawd in a hard hat chips at a rock face by lantern light, ore cart rolls in, a gem labelled {what} finally pops out","source":"claude-opus-5-5","scene":{"concept":"underground mine: Clawd in a hard hat chips at a rock face by lantern light, ore cart rolls in, a gem labelled {what} finally pops out","code":"let gem=0\nfunction frame(t,dt){\nfor(let x=0;x<w;x++){put(x,0,x%5?'▀':'▓','#6b5a48');put(x,h-1,x%3?'═':'╪','#8a7a66')}\nfor(let y=1;y<h-1;y++)for(let x=0;x<12;x++){const n=noise2(x*0.5,y*0.7);put(w-12+x,y,n>0.6?'▓':n>0.4?'▒':'░','#9a8070')}\nfor(let i=0;i<4;i++){const bx=10+i*24;sprite(bx,1,'╥\\n║\\n║\\n║\\n║\\n║\\n║','#a07a50');text(bx-2,1,'═════','#a07a50')}\nconst cyc=mod(t,12),sw=Math.sin(t*6)>0.3\nconst cx=w-30\nconst c=clawd(cx,h-5,{facing:1,arms:sw?'up':{left:'down',right:'out'},pose:'stand'})\ntext(c.top.x-3,c.top.row-1,'▄███▄','#f2c84b');put(c.top.x,c.top.row-1,'●','#fff6a0')\nconst px=c.right.x,pr=c.right.row\ntext(px,sw?pr-1:pr,sw?'┐':'─╼','#cfd6dc')\nif(!sw&&fract(t*3)<0.3)for(let i=0;i<3;i++)put(w-13+rand(i+t)*2,pr-1+rand(i*7+t)*3,'*','#ffd27a')\nconst lx=8+Math.sin(t*0.7)*2;text(lx,2,'╤','#aaa');text(lx-1,3,'[◊]','#ffcf60')\nconst k=smoothstep(0,4,cyc);const kx=lerp(-20,cx-24,k)\nsprite(kx,h-4,'\\\\▓▓▓▓▓▓/\\n └o──o┘','#b08060')\nif(cyc>7){const g=smoothstep(7,9,cyc);const gx=lerp(w-13,kx+3,g),gy=h-4-Math.sin(g*3.14)*3;put(gx,gy,'◆','#5ff0ff');if(cyc>7.5&&cyc<11)text(gx-4,gy-1,'{what}','#9ff')}\nif(cyc>1&&cyc<6.5)say('Still chipping at {what}. The rock is stubborn.',c.top.x,c.row)\nelse if(cyc>9)say('Found it! A gem of an idea.',c.top.x,c.row)\n}","actors":[],"particles":[{"glyphs":".·'","count":20,"x":"w-14+rand(k)*6","y":"mod(t*(2+rand(k+3)*3)+rand(k+5)*7,7)+1","color":"#7a6a5a"}],"background":{"effect":"pulse","palette":["#2a1d12","#3a2a18","#4a3820"],"speed":0.4,"intensity":0.15}}},
106  {"phase":"searching","world":"construction site","style":"3D","concept":"race track pit stop meets construction site: Clawd in a hard hat drives a 3D track crane sweeping a searchlight over stacked crates in the p","source":"claude-opus-5-5","scene":{"concept":"race track pit stop meets construction site: Clawd in a hard hat drives a 3D track crane sweeping a searchlight over stacked crates in the pit lane, hunting for {what}","code":"const crate=box(1.2,1,1.2),post=cylinder(0.12,3,6),cone1=cone(0.25,0.5,6);\nconst cr=Array.from({length:14},(_,i)=>({x:-12+i*2,z:-2-(i%3)*1.6,c:i%3}));\nconst cols=['#e0a060','#c9b37a','#d98f5a'];\nfunction frame(t,dt){\ncamera(Math.sin(t*0.1)*3,3,9,0,0.8,-2,90);fog(8,38,'#000');light(0.4,1,0.6,0.35);\nfor(let i=-6;i<7;i++){mesh3d(cone1,{x:i*3,z:2,color:i%2?'#ff8c3a':'#eee'});}\nline3d(-20,0,1,20,0,1,'#aaa');line3d(-20,0,-7,20,0,-7,'#888');\nfor(let i=-20;i<20;i+=2)line3d(i,0.01,1,i+1,0.01,1,'#e33');\nconst sweep=Math.sin(t*0.35)*13,cyc=fract(t/16),tg=5;\nfor(let i=0;i<cr.length;i++){const c=cr[i];const hit=i==tg&&cyc>0.55;const lift=hit?smoothstep(0.6,0.75,cyc)*1.5:0;\nmesh3d(crate,{x:c.x,y:0.5+lift,z:c.z,color:hit?'#ffd75e':cols[c.c]});}\nmesh3d(post,{x:-13,z:-1,color:'#f2c14e'});line3d(-13,3,-1,sweep,3,-1,'#f2c14e');\nconst sx=cyc>0.55?cr[tg].x:sweep;line3d(sx,3,-1,sx,1.2,cr[tg].z,'#ffe08a');\nconst p=project(sx,0.2,-2);if(p){for(let k=-3;k<=3;k++)put(p.x+k,p.y,'░','#ffe08a');}\nconst q=project(cr[tg].x,2.4,cr[tg].z);if(q&&cyc>0.6)text(q.x-4,q.y,'{what}','#fff');\nconst me=clawd3d(-11,0,0.5,{facing:1,arms:cyc>0.6?'up':'out',size:1.1});\nif(me){pixels(me.top.x-4,me.top.py-3,' ######\\n########','{\"#\":\"#ffd400\"}'.length?{'#':'#ffd400'}:{});\nif(cyc>0.6)say('Found {what}! Hoisting it to the pit.',me.top.x,me.row);else if(cyc>0.1&&cyc<0.4)say('Sweeping every crate for {what}...',me.top.x,me.row);}\n}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#778"],"speed":0.3,"intensity":0}}},
107  {"phase":"searching","world":"cooking show","style":"3D","concept":"race track pit stop in 3D: Clawd in pit lane scans tire stacks one by one hunting the right set while cars scream past on the track","source":"claude-opus-5-5","scene":{"concept":"race track pit stop in 3D: Clawd in pit lane scans tire stacks one by one hunting the right set while cars scream past on the track","code":"const tire=cylinder(0.45,0.3,12),car=box(1.8,0.5,0.9),cab=box(0.8,0.35,0.7),post=box(0.15,1.6,0.15);\nconst sx=[-8,-5,-2,1,4,7];\nfunction frame(t,dt){camera(0,3,8,0,0.8,-2,80);light(0.4,1,0.6,0.35);fog(10,40,'#000');\nfor(let z of [-4,-8])line3d(-30,0,z,30,0,z,'#bbb');line3d(-30,0,-2.5,30,0,-2.5,'#e44');\nfor(let i=-6;i<7;i++)mesh3d(post,{x:i*4,y:0.8,z:-9,color:'#99a'});line3d(-26,1.6,-9,26,1.6,-9,'#ccd');\nfor(let c=0;c<3;c++){const x=mod(t*22+c*20,60)-30,z=c%2?-5:-7,col=['#e55','#5ae','#ec5'][c];mesh3d(car,{x,y:0.3,z,color:col});mesh3d(cab,{x:x-0.2,y:0.7,z,color:'#eee'});}\nconst cy=mod(t,13),i=Math.min(5,Math.floor(cy/2)),found=cy>=11;\nfor(let k=0;k<6;k++){const hit=found&&k==5;for(let j=0;j<4;j++)mesh3d(tire,{x:sx[k],y:j*0.32,z:0,color:hit?'#7e7':(k<i||(k==i&&cy%2>1.2)?'#866':'#888')});}\nconst seg=Math.min(cy,11),fi=Math.floor(seg/2),fr=fract(seg/2),cx=lerp(sx[Math.min(fi,5)],sx[Math.min(fi+1,5)],smoothstep(0,0.5,fr))+0.9;\nconst me=clawd3d(cx,0,1,{facing:fr<0.5&&!found?1:0,stride:fr<0.5?Math.floor(t*8)%4:-1,arms:found?'up':{left:'down',right:'out'},size:0.8});\nconst p=project(sx[5],1.6,0);if(p)text(p.x-5,p.y-1,'{what}',found?'#7f7':'#ccc');\nif(me){if(!found&&fr>0.5)put(me.right.x+1,me.right.row,'≈','#6cf');\nif(found)say('Found {what}! Fastest pit stop in the codebase.',me.top.x,me.row);else if(cy<3)say('Box box! Hunting for {what}...',me.top.x,me.row);else if(cy>6&&cy<9)say('Wrong compound. Next stack!',me.top.x,me.row);}}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#778"],"speed":0.5,"intensity":0}}},
108  {"phase":"writing","world":"underground mine","style":"3D","concept":"ski slope: Clawd slaloms down a snowy run through red and blue gates toward a FINISH banner for {what}, delivering the reply at speed","source":"claude-opus-5-5","scene":{"concept":"ski slope: Clawd slaloms down a snowy run through red and blue gates toward a FINISH banner for {what}, delivering the reply at speed","code":"const tree=cone(0.9,2.2,6),trunk=cylinder(0.15,0.5,5),pole=cylinder(0.06,1.3,4),post=cylinder(0.1,2.6,5),gnd=plane(60,220);\nconst L=['Carving the reply for {what}. Clean lines only.','Gate after gate, every point covered.','No wipeouts on {what} today.','Tucking in for the final stretch!'];\nfunction frame(t,dt){const z=-t*5,px=Math.sin(z*0.39)*3;camera(px*0.5,3,z+8,px*0.4,0.4,z-12,90);fog(10,50,'#000');light(0.4,1,0.5,0.35);\nmesh3d(gnd,{x:0,y:0,z:z-90,color:'#dce8f6'});\nfor(let i=-1;i<14;i++){const tz=Math.floor(z/5)*5-i*5;for(const s of [-1,1]){const tx=s*(8+rand(tz*s+3)*4);mesh3d(trunk,{x:tx,z:tz,color:'#a07850'});mesh3d(tree,{x:tx,y:0.5,z:tz,color:'#5fb070'});}}\nfor(let j=0;j<6;j++){const gz=Math.floor(z/8)*8-j*8,gx=Math.sin(gz*0.39)*3,c=mod(Math.floor(gz/8),2)?'#ff5a5a':'#5a8cff';mesh3d(pole,{x:gx-1.3,z:gz,color:c});mesh3d(pole,{x:gx+1.3,z:gz,color:c});line3d(gx-1.3,1.2,gz,gx+1.3,1.2,gz,c);}\nconst fz=Math.floor(z/100)*100-60;mesh3d(post,{x:-5,z:fz,color:'#ffcc55'});mesh3d(post,{x:5,z:fz,color:'#ffcc55'});line3d(-5,2.5,fz,5,2.5,fz,'#ffcc55');line3d(-5,2.2,fz,5,2.2,fz,'#ffcc55');const p=project(0,2.4,fz);if(p&&p.depth>0)text(p.x-8,p.y,'FINISH: {what}','#ffe066');\nline3d(px-0.35,0.02,z+0.6,px-0.35,0.02,z-0.6,'#ff8844');line3d(px+0.35,0.02,z+0.6,px+0.35,0.02,z-0.6,'#ff8844');\nconst d=Math.cos(z*0.39),me=clawd3d(px,0,z,{facing:d<-0.2?1:d>0.2?-1:0,arms:'out',pose:'stand'});\nfor(let k=0;k<8;k++){const sp=project(px+(rand(k+Math.floor(t*6))-0.5)*1.5,0.1,z+0.8+rand(k)*0.6);if(sp)put(sp.x,sp.y,'*','#ffffff');}\nconst li=Math.floor(t/7)%L.length;if(me&&fract(t/7)<0.7)say(L[li],me.top.x,me.row);}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#ffffff","#aaccff","#ccddee"],"speed":0.5,"intensity":0}}},
109  {"phase":"reading","world":"museum after hours","style":"3D","concept":"ant colony up close, 3D: Clawd crouches beside a winding ant trail, studying the workers as they haul crumbs of {what} back from the anthill","source":"claude-opus-5-5","scene":{"concept":"ant colony up close, 3D: Clawd crouches beside a winding ant trail, studying the workers as they haul crumbs of {what} back from the anthill","code":"const sp=sphere(0.16,6),cr=box(0.32,0.28,0.32),hill=cone(5,3.5,12),gr=plane(80,80),pb=sphere(0.5,6),gs=cone(0.08,1.2,4);\nconst ants=Array.from({length:16},(_,i)=>({z:-38+i*2.8,c:i%3==0}));\nconst peb=Array.from({length:8},(_,i)=>({x:(rand(i)-0.5)*16,z:-rand(i+7)*30,s:0.5+rand(i+3)}));\nconst grass=Array.from({length:14},(_,i)=>({x:(rand(i+20)<0.5?-1:1)*(5+rand(i+30)*6),z:-rand(i+40)*30}));\nconst L=['Following the trail to see where {what} goes.','Every ant carries one piece of {what}. Neat.','Ah, so THAT function feeds the queen.','The whole colony runs on {what}. Mapping it.'];\nfunction frame(t,dt){camera(4+Math.sin(t*0.12)*2,1.7,9,0,0.4,-8,80);light(0.4,1,0.6,0.35);fog(8,42,'#000');\nmesh3d(gr,{color:'#9a7a50'});mesh3d(hill,{x:-3,z:-32,color:'#c09a60'});\nfor(const p of peb)mesh3d(pb,{x:p.x,y:0.1,z:p.z,scale:p.s,color:'#a8a090'});\nfor(const g of grass)mesh3d(gs,{x:g.x,z:g.z,rz:Math.sin(t+g.x)*0.1,color:'#7cc060'});\nlet near=null;for(const a of ants){a.z+=dt*1.3;if(a.z>8)a.z-=44.8;const x=Math.sin(a.z*0.25)*3,b=Math.sin(t*12+a.z)*0.03;\nfor(let k=0;k<3;k++)mesh3d(sp,{x,y:0.18+b,z:a.z+k*0.3-0.3,scale:k==1?0.8:1,color:'#d8573c'});\nif(a.c){mesh3d(cr,{x,y:0.48+b,z:a.z,ry:a.z,color:'#f4e4a8'});if(a.z<5&&(!near||a.z>near.z))near={x,z:a.z};}}\nconst p=near&&project(near.x,0.9,near.z);if(p&&p.depth<20)text(p.x-4,p.y,'{what}','#fff6c0');\nconst wx=4+Math.sin(t*0.2)*0.6;const me=clawd3d(wx,0,1.5,{facing:-1,stride:-1,look:{x:-2,y:1},arms:fract(t/8)<0.5?'down':{left:'out',right:'down'},blink:fract(t*0.3)<0.04});\nif(me&&fract(t/8)>0.1)say(L[Math.floor(t/8)%4],me.top.x,me.row);}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#3a2a1a","#5a4028","#2a3a20"],"speed":0.3,"intensity":0}}},
110  {"phase":"running","world":"volcano island","style":"3D","concept":"submarine off a volcano island: Clawd peers from a 3D sub's porthole as the {what} command runs, an undersea vent rumbling and bubbles risin","source":"claude-opus-5-5","scene":{"concept":"submarine off a volcano island: Clawd peers from a 3D sub's porthole as the {what} command runs, an undersea vent rumbling and bubbles rising while depth ticks down","code":"const hull=sphere(1,14),tower=box(0.8,0.7,1),fin=box(0.1,0.8,0.6),rock=cone(2.5,3,8),vent=cone(1.2,1.6,8),bub=sphere(0.12,5);\nconst bs=Array.from({length:14},(_,i)=>({x:rand(i)*2-1,y:rand(i+9)*5,v:0.6+rand(i+3)}));\nfunction frame(t,dt){camera(Math.sin(t*0.1)*2,2,9,0,1,0,80);light(0.3,1,0.5,0.35);fog(6,30,'#021220');\nfor(let x=0;x<w;x++)for(let y=0;y<h;y++){const n=noise2(x*0.05+t*0.2,y*0.3);if(n>0.72)put(x,y,'~',mix('#0a3050','#1a6080',n));}\nmesh3d(plane(60,30),{y:-0.5,color:'#7a6a50'});\nmesh3d(rock,{x:-8,y:-0.5,z:-6,color:'#6a5a5a'});mesh3d(rock,{x:9,y:-0.5,z:-8,scale:1.4,color:'#5a5050'});\nmesh3d(vent,{x:5,y:-0.5,z:-2,color:'#8a5040'});\nconst glow=0.5+0.5*Math.sin(t*3);const vp=project(5,1.1,-2);if(vp)text(vp.x-1,vp.y,'▲',mix('#ff4020','#ffcc40',glow));\nfor(const b of bs){b.y+=b.v*dt;if(b.y>6)b.y=0;mesh3d(bub,{x:5+b.x*0.6+Math.sin(t+b.y)*0.3,y:1+b.y,z:-2,color:'#aee'});}\nconst sx=-3+Math.sin(t*0.2)*1.5,sy=1.4+Math.sin(t*0.8)*0.15;\nmesh3d(hull,{x:sx,y:sy,z:0,scale:1,sx:1,color:'#e8c040'});mesh3d(hull,{x:sx+1.4,y:sy,z:0,scale:0.8,color:'#e8c040'});mesh3d(hull,{x:sx-1.4,y:sy,z:0,scale:0.75,color:'#e8c040'});\nmesh3d(tower,{x:sx+0.3,y:sy+0.8,z:0,color:'#d0a830'});mesh3d(fin,{x:sx-2.2,y:sy,z:0,ry:t*6,color:'#c09020'});\nconst p=project(sx+0.2,sy+0.1,1.1);if(p){const c=clawd(p.x-7,p.y-2,{facing:1,eyes:fract(t*0.3)<0.04?'closed':'wide',look:{x:2,y:0}});\nconst lp=project(sx+0.3,sy+1.6,0);if(lp)text(lp.x-Math.floor('{what}'.length/2),lp.y-1,'{what}','#ffe');\nconst m=Math.floor(t/12)%4;const L=['Diving deep into {what}. Pressure: holding.','Vent rumbling. That is just the CPU fans.','Sonar pings, no output yet. Patience, crew.','Still sinking toward the result. Bubbles say soon.'];if(fract(t/12)>0.25)say(L[m],c.top.x,c.row);}\nconst d=Math.floor(t*3);text(w-14,h-1,'DEPTH '+String(120+d).padStart(4,'0')+'m','#6cf');}","actors":[],"particles":[],"background":{"effect":"waves","palette":["#03283d","#064060","#0a5070"],"speed":0.5,"intensity":0}}},
111  {"phase":"editing","world":"desert caravan","style":"3D","concept":"ancient tomb: Clawd the stonemason rebuilding a crumbling hieroglyph wall block by block, torches flickering, in a 3D burial chamber","source":"claude-opus-5-5","scene":{"concept":"ancient tomb: Clawd the stonemason rebuilding a crumbling hieroglyph wall block by block, torches flickering, in a 3D burial chamber","code":"const blk=box(0.95,0.55,0.6),pil=cylinder(0.35,3,8),fl=plane(30,30),tor=cone(0.15,0.4,6);\nfunction frame(t,dt){camera(Math.sin(t*0.15)*1.5,2.2,6,0,1,-6,70);light(0.4,0.8,0.6,0.35);fog(6,30,'#000');\nmesh3d(fl,{y:0,z:-6,color:'#b89a6a'});\nfor(const sx of[-6,6])for(let z=-2;z>-14;z-=4){mesh3d(pil,{x:sx,z,color:'#c9a96e'});mesh3d(tor,{x:sx*0.88,y:2,z,color:'#f6a23a',unlit:true});point3d(sx*0.88,2.5+Math.random()*0.15,z,Math.random()<0.5?'#ffd25a':'#ff7a2a');}\nconst s=mod(t,40),cur=Math.min(23,Math.floor(s/1.5));let cx=0;\nfor(let i=0;i<24;i++){const c=i%8,r=Math.floor(i/8),x=-3.5+c,y=0.3+r*0.58;const k=smoothstep(i*1.5,i*1.5+1,s);if(i==cur)cx=x;if(k<=0)continue;mesh3d(blk,{x,y:lerp(4,y,k),z:-6,ry:(1-k)*1.5,color:(c+r)%3==0?'#e0c48a':'#d4b27a'});}\nif(s>36){const p=project(0,2.2,-6);if(p)text(p.x-8,p.y,'✦ SEALED ✦','#ffe08a');}\nconst p=project(0,2.1,-5.6);if(p&&cur>18)text(p.x-6,p.y,'{what}','#5a2a10');\nconst me=clawd3d(cx,0,-4.3,{facing:0,arms:fract(t*1.5)<0.3?'up':'out',stride:-1});\nif(me){pixels(me.top.x-3,me.top.py-3,'.yyyyy.\\nyyyyyyy','{\"y\":\"#e8d070\"}'.length?{y:'#e8d070'}:{});\nif(fract(t*1.5)<0.25)text(me.right.x+1,me.right.row-1,'*','#fff6c0');\nif(s>2&&s<8)say('Restoring {what}, one stone at a time.',me.top.x,me.row);\nelse if(s>20&&s<26)say('Careful. The curse is in the indentation.',me.top.x,me.row);\nelse if(s>36)say('Wall sealed. Pharaoh would approve.',me.top.x,me.row);}}","actors":[],"particles":[{"glyphs":"·.","count":25,"x":"rand(k)*w","y":"mod(t*(0.3+rand(k+5)*0.5)+rand(k+2)*h,h)","color":"#8a7650"}],"background":{"effect":"fire","palette":["#2a1408","#4a2410","#6a3a18"],"speed":0.4,"intensity":0.15}}},
112  {"phase":"editing","world":"dinosaur era","style":"pixel art","concept":"desert caravan: Clawd rides a camel across scrolling dunes, packing saddlebags while signposts for {what} roll past","source":"claude-opus-5-5","scene":{"concept":"desert caravan: Clawd rides a camel across scrolling dunes, packing saddlebags while signposts for {what} roll past","code":"const cam=['......cc........','.....cccc.......','.....cc..hhh....','....cc..hhhhh...','...cc.hhhhhhhhh.','...ccccccccccccc','....cccccccccccc'].map(s=>s.split('').reverse().join(''));\nconst lg=[['.c.c.....c.c....','.c.c.....c.c....','.c.c.....c.c....'],['..c.c...c...c...','.c...c...c.c....','c.....c.c...c...']];\nconst P={c:'#c8955a',h:'#a8703a',b:'#7a4a2a',s:'#e8d070',k:'#5a3a1a'};\nfunction frame(t,dt){\nfor(let x=0;x<w;x++){const f=Math.floor(4+noise2(x*0.03+t*0.05,1)*4);for(let p=f;p<10;p++)pixel(x,p,'#b07a48');const n=Math.floor(11+noise2(x*0.06+t*0.25,5)*4);for(let p=n;p<18;p++)pixel(x,p,p==n?'#f0c878':'#d8a860');}\npixels(100,1,'.sss.\\nsssss\\nsssss\\n.sss.',{s:'#ffd84a'});\nconst sx=mod(-t*7,140)+120;pixels(Math.floor(sx),8,'k\\nk\\nk\\nk\\nk\\nk\\nk',P);text(Math.floor(sx)-4,3,'[{what}]','#ffe9a0','#5a3a1a');\nconst cx=50,bob=Math.floor(t*4)%2;\npixels(cx,4+bob,cam.join('\\n'),P);pixels(cx,11,lg[Math.floor(t*4)%2].join('\\n'),P);\nconst nb=Math.floor(t/6)%5;for(let i=0;i<Math.min(nb,3);i++)pixels(cx+2+i*4,8+bob,'bbb\\nbsb\\nbbb',P);\nconst c=clawd(cx+2,0+bob*0.5,{pose:'sit',facing:1,arms:fract(t/6)<0.2?'up':'down'});\npixels(c.top.x-3,c.top.py-2,'..kkk..\\nsssssss',P);\nconst q=Math.floor(t/9)%4;const L=['Packing {what} for the long haul.','One more edit in the saddlebag.','Camel says the diff is heavy.','Oasis = tests. Keep riding.'];if(fract(t/9)<0.6)say(L[q],c.top.x+8,0);\nfor(let i=0;i<6;i++){const x=mod(i*23-t*12,w);pixel(Math.floor(x),16+i%2,'#f5dca0');}\n}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#5a3060","#a05040","#e09050"],"speed":0.3,"intensity":0}}},
113  {"phase":"agents","world":"snow globe","style":"3D","concept":"rush hour traffic: Clawd as a traffic cop on the shoulder waving parallel subagent cars down a 3D highway under a {what} gantry sign","source":"claude-opus-5-5","scene":{"concept":"rush hour traffic: Clawd as a traffic cop on the shoulder waving parallel subagent cars down a 3D highway under a {what} gantry sign","code":"const car=box(1.4,0.8,2.2),cab=box(1.1,0.5,1.1),post=cylinder(0.12,3.2,6),sign=box(9,0.9,0.2),cone1=cone(0.25,0.6,6);\nconst lanes=[-4,-1.4,1.4,4];\nconst cs=Array.from({length:10},(_,i)=>({l:lanes[i%4],z:-rand(i)*60,v:5+rand(i+7)*6,c:hsl(rand(i+3)*360,0.6,0.62)}));\nfunction frame(t,dt){camera(0,3.2,9,0,1,-25,90);fog(10,55,'#000');light(0.4,1,0.6,0.35);\nfor(let i=0;i<5;i++){const x=-5.4+i*2.7;line3d(x,0,10,x,0,-80,i%4?'#777':'#bbb')}\nfor(let i=0;i<14;i++){const z=6-i*6+mod(t*8,6);line3d(-2.7,0.01,z,-2.7,0.01,z-2,'#ee8');line3d(0,0.01,z,0,0.01,z-2,'#ee8');line3d(2.7,0.01,z,2.7,0.01,z-2,'#ee8')}\nmesh3d(post,{x:-5.8,z:-12,color:'#999'});mesh3d(post,{x:5.8,z:-12,color:'#999'});mesh3d(sign,{y:3.4,z:-12,color:'#3a9a5a'});\nconst sp=project(0,3.4,-12);if(sp)text(Math.round(sp.x-7),sp.y,'{what} -> EXIT 4','#fff');\nfor(const c of cs){c.z-=c.v*dt*(0.6+0.4*Math.sin(t*0.7+c.l));if(c.z<-70){c.z=6;c.c=hsl(Math.random()*360,0.6,0.62)}\nmesh3d(car,{x:c.l,y:0.4,z:c.z,color:c.c});mesh3d(cab,{x:c.l,y:1.05,z:c.z+0.2,color:'#cde'})}\nfor(let i=0;i<4;i++)mesh3d(cone1,{x:-6.2,z:3-i*3,color:'#f80'});\nconst wave=Math.floor(t*2)%2;const me=clawd3d(-7.5,0,3,{facing:1,size:1.1,arms:wave?{left:'down',right:'up'}:{left:'down',right:'out'}});\nif(me){const L=['Go go go! Every lane gets a piece of {what}.','Four helpers, four lanes, zero honking.','Rush hour, but make it parallel.','Waiting for my helpers to come back with answers.'];say(L[Math.floor(t/7)%4],me.top.x,me.row)}}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#335","#557","#99b"],"speed":0.3,"intensity":0}}},
114  {"phase":"web","world":"jungle safari","style":"3D","concept":"mad science lab: Clawd throws the switch as a tesla coil yanks web pages out of the sky, bottling them as glowing packets in a flask labeled","source":"claude-opus-5-5","scene":{"concept":"mad science lab: Clawd throws the switch as a tesla coil yanks web pages out of the sky, bottling them as glowing packets in a flask labeled {what}","code":"const bench=box(12,0.8,2.4),pole=cylinder(0.25,2.6,8),orb=sphere(0.7,12),fl=cylinder(0.6,1.6,12),pk=box(0.35,0.35,0.35),fz=plane(40,14),lv=cone(0.3,0.6,8);\nfunction frame(t,dt){\ncamera(Math.sin(t*0.15)*3,2.6,9,0,1.2,0,90);light(0.3,1,0.6,0.3);fog(10,40,'#000');\nmesh3d(fz,{color:'#556677'});\nmesh3d(bench,{y:0.4,color:'#b08860'});\nmesh3d(pole,{x:5,y:0.8,color:'#aab0c0'});\nmesh3d(orb,{x:5,y:3.6,color:'#c8d8ff'});\nconst zap=fract(t*0.7)<0.5;\nif(zap){for(let j=0;j<3;j++){let x=5,y=3.6;for(let s=0;s<4;s++){const nx=x+(Math.random()-0.5)*1.4,ny=y+0.7;line3d(x,y,0,nx,ny,-0.3,'#aef4ff');x=nx;y=ny}}\nlet x=5,y=3.6;for(let s=1;s<=6;s++){const nx=lerp(5,-4,s/6),ny=lerp(3.6,2.6,s/6)+(Math.random()-0.5)*0.6;line3d(x,y,0,nx,ny,0,'#ffef80');x=nx;y=ny}}\nfor(let i=0;i<4;i++){const p=mod(t*1.6+i*1.3,5.2);if(p<3.4)mesh3d(pk,{x:-4,y:5.4-p*1.3,ry:t*3+i,color:hsl(90+i*40,0.8,0.65),unlit:true})}\nconst lvl=0.2+mod(t*0.04,1)*1.2;mesh3d(box(1,lvl,1),{x:-4,y:0.8+lvl/2,color:'#7cff8a'});\nmesh3d(fl,{x:-4,y:0.8,color:'#9fdcc0',wire:true});\nmesh3d(lv,{x:2,y:0.8,color:'#ff6060'});\nconst q=project(-4,0.6,1.2);if(q)text(q.x-4,q.y,'{what}','#ffffa0');\nconst up=fract(t*0.35)<0.5;\nconst c=clawd3d(0.2,0.8,0.6,{facing:0,arms:up?'up':{left:'down',right:'out'},eyes:zap?'wide':'open',color:'#d97757'});\nconst L=['Fetching {what}... IT\\'S ALIVE!','Catch it! The internet is pouring in!','Bottling {what}, one packet at a time.','More voltage! The page is still loading!'];\nif(c&&fract(t/6)<0.75)say(L[Math.floor(t/6)%4],c.top.x,c.row);\n}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#203040","#304060","#405070"],"speed":0.5,"intensity":0}}},
115  {"phase":"git","world":"outer space","style":"pixel art","concept":"lighthouse in a storm: Clawd the keeper guides commit-ships through the squall to dock in a row, each a link in the harbor's history","source":"claude-opus-5-5","scene":{"concept":"lighthouse in a storm: Clawd the keeper guides commit-ships through the squall to dock in a row, each a link in the harbor's history","code":"const S=[];for(let i=0;i<40;i++)S.push(i);\nconst H=['a1f3e9','7c02bd','e5d41a','09bb7f','3f6c22','d8e110'];\nconst tw=' .rr. \\n.yyyy.\\n.yyyy.\\n gggg \\n wwww \\n rrrr \\n wwww \\n rrrr \\n wwww \\n rrrr \\n wwwwww\\nwwwwwww\\nkkkkkkkk';\nconst tp={r:'#d04040',y:'#ffe066',g:'#555',w:'#eeeeee',k:'#666677','.':null};\nconst bt='....mm....\\n....m.....\\nbbbbbbbbbb\\n.bbbbbbbb.';\nfunction frame(t,dt){const fl=fract(t/9)<0.03;if(fl)fill(0,0,w,h-2,' ',null,'#2a2f45');\nfor(const k of S){const x=mod(rand(k)*w-t*14,w),y=mod(rand(k+5)*9+t*16,7);put(x,y,'/','#7a9cc6')}\nconst a=Math.sin(t*0.9);for(let s=1;s<70;s++){const x=12+s,y=2-a*s*0.04;if(y>=0&&y<6&&a>-0.2)put(x,y,'░','#fff3a0')}\npixels(6,1,tw,tp);for(let x=0;x<w;x++){put(x,h-1,'≈','#3a6ea5');put(x,h-2,mod(x+Math.floor(t*4),6)<2?'~':' ','#5a8fd0')}\nfor(let x=36;x<w;x++)put(x,7,x%4?'═':'╦','#a07850');\nconst n=Math.floor(t/6)+1;for(let i=0;i<n;i++){const sl=40+(i%6)*13,bx=Math.max(sl,w-(t-i*6)*10);if(i%6!=(n-1)%6||i==n-1){pixels(bx,10,bt,{m:'#ccc',b:i==n-1?'#e0a060':'#b07040'});text(bx+1,4,i==n-1?'{what}'.slice(0,12):H[i%6],i==n-1?'#ffe066':'#9fb')}}\nconst c=clawd(16,3.5,{facing:1,arms:{left:'down',right:'out'},color:'#d97757'});pixels(c.top.x-4,c.top.py-2,'..yyyyy..\\nyyyyyyyyy','',{y:'#ffd23f'});pixels(c.top.x-4,c.top.py-2,'..yyyyy..\\nyyyyyyyyy',{y:'#ffd23f'});\nconst L=['Steady now. {what} incoming!','Docked. Another commit in the log.','Storm or not, history stays tidy.','Beam on. No commit left at sea.'];say(L[Math.floor(t/6)%4],c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"rain","palette":["#1b2440","#2a3a60","#3a5080"],"speed":1.2,"intensity":0}}},
116  {"phase":"git","world":"skate park","style":"3D","concept":"train station meets skate park: 3D branching rails where commit-cars leave a station, Clawd skates the main line grinding rails as trains sp","source":"claude-opus-5-5","scene":{"concept":"train station meets skate park: 3D branching rails where commit-cars leave a station, Clawd skates the main line grinding rails as trains split onto branches","code":"const car=box(1.6,0.9,1),plat=box(30,0.4,3),post=cylinder(0.1,2.2,6),ramp=box(2,0.3,1.4);\nfunction frame(t,dt){camera(0,3,9,0,0.8,-6,90);light(0.4,1,0.6,0.35);fog(10,42,'#000');\nmesh3d(plat,{x:0,y:0.2,z:2.5,color:'#9aa4b8'});\nfor(let i=-3;i<=3;i++)mesh3d(post,{x:i*4,z:3.5,color:'#c8a060'});\nfor(let s=-1;s<=1;s+=2){line3d(s*0.6,0,6,s*0.6,0,-60,'#aaa');}\nfor(let z=0;z>-60;z-=1.5)line3d(-0.9,0,z,0.9,0,z,'#554');\nconst bz=-8;line3d(0.6,0,bz,7,0,bz-25,'#8cf');line3d(-0.6,0,bz,5.8,0,bz-25,'#8cf');\nline3d(-0.6,0,bz-4,-7,0,bz-29,'#f8c');line3d(0.6,0,bz-4,-5.8,0,bz-29,'#f8c');\nconst cyc=mod(t,8),tr=cyc*4;const br=mod(floor(t/8),2)?1:-1;\nfor(let i=0;i<3;i++){let z=2-tr-i*1.9,x=0;if(z<bz)x=br*(bz-z)*0.26;mesh3d(car,{x,y:0.55,z,ry:z<bz?-br*0.25:0,color:i?'#e0b060':'#7fd4a0'});if(i==0){const p=project(x,1.4,z);if(p&&cyc<6)text(p.x-3,p.y,'{what}','#fff');}}\nmesh3d(ramp,{x:-4,y:0.15,z:0.5,rz:0.3,color:'#d0d0e0'});\nconst sx=-9+mod(t*3,18),air=Math.max(0,Math.sin(Math.max(0,sx+4.5)*1.2))*(sx>-4.5&&sx<-1.9?1:0);\nconst c=clawd3d(sx,0.4+air*1.2,1.2,{facing:1,stride:-1,arms:air>0.1?'up':'out',size:0.8});\nif(c){pixels(c.feet.x-6,c.feet.py,'#############\\n.o.........o.',{'#':'#c33','o':'#ddd'});\nconst lines=['kickflip onto main. {what} incoming!','new branch? grind it.','commit hash, nollie, merge.','HEAD is wherever I land.'];if(fract(t/8)<0.6)say(lines[floor(t/8)%4],c.top.x,c.row);}}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#778"],"speed":0.4,"intensity":0.2}}},
117  {"phase":"web","world":"film noir city at night","style":"text art","concept":"cooking show: chef Clawd stirs a pot while ingredient crates labeled {what} zip in on a kitchen zipline from the web outside","source":"claude-opus-5-5","scene":{"concept":"cooking show: chef Clawd stirs a pot while ingredient crates labeled {what} zip in on a kitchen zipline from the web outside","code":"const P={'#':'#b0b8c0','d':'#6a7480','o':'#ff8030','y':'#ffd040'};\nfunction frame(t,dt){\nfor(let x=0;x<w;x++){put(x,7,'▀','#c08850','#7a5030');put(x,8,x%10==0?'│':' ',null,'#5a3820');}\nfor(let x=0;x<w;x+=6)put(x,0,'▄','#3a5a6a');\nfill(96,1,22,5,' ',null,'#123050');text(97,1,'~ THE WEB ~','#8fd0ff','#123050');for(let i=0;i<6;i++)put(98+i*3,3+(i%2),'✦','#ffe080','#123050');\nline(60,1,w,1,'─','#909090');\nconst c=fract(t/8),k=t/8|0,lx=lerp(w,64,smoothstep(0,0.55,c)),fall=smoothstep(0.55,0.7,c),ly=1+fall*4;\nif(c<0.7){fill(lx-4,ly,9,1,' ',null,'#a06a30');text(lx-4,ly,('{what}').slice(0,9),'#fff','#a06a30');if(c<0.55)put(lx,ly-1,'┴','#ccc');}\npixels(58,10,'..dddddddddd..\\n.d##########d.\\n.d##########d.\\n..d########d..',P);\nconst f=c>0.7?1:0.3;for(let i=0;i<3;i++)put(60+i*4,5+Math.sin(t*3+i)*0.5-fract(t*0.7+i*0.3)*3,'~',mix('#555555','#ffffff',f));\nfor(let i=0;i<7;i++)put(60+i*1.5,7,(t*6+i)%2<1?'▲':'^',i%2?'#ff5020':'#ffb030');\nconst st=c>0.7&&c<0.95,ar=st?(Math.sin(t*10)>0?'up':'out'):'down';\nconst m=clawd(42,3,{facing:1,arms:{left:'down',right:ar},blink:fract(t*0.3)<0.04});\npixels(m.top.x-4,m.top.py-5,'.wwwww.\\nwwwwwww\\n.wwwww.\\n.wwwww.',{w:'#ffffff'});\nline(m.right.x,m.right.row,m.right.x+6,m.right.row+1,'/','#d0a060');\nconst L=['Ordering fresh {what} straight from the web!','Here it comes, still warm from the server.','Into the pot! A pinch of HTML, a dash of JSON.','Another crate! This recipe needs more sources.'];\nif(c>0.1&&c<0.5)say(L[(k*2)%4],m.top.x,m.row);else if(c>0.72&&c<0.97)say(L[(k*2+1)%4]+'',m.top.x,m.row);\n}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#3a2a20","#4a3020","#2a2018"],"speed":0.5,"intensity":0}}},
118  {"phase":"agents","world":"mad science lab","style":"pixel art","concept":"bowling alley, pixel art: Clawd as alley manager cheering while three subagent bowlers roll balls down parallel lanes, knocking pins for {wh","source":"claude-opus-5-5","scene":{"concept":"bowling alley, pixel art: Clawd as alley manager cheering while three subagent bowlers roll balls down parallel lanes, knocking pins for {what}","code":"const L=[{p:3.1,c:'#5af',o:0},{p:3.7,c:'#e5a',o:1.2},{p:2.9,c:'#7e6',o:2}];\nconst lines=['Three lanes, three helpers, all bowling {what}.','Strike! That pin was a bug in {what}.','I just keep score. Delegation is my sport.'];\nfunction frame(t,dt){\nfor(let i=0;i<3;i++){const L0=L[i],y0=i*3,ph=mod(t+L0.o,L0.p),s=clamp(ph/(L0.p*0.6),0,1),g=floor((t+L0.o)/L0.p);\nfor(let x=18;x<w-1;x++){put(x,y0,x%9==0?'·':' ','#c96',x<104?'#6b4422':'#4a2e18');put(x,y0+1,' ',null,x<104?'#7a5230':'#4a2e18');}\nfor(let x=18;x<w-1;x++)put(x,y0+2,'▔','#333');\ntext(19,y0,'{what}',mix(L0.c,'#000',0.4),'#6b4422');\nconst bx=30+s*76,py=y0*2+1;\nif(s<1)pixels(floor(bx),py,'oo\\noo',{o:L0.c});\nconst k=Math.max(0,ph-L0.p*0.6);\nfor(let j=0;j<3;j++){const px=107+j*4+k*(8+j*6),pp=py-k*(6-j*2)+k*k*6;pixels(floor(px),floor(pp),'w\\nr',{w:'#fff',r:'#e33'});}\nif(k>0&&k<0.6)text(100,y0,'STRIKE','#fd4');\ntext(w-6,y0+1,('x'+g).padStart(4),'#fd4','#4a2e18');}\nconst cheer=fract(t/3)<0.3;\nconst c=clawd(2,3,{facing:1,arms:cheer?'up':'down',pose:'stand'});\npixels(c.top.x-3,c.top.py-2,'bbbbbb\\n.bbbb.',{b:'#c22'});\nconst q=floor(t/10)%3;if(fract(t/10)<0.45)say(lines[q],c.top.x,c.row);}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#311","#213","#123"],"speed":0.5,"intensity":0}}},
119  {"phase":"reading","world":"film noir city at night","style":"pixel art","concept":"noir pond at night: fedora-wearing detective Clawd fishes a rain-slick city pond under a streetlamp, reeling up suspect fish tagged {what} f","source":"claude-opus-5-5","scene":{"concept":"noir pond at night: fedora-wearing detective Clawd fishes a rain-slick city pond under a streetlamp, reeling up suspect fish tagged {what} for questioning","code":"const fishA=['..oooo.ooo','.oooo#oo..','oooooooooo','.oooooooo.','..oooo.ooo'];\nconst hat=['..kkkk..','..kkkk..','bbbbbbbb','kkkkkkkk'];\nconst lines=['Pond full of suspects. Hooked {what}.','Talk, fish. What does {what} really do?','It calls three helpers. Classic alibi.','Throwing it back. Story checks out.'];\nfunction frame(t,dt){\nfor(let x=0;x<w;x++){const b=floor(x/7),hh=3+floor(rand(b)*8);for(let py=12-hh;py<13;py++)pixel(x,py,'#2a2e44');if(rand(b*7+x)<.25&&rand(floor(t*.3)+b)<.7)pixel(x,13-hh+2+floor(rand(x)*hh*.6),'#e8c35a');}\nfor(let py=0;py<4;py++)for(let x=0;x<8;x++)if((x-3.5)**2/4+(py-1.5)**2<4)pixel(100+x,py+1,'#e6e2c8');\nfor(let x=0;x<w;x++)for(let py=14;py<18;py++){const s=noise2(x*.15+t*.5,py*.7);pixel(x,py,s>.7?'#4a6a9a':s>.5?'#24385a':'#152238');}\nfor(let py=4;py<14;py++)pixel(8,py,'#667');pixels(6,2,['ooooo','.yyy.'],{o:'#889',y:'#ffe08a'});\nfor(let x=3;x<14;x++)if(rand(x)>.3)pixel(x,14+floor(rand(x+5)*2),'#ffe08a');\nfill(14,7,28,1,'▀','#7a5235');for(let x=16;x<42;x+=8)put(x,8,'█','#5a3a25');\nfor(let i=0;i<40;i++){const rx=floor(mod(rand(i)*w-t*6,w)),ry=mod(t*14+rand(i+3)*18,18);pixel(rx,floor(ry),'#5d6d8a');}\nconst p=t%8,cy=floor(t/8);\nconst c=clawd(22,3,{pose:'sit',facing:1,arms:{left:'down',right:p>4&&p<6?'up':'out'},blink:fract(t*.3)<.05});\npixels(c.top.x-4,c.top.py-3,hat,{k:'#3c3c4c',b:'#a33'});\nconst tx=58,ty=p>4&&p<7?1:3;line(c.right.x,c.right.row,tx,ty,'·','#a98');\nlet by=7;if(p>3&&p<4)by=7+(fract(t*6)<.5?0:0);\nif(p<4){const bob=p>3?(fract(t*5)<.5?15:16):14+round(sin(t*2)*.5);line(tx,ty,70,floor(bob/2),'│','#bbb');pixels(70,bob-1,['r','w'],{r:'#e44',w:'#eee'});}\nelse{const k=smoothstep(4,5.5,p),fy=floor(lerp(16,4,k)),fx=67;line(tx,ty,fx+4,floor(fy/2),'│','#bbb');if(p<7.3){pixels(fx,fy,fishA,{o:p>6&&p<7?'#e8a04a':'#9ab8c8','#':'#111'});if(p>5.5)text(fx-3,floor(fy/2)-1,'{what}','#ffe08a');}else for(let i=0;i<6;i++)pixel(fx+2+i*1,15-floor(rand(i+cy)*3),'#cde');}\nif(p>1&&p<7.5)say(lines[cy%4],c.top.x,c.row);\n}","actors":[],"particles":[],"background":{"effect":"rain","palette":["#223","#334466","#445577"],"speed":1,"intensity":0}}},
120  {"phase":"writing","world":"volcano island","style":"pixel art","concept":"volcano island at night: a UFO beams up the finished {what} crate from the beach while Clawd in a lei waves it off, volcano puffing behind","source":"claude-opus-5-5","scene":{"concept":"volcano island at night: a UFO beams up the finished {what} crate from the beach while Clawd in a lei waves it off, volcano puffing behind","code":"function frame(t,dt){for(let x=0;x<w;x++){put(x,h-1,'▀','#c9a46a','#8a6a3a');}for(let x=0;x<w;x++){const y=6+Math.floor(1.5+Math.sin(x*0.3+t*2));if(x<30)put(x,7,'~',x%2?'#4aa8d8':'#2f7fb0');}const vx=82;for(let r=0;r<7;r++){const hw=3+r*3;fill(vx-hw,2+r,hw*2,1,'█',r<1?'#e05030':'#6a4a3a');}for(let i=0;i<6;i++){const k=fract(t*0.4+i/6);put(vx-1+Math.sin(i*3+t)*k*4,1.5-k*2+1,'░',mix('#ff8040','#776655',k));}pixels(104,6,'..g..\n.ggg.\nggggg\n..b..\n..b..\n..b..',{g:'#3fbf5f',b:'#a07040'});const ux=lerp(-20,44,smoothstep(0,5,t))+Math.sin(t*0.7)*3;pixels(ux,0,'....cccc....\n..cccccccc..\nmmmmmmmmmmmm\n.yy.yy.yy.yy',{c:'#9fe8ff',m:'#b0b8c8',y:Math.floor(t*4)%2?'#ffe060':'#ff60c0'});const on=t>5;if(on){for(let y=2;y<h-1;y++)text(ux+2,y,'░░░░░░░░',y%2?'#7fffaa':'#4fdf8a');}const lift=on?clamp((t-6)*0.8,0,1):0,cy=lerp(5,1.5,fract(lift>=1?(t-7.25)*0.15:0)*0+lift);if(fract((t-7)*0.15)<0.9||t<7)pixels(ux+3,cy*2,'oooooo\noDDDDo\noDDDDo\noooooo',{o:'#c08040',D:'#e0b070'});if(on&&lift<1)text(ux+1,cy-1,'{what}','#ffe');const c=clawd(60,4,{facing:-1,arms:on?{left:Math.floor(t*3)%2?'up':'out',right:'down'}:'down',blink:fract(t*0.3)<0.05});pixels(c.top.x-4,c.top.py+5,'p.y.p.y.p',{p:'#ff70b0',y:'#ffe050'});if(t>2&&t<5)say('Ride for {what}? Neat.',c.top.x,c.row);if(t>8)say('{what} beamed up. Aloha, little crate!',c.top.x,c.row);}","actors":[],"particles":[{"glyphs":"·✧","count":25,"x":"rand(k)*w","y":"rand(k+5)*3","color":"#aabbff"}],"background":{"effect":"starfield","palette":["#334","#556","#99a"],"speed":0.4,"intensity":0.3}}},
121  {"phase":"writing","world":"weather report","style":"pixel art","concept":"fishing pond weather report: Clawd in a rain hat fishes from a dock as the forecast clears from rain to sun, reeling in a fish labeled {what","source":"claude-opus-5-5","scene":{"concept":"fishing pond weather report: Clawd in a rain hat fishes from a dock as the forecast clears from rain to sun, reeling in a fish labeled {what}","code":"let ph=0\nfunction frame(t,dt){\nconst cyc=fract(t/24),sun=smoothstep(0.3,0.6,cyc)\nfor(let y=0;y<2*h;y++){const c=y<10?mix('#26304a','#3a78b8',sun):mix('#1c4a6a','#2a6a8a',sun);for(let x=0;x<w;x+=1)if(y>=10||y%2==0)pixel(x,y,y>=10&&((x+Math.floor(t*4)+y*3)%17==0)?'#8fd0ee':c)}\nfor(let x=0;x<w;x++)pixel(x,10,'#6ab0d0')\nconst sx=w-18,sy=lerp(18,2,sun);pixels(sx,sy,' yyy \nyyyyy\nyyyyy\n yyy ',{y:'#ffd34a'})\nconst cl=1-sun;for(let i=0;i<3;i++){const cx=20+i*30+Math.sin(t*0.2+i)*4;pixels(cx,1,' ggg  gg \nggggggggg\n ggggggg ',{g:mix('#9aa4b4','#e8eef4',sun)});if(cl>0.2)for(let k=0;k<4;k++){const ry=4+mod(t*12+k*3+i*5,10);pixel(cx+1+k*2,ry,'#9cc8ff')}}\nfor(let x=0;x<34;x++){pixel(x,12,'#8a5a32');pixel(x,13,'#6a4022')}for(let x=3;x<34;x+=8)for(let y=14;y<18;y++)pixel(x,y,'#5a3418')\nconst c=clawd(12,2,{facing:1,pose:'sit',arms:{left:'down',right:'out'},blink:fract(t*0.3)<0.04})\npixels(c.top.x-5,c.top.py-2,'  yyyyy  \nyyyyyyyyyy',{y:'#f0c020'})\nconst rx=c.right.x,ry=c.right.py,tx=60,bite=fract(t/8)>0.6,dip=bite?Math.sin(t*20):0,ty=11+dip\nline(rx,c.right.row,rx+6,c.right.row-2,'/','#c8a070')\nfor(let i=0;i<=20;i++){const k=i/20;pixel(Math.round(lerp(rx+6,tx,k)),Math.round(lerp(ry-4,ty,k)+Math.sin(k*3.14)*-2),'#dddddd')}\npixels(tx-1,ty,'r\\nw',{r:'#ff4040',w:'#ffffff'})\nconst jt=fract(t/8)*8-6.5;if(jt>0&&jt<1.5){const fx=tx+jt*6,fy=12-Math.sin(jt*2.1)*8;pixels(fx,fy,' ooo \\nooooo>\\n ooo ',{o:'#ff9a3a','>':'#ff9a3a'});text(fx-2,Math.floor(fy/2)-1,'{what}','#ffe0a0')}\nfor(let i=0;i<5;i++){const lx=80+i*7;pixels(lx,15,'gg\\ngg',{g:'#3a8a3a'});pixel(lx+1,13-(i%2),'#5aba4a')}\nconst msg=sun<0.5?'Forecast: drizzle over {what}. Fish still biting.':'Skies cleared on {what}. Reeling it in!';if(fract(t/12)<0.6)say(msg,c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"rain","palette":["#334","#557","#88a"],"speed":1,"intensity":0}}},
122  {"phase":"agents","world":"ancient tomb","style":"pixel art","concept":"snow globe: three tiny helper crabs each build a snowman labelled with their part of {what} inside a globe, while Clawd waits outside and gi","source":"claude-opus-5-5","scene":{"concept":"snow globe: three tiny helper crabs each build a snowman labelled with their part of {what} inside a globe, while Clawd waits outside and gives it an impatient shake now and then","code":"const cx=72,R=24,cy=7;let fl=Array.from({length:70},(_,i)=>({x:cx-R+rand(i)*2*R,y:rand(i+70)*12,v:1+rand(i+5)}));\nfunction hw(py){const d=(py-cy)/7.5;return d*d>=1?0:R*Math.sqrt(1-d*d)}\nfunction frame(t,dt){const m=mod(t,16),sh=m>13&&m<14.5,o=sh?Math.round(Math.sin(t*40)*1.5):0;\nfor(let py=0;py<14;py++){const k=hw(py);pixel(Math.round(cx-k)+o,py,'#9fd3ff');pixel(Math.round(cx+k)+o,py,'#9fd3ff');if(py>=12)for(let x=Math.ceil(cx-k)+1;x<cx+k;x++)pixel(x+o,py,'#eef6ff')}\nfill(cx-20+o,7,41,2,'▓','#8a5a3a');text(cx-Math.floor(('{what}').length/2)+o,8,'{what}','#ffd27a','#5a3520');\nfor(let i=0;i<3;i++){const sx=cx-14+i*14,n=Math.min(6,1+Math.floor((t+i*4)/9));for(let j=0;j<n;j++){const ww=j<3?2:1;for(let q=-ww;q<=ww;q++)pixel(sx+q+o,11-j,'#ffffff')}if(n>=6){pixel(sx+o,5,'#ff8c42');pixel(sx-1+o,6,'#222');}\nconst hx=Math.round(sx+4+Math.sin(t*1.3+i*2)*2);pixels(hx+o,10,'a.a\\naaa',{a:'#d97757'});if(fract(t*0.7+i/3)<0.15)pixel(hx+1+o,9,'#fff')}\nfor(const f of fl){if(sh)f.v=-6*rand(f.x+t);f.v=Math.min(1.5,f.v+dt*4);f.y+=f.v*dt;f.x+=Math.sin(t+f.y)*dt*(sh?8:1);if(f.y>11.5||f.y<0.5){f.y=f.y<0.5?0.6:1;f.x=cx-R+rand(t+f.x)*2*R}if(Math.abs(f.x-cx)<hw(f.y)-1)pixel(Math.round(f.x)+o,Math.round(f.y),'#dfefff')}\nconst c=clawd(30,4.5,{facing:1,arms:sh?'up':'down',stride:-1,blink:fract(t*0.3)<0.05,eyes:sh?'wide':'open'});\npixels(c.top.x-3,c.top.py-2,'.rrr.\\nwwwww',{r:'#e04848',w:'#fff'});\nfill(0,8,40,1,'▀','#cfe3f5');\nconst L=['Three helpers, one globe, all on {what}.','Shaking it does NOT make subagents faster.','...okay maybe a little.','Snowman #3 has a nose. {what} is nearly done.'];\nconst li=Math.floor(t/8)%4;if(fract(t/8)<0.7)say(L[li],c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334466","#556688","#8899bb"],"speed":0.3,"intensity":0.25}}},
123  {"phase":"web","world":"sports broadcast","style":"pixel art","concept":"ant colony as sports broadcast: Clawd the commentator in a booth while a scout ant races out across the soil to fetch a crumb labeled {what}","source":"claude-opus-5-5","scene":{"concept":"ant colony as sports broadcast: Clawd the commentator in a booth while a scout ant races out across the soil to fetch a crumb labeled {what} and haul it back to the nest, scoreboard ticking","code":"const G=h*2;function frame(t,dt){for(let y=0;y<h;y++)for(let x=0;x<w;x++){const s=y>=6?(rand(x*7+y)<0.15?'#4a3020':'#3a2416'):(y==5?'#2f5a2a':null);if(s)put(x,y,' ',null,s)}for(let x=0;x<w;x+=3)put(x,5,rand(x)<0.5?'\"':\"'\",'#6fbf5a');pixels(2,10,'..####..\\n.#oooo#.\\n#oooooo#\\n########','{\"#\":\"#8a5a34\",\"o\":\"#1a0e08\"}');text(1,8,'NEST','#d8b080');const cy=t%30,P=cy/30,out=P<0.5,k=out?smoothstep(0,0.45,P):1-smoothstep(0.5,0.95,P),ax=12+k*(w-40);const f=Math.floor(t*8)%2;pixels(ax,10,f?'.o.o.o\\nOOoOO.':'o.o.o.\\nOOoOO.','{\"o\":\"#1a0e08\",\"O\":\"#2a1a10\"}');const cx=out?w-26:ax+(f?-3:-4);if(out||P<0.96){pixels(cx,9,'.##.\\n####','{\"#\":\"#f0d890\"}');text(cx-2,7,'{what}'.slice(0,12),'#ffe9a0')}const bx=w-22;fill(bx,0,22,5,' ',null,'#202a3a');text(bx+1,0,'LIVE ●','#ff5050');const sc=Math.floor(t/30);text(bx+9,0,'CRUMBS '+sc,'#ffd23f');const c=clawd(bx+4,1,{facing:-1,arms:{left:'up',right:'down'},blink:fract(t*0.3)<0.04,look:{x:-2,y:1}});pixels(c.top.x-3,c.top.py-1,'#.....#','{\"#\":\"#9aa\"}');const lines=['And the scout is OFF toward {what}!','Lead at the crumb! She grabs it!','Hauling it home, crowd on its feet!','Back in the nest! Fetch complete!'];say(lines[Math.floor(P*4)],c.top.x,c.row)}","actors":[],"particles":[{"glyphs":"·","count":15,"x":"rand(k)*w*0.7","y":"7+rand(k+5)*2","color":"#7a5a3a"}],"background":{"effect":"pulse","palette":["#203040","#304050","#203828"],"speed":0.4,"intensity":0}}},
124  {"phase":"writing","world":"orchestra concert","style":"pixel art","concept":"cooking show meets orchestra: Clawd as chef-conductor plating the final dish while pots play notes, the finished reply served under a cloche","source":"claude-opus-5-5","scene":{"concept":"cooking show meets orchestra: Clawd as chef-conductor plating the final dish while pots play notes, the finished reply served under a cloche","code":"const W=w,H=h;\nfunction frame(t,dt){\nfor(let x=0;x<W;x++){pixel(x,2*H-3,'#7a5a3a');pixel(x,2*H-2,'#5a3f28');pixel(x,2*H-1,'#5a3f28');}\nfor(let x=0;x<W;x+=8)pixels(x,0,'..##..\\n.#..#.',{'#':'#c9a227'});\nconst pots=[[10,'#4fa3e0'],[24,'#e06c9f'],[38,'#7fd36a']];\nfor(let i=0;i<pots.length;i++){const [px,c]=pots[i];const b=Math.sin(t*4+i*2)>0.6;pixels(px,8,'.########.\\n##########\\n.########.\\n.########.\\n..######..',{'#':c});pixels(px+2,6+(b?-1:0),'.oooo.',{'o':'#ddd'});if(b)text(px+4,1,'♪','#ffe066');}\nconst cx=56,c=clawd(cx,H-6,{pose:'stand',facing:-1,arms:{left:Math.sin(t*3)>0?'up':'out',right:'out'}});\npixels(c.top.x-3,c.top.py-5,'.#.#.#.\\n#######\\n.#####.',{'#':'#ffffff'});\nconst l=c.left;line(l.x,l.row,l.x-3,l.row-2+Math.round(Math.sin(t*3)),'/','#eee');\nconst k=smoothstep(10,14,fract(t/20)*20),dx=88;\npixels(dx,12,'##################\\n.################.',{'#':'#cfd6de'});\nif(k<1){pixels(dx+2,4-Math.round(k*6),'.....######.....\\n...##########...\\n..############..\\n.##############.\\n################',{'#':'#b8c2cc'});}\nelse{pixels(dx+4,7,'..######..\\n.##****##.\\n##########',{'#':'#d97757','*':'#ffe066'});text(dx+1,H-1,'{what}','#ffe066');for(let i=0;i<5;i++)put(dx+3+i*3,1+Math.round(Math.sin(t*5+i)),'✦','#ffd166');}\nconst ph=fract(t/20);\nsay(ph<0.45?'Pots in tune, sauce in tempo. Plating {what}.':ph<0.7?'And... the big reveal!':'Bon appetit! Fresh from the code kitchen.',c.top.x,c.row);\n}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#3a2a4a","#4a3a6a","#2a3a5a"],"speed":0.4,"intensity":0.2}}},
125  {"phase":"web","world":"mad science lab","style":"pixel art","concept":"ski slope: Clawd skis down from a mountaintop lab with a {what} flag, catching air off a mogul each run","source":"claude-opus-5-5","scene":{"concept":"ski slope: Clawd skis down from a mountaintop lab with a {what} flag, catching air off a mogul each run","code":"const tr=[40,72,104];\nfunction frame(t){\nfor(let x=0;x<w;x++){const s=Math.floor(3+x*0.125);for(let p=s;p<2*h;p++)pixel(x,p,p==s?'#ffffff':(p+x)%5?'#cfe3f0':'#a9c8de');}\nfor(const a of tr){const s=Math.floor(3+a*0.125);pixels(a-2,s-7,'..g..\\n.ggg.\\n.ggg.\\nggggg\\nggggg\\n..b..\\n..b..',{g:'#3c9a5a',b:'#8a5a3a'});}\npixels(1,0,'..aaaaa..\\n.acccccа.\\naacccccaa\\nrrrrrrrrr',{a:'#9aa',c:'#6cf',r:'#c55','а':'#9aa'});\nif(fract(t*1.5)<0.5)pixel(5,0,'#ff0');\nconst P=9,p=fract(t/P),cx=p*135-12,sp=3+(cx+7)*0.125;\nconst mx=60,air=Math.max(0,1-Math.abs(cx-mx)/12)*4*(cx>mx-12&&cx<mx+12?1:0);\nconst ms=Math.floor(3+mx*0.125);pixels(mx-3,ms-2,'..www..\\nwwwwwww',{w:'#ffffff'});\nconst c=clawd(cx,(Math.round(sp-8-air*1.5))/2,{facing:1,pose:air>0.5?'jump':'stand',arms:air>0.5?'up':'out',color:'#d97757'});\nfor(let i=-2;i<16;i++)pixel(cx+i,c.feet.py+1+Math.floor(i*0.125),'#e33');\npixels(c.top.x-3,c.top.py-2,'.bbbbb.\\nyyyyyyy',{b:'#36c',y:'#fd3'});\nline(c.left.x,c.left.row,c.left.x,c.left.row-3,'│','#ccc');text(c.left.x+1,c.left.row-3,'{what}','#ff6');\nfor(let i=0;i<10;i++){const sx=cx-3-i*2;put(sx,Math.floor((3+sx*0.125)/2)-0.5+rand(i+Math.floor(t*8))*1,'·','#eef');}\nif(p>0.35&&p<0.6)say('Fetching {what} at 80 mph downhill!',c.top.x,c.row);\nif(p>0.05&&p<0.2)say('Lab says the page is at the bottom. Wheee!',c.top.x+8,c.row+2);\n}","actors":[],"particles":[{"glyphs":"*·.","count":40,"x":"mod(rand(k)*w - t*3,w)","y":"mod(t*(1+rand(k+9)*2)+rand(k+3)*h,h)","color":"#e8f4ff"}],"background":{"effect":"aurora","palette":["#2a4a8a","#3a6aa0","#5a4a9a"],"speed":0.4,"intensity":0.3}}},
126  {"phase":"thinking","world":"zen rock garden","style":"pixel art","concept":"lighthouse in a storm: Clawd as keeper atop a pixel-art lighthouse, beam sweeping the dark sea for {what} on a tossing boat","source":"claude-opus-5-5","scene":{"concept":"lighthouse in a storm: Clawd as keeper atop a pixel-art lighthouse, beam sweeping the dark sea for {what} on a tossing boat","code":"const drops=Array.from({length:50},(_,i)=>({x:rand(i)*w,y:rand(i+9)*h*2}));\nfunction frame(t,dt){\nfor(const d of drops){d.y+=dt*30;d.x-=dt*8;if(d.y>h*2){d.y=0;d.x=rand(t+d.x)*w+20}pixel(d.x,d.y,'#6a8cc0')}\nconst fl=fract(t*0.13)<0.02;if(fl)fill(0,0,w,3,' ',null,'#2a3048');\nif(fl||fract(t*0.13+0.02)<0.01)line(70,0,74,4,'/','#fff6a0');\nfor(let x=0;x<w;x++){const y=13+Math.sin(x*0.25+t*2)*1.5+Math.sin(x*0.07-t)*1;for(let p=Math.floor(y);p<18;p++)pixel(x,p,p<y+1?'#cfe8ff':'#2d5a8a')}\nconst lx=12;pixels(lx,4,'..rrrr..\\n.wwwwww.\\n.rrrrrr.\\n..wwww..\\n..rrrr..\\n..wwww..\\n..rrrr..\\n.wwwwww.\\n.rrrrrr.\\nggggggggg\\nggggggggg\\nggggggggg\\nggggggggg\\nggggggggg',{r:'#e05050',w:'#eeeeee',g:'#7a7a80'});\nconst a=Math.sin(t*0.6),bx=Math.max(0,60+a*55),ex=Math.max(0,1+lx+4);\nfor(let i=0;i<40;i++){const k=i/40,x=lerp(lx+18,bx,k),y=lerp(2,10+a,k);put(x,y/2,'░','#fff3a0')}\nconst c=clawd(lx-3,-1.5,{facing:a>0?1:-1,arms:{left:'down',right:'out'},eyes:fl?'wide':'open'});\npixels(c.top.x-3,c.top.py-2,'.yyyyy.\\nyyyyyyy',{y:'#f0d040'});\nconst bx2=70+Math.sin(t*0.2)*20,by=12+Math.sin(bx2*0.25+t*2)*1.5;\npixels(bx2,by-5,'...m....\\n...m....\\nbbbbbbbb\\n.bbbbbb.',{m:'#ddd',b:'#c08040'});\nconst hit=Math.abs(bx-bx2-4)<10;if(hit)text(bx2-2,Math.floor(by/2)-3,'{what}','#fff6a0');\nconst s=fract(t/16);say(s<0.3?'Somewhere out there is {what}. Sweeping...':s<0.6?(hit?'Got you in the beam, {what}!':'Lost it in the swell.'):s<0.8?'Thunder means think harder.':'Keep the light turning.',c.top.x,c.row);\n}","actors":[],"particles":[],"background":{"effect":"rain","palette":["#1a2238","#24304e","#3a4870"],"speed":1.2,"intensity":0.2}}},
127  {"phase":"reading","world":"cyberpunk street","style":"pixel art","concept":"beehive: Clawd in a beekeeper veil reading the waggle dances of bees on a honeycomb wall to learn where {what} came from","source":"claude-opus-5-5","scene":{"concept":"beehive: Clawd in a beekeeper veil reading the waggle dances of bees on a honeycomb wall to learn where {what} came from","code":"const bees=Array.from({length:7},(_,i)=>({p:rand(i)*6,c:20+i*14}));\nfunction frame(t,dt){for(let y=0;y<h;y++)for(let x=0;x<w;x++){const o=(Math.floor(x/4)+y)%2;put(x,y,o?'⬡'.length?'▒':'▒':'░',o?'#7a5a10':'#5a4208');}\nfor(let i=0;i<6;i++){const cx=60+i*9,cy=1+(i%2)*2;pixels(cx,cy*2,'.####.\\n######\\n######\\n.####.',{'#':i<=Math.floor(t/4)%7?'#ffc83a':'#8a6a20'});}\ntext(62,8,'{what}','#fff2b0');\nfor(const b of bees){const a=t*3+b.p,bx=b.c+Math.sin(a)*4+(Math.floor(t)%2?Math.sin(a*2)*2:0),by=4+Math.sin(a*2)*3;pixels(Math.floor(bx),Math.floor(by),'.w.\\nyky\\n.y.',{w:'#e8f4ff',y:'#ffd23a',k:'#222'});}\nconst cx=6+Math.sin(t*0.3)*3;const c=clawd(cx,4.5,{facing:1,arms:{left:'down',right:fract(t*0.25)<0.5?'out':'up'}});\npixels(c.top.x-5,c.top.py-3,'..wwwwwwwww..\\nwwwwwwwwwwwww',{w:'#f4f4e8'});\nfill(c.x,c.eyes.row,14,1,'░','#cfd8c0');\npixels(c.right.x,c.right.py-2,'.s.\\nsss\\n.s.',{s:'#aaa'});\nconst L=['Each waggle points to a line in {what}.','The bees know where this function lives.','Figure-eight means: follow the import.','Honey cells fill as I understand it.'];\nif(t>2)say(L[Math.floor((t-2)/7)%4],c.top.x,c.row);}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#3a2a05","#5a4208","#7a5a10"],"speed":0.4,"intensity":0}}},
128  {"phase":"editing","world":"outer space","style":"pixel art","concept":"airport runway: Clawd as ground crew patching a jet labeled {what} on the tarmac, then waving it off to takeoff, next jet taxis in","source":"claude-opus-5-5","scene":{"concept":"airport runway: Clawd as ground crew patching a jet labeled {what} on the tarmac, then waving it off to takeoff, next jet taxis in","code":"const L='{what}'.padEnd(20).slice(0,20);const P=[' __                              ','|  \\____________________________ ','|==  '+L+' ◘◘◘◘ \\__ ',' \\______________________________=_>','         ‾o‾            ‾o‾      '];let n=0;function frame(t,dt){const c=t%12;n=Math.floor(t/12);for(let x=0;x<w;x++){put(x,8,'═','#667');if(x%8<4)put(x,7,x%16<4?'─':' ','#cc4')}sprite(2,1,'┌─┐\\n│▓│\\n│ │\\n│ │\\n│ │\\n│ │','#8ab');put(3,0,fract(t)<.5?'●':'○','#f44');let px,py=2;if(c<3)px=lerp(-40,30,Math.sin(c/3*Math.PI/2));else if(c<8.5)px=30;else{const k=c-8.5;px=30+k*k*9;py=2-Math.max(0,k-1.2)*1.6}sprite(px,py,P.join('\\n'),'#dde');text(px+5,py+2,L,'#fc6');let cx=72,arms='up',st=-1;if(c>3&&c<8){cx=lerp(72,px+18,smoothstep(3,4,c));arms=fract(c*3)<.5?'up':'out';st=Math.floor(cx/1.5)%4}else if(c>=8&&c<8.5)cx=72;const g=clawd(cx,4,{facing:c>3&&c<4?-1:0,stride:st,arms});pixels(g.top.x-3,g.top.py-2,'.oooooo.\\noooooooo','{\"o\":\"#fa0\"}'.length?{o:'#fa0'}:{});if(c>4&&c<8){for(let i=0;i<5;i++)put(cx+6+rand(i+Math.floor(t*10))*8,3-rand(i*3+Math.floor(t*10))*2,'*✦'[i%2],'#ff8')}if(c>=8.5){put(g.left.x-1,g.left.row,'/','#f84');put(g.right.x+1,g.right.row,'\\\\','#f84')}if(c>4.5&&c<7.5)say(n%2?'Patching {what} mid-taxi. Do not tell the FAA.':'Tightening {what}. Seatbelts, everyone.',g.top.x,g.row);if(c>9&&c<11.5)say('Cleared for takeoff! Next edit taxiing in.',g.top.x,g.row)}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#99a"],"speed":0.3,"intensity":0.2}}},
129  {"phase":"editing","world":"submarine","style":"text art","concept":"underground mine: Clawd in a hard hat chiseling a glowing ore vein labeled {what}, filling a mine cart that rolls off down the rails","source":"claude-opus-5-5","scene":{"concept":"underground mine: Clawd in a hard hat chiseling a glowing ore vein labeled {what}, filling a mine cart that rolls off down the rails","code":"let sp=[];\nfunction frame(t,dt){\nfor(let x=0;x<w;x++){put(x,0,x%9==0?'┬':'▀','#6b5a4a');put(x,h-1,x%4==0?'╪':'═','#8a7a6a');}\nfor(let x=0;x<w;x+=23){for(let y=1;y<h-1;y++)put(x,y,'║','#9a6a3a');text(x-1,1,'═══','#9a6a3a');}\nfor(let i=0;i<60;i++){let x=(i*37)%w,y=1+(i*13)%7;put(x,y,'░','#4a3e34');}\nconst vx=70;sprite(vx,2,'▓▒◆▒▓▒\\n▒◆▓◆▒◆\\n▓▒◆▓◆▒\\n▒▓▒◆▒▓','#5fd0e0');\ntext(vx,1,'{what}','#ffe08a');\nconst p=fract(t/8),hit=fract(t*1.5)<0.2;\nconst c=clawd(52,h-5,{facing:1,arms:hit?{right:'up'}:{right:'out'}});\nsprite(c.top.x-3,c.row-1,'▄███▄','#ffcc33');put(c.top.x+1,c.row-1,'●','#fff8b0');\nsprite(c.right.x,c.right.row-(hit?1:0),hit?'⌐':'─╼','#c0c0c0');\nif(hit&&Math.random()<0.6)sp.push({x:vx,y:4,vx:-3-Math.random()*6,vy:-3,l:1});\nfor(const s of sp){s.x+=s.vx*dt;s.y+=s.vy*dt;s.vy+=12*dt;s.l-=dt;if(s.l>0)put(s.x,s.y,'✦','#9ff0ff');}\nsp=sp.filter(s=>s.l>0);\nconst cx=p<0.6?30:30-smoothstep(0.6,1,p)*60;\nconst fillN=Math.floor(clamp(p/0.6,0,1)*5);\nsprite(cx,h-4,' '+'◆'.repeat(fillN).padEnd(5,' ')+' \\n╲█████╱\\n ◎   ◎ ','#a08060');\nif(fillN)text(cx+1,h-4,'◆'.repeat(fillN),'#5fd0e0');\nif(p>0.2&&p<0.55)say('Chipping out the bad bits of {what}. Cart\\'s filling up!',c.top.x,c.row-1);\nif(p>0.62&&p<0.9)say('Load one rolling. More fix in this vein.',c.top.x,c.row-1);\n}","actors":[],"particles":[{"glyphs":"·.","count":20,"x":"rand(k)*w","y":"1+mod(t*0.6+rand(k+5)*7,7)","color":"#7a6a5a"}],"background":{"effect":"pulse","palette":["#1a1410","#2a2018","#3a2a1a"],"speed":0.4,"intensity":0.2}}},
130  {"phase":"building","world":"mountain climb","style":"text art","concept":"skate park: Clawd carving endless halfpipe runs while a graffiti build-progress mural fills in on the wall behind","source":"claude-opus-5-5","scene":{"concept":"skate park: Clawd carving endless halfpipe runs while a graffiti build-progress mural fills in on the wall behind","code":"let tags=0\nfunction frame(t,dt){\nconst L=8,R=w-9,P=22;\nlet pr=Math.min(1,t/90);\ntext(Math.floor(w/2)-14,0,'╔══ BUILDING '+'{what}'.padEnd(20)+'═╗','#7aa2f7');\nconst bw=30,bx=Math.floor(w/2)-15;\nfor(let i=0;i<bw;i++)put(bx+i,1,i<pr*bw?'▓':'░',i<pr*bw?hsl(i*12+t*40,0.8,0.6):'#445');\nfor(let x=0;x<w;x++){let r=0;if(x<L+P)r=Math.pow(Math.max(0,(L+P-x)/P),2)*5;else if(x>R-P)r=Math.pow(Math.max(0,(x-R+P)/P),2)*5;\nconst y=Math.round(8-r);if(x>=L-2&&x<=R+2){put(x,y,r>0.5?(x<w/2?'╲':'╱'):'═','#c0c0c0');for(let yy=y+1;yy<h;yy++)put(x,yy,'░','#556')}}\nconst s=(1-Math.cos(t*1.1))/2,cx=L+2+s*(R-L-18),mid=cx+7;\nlet r=0;if(mid<L+P)r=Math.pow((L+P-mid)/P,2)*5;else if(mid>R-P)r=Math.pow((mid-R+P)/P,2)*5;\nconst air=(s<0.04||s>0.96)?1.5:0,top=Math.max(-1,7-4-r-air);\nconst dir=Math.sin(t*1.1)>0?1:-1;\nconst c=clawd(cx,top,{facing:dir,arms:r>2?'up':'out',eyes:air>0?'wide':'open'});\ntext(cx+3,top+4,'▀o══o▀','#e0af68');\nsprite(c.top.x-3,c.top.row-1,'▄▄▄▄▄▄','#f7768e');\nif(air>0)text(cx+16*(dir<0?0:1)-2,top,'✦','#ffd700');\nconst lines=['{what} is compiling. Time for a few laps.','Kickflip per module. Still going.','Bundler says almost. Bundler lies.','Halfpipe count: lots. Build: not yet.'];\nif(fract(t/12)<0.5)say(lines[Math.floor(t/12)%4],c.top.x,c.row);\n}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#223","#334","#2a2a44"],"speed":0.5,"intensity":0}}},
131  {"phase":"agents","world":"outer space","style":"text art","concept":"ancient tomb: Clawd as head archaeologist sends little torch-bearing helper Clawds down three tomb shafts, waiting by the sarcophagus labele","source":"claude-opus-5-5","scene":{"concept":"ancient tomb: Clawd as head archaeologist sends little torch-bearing helper Clawds down three tomb shafts, waiting by the sarcophagus labeled {what} as relics come back up","code":"const sh=[22,52,82];let th=[0,0,0];function frame(t,dt){for(let x=0;x<w;x++){put(x,0,x%6==0?'╦':'═','#a08850');put(x,h-1,x%5==0?'┴':'▀','#7a6440')}for(let i=0;i<w;i+=11)text(i+2,1,'𓀀'.length>1?'◇ ▲ ○':'◇','#6a5a3a');for(const s of sh){fill(s,2,8,6,' ',null,'#1a1208');text(s,2,'╔══════╗','#b09060');for(let y=3;y<8;y++){put(s,y,'║','#b09060');put(s+7,y,'║','#b09060')}}for(let i=0;i<3;i++){const p=fract(t*0.12+i*0.33),d=p<0.5?p*2:2-p*2,yy=3+d*3.5,s=sh[i];text(s+2,Math.floor(yy),'(o)','#ffcf6a');put(s+3,Math.floor(yy)-1,'*',fract(t*5+i)<0.5?'#ff9a30':'#ffe080');if(p>0.5)put(s+5,Math.floor(yy),'◆',hsl(i*120+40,0.8,0.6));if(p>0.97&&th[i]<t-2){th[i]=t}if(t-th[i]<1.2&&th[i]>0)text(s+1,1,'✦ relic!','#ffe060')}sprite(100,4,'┌──────────┐\\n│ ☥  ☥  ☥  │\\n└──────────┘','#d0b070');text(101,3,'{what}'.slice(0,12),'#ffd890');const c=clawd(2,h-5,{facing:1,arms:fract(t*0.3)<0.15?'up':'down'});text(c.top.x-3,c.top.row-1,'▄███▄','#c8a060');const m=Math.floor(t/12)%3;const L=['Three helpers, three shafts, one {what}.','Nobody touch the cursed tile. Again.','Relics in. Curses out. Mostly.'];if(fract(t/12)<0.6)say(L[m],c.top.x,c.row)}","actors":[],"particles":[{"glyphs":"·.","count":25,"x":"rand(k)*w","y":"2+mod(t*(0.5+rand(k+3))+rand(k+7)*6,6)","color":"#8a7650"}],"background":{"effect":"fire","palette":["#2a1a08","#4a2c10","#6a4018"],"speed":0.4,"intensity":0.15}}},
132  {"phase":"searching","world":"haunted house","style":"text art","concept":"haunted train station: Clawd as night porter with a lantern searching a ghostly platform; ghost trains pass, one finally stops carrying {wha","source":"claude-opus-5-5","scene":{"concept":"haunted train station: Clawd as night porter with a lantern searching a ghostly platform; ghost trains pass, one finally stops carrying {what}","code":"let gs=[0,1,2,3].map(i=>({x:rand(i)*120,y:1+rand(i+5)*3,p:rand(i+9)*6}))\nfunction frame(t,dt){\nfor(let x=0;x<w;x++){put(x,h-1,x%6==0?'┴':'═','#776655');put(x,h-3,'─','#554466')}\nfor(let i=0;i<w;i+=30){sprite(i+2,0,'┌────┐\\n│ ◷  │\\n└─┬──┘','#8877aa')}\nconst cyc=mod(t,20),stop=cyc>8&&cyc<15\nlet tx=stop?30:(cyc<=8?lerp(w+5,30,smoothstep(3,8,cyc)):lerp(30,-80,smoothstep(15,19,cyc)))\nconst car='╔══════════════╗\\n║ ○  ○  ○  ○  ○ ║\\n╚═◎════════◎═╝'\nfor(let k=0;k<3;k++)sprite(tx+k*18,h-6,car,mix('#88ccbb','#335555',0.5+0.5*Math.sin(t*3+k)))\nif(stop)text(tx+20,h-7,'[ {what} ]','#ffdd66')\nfor(const g of gs){g.x-=dt*3;if(g.x<-5)g.x=w+5;text(g.x,g.y+Math.sin(t+g.p)*0.6,'(o_o)','#ccccff')}\nconst cx=stop?lerp(4,14,smoothstep(9,11,cyc)):4+Math.sin(t*0.5)*2\nconst c=clawd(cx,h-7,{facing:1,arms:{left:'down',right:'out'},stride:Math.floor(cx/1.5)%4})\ntext(c.right.x,c.right.row,'☼','#ffcc33')\nif(cyc>9&&cyc<14)say('Last train carries {what}. Found it!',c.top.x,c.row)\nelse if(cyc<6)say('Searching the platforms for {what}...',c.top.x,c.row)\n}","actors":[],"particles":[{"glyphs":"·˙","count":20,"x":"rand(k)*w","y":"mod(t*0.5+rand(k+3)*h,h)","color":"#665588"}],"background":{"effect":"aurora","palette":["#221133","#332255","#224444"],"speed":0.4,"intensity":0.2}}},
133  {"phase":"searching","world":"farm","style":"text art","concept":"weather report on a farm: Clawd as TV weatherman pointing at a radar map sweeping fields for {what}, a storm front of matches rolling in","source":"claude-opus-5-5","scene":{"concept":"weather report on a farm: Clawd as TV weatherman pointing at a radar map sweeping fields for {what}, a storm front of matches rolling in","code":"let tk=0\nfunction frame(t,dt){\nconst cx=78,cy=4\nfor(let y=0;y<h;y++)for(let x=0;x<64;x++){const f=(x>>3)+(y>>1);put(x,y,(f%3==0)?'░':(f%3==1?'▒':' '),f%2?'#4a8a3a':'#7a6a2a')}\ntext(2,0,'FARM COUNTY DOPPLER','#ffffff')\nconst a=t*1.2\nfor(let r=0;r<16;r++){const x=32+Math.cos(a)*r*2,y=cy+Math.sin(a)*r*0.5;put(x,y,'•','#6cf')}\nfor(let i=0;i<6;i++){const sx=10+i*9+Math.sin(t*0.3+i)*3,sy=2+(i%3)*2;const hit=Math.abs(mod(a,6.283)-mod(Math.atan2((sy-cy)*2,(sx-32)/2),6.283))<0.4;text(sx,sy,hit?'▓▓▓':'░░░',hit?'#ff5':'#5af')}\nconst fx=mod(t*3,70)-6\ntext(fx,6,'≈≈☁☁≈≈','#ccd')\ntext(2,8,'grep front: {what}','#ffd060')\nfor(let y=0;y<h;y++)put(64,y,'║','#888')\nconst ph=Math.floor(t/4)%3\nconst c=clawd(cx,4,{facing:-1,arms:{left:'up',right:'down'}})\nsprite(cx-6,c.left.row,'━━━━─','#ccc')\nsprite(cx+2,c.top.row-1,'▄▄▄▄▄▄▄▄▄','#335')\nsprite(cx+5,c.eyes.row+2,'▼','#e33')\nconst lines=['Scattered matches over the barn!','Heavy grep showers near {what}.','100% chance of finding it.']\nsay(lines[ph],cx+7,c.row)\nfor(let x=96;x<w;x++){put(x,7,'▄','#6a4')}\ntext(98,4,'/▔▔\\\\','#c55');text(98,5,'|▢▢|','#c55');text(98,6,'|_▯|','#c55')\nconst cow=104+Math.sin(t*0.4)*6;text(cow,5,'(__)','#fff');text(cow,6,'/||\\\\','#eee')\n}","actors":[],"particles":[],"background":{"effect":"rain","palette":["#224","#336","#448"],"speed":1,"intensity":0}}},
134  {"phase":"testing","world":"fishing pond","style":"text art","concept":"arctic ice fishing: Clawd in a parka sits by an ice hole, each bite on the line is a test result pulled up as a fish, tally grows on a snowb","source":"claude-opus-5-5","scene":{"concept":"arctic ice fishing: Clawd in a parka sits by an ice hole, each bite on the line is a test result pulled up as a fish, tally grows on a snowbank sign","code":"let fish=[],tally=0,next=3,state=0,st=0;\nfunction frame(t,dt){\nfor(let x=0;x<w;x++){put(x,6,'▀','#dfefff');put(x,7,'░','#9fc4e0');put(x,8,'▒','#6f9cc0')}\nsprite(4,1,'   /\\\\   \\n  /  \\\\  \\n /____\\\\ \\n │ ▄▄ │','#cfe6ff');\nsprite(w-30,2,'┌──────────────────┐\\n│{what}'.padEnd(20)+'\\n└──────────────────┘','#ffd27a');\ntext(w-27,3,'catch: '+tally+' ✓','#9fe0a0');\nconst hx=52;text(hx-2,6,'(    )','#3a7bd5');put(hx+1,6,'~','#7ab8ff');\nconst c=clawd(30,2,{pose:'sit',facing:1,arms:{left:'down',right:'up'},eyes:state==1?'wide':'open'});\nsprite(c.top.x-3,c.top.row-1,'▄▄▄▄▄▄','#e05050');\nconst tip=c.right;let ty=6;\nif(t>next&&state==0){state=1;st=t}\nif(state==1){ty=6+Math.sin((t-st)*20)*0.6;if(t-st>1){state=2;st=t}}\nif(state==2){const k=smoothstep(0,0.8,t-st);ty=lerp(6,0,k);if(k>=1){fish.push({x:hx,y:0,vx:4+Math.random()*3,vy:-3,ok:Math.random()<0.85});tally++;state=0;next=t+3+Math.random()*3}}\nline(tip.x,tip.row,hx+1,ty,'·','#ccc');\nif(state==2)text(hx,ty,'><>','#ff9a6a');\nfor(const f of fish){f.vy+=12*dt;f.x+=f.vx*dt;f.y+=f.vy*dt;if(f.y>5){f.y=5;f.vx=0;f.vy=0}text(f.x,f.y,f.ok?'><>':'>✗>',f.ok?'#7fd0ff':'#ff6060')}\nif(fish.length>12)fish.shift();\nif(t>2&&t<7)say('Line in {what}. Bite = pass. Hope.',c.top.x,c.row);\nelse if(t>14&&t<19)say('Frozen paws, warm tests. Keep biting!',c.top.x,c.row);\n}","actors":[],"particles":[{"glyphs":"*·.","count":40,"x":"mod(rand(k)*w+t*2,w)","y":"mod(t*(1+rand(k+9)*1.5)+rand(k+3)*6,6)","color":"#ffffff"}],"background":{"effect":"aurora","palette":["#2fbf8f","#3a7bd5","#8f5fd0"],"speed":0.5,"intensity":0.35}}},
135  {"phase":"testing","world":"underground mine","style":"text art","concept":"film noir city at night: Clawd as a fedora detective under a rain-soaked streetlamp, running a police lineup where each test steps up and is","source":"claude-opus-5-5","scene":{"concept":"film noir city at night: Clawd as a fedora detective under a rain-soaked streetlamp, running a police lineup where each test steps up and is cleared or marked guilty","code":"let R=Array.from({length:70},(_,i)=>({x:rand(i)*w,y:rand(i+7)*h,v:9+rand(i+3)*6}))\nfunction frame(t,dt){\nfor(let x=0;x<72;x++){const b=Math.floor(x/8),bh=2+Math.floor(rand(b)*5);for(let y=h-1-bh;y<h-1;y++){const win=x%8>1&&x%8<6&&y%2==0&&rand(b*31+y*7+x)>0.55;put(x,y,win?'▪':'█',win?'#e8c860':'#34405e')}}\nfor(let x=0;x<w;x++)put(x,h-1,'▁','#667')\nconst lx=40;put(lx,1,'╤','#aab');for(let y=2;y<h-1;y++)put(lx,y,'│','#889');for(let y=2;y<h-1;y++){const s=y-1;for(let d=-s;d<=s;d++)if(d)put(lx+d,y,'░','#6a6040')}\nconst L=78,Rx=116;for(let y=0;y<h-1;y++){put(L,y,'║','#8899aa');put(Rx,y,'║','#8899aa')}for(let y=1;y<h-1;y+=2)for(let x=L+1;x<Rx;x++)put(x,y,'─','#3f4a5a');text(L+2,0,'LINEUP: {what}','#c9d4e0')\nconst i=Math.floor(t/7),p=t%7,bad=rand(i*3+1)<0.22,sx=p<1.5?lerp(Rx+2,95,smoothstep(0,1.5,p)):p<5.5?95:lerp(95,L-8,smoothstep(5.5,7,p));\nif(p>1.4&&p<1.7)fill(L+1,1,Rx-L-1,h-2,' ',null,'#d8d8c0')\nsprite(sx,h-5,' ▄█▄ \\n ███ \\n█████\\n █ █ ',p>3&&bad?'#e05555':'#b8bcc8');text(sx,h-6,'#'+(i+1),'#9ab')\nif(p>3&&p<5.8)text(sx-2,1,bad?'✗ GUILTY':'✓ CLEAR',bad?'#ff6060':'#70e090')\nconst c=clawd(lx-7,h-5,{facing:1,look:{x:2,y:0},arms:p>3&&p<4?{left:'down',right:'out'}:'down',color:'#d97757'});\nfill(c.top.x-5,c.top.row-1,11,1,'▄','#555a66');text(c.top.x-3,c.top.row-2,'▄▄▄▄▄▄▄','#555a66')\nput(c.right.x+1,c.right.row,'~','#ccc')\nconst ln=t<7?'Stakeout on {what}. Every test takes the lineup.':(p>3?(bad?'That one has no alibi. Booking it.':'Alibi checks out. Next suspect.'):'Step into the light, pal.');say(ln,c.top.x,c.row-1)\nfor(const d of R){d.y+=d.v*dt;if(d.y>h){d.y=0;d.x=rand(t*13+d.x)*w}put(d.x,d.y,'╱','#5a7aa0')}}","actors":[],"particles":[],"background":{"effect":"rain","palette":["#223355","#334466","#445577"],"speed":1,"intensity":0}}},
136  {"phase":"searching","world":"bowling alley","style":"text art","concept":"ant colony up close: Clawd as a tiny ant-scout tunneling through an underground cross-section, ants carrying crumbs labeled {what} through b","source":"claude-opus-5-5","scene":{"concept":"ant colony up close: Clawd as a tiny ant-scout tunneling through an underground cross-section, ants carrying crumbs labeled {what} through branching tunnels while Clawd follows scent trails","code":"let ants=Array.from({length:9},(_,i)=>({p:rand(i)*200,s:3+rand(i+7)*4,r:1+(i%3)*3}))\nfunction frame(t,dt){\nfor(let x=0;x<w;x++){for(let y=0;y<h;y++){const n=noise2(x*0.15,y*0.4);put(x,y,n>0.6?'░':' ','#5a4030','#1a110b')}}\nfor(let x=0;x<w;x++){put(x,0,x%5==0?'ψ':'▀','#5c9a3c');put(x,2,' ',null,'#2b1d12');put(x,5,' ',null,'#2b1d12');put(x,8,' ',null,'#2b1d12')}\nfor(const c of [20,55,90]){for(let y=1;y<9;y++)put(c,y,'║','#7a5a3a','#2b1d12')}\ntext(96,5,'◆ {what}','#ffd27f','#2b1d12')\nconst k=smoothstep(0,40,t%50);const cx=lerp(-14,80,k);const row=t%50<20?1.5:4.5\nfor(const a of ants){a.p+=a.s*dt;const x=mod(a.p,w+10)-5;const y=a.r>=7?8:a.r>=4?5:2;put(x,y,'ж','#c94a3a','#2b1d12');put(x+1,y,'•','#c94a3a','#2b1d12');if(a.r==1)put(x+2,y,'▪','#ffd27f','#2b1d12')}\nfor(let i=0;i<12;i++){const px=cx-2-i*2;if(px>0)put(px,row+2.5,'·','#b0e070')}\nconst c=clawd(cx,row-1,{pose:'walk',facing:1,stride:Math.floor(cx/1.5)%4,scale:1})\nput(c.top.x-2,c.top.row-1,'\\\\','#d97757');put(c.top.x+2,c.top.row-1,'/','#d97757')\nconst m=t%50;if(m>5&&m<12)say('Sniffing every tunnel for {what}...',c.top.x,c.row);else if(m>28&&m<36)say('Scent is strong. The crumb trail never lies.',c.top.x,c.row);else if(m>42)say('Found {what}! The colony delivers.',c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"plasma","palette":["#2b1d12","#3a2818","#4a3420"],"speed":0.3,"intensity":0}}},
137  {"phase":"searching","world":"submarine","style":"pixel art","concept":"ancient tomb: Clawd as explorer with a torch slides along a hieroglyph wall, torchlight revealing glyphs until one matches {what} and a hidd","source":"claude-opus-5-5","scene":{"concept":"ancient tomb: Clawd as explorer with a torch slides along a hieroglyph wall, torchlight revealing glyphs until one matches {what} and a hidden door grinds open","code":"let gl='☥◇○△♣▲◆●□♠♦★✦◐◑▼';let tgt=w-30;function frame(t,dt){const T=t%24;for(let y=0;y<h;y++)for(let x=0;x<w;x++){put(x,y,' ',null,y==h-1?'#3a2a18':'#1c140c')}for(let x=0;x<w;x+=6)text(x,0,'▀▀▀▀▀▀','#5a4428');const cx=lerp(2,tgt-16,smoothstep(0,14,T));const tx=cx+15;for(let x=0;x<w;x+=4){for(let y=1;y<4;y++){const d=Math.abs(x-tx);const k=clamp(1-d/18,0,1);if(k>0.05)put(x+1,y,gl[Math.floor(rand(x*7+y)*16)],mix('#2a1e10','#e8c070',k))}}const found=T>14;const dx=tgt+2;const open=smoothstep(15,18,T);fill(dx,1,10,7,' ',null,'#0a0604');const sh=Math.floor(open*7);fill(dx,1+sh,10,7-sh,'▓',found?'#c8a050':'#5a4428');if(!found||open<0.5)text(dx-2,0,'{what}'.slice(0,20),found?'#ffd75f':'#6a5030');if(found&&open>0.6){for(let i=0;i<6;i++)put(dx+1+Math.floor(rand(i+Math.floor(t*4))*8),2+Math.floor(rand(i+9)*4),'✦','#ffd75f')}const walk=T<14;const c=clawd(cx,h-5,{pose:walk?'walk':'stand',stride:Math.floor(cx/1.5)%4,facing:1,arms:{left:'down',right:'up'},eyes:found?'wide':'open'});pixels(c.top.x-4,c.top.py-2,'..####..\\n########','{\"#\":\"#b08850\"}'.length?{'#':'#b08850'}:{});const f=Math.floor(t*8)%2;pixels(c.right.x,c.right.py-4,f?'.o.\\nooo\\n.#.\\n.#.':'o..\\n.oo\\n.#.\\n.#.',{o:'#ff9a30','#':'#7a5030'});for(let i=0;i<4;i++)put(c.right.x+1+Math.sin(t*3+i)*1,c.right.row-2-((t*2+i*0.5)%1.5),'·','#ffcc66');if(T>2&&T<7)say('Reading walls for {what}... glyph by glyph.',c.top.x,c.row);if(T>15&&T<22)say('Found it! {what} was behind this door.',c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"plasma","palette":["#1a1008","#2a1a0c","#3a2410"],"speed":0.3,"intensity":0}}},
138  {"phase":"searching","world":"ski slope","style":"pixel art","concept":"carnival midway: Clawd at a shooting-gallery booth, popping tin ducks on a conveyor until one labeled {what} gets hit","source":"claude-opus-5-5","scene":{"concept":"carnival midway: Clawd at a shooting-gallery booth, popping tin ducks on a conveyor until one labeled {what} gets hit","code":"let sc=0,hit=-1;\nfunction frame(t,dt){\nfor(let x=0;x<w;x++){put(x,0,x%4<2?'▀':' ',x%8<4?'#e05555':'#f0f0f0');}\nfill(20,1,80,1,'─','#c9a227');fill(20,5,80,1,'═','#c9a227');\nfor(let i=0;i<8;i++){const dx=20+mod(i*11+t*6,80);const id=Math.floor((i*11+t*6)/80)*8+i;const tg=id%5==3;const dn=mod(t,4)>3.2&&Math.abs(dx-60)<4;\nif(dn){put(dx,4,'▁','#888');if(tg)hit=id;}else{pixels(dx,6,'.yy.\\nyyyyy\\n.yyy.',{y:tg?'#ffd84a':'#7fd6ff'});put(dx+1,3,'·','#000');}\nif(tg)text(dx-2,2,'{what}'.slice(0,8),'#ffd84a');}\nfor(let x=0;x<20;x++)put(x,8,'▓','#7a3b2a');for(let x=100;x<w;x++)put(x,8,'▓','#7a3b2a');\ntext(102,1,'SCORE '+Math.floor(t/4),'#9fef7f');\nconst c=clawd(52,5,{facing:0,arms:{left:'down',right:'up'},look:{x:2,y:-1}});\nsprite(c.right.x,c.right.row-1,'═╗','#aaa');\nif(mod(t,4)>3&&mod(t,4)<3.3)put(60,3,'✦','#fff');\nif(t>4&&mod(t,12)<4)say('grep shooting gallery: aiming for {what}',c.top.x,c.row);}","actors":[],"particles":[{"glyphs":"✦✧·","count":15,"x":"rand(k)*w","y":"mod(t*2+rand(k+3)*9,9)","color":"#ff9ff3"}],"background":{"effect":"pulse","palette":["#2a1040","#401a50","#1a2050"],"speed":0.6,"intensity":0.25}}},
139  {"phase":"running","world":"ancient tomb","style":"pixel art","concept":"ancient tomb staged as a theater play: Clawd as explorer waits by a sarcophagus labeled {what} while curtains, torches and a slowly opening ","source":"claude-opus-5-5","scene":{"concept":"ancient tomb staged as a theater play: Clawd as explorer waits by a sarcophagus labeled {what} while curtains, torches and a slowly opening lid play out","code":"function frame(t,dt){for(let x=0;x<w;x++){put(x,0,'▼',x%2?'#8b1e2b':'#a8323e');put(x,h-1,'▀','#6b4a2b')}let cw=Math.floor(lerp(w/2,4,smoothstep(0,3,t)));fill(0,1,cw,h-2,'║','#a8323e');fill(w-cw,1,cw,h-2,'║','#a8323e');if(cw>10)return;for(let i=0;i<3;i++){let cx=14+i*40;sprite(cx,1,'╔══╗\\n║◆◆║\\n║  ║\\n╚══╝','#c9a227');let f=noise(t*3+i);put(cx+1+(f>0.5?1:0),0,f>0.5?'^':'*','#ffb030')}let sx=60;sprite(sx,4,'╔════════════╗\\n║  ▲ ☥  ▲    ║\\n╚════════════╝','#d8b25a');text(sx+2,5,'{what}'.slice(0,10),'#ffe08a');let p=fract(t/20),lid=smoothstep(0.3,0.9,p);sprite(sx+Math.round(lid*10),3,'▄▄▄▄▄▄▄▄▄▄▄▄▄▄','#a88a3a');if(lid>0.5){for(let k=0;k<6;k++)put(sx+3+k*2,2-((t*2+k)%2),'░','#bfe')}let cx=lerp(20,42,smoothstep(3,6,t));let c=clawd(cx,4,{pose:t<6?'walk':'stand',stride:Math.floor(cx/1.5)%4,facing:1,arms:lid>0.6?'up':{left:'down',right:'out'},eyes:lid>0.6?'wide':'open'});pixels(c.top.x-4,c.top.py-2,'..####..\\n########',{'#':'#c2a060'});put(c.right.x+1,c.right.row,'¥','#ffb030');let m=t%20;if(m>6&&m<11)say('Act I: {what} runs. The tomb waits.',c.top.x,c.row);else if(m>13&&m<18)say('The lid moves! ...or is it still loading?',c.top.x,c.row)}","actors":[],"particles":[{"glyphs":"·.","count":20,"x":"rand(k)*w","y":"1+mod(t*0.5+rand(k+3)*7,7)","color":"#7a6a50"}],"background":{"effect":"pulse","palette":["#2a1a10","#3a2414","#4a2e18"],"speed":0.4,"intensity":0.2}}},
140  {"phase":"searching","world":"jungle safari","style":"pixel art","concept":"dinosaur-era jungle safari: Clawd in a pith helmet rides a long-necked brontosaurus through ferns, sniffing out giant {what} footprints that","source":"claude-opus-5-5","scene":{"concept":"dinosaur-era jungle safari: Clawd in a pith helmet rides a long-necked brontosaurus through ferns, sniffing out giant {what} footprints that lead to a hidden egg nest","code":"let ft=Array.from({length:30},(_,i)=>({x:i*9+4,s:rand(i)}))\nfunction frame(t,dt){\nconst sc=t*4;\nfor(let x=0;x<w;x++){const hh=Math.floor(noise((x+sc*0.3)*0.05)*3);for(let y=0;y<hh;y++)put(x,4-y,'▓','#2f5a3a');put(x,8,'▀','#6b4f2a','#3a2a18')}\nput(w-12,0,'●','#ffb347');\nfor(const f of ft){const fx=((f.x-sc)%270+270)%270-10;text(fx,7,'ψ','#c9a36b')}\nfor(let i=0;i<14;i++){const fx=((i*23-sc*1.6)%(w+30)+w+30)%(w+30)-15;sprite(fx,5,' \\|/ \n\\\\|//','#4caf50')}\nconst bx=28,by=3,st=Math.floor(t*4)%2;\npixels(bx,by*2+2,'......................#####\\n....................###o###\\n..................####.....\\n....############.####......\\n..##################.......\\n.#################.........\\n##################.........\\n.##..##....##..##..........\\n'+(st?'.##...##..##...##..........':'..##.##....##.##...........'),{'#':'#7fb069','o':'#111'});\nconst c=clawd(bx+9,by-1.5,{facing:1,arms:t%8<4?'down':{left:'down',right:'out'},scale:1});\npixels(c.top.x-4,c.top.py-3,'..####..\\n########',{'#':'#e8d8a8'});\nif(t%8>=4)text(c.right.x+1,c.right.row,'→','#ffd166');\nconst nx=w-30-((t*3)%60);if(t%20>12){pixels(nx+20,12,'.o.o.\\nooooo',{o:'#f2e6c9'});text(nx+16,5,'{what}','#ffd166')}\nconst lines=['Fresh tracks. {what} came through here.','These prints are huge. Big definition ahead.','Grep-osaurus, follow that scent!','A nest! Found where it hatched.'];\nconst k=Math.floor(t/5)%4;if(t%5<3.5)say(lines[k],c.top.x,c.row);\n}","actors":[],"particles":[{"glyphs":"v^","count":5,"x":"mod(rand(k)*w - t*(2+k), w)","y":"1+sin(t+k)*0.8","color":"#aab7c4"}],"background":{"effect":"aurora","palette":["#3a2a5a","#7a3a4a","#c86a3a"],"speed":0.4,"intensity":0.3}}},
141  {"phase":"reading","world":"farm","style":"pixel art","concept":"ski slope farm: Clawd skis down a snowy hillside past a barn, reading tracks in the snow that spell out {what}, a cow on a chairlift watches","source":"claude-opus-5-5","scene":{"concept":"ski slope farm: Clawd skis down a snowy hillside past a barn, reading tracks in the snow that spell out {what}, a cow on a chairlift watches","code":"let sx=0;\nfunction frame(t,dt){\nfor(let x=0;x<w;x++){let g=Math.floor(2+x*6/w);for(let y=g;y<h;y++)put(x,y,' ',null,y==g?'#dfe8f0':'#b8c8d8');}\nfor(let i=0;i<5;i++){let px=8+i*26,py=Math.floor(2+px*6/w)-2;sprite(px,py,' ▲ \n▲▲▲\n │ ','#3c8a4a');}\nlet bx=w-22,by=1;pixels(bx,by*2,'....RRRR....\\n..RRRRRRRR..\\nRRRRRRRRRRRR\\nRWWRRRRRRWWR\\nRWWRRDDRRWWR\\nRRRRRDDRRRRR',{R:'#c0392b',W:'#f0f0f0',D:'#5a2a1a'});\nline(0,0,w-1,0,'─','#888');\nlet cx=mod(t*6,w+40)-20;sprite(Math.floor(cx),0,'│\\n▙▄▟','#aaa');text(Math.floor(cx)+1,1,'☻','#fff');\nlet p=fract(t/14),x=lerp(-10,w-30,smoothstep(0,1,p)),y=lerp(-1,3.5,p);\nfor(let k=1;k<30;k++){let tx=x-k*2;if(tx>0)put(tx,Math.floor(lerp(-1,3.5,(tx+10)/(w+20)))+4,'=','#8aa0b8');}\nlet c=clawd(x,y,{pose:'jump',facing:1,arms:'out'});\nline(Math.floor(x)-2,Math.floor(y)+4,Math.floor(x)+15,Math.floor(y)+4,'━','#3366ff');\npixels(c.top.x-3,c.top.py-3,'.YYYY.\\nYYYYYY',{Y:'#ffcc33'});\ntext(4,h-1,'tracks: {what}','#335577');\nif(p>0.35&&p<0.75)say('Reading {what} at speed. Every line a mogul.',c.top.x,c.row);}\n","actors":[],"particles":[{"glyphs":"*·","count":40,"x":"rand(k)*w","y":"mod(t*(1+rand(k+9)*2)+rand(k+3)*h,h)","color":"#ffffff"}],"background":{"effect":"aurora","palette":["#223355","#334477","#446688"],"speed":0.5,"intensity":0.2}}},
142  {"phase":"editing","world":"cooking show","style":"text art","concept":"dinosaur-era cooking show: caveman-chef Clawd grills a giant bone-in steak labeled {what} on a volcano stove while a T-rex judge watches and","source":"claude-opus-5-5","scene":{"concept":"dinosaur-era cooking show: caveman-chef Clawd grills a giant bone-in steak labeled {what} on a volcano stove while a T-rex judge watches and a tiny pterodactyl steals garnish","code":"let T=0\nfunction frame(t,dt){\nfor(let x=0;x<w;x++)put(x,8,x%9==0?'┴':'▀','#6b4f2a')\nsprite(2,1,'   ▄▄▄   \\n  ▟███▙  \\n ▟█████▙ \\n▟███████▙','#7a5a3a')\nfor(let i=0;i<4;i++){let a=fract(t*0.7+i/4);put(5+Math.round(Math.sin(t*3+i)),Math.round(1-a*2),'░▒'[i%2],mix('#ff7a2a','#666666',a))}\nconst cx=44;\nsprite(cx-2,6,'╔══════════════════╗\\n╚═▲▲═▲▲═▲▲═▲▲═▲▲══╝','#a0a0a0')\nconst p=fract(t/8),fl=p>0.45&&p<0.6;\nconst sy=fl?5-2.5*Math.sin(Math.PI*(p-0.45)/0.15):5;\nconst cook=Math.min(1,p*1.5);\ntext(cx+2,Math.round(sy),'(▓▓{what}▓▓)=○',mix('#e06070','#a0522d',cook))\nfor(let i=0;i<5;i++)put(cx+1+i*3,4-Math.floor(fract(t*1.5+i*0.3)*3),'~','#cccccc')\nconst c=clawd(cx-18,4,{pose:'stand',facing:1,arms:fl?'up':{left:'down',right:'out'}})\nsprite(c.top.x-3,c.top.row-1,'▄█▀█▄','#e8e8e8')\ntext(c.right.x,c.right.row,'═╕','#c8b088')\nsprite(84,0,'   ▄████▄\\n   █ ●▀▀█▄▄\\n   ███▄▄▄▄▀\\n  ▟██▌\\n ▟███▙▀\\n▟█████\\n██▀ ▀█','#5fa35a')\nconst sc=Math.floor(t/8)%3;\nif(p>0.7)sprite(92,4,'┌──┐\\n│'+(8+sc)+' │\\n└──┘','#ffd75e')\nconst px=w+10-mod(t*12,w+30);\nsprite(px,1+Math.round(Math.sin(t*4)),'\\\\▼/','#c08060')\nconst lines=['Searing {what} at 600°, volcano heat.','Flip! Even crust on every function.','Judge Rex says: more salt, fewer bugs.'];\nsay(lines[Math.floor(t/8)%3],c.top.x,c.row)\n}","actors":[],"particles":[],"background":{"effect":"fire","palette":["#3a1a0a","#7a2a0a","#c0501a"],"speed":0.6,"intensity":0.15}}},
143  {"phase":"git","world":"haunted house","style":"text art","concept":"circus big top: Clawd as tightrope walker crossing between branch platforms while commits juggle above, a trapeze merge at the end","source":"claude-opus-5-5","scene":{"concept":"circus big top: Clawd as tightrope walker crossing between branch platforms while commits juggle above, a trapeze merge at the end","code":"function frame(t,dt){for(let x=0;x<w;x++){const s=Math.floor(x/6)%2;put(x,0,'▀',s?'#e05555':'#f0e0d0');}put(2,1,'╔═══╗','#c0a060');text(3,2,'main','#ffd866');put(w-14,1,'╔═════════╗','#c0a060');text(w-13,2,'{what}'.slice(0,9),'#7fd6ff');for(let x=2;x<w-3;x++)put(x,5,'─','#b0b0c0');put(4,6,'│','#806040');put(4,7,'│','#806040');put(w-6,6,'│','#806040');put(w-6,7,'│','#806040');for(let x=0;x<w;x++)put(x,8,'▒','#704020');const p=fract(t/16),x=lerp(6,w-22,smoothstep(0.05,0.85,p));const mv=p>0.05&&p<0.85;const c=clawd(x,1.5,{pose:mv?'walk':'stand',stride:Math.floor(x/1.5)%4,facing:mv?1:0,arms:p>0.88?'up':'out',color:'#d97757'});text(c.left.x-5,c.left.row,'━━━','#ffd866');text(c.right.x+2,c.right.row,'━━━','#ffd866');for(let i=0;i<3;i++){const a=t*4+i*2.1;put(c.top.x+Math.cos(a)*5,c.row-1-Math.abs(Math.sin(a))*0.9,'●',['#ff6b6b','#7fd6ff','#a6e22e'][i]);}if(p>0.88){for(let i=0;i<14;i++)put(rand(i+Math.floor(t))*w,1+rand(i*7)*6,'✦',hsl(i*40,0.8,0.65));}const sy=p<0.3?'Commit juggling on a tightrope. No net.':p<0.88?'Staging {what}... steady... steady...':'Ta-da! Committed. Hold your applause.';if(p>0.08)say(sy,c.top.x,c.row);text(Math.floor(w/2)-6,7,'○ ○ ○ ○ ○ ○','#888899');}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#401020","#602030","#302050"],"speed":0.5,"intensity":0.2}}},
144  {"phase":"git","world":"fishing pond","style":"text art","concept":"haunted house: Clawd as ghost-hunter lantern-bearer watching commit ghosts float out of a creaky family-tree portrait (git log) and drift al","source":"claude-opus-5-5","scene":{"concept":"haunted house: Clawd as ghost-hunter lantern-bearer watching commit ghosts float out of a creaky family-tree portrait (git log) and drift along branching corridors","code":"let gs=Array.from({length:6},(_,i)=>({p:-i*0.18,b:i%2}))\nfunction frame(t,dt){\nconst W='#4a3b5c',G='#cfe8ff';\nfor(let x=0;x<w;x++){put(x,8,x%9==0?'┴':'═',W);}\nsprite(4,0,'╔════════════╗\\n║ ▄▀▀▄  ▄▀▀▄ ║\\n║ █○○█  █○○█ ║\\n║  ▀▀    ▀▀  ║\\n║  git  log  ║\\n╚════════════╝','#8a6d3b');\nconst y0=3;\nline(19,y0,w-4,y0,'─','#6b5a7a');\nline(50,y0,62,6,'╲','#6b5a7a');line(62,6,w-30,6,'─','#6b5a7a');line(w-30,6,w-24,y0,'╱','#6b5a7a');\nfor(let x=26;x<w-6;x+=14)put(x,y0,'●','#e0b050');\nfor(const g of gs){g.p+=dt*0.05;if(g.p>1)g.p-=1.1;if(g.p<0)continue;\nlet gx=19+g.p*(w-30),gy=y0-2+Math.sin(t*3+g.p*20)*0.5;\nif(g.b&&gx>55&&gx<w-28)gy=5+Math.sin(t*3)*0.4;\nsprite(gx,gy,' ▄▄ \\n▐°°▌\\n ▀▀▘',mix(G,'#ffffff',0.5+0.5*Math.sin(t*4+g.p*9)));}\nconst cx=Math.min(w*0.4,-14+t*6),wk=cx<w*0.4;\nconst c=clawd(cx,4,{pose:wk?'walk':'stand',stride:Math.floor(cx/1.5)%4,facing:wk?1:-1,arms:{left:'down',right:'out'},eyes:fract(t/7)<0.15?'wide':'open'});\ntext(c.right.x,c.right.row-1,'♦','#ffd166');\nfor(let i=0;i<5;i++)put(c.right.x+2+i*2,c.right.row,'·','#ffd16688');\nif(!wk&&fract(t/16)<0.5)say(fract(t/32)<0.5?'Every commit in {what} haunts this house forever.':'That branch ghost? Never merged. Still wandering.',c.top.x,c.row);\n}","actors":[],"particles":[{"glyphs":"·˙","count":20,"x":"rand(k)*w","y":"noise(t*0.3+k)*7","color":"#7a6a9a"}],"background":{"effect":"aurora","palette":["#1a1030","#2a1a40","#3a2a55"],"speed":0.4,"intensity":0.25}}},
145  {"phase":"running","world":"rocket launch pad","style":"text art","concept":"volcano island launch pad: Clawd in a hard hat watches a rocket labeled {what} on a pad beside a smoking volcano, countdown, liftoff, then w","source":"claude-opus-5-5","scene":{"concept":"volcano island launch pad: Clawd in a hard hat watches a rocket labeled {what} on a pad beside a smoking volcano, countdown, liftoff, then waits for the next","code":"let L=6;function frame(t,dt){const g=h-1;for(let x=0;x<w;x++)put(x,g,x%5==0?'┴':'─','#7a6a5a');const vx=w-34;sprite(vx,2,'      ▄██▄      \n    ▄██▓▓██▄    \n  ▄███▓▓▓▓███▄  \n▄██████████████▄\n████████████████\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀','#6b4a3a');for(let i=0;i<6;i++){const s=fract(t*0.3+i/6);put(vx+7+Math.sin(i*3+t)*3*s,1-s*3+2,'░','#999999')}fill(vx+7,2,2,1,'▓','#ff6a2a');for(let x=0;x<w;x+=1){if(x<vx-4||x>vx+18){const y=g-1;if(noise(x*0.3+t)>0.7)put(x,y,'~','#3aa0d0')}}const cyc=mod(t,22),px=44;sprite(px-2,g-1,'╔═══════╗','#aaaaaa');sprite(px+9,1,'│\n├\n│\n├\n│\n├\n│','#888888');let lift=cyc>10?Math.pow(cyc-10,2)*1.2:0;const ry=g-8-lift;if(cyc<16){sprite(px+1,ry,' ▲ \n█ █\n█ █\n█ █\n███\n▀█▀','#e8e8f0');text(px+5,ry+2,'{what}','#ffd060');if(cyc>9.5)for(let i=0;i<8;i++){put(px+1+i%3,ry+6+Math.floor(rand(i+Math.floor(t*12))*3),'▓▒░'[i%3],i%2?'#ff9030':'#ffe060')}}if(cyc>=16&&cyc<18)for(let i=0;i<20;i++){const a=i*2.4,r=(cyc-16)*8;put(px+2+Math.cos(a)*r*2,1+Math.sin(a)*r*0.5,'*',hsl(i*40,0.9,0.6))}if(cyc>=18){sprite(px+1,g-7+Math.max(0,(20-cyc))*-3,' ▲ \n█ █\n█ █\n█ █\n███\n▀█▀','#e8e8f0')}const c=clawd(14,g-4,{facing:1,arms:cyc>10&&cyc<16?'up':'down',eyes:cyc>10&&cyc<14?'wide':'open',blink:fract(t*0.3)<0.04});sprite(c.top.x-3,c.top.row-1,'▄███▄','#ffcc00');if(cyc>3&&cyc<10)text(31,g-6,'T-'+Math.max(1,Math.ceil(10-cyc)),'#ff5050');if(cyc<3)say('{what} fueled. volcano says hi.',c.top.x,c.row);else if(cyc>11&&cyc<15)say('liftoff! go {what}, go!',c.top.x,c.row);else if(cyc>18)say('next one on the pad...',c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334466","#556688","#8899bb"],"speed":0.4,"intensity":0.3}}},
146  {"phase":"agents","world":"bowling alley","style":"text art","concept":"race track pit stop: Clawd as crew chief waits while helper mini-Clawd pit crews service a car labeled {what}, cars lapping past in the back","source":"claude-opus-5-5","scene":{"concept":"race track pit stop: Clawd as crew chief waits while helper mini-Clawd pit crews service a car labeled {what}, cars lapping past in the background","code":"let cars=[0,1,2].map(i=>({x:rand(i)*w,v:30+rand(i+5)*20,c:hsl(rand(i+9)*360,0.7,0.6)}))\nfunction frame(t,dt){\nfor(let x=0;x<w;x++){put(x,1,x%4<2?'▀':' ','#e0e0e0');put(x,3,'─','#555566');put(x,8,'▀','#777788')}\nfor(const c of cars){c.x+=c.v*dt;if(c.x>w+10)c.x=-12;text(c.x,2,'▄█▀█▄▶',c.c)}\nconst px=50;text(px,7,'═'.repeat(30),'#ffd75f');text(px+2,4,'PIT 1','#ffd75f')\nconst ph=fract(t/12),dn=ph>0.8;const cx=dn?px+6+(ph-0.8)*400:px+6\nsprite(cx,5,' ▄▄██████▄▄▄ \n▐█({o})█▀█({o})▌',dn?'#5fd7ff':'#e06c75')\ntext(cx+3,4,'{what}'.slice(0,8),'#ffffff')\nconst helpers=[[px-2,'#8fd18f'],[px+20,'#b19cd9'],[px+9,'#f0c674']]\nhelpers.forEach(([hx,col],i)=>{if(dn)return;const b=Math.sin(t*12+i*2)>0;clawd(hx,4.5+(i==2?-0.5:0)+(b?0:0.5),{scale:1,color:col,arms:b?'up':'down',facing:i==1?-1:1});if(b)put(hx+(i==1?-1:14),6,'*','#ffffff')})\nconst c=clawd(10,4,{facing:1,arms:dn?'up':{left:'down',right:'out'},eyes:dn?'wide':'open'})\npixels&&0;text(c.top.x-3,c.row-1,'▄███▄','#333333')\ntext(c.right.x+1,c.right.row,dn?'⚑':'▭','#e0e0e0')\nconst m=Math.floor(t/12)%3\nconst lines=['Pit crew on {what}. Four subagents, four tires.','Helpers still wrenching. Box box, no rush.','Out of the pits! Next car in for {what}...']\nif(fract(t/12)>0.1&&fract(t/12)<0.6)say(lines[m],c.top.x,c.row)\nelse if(dn)say('GO GO GO!',c.top.x,c.row)\n}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#202030","#303045","#252535"],"speed":0.5,"intensity":0}}},
147]
148
hooks/script.ts 1292 lines
1// Scenes the buddy scripts: actors with frames of ASCII art, moved by small
2// math expressions of time, with speech, particle swarms and a faint backdrop.
3// The expressions run in an interpreter of their own that knows arithmetic and
4// a handful of functions, so a script can move things and nothing else.
5
6import {
7  CLEAR,
8  cleanBackground,
9  cleanText,
10  clamp,
11  dim,
12  encode,
13  hash,
14  mix,
15  paintBackground,
16  parseHex,
17  type Background,
18  type Rgb,
19} from './effects'
20import { ARMS, CLAWD_H, CLAWD_ORANGE, CLAWD_W, LOOK, clawdAnchors, clawdMesh, clawdPixels, clawdPoints, type Anchors, type Arm, type ClawdLook, type Eyes, type Pose } from './clawd'
21import { ScriptError, obj, parseProgram, type Program } from './lang'
22import {
23  DEFAULT_CAMERA,
24  DEFAULT_FOG,
25  DEFAULT_LIGHT,
26  MOST_TRIANGLES,
27  SX,
28  SY,
29  box,
30  claimPixel,
31  composite,
32  createFrame,
33  cylinder,
34  drawLine,
35  drawMesh,
36  drawPoint,
37  plane,
38  project,
39  resolve,
40  sphere,
41  type Camera,
42  type Fog,
43  type Frame,
44  type Light,
45  type Mesh,
46  type Transform,
47  type View,
48} from './render3d'
49
50type Env = { t: number; w: number; h: number; k: number; n: number }
51type Expr = (env: Env) => number
52
53const fract = (v: number) => v - Math.floor(v)
54const mod = (a: number, b: number) => (b === 0 ? 0 : ((a % b) + b) % b)
55
56const FUNCTIONS: Record<string, (...args: number[]) => number> = {
57  sin: Math.sin,
58  cos: Math.cos,
59  tan: Math.tan,
60  abs: Math.abs,
61  min: (...v) => Math.min(...v),
62  max: (...v) => Math.max(...v),
63  floor: Math.floor,
64  ceil: Math.ceil,
65  round: Math.round,
66  sqrt: v => Math.sqrt(Math.max(0, v)),
67  pow: Math.pow,
68  exp: Math.exp,
69  sign: Math.sign,
70  mod,
71  fract,
72  clamp: (v, lo = 0, hi = 1) => Math.min(hi, Math.max(lo, v)),
73  lerp: (a, b, k) => a + (b - a) * k,
74  // 0 up to 1 and back down over each unit of x.
75  tri: v => 1 - Math.abs(2 * fract(v) - 1),
76  // A steady pseudo-random number in [0, 1) for each x.
77  rand: v => hash(v, 17),
78  step: (edge, v) => (v >= edge ? 1 : 0),
79  // 1 while lo <= v < hi.
80  between: (v, lo, hi) => (v >= lo && v < hi ? 1 : 0),
81  // 0 below a, 1 above b, eased in between.
82  smoothstep: (a, b, v) => {
83    const k = Math.min(1, Math.max(0, b === a ? (v >= b ? 1 : 0) : (v - a) / (b - a)))
84
85    return k * k * (3 - 2 * k)
86  },
87  atan2: Math.atan2,
88  hypot: (...v) => Math.hypot(...v),
89  // Smooth wandering noise in [0, 1) along x.
90  noise: v => {
91    const i = Math.floor(v)
92    const k = fract(v)
93
94    return hash(i, 17) + (hash(i + 1, 17) - hash(i, 17)) * k * k * (3 - 2 * k)
95  },
96}
97
98const VARIABLES = new Set(['t', 'w', 'h', 'k', 'n'])
99
100// Compiles an expression to a function of the scene's variables; anything it
101// cannot read compiles to a constant 0 rather than failing the scene.
102export function compile(source: string): Expr {
103  const tokens = source.slice(0, 240).match(/\d+\.?\d*|\.\d+|[a-z_]+|[-+*/%^(),]/gi) ?? []
104  let at = 0
105  const peek = () => tokens[at]
106  const take = () => tokens[at++]
107  type Node = Expr
108
109  function primary(): Node {
110    const token = take()
111    if (token === undefined) throw new Error('end')
112    if (token === '(') {
113      const inner = sum()
114      if (take() !== ')') throw new Error(')')
115
116      return inner
117    }
118    if (token === '-') {
119      const inner = power()
120
121      return env => -inner(env)
122    }
123    if (token === '+') return power()
124    if (/^[\d.]/.test(token)) {
125      const value = Number(token)
126
127      return () => value
128    }
129    const name = token.toLowerCase()
130    if (name === 'pi') return () => Math.PI
131    if (VARIABLES.has(name)) return env => env[name as keyof Env]
132    const fn = FUNCTIONS[name]
133    if (!fn || take() !== '(') throw new Error(name)
134    const args: Node[] = []
135    if (peek() !== ')') {
136      args.push(sum())
137      while (peek() === ',') {
138        take()
139        args.push(sum())
140      }
141    }
142    if (take() !== ')') throw new Error(')')
143
144    return env => fn(...args.map(arg => arg(env)))
145  }
146  function power(): Node {
147    const base = primary()
148    if (peek() !== '^') return base
149    take()
150    const exponent = power()
151
152    return env => Math.pow(base(env), exponent(env))
153  }
154  function product(): Node {
155    let left = power()
156    while (peek() === '*' || peek() === '/' || peek() === '%') {
157      const op = take()
158      const right = power()
159      const l = left
160      left = op === '*' ? env => l(env) * right(env) : op === '/' ? env => l(env) / right(env) : env => mod(l(env), right(env))
161    }
162
163    return left
164  }
165  function sum(): Node {
166    let left = product()
167    while (peek() === '+' || peek() === '-') {
168      const op = take()
169      const right = product()
170      const l = left
171      left = op === '+' ? env => l(env) + right(env) : env => l(env) - right(env)
172    }
173
174    return left
175  }
176  try {
177    const expr = sum()
178    if (at !== tokens.length) return () => 0
179
180    return env => {
181      const v = expr(env)
182
183      return Number.isFinite(v) ? v : 0
184    }
185  } catch {
186    return () => 0
187  }
188}
189
190export type Actor = {
191  // A sprite of the script's own frames, or Clawd, whom the stage draws.
192  kind: 'sprite' | 'clawd'
193  frames: string[][]
194  fps: number
195  x: Expr
196  y: Expr
197  color: string
198  say: string
199  // When the speech shows: seconds into the scene.
200  sayAt: number
201  // Drawn only while this is above 0; always when absent.
202  show?: Expr
203  // Which frame shows (wrapped to the frame count); by fps when absent.
204  frame?: Expr
205}
206
207export type Swarm = { glyphs: string[]; count: number; x: Expr; y: Expr; color: string }
208
209// A scene's own code: a program with a frame(t, dt) function that draws on
210// the strip every frame, its state kept between frames.
211export type Code = {
212  program?: Program
213  isStarted: boolean
214  lastT: number
215  // Why the code stopped (or never parsed), for the narrator to hear.
216  error?: string
217  // Whether the narrator has heard about the error.
218  isReported?: boolean
219  // When each line the code has Clawd say first showed, for the typing.
220  said: Map<string, number>
221  // Where each line's bubble sat last, so it does not hop about.
222  bubblesAt: Map<string, { x: number; y: number }>
223  // The 3D settings, kept between frames once the code sets them.
224  cam?: Camera
225  light?: Light
226  fog?: Fog
227}
228
229export type Script = {
230  background: Background
231  actors: Actor[]
232  swarms: Swarm[]
233  code?: Code
234}
235
236// The fuel the code's setup and each frame may burn: statements, loop turns
237// and calls. A frame over budget stops the code, not the strip.
238const SETUP_FUEL = 1_000_000
239const FRAME_FUEL = 150_000
240// Fuel bounds the code's own steps; this bounds the drawing it asks for.
241const FRAME_MS = 80
242// The setup may build a world; it gets longer, once.
243const SETUP_MS = 500
244
245function cleanCode(raw: unknown): Code | undefined {
246  if (typeof raw !== 'string' || !raw.trim()) return undefined
247  const code: Code = { isStarted: false, lastT: 0, said: new Map(), bubblesAt: new Map() }
248  try {
249    code.program = parseProgram(raw.slice(0, 20_000))
250  } catch (e) {
251    code.error = e instanceof Error ? e.message : String(e)
252  }
253
254  return code
255}
256
257// For the quarter blocks U+2596 to U+259F, which halves (1 top, 2 bottom)
258// hold ink: ▖▗▘▙▚▛▜▝▞▟.
259const QUARTER_HALVES = [2, 2, 1, 3, 3, 3, 3, 1, 3, 3]
260
261// Characters a sprite may hold: what one terminal cell draws at one width.
262const DRAWABLE = /[\x21-\x7e¡-ÿ←-⇿─-◿⠀-⣿★☆♥♦♣♠♪♫☺☻✓✗✦✧]/
263
264const glyphsOf = (line: string) => [...line].map(c => (c === ' ' || DRAWABLE.test(c) ? c : ' '))
265
266const isColor = (c: unknown): c is string => typeof c === 'string' && /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.test(c.trim())
267
268export function cleanScript(raw: unknown): Script | undefined {
269  if (typeof raw !== 'object' || raw === null) return undefined
270  const s = raw as Record<string, unknown>
271  const background = cleanBackground(s.background)
272  if (!background) return undefined
273  const actors: Actor[] = (Array.isArray(s.actors) ? s.actors : []).slice(0, 20).flatMap(a => {
274    if (typeof a !== 'object' || a === null) return []
275    const o = a as Record<string, unknown>
276    const kind = o.kind === 'clawd' ? 'clawd' : 'sprite'
277    const frames = (Array.isArray(o.frames) ? o.frames : [])
278      .filter((f): f is string => typeof f === 'string')
279      .slice(0, 12)
280      .map(f => f.split('\n').slice(0, 9).map(line => glyphsOf(line.slice(0, 80)).join('')))
281    if (kind === 'sprite' && frames.length === 0) return []
282
283    return [
284      {
285        kind,
286        frames: kind === 'clawd' ? [CLAWD_BOX] : frames,
287        fps: clamp(typeof o.fps === 'number' ? o.fps : 4, 0, 20),
288        x: compile(typeof o.x === 'string' ? o.x : '0'),
289        y: compile(typeof o.y === 'string' ? o.y : '0'),
290        color: isColor(o.color) ? o.color : '#d97757',
291        say: cleanText(typeof o.say === 'string' ? o.say : '', 70),
292        sayAt: clamp(typeof o.sayAt === 'number' ? o.sayAt : 0.5, 0, 30),
293        show: typeof o.show === 'string' && o.show.trim() ? compile(o.show) : undefined,
294        frame: typeof o.frame === 'string' && o.frame.trim() ? compile(o.frame) : undefined,
295      },
296    ]
297  })
298  const swarms: Swarm[] = (Array.isArray(s.particles) ? s.particles : []).slice(0, 6).flatMap(p => {
299    if (typeof p !== 'object' || p === null) return []
300    const o = p as Record<string, unknown>
301    const glyphs = glyphsOf(typeof o.glyphs === 'string' ? o.glyphs : '*').filter(c => c !== ' ').slice(0, 16)
302
303    return [
304      {
305        glyphs: glyphs.length > 0 ? glyphs : ['*'],
306        count: Math.round(clamp(typeof o.count === 'number' ? o.count : 10, 0, 120)),
307        x: compile(typeof o.x === 'string' ? o.x : 'rand(k)*w'),
308        y: compile(typeof o.y === 'string' ? o.y : 'rand(k+7)*h'),
309        color: isColor(o.color) ? o.color : '#ffffff',
310      },
311    ]
312  })
313
314  return { background, actors, swarms, code: cleanCode(s.code) }
315}
316
317// For layout Clawd fills a 14 by 4 cell box (its 8 pixel rows).
318const CLAWD_BOX = Array.from({ length: CLAWD_H / 2 }, () => '█'.repeat(CLAWD_W))
319
320// What the scene's code draws through: set while its frame runs.
321type Canvas = {
322  cols: number
323  rows: number
324  code: Code
325  put: (col: number, row: number, char: string, color: Rgb, back?: Rgb) => void
326  clear: (col: number, row: number) => void
327  // Marks a fill as under way: its cells do not count as drawn on.
328  fill: (on: boolean) => void
329  // One pixel: a column and a pixel row (two to a cell).
330  pixel: (col: number, py: number, color: Rgb) => void
331  // Clawd with its top-left at a column and a pixel row; where things attach.
332  clawd: (x: number, py: number, look: ClawdLook) => Anchors
333  // One pixel in the 3D world: drawn where nothing nearer has been.
334  pixel3d: (col: number, py: number, color: Rgb, depth: number) => void
335  // The cells a Clawd drawn some other way covers, for its speech.
336  mark: (box: Box) => void
337  say: (text: string, x: number, y: number, color: Rgb) => void
338  // The frame's 3D world, made on first use; drawing in it is noted.
339  scene3d: (isDrawing?: boolean) => { frame: Frame; view: View; light: Light; fog: Fog }
340}
341let canvas: Canvas | undefined
342
343// A color from the code: 0xRRGGBB, "#rrggbb" or "#rgb". A string is parsed
344// once and remembered, since a fill names the same one for every cell.
345const COLORS = new Map<string, Rgb>()
346const colorOf = (c: unknown, fallback: Rgb = [255, 255, 255]): Rgb => {
347  if (typeof c === 'number' && Number.isFinite(c)) return [(c >> 16) & 255, (c >> 8) & 255, c & 255]
348  if (typeof c !== 'string') return fallback
349  let rgb = COLORS.get(c)
350  if (!rgb) {
351    if (!isColor(c)) return fallback
352    if (COLORS.size >= 512) COLORS.clear()
353    rgb = parseHex(c)
354    COLORS.set(c, rgb)
355  }
356
357  return [rgb[0], rgb[1], rgb[2]]
358}
359// Text from the code: a plain object (which has no toString in the sandbox)
360// or an array reads as it would in JavaScript rather than throwing.
361const strOf = (v: unknown): string =>
362  v === null || v === undefined ? '' : typeof v === 'object' ? (Array.isArray(v) ? v.slice(0, 400).map(strOf).join(',') : '[object Object]') : String(v)
363const packRgb = (r: number, g: number, b: number) =>
364  (clamp(Math.round(r), 0, 255) << 16) | (clamp(Math.round(g), 0, 255) << 8) | clamp(Math.round(b), 0, 255)
365const num = (v: unknown, fallback = 0) => (typeof v === 'number' && Number.isFinite(v) ? v : fallback)
366// One drawable character, or undefined.
367const charOf = (c: unknown) => {
368  const cp = strOf(c).codePointAt(0)
369  const first = cp === undefined ? undefined : String.fromCodePoint(cp)
370
371  return first === ' ' || (first && DRAWABLE.test(first)) ? first : undefined
372}
373
374// The functions the scene's code draws with.
375const API = {
376  put: (x: unknown, y: unknown, ch: unknown, color?: unknown, bg?: unknown) => {
377    const char = charOf(ch)
378    if (char) canvas?.put(Math.round(num(x)), Math.round(num(y)), char, colorOf(color), bg === undefined ? undefined : colorOf(bg))
379  },
380  text: (x: unknown, y: unknown, str: unknown, color?: unknown, bg?: unknown) => {
381    const col = Math.round(num(x))
382    const row = Math.round(num(y))
383    ;[...strOf(str)].slice(0, 400).forEach((c, i) => API.put(col + i, row, charOf(c) ?? ' ', color, bg))
384  },
385  // Multi-line art, solid inside its outline like a sprite actor.
386  sprite: (x: unknown, y: unknown, art: unknown, color?: unknown, bg?: unknown) => {
387    const col = Math.round(num(x))
388    const row = Math.round(num(y))
389    strOf(art).split('\n').slice(0, 40).forEach((line, dy) => {
390      const chars = [...line.slice(0, 300)]
391      const start = chars.findIndex(c => c !== ' ')
392      if (start < 0) return
393      const end = Math.min(chars.length, line.trimEnd().length)
394      for (let dx = start; dx < end; dx++) {
395        const c = charOf(chars[dx]) ?? ' '
396        if (c === ' ' && bg === undefined) canvas?.clear(col + dx, row + dy)
397        else API.put(col + dx, row + dy, c, color, bg)
398      }
399    })
400  },
401  fill: (x: unknown, y: unknown, w: unknown, h: unknown, ch?: unknown, color?: unknown, bg?: unknown) => {
402    const x0 = Math.round(num(x))
403    const y0 = Math.round(num(y))
404    canvas?.fill(true)
405    for (let row = y0; row < y0 + Math.min(60, Math.round(num(h))); row++) {
406      for (let col = x0; col < x0 + Math.min(400, Math.round(num(w))); col++) API.put(col, row, ch ?? ' ', color, bg)
407    }
408    canvas?.fill(false)
409  },
410  line: (x0: unknown, y0: unknown, x1: unknown, y1: unknown, ch?: unknown, color?: unknown) => {
411    const ax = num(x0)
412    const ay = num(y0)
413    const steps = Math.min(800, Math.ceil(Math.max(Math.abs(num(x1) - ax), Math.abs(num(y1) - ay))) || 0)
414    for (let i = 0; i <= steps; i++) {
415      const k = steps === 0 ? 0 : i / steps
416      API.put(ax + (num(x1) - ax) * k, ay + (num(y1) - ay) * k, ch ?? '*', color)
417    }
418  },
419  // A circle of radius r rows, twice as wide in columns so it looks round.
420  circle: (cx: unknown, cy: unknown, r: unknown, ch?: unknown, color?: unknown) => {
421    const radius = Math.min(100, Math.abs(num(r)))
422    const steps = Math.max(8, Math.ceil(radius * 12))
423    for (let i = 0; i < steps; i++) {
424      const a = (i / steps) * Math.PI * 2
425      API.put(num(cx) + Math.cos(a) * radius * 2, num(cy) + Math.sin(a) * radius, ch ?? 'o', color)
426    }
427  },
428  disc: (cx: unknown, cy: unknown, r: unknown, ch?: unknown, color?: unknown, bg?: unknown) => {
429    const radius = Math.min(60, Math.abs(num(r)))
430    for (let dy = -Math.ceil(radius); dy <= Math.ceil(radius); dy++) {
431      for (let dx = -Math.ceil(radius * 2); dx <= Math.ceil(radius * 2); dx++) {
432        if ((dx / 2) ** 2 + dy ** 2 <= radius ** 2) API.put(Math.round(num(cx)) + dx, Math.round(num(cy)) + dy, ch ?? '█', color, bg)
433      }
434    }
435  },
436  // Clawd at x, y (top-left; y in rows, halves allowed), with either the
437  // short form (facing, stride, blink, color) or an options object.
438  clawd: (x: unknown, y: unknown, facing?: unknown, stride?: unknown, blink?: unknown, color?: unknown) => {
439    const look = lookOf(facing, stride, blink, color)
440
441    return anchorsOut(canvas?.clawd(Math.round(num(x)), Math.round(num(y) * 2), look), Math.round(num(x)), Math.round(num(y) * 2))
442  },
443  pixel: (x: unknown, py: unknown, color?: unknown) => {
444    canvas?.pixel(Math.round(num(x)), Math.round(num(py)), colorOf(color))
445  },
446  // Pixel art: lines of characters, each mapped to a color by the palette
447  // ("." and " " are transparent; an unmapped character is white).
448  pixels: (x: unknown, py: unknown, art: unknown, palette?: unknown) => {
449    const col = Math.round(num(x))
450    const row = Math.round(num(py))
451    const map = typeof palette === 'object' && palette !== null ? (palette as Record<string, unknown>) : {}
452    strOf(art).split('\n').slice(0, 80).forEach((line, dy) => {
453      ;[...line.slice(0, 300)].forEach((c, dx) => {
454        if (c === ' ' || c === '.') return
455        canvas?.pixel(col + dx, row + dy, colorOf(map[c]))
456      })
457    })
458  },
459  // The 3D world: a camera (eye, target, the angle it sees across in degrees),
460  // one light (the direction toward it, and ambient 0 to 1), and fog that
461  // fades things out between two depths. Each keeps until set again.
462  camera: (ex: unknown, ey: unknown, ez: unknown, tx?: unknown, ty?: unknown, tz?: unknown, fov?: unknown) => {
463    if (canvas) canvas.code.cam = { eye: [num(ex), num(ey, 2.5), num(ez, 9)], target: [num(tx), num(ty, 1), num(tz)], fov: num(fov, 90) }
464  },
465  light: (dx: unknown, dy: unknown, dz: unknown, ambient?: unknown) => {
466    if (canvas) canvas.code.light = { dir: [num(dx, 0.4), num(dy, 1), num(dz, 0.6)], ambient: clamp(num(ambient, 0.35)) }
467  },
468  fog: (near: unknown, far: unknown, color?: unknown) => {
469    if (canvas) canvas.code.fog = { near: num(near, 8), far: Math.max(num(near, 8) + 0.01, num(far, 40)), color: colorOf(color, [0, 0, 0]) }
470  },
471  box: (w: unknown, h: unknown, d: unknown) => meshOut('box', [num(w, 1), num(h, 1), num(d, 1)], (a, b, c) => box(a, b, c)),
472  sphere: (r: unknown, segments?: unknown) => meshOut('sphere', [num(r, 1), num(segments, 8)], (a, b) => sphere(a, b)),
473  cylinder: (r: unknown, h: unknown, segments?: unknown) => meshOut('cylinder', [num(r, 1), num(h, 1), num(segments, 8)], (a, b, c) => cylinder(a, a, b, c)),
474  cone: (r: unknown, h: unknown, segments?: unknown) => meshOut('cone', [num(r, 1), num(h, 1), num(segments, 8)], (a, b, c) => cylinder(a, 0, b, c)),
475  plane: (w: unknown, d: unknown) => meshOut('plane', [num(w, 1), num(d, 1)], (a, b) => plane(a, b)),
476  // Draws a mesh placed by its options: x, y, z; rx, ry, rz (radians);
477  // scale (one number or [sx, sy, sz]); color; wire (edges only); unlit.
478  mesh3d: (mesh: unknown, options?: unknown) => {
479    if (!canvas) return
480    const m = meshIn(mesh)
481    if (!m) return
482    const o = (typeof options === 'object' && options !== null ? options : {}) as Record<string, unknown>
483    const sc = Array.isArray(o.scale) ? o.scale.map(v => num(v, 1)) : [num(o.scale, 1), num(o.scale, 1), num(o.scale, 1)]
484    const t: Transform = {
485      at: [num(o.x), num(o.y), num(o.z)],
486      rot: [num(o.rx), num(o.ry), num(o.rz)],
487      scale: [sc[0] ?? 1, sc[1] ?? 1, sc[2] ?? 1],
488    }
489    const { frame, view, light, fog } = canvas.scene3d()
490    drawMesh(frame, view, light, fog, m, t, { color: colorOf(o.color), wire: Boolean(o.wire), unlit: Boolean(o.unlit), ascii: Boolean(o.ascii) })
491  },
492  line3d: (x0: unknown, y0: unknown, z0: unknown, x1: unknown, y1: unknown, z1: unknown, color?: unknown) => {
493    if (!canvas) return
494    const { frame, view, fog } = canvas.scene3d()
495    drawLine(frame, view, fog, [num(x0), num(y0), num(z0)], [num(x1), num(y1), num(z1)], colorOf(color))
496  },
497  point3d: (x: unknown, y: unknown, z: unknown, color?: unknown) => {
498    if (!canvas) return
499    const { frame, view, fog } = canvas.scene3d()
500    drawPoint(frame, view, fog, [num(x), num(y), num(z)], colorOf(color))
501  },
502  // Where a world point lands: x and y in cells, px and py in pixels, its
503  // depth, and the pixels one world unit covers there; null behind the camera.
504  project: (x: unknown, y: unknown, z: unknown) => {
505    if (!canvas) return null
506    const { view } = canvas.scene3d(false)
507    const p = project(view, [num(x), num(y), num(z)])
508
509    return p
510      ? obj({ x: Math.round(p.x / SX), y: Math.round(p.y / SY), px: Math.round(p.x / SX), py: Math.round(p.y / (SY / 2)), depth: p.depth, scale: view.f / p.depth / SX })
511      : null
512  },
513  // Clawd standing at a world point (its feet there), facing the camera: a
514  // lit solid sized by distance (`size` is its height in world units, 0.9 by
515  // default), its eyes flat pixel art on its face. Takes the same options as
516  // clawd and returns the same anchors, or null when it is behind the camera.
517  clawd3d: (x: unknown, y: unknown, z: unknown, options?: unknown) => {
518    if (!canvas) return null
519    const { frame, view, light, fog } = canvas.scene3d()
520    const at: [number, number, number] = [num(x), num(y), num(z)]
521    const p = project(view, at)
522    if (!p) return null
523    const o = (typeof options === 'object' && options !== null ? options : {}) as Record<string, unknown>
524    const look = lookOf(o)
525    // Pixel rows per world unit at its depth; its height is capped so it is
526    // never taller than most of the strip: Clawd is small in a big world.
527    const perUnit = view.f / p.depth / (SY / 2)
528    const size = Math.min(num(o.size, 0.9), (canvas.rows * 2 * 0.7) / perUnit)
529    // World units per sprite pixel, and strip pixels per sprite pixel.
530    const u = size / CLAWD_H
531    const s = (size * perUnit) / CLAWD_H
532    // Its feet at the point, unless that puts them below the strip: a Clawd
533    // near the camera stands on the bottom edge rather than out of view.
534    const below = p.y / (SY / 2) - canvas.rows * 2
535    if (below > 0) at[1] += below / perUnit
536    // It faces the camera.
537    const ry = Math.atan2(view.eye[0] - at[0], view.eye[2] - at[2])
538    const sin = Math.sin(ry)
539    const cos = Math.cos(ry)
540    const place = (q: { x: number; y: number; z: number }) => {
541      const w = project(view, [at[0] + (q.x * cos + q.z * sin) * u, at[1] + q.y * u, at[2] + (q.z * cos - q.x * sin) * u])
542
543      return w ? { x: w.x / SX, py: w.y / (SY / 2), depth: w.depth } : undefined
544    }
545    drawMesh(frame, view, light, fog, clawdMesh(look), { at, rot: [0, ry, 0], scale: [u, u, u] }, { color: look.color, wire: false, unlit: false, ascii: false })
546    const points = clawdPoints(look)
547    const feet = place(points.feet)
548    if (!feet) return null
549    // The eyes: whole pixels at a size that steps with distance, so they
550    // never smear with perspective, and none on a Clawd too small for them.
551    // Both sit on one row, from the point between them: projected apart, a
552    // Clawd off the camera's axis under a high camera gets one eye lower
553    // than the other, which reads as lopsided rather than as a turned face.
554    const ew = Math.max(1, Math.round(s))
555    const [one, two] = points.eyes
556    const between = one && two ? place({ x: (one.x + two.x) / 2, y: (one.y + two.y) / 2, z: (one.z + two.z) / 2 }) : undefined
557    if (s * CLAWD_H >= 3 && between) {
558      for (const eye of points.eyes) {
559        const e = place(eye)
560        if (!e) continue
561        const wide = eye.kind === 'open' ? ew : 2 * ew
562        const col0 = Math.round(e.x - wide / 2)
563        const row0 = 2 * Math.round((between.py - ew) / 2)
564        const from = eye.kind === 'shut' ? row0 + ew : row0
565        for (let prow = from; prow < row0 + 2 * ew; prow++) for (let col = col0; col < col0 + wide; col++) canvas.pixel3d(col, prow, look.eyeColor, e.depth * 0.98)
566      }
567    }
568    const w = Math.round(CLAWD_W * s)
569    const h = Math.round(CLAWD_H * s)
570    const left = Math.round(feet.x - w / 2)
571    const top = Math.round(feet.py - h)
572    canvas.mark({ x0: left - 1, y0: Math.floor(top / 2), x1: left + w, y1: Math.floor((top + h - 1) / 2) })
573    const anchor = (q: { x: number; y: number; z: number }) => {
574      const e = place(q) ?? feet
575      const py = Math.round(e.py)
576
577      return obj({ x: Math.round(e.x), py, row: Math.floor(py / 2) })
578    }
579
580    return obj({
581      x: left,
582      py: top,
583      row: Math.floor(top / 2),
584      w,
585      h,
586      top: anchor(points.top),
587      left: anchor(points.left),
588      right: anchor(points.right),
589      feet: anchor(points.feet),
590      eyes: obj({ ...anchor(points.eyeSpan), w: Math.round(points.eyeSpan.w * s) }),
591    })
592  },
593  // A speech bubble pointing at x, y.
594  say: (text: unknown, x: unknown, y: unknown, color?: unknown) => {
595    const line = cleanText(strOf(text), 70)
596    if (line) canvas?.say(line, Math.round(num(x)), Math.round(num(y)), colorOf(color, [217, 119, 87]))
597  },
598  rgb: (r: unknown, g: unknown, b: unknown) => packRgb(num(r), num(g), num(b)),
599  // Hue in degrees, saturation and lightness 0 to 1.
600  hsl: (hue: unknown, sat: unknown, light: unknown) => {
601    const hh = (((num(hue) % 360) + 360) % 360) / 60
602    const ss = clamp(num(sat))
603    const ll = clamp(num(light))
604    const c = (1 - Math.abs(2 * ll - 1)) * ss
605    const x = c * (1 - Math.abs((hh % 2) - 1))
606    const [r, g, b] = hh < 1 ? [c, x, 0] : hh < 2 ? [x, c, 0] : hh < 3 ? [0, c, x] : hh < 4 ? [0, x, c] : hh < 5 ? [x, 0, c] : [c, 0, x]
607    const m = ll - c / 2
608
609    return packRgb((r + m) * 255, (g + m) * 255, (b + m) * 255)
610  },
611  mix: (a: unknown, b: unknown, k: unknown) => {
612    const [r, g, bb] = mix(colorOf(a), colorOf(b), clamp(num(k)))
613
614    return packRgb(r, g, bb)
615  },
616}
617
618// Clawd's look from a call's arguments: an options object, or the short
619// form's facing, stride, blink and color.
620function lookOf(facing: unknown, stride?: unknown, blink?: unknown, color?: unknown): ClawdLook {
621  if (typeof facing === 'object' && facing !== null) {
622    const o = facing as Record<string, unknown>
623    const poses: Pose[] = ['stand', 'walk', 'jump', 'sit']
624    const eyes: Eyes[] = ['open', 'closed', 'wide']
625    const strideIn = num(o.stride, -1)
626    const pose = poses.includes(o.pose as Pose) ? (o.pose as Pose) : strideIn >= 0 ? 'walk' : 'stand'
627    const lookIn = (typeof o.look === 'object' && o.look !== null ? o.look : {}) as Record<string, unknown>
628    // arms: one pose for both, or {left, right}.
629    const armOf = (v: unknown, fallback: Arm): Arm => (ARMS.includes(v as Arm) ? (v as Arm) : fallback)
630    const armsIn = o.arms
631    const arms =
632      typeof armsIn === 'object' && armsIn !== null
633        ? { left: armOf((armsIn as Record<string, unknown>).left, 'up'), right: armOf((armsIn as Record<string, unknown>).right, 'up') }
634        : { left: armOf(armsIn, 'up'), right: armOf(armsIn, 'up') }
635
636    return {
637      pose,
638      arms,
639      stride: Math.round(Math.max(0, strideIn)),
640      facing: Math.sign(num(o.facing)),
641      eyes: eyes.includes(o.eyes as Eyes) ? (o.eyes as Eyes) : 'open',
642      look: { x: num(lookIn.x, num(o.lookX)), y: num(lookIn.y, num(o.lookY)) },
643      isBlinking: Boolean(o.blink),
644      color: colorOf(o.color, CLAWD_ORANGE),
645      eyeColor: colorOf(o.eyeColor, LOOK.eyeColor),
646      scale: clamp(Math.round(num(o.scale, 1)), 1, 4),
647    }
648  }
649  const strideIn = Math.round(num(stride, -1))
650
651  return {
652    ...LOOK,
653    pose: strideIn >= 0 ? 'walk' : 'stand',
654    stride: Math.max(0, strideIn),
655    facing: Math.sign(num(facing)),
656    isBlinking: Boolean(blink),
657    color: colorOf(color, CLAWD_ORANGE),
658  }
659}
660
661// Anchors as the code reads them: absolute pixel positions.
662function anchorsOut(a: Anchors | undefined, left: number, top: number) {
663  if (!a) return null
664  const at = (p: { x: number; y: number }) => obj({ x: left + p.x, py: top + p.y, row: Math.floor((top + p.y) / 2) })
665
666  return obj({
667    x: left,
668    py: top,
669    row: Math.floor(top / 2),
670    w: a.w,
671    h: a.h,
672    top: at(a.top),
673    left: at(a.left),
674    right: at(a.right),
675    feet: at(a.feet),
676    eyes: obj({ ...at(a.eyes), w: a.eyes.w }),
677  })
678}
679
680// A primitive the code asked for, the same object for the same arguments:
681// code that builds sphere(1, 48) inside frame() (the natural way to write
682// it) would otherwise read and prepare a fresh mesh every frame. Its arrays
683// are frozen, so a change to one cannot leave its prepared normals stale.
684const PRIMITIVES = new Map<string, ReturnType<typeof obj>>()
685function meshOut(kind: string, args: number[], make: (...a: number[]) => Mesh) {
686  const key = `${kind}|${args.join(',')}`
687  let out = PRIMITIVES.get(key)
688  if (!out) {
689    if (PRIMITIVES.size >= 64) PRIMITIVES.clear()
690    const m = make(...args)
691    Object.freeze(m.verts)
692    Object.freeze(m.faces)
693    out = obj({ verts: m.verts, faces: m.faces })
694    meshesIn.set(out, { verts: m.verts, faces: m.faces, mesh: m })
695    PRIMITIVES.set(key, out)
696  }
697
698  return out
699}
700
701// A mesh as the code gives it: verts flat, faces flat triangles or lists of
702// indices (polygons, fanned into triangles). A mesh built once and drawn
703// every frame is read once, so its normals (kept per Mesh) are kept too.
704const meshesIn = new WeakMap<object, { verts: unknown; faces: unknown; mesh: Mesh }>()
705function meshIn(raw: unknown): Mesh | undefined {
706  if (typeof raw !== 'object' || raw === null) return undefined
707  const o = raw as Record<string, unknown>
708  if (!Array.isArray(o.verts) || !Array.isArray(o.faces)) return undefined
709  const known = meshesIn.get(raw)
710  if (known && known.verts === o.verts && known.faces === o.faces) return known.mesh
711  const mesh = readMesh(o)
712  meshesIn.set(raw, { verts: o.verts, faces: o.faces, mesh })
713
714  return mesh
715}
716
717function readMesh(o: Record<string, unknown>): Mesh {
718  if (!Array.isArray(o.verts) || !Array.isArray(o.faces)) return { verts: [], faces: [] }
719  const verts = o.verts.slice(0, 60_000).map(v => num(v))
720  const faces: number[] = []
721  const count = Math.floor(verts.length / 3)
722  const index = (v: unknown) => {
723    const i = Math.round(num(v, -1))
724
725    return i >= 0 && i < count ? i : -1
726  }
727  if (o.faces.every(f => Array.isArray(f))) {
728    // Polygons fan into triangles, up to the most one frame draws anyway.
729    for (const poly of o.faces.slice(0, 20_000) as unknown[][]) {
730      if (faces.length >= 3 * MOST_TRIANGLES) break
731      const ids = poly.slice(0, 3 * MOST_TRIANGLES).map(index)
732      if (ids.some(i => i < 0)) continue
733      for (let i = 1; i + 1 < ids.length && faces.length < 3 * MOST_TRIANGLES; i++) faces.push(ids[0]!, ids[i]!, ids[i + 1]!)
734    }
735  } else {
736    const ids = o.faces.slice(0, 60_000).map(index)
737    for (let i = 0; i + 2 < ids.length; i += 3) if (ids[i]! >= 0 && ids[i + 1]! >= 0 && ids[i + 2]! >= 0) faces.push(ids[i]!, ids[i + 1]!, ids[i + 2]!)
738  }
739
740  return { verts, faces }
741}
742
743type Bubble = { lines: string[]; x: number; y: number; wide: number; tall: number; base: Rgb; tail?: { col: number; row: number; char: string } }
744
745// A bubble's text as typed out so far, and its size.
746function typedBubble(text: string, age: number, cols: number) {
747  const all = wrap(text, Math.max(10, Math.min(30, cols - 6)))
748  let left = Math.floor((age * 1000) / BUBBLE_MS)
749  const lines = all.map(line => {
750    const part = line.slice(0, Math.max(0, left))
751    left -= line.length + 1
752
753    return part
754  })
755
756  return { lines, wide: Math.max(...all.map(line => line.length)) + 4, tall: all.length + 2 }
757}
758
759// Where a bubble goes: the clearest spot near its speaker, scored by how
760// many drawn cells it would cover and how far it strays from the speaker's
761// box; the last spot keeps its place unless a clearly better one opens, so
762// the bubble does not hop about. The tail notches the border on the side
763// that faces the speaker.
764function placeBubble(args: { speaker: Box; wide: number; tall: number; cols: number; rows: number; covered: Uint8Array; last?: { x: number; y: number } }) {
765  const { speaker, wide, tall, cols, rows, covered, last } = args
766  const bottom = rows - 1
767  const cx = (speaker.x0 + speaker.x1 + 1) / 2
768  const cost = (x: number, y: number) => {
769    let blocked = 0
770    for (let row = y; row < y + tall; row++) {
771      for (let col = x; col < x + wide; col++) if (covered[row * cols + col]) blocked += 1
772    }
773    const gap = y + tall <= speaker.y0 ? speaker.y0 - (y + tall) : y > speaker.y1 ? y - speaker.y1 - 1 : 0
774    const side = x + wide <= speaker.x0 ? speaker.x0 - (x + wide) : x > speaker.x1 ? x - speaker.x1 - 1 : 0
775
776    return blocked * 10 + Math.abs(x + wide / 2 - cx) * 0.08 + gap * 0.6 + side * 0.15
777  }
778  let best = { x: Math.max(0, Math.min(cols - wide, Math.round(cx - wide / 2))), y: Math.max(0, speaker.y0 - tall) }
779  let bestCost = Infinity
780  const lo = Math.max(0, Math.round(cx) - 50)
781  const hi = Math.min(cols - wide, Math.round(cx) + 50)
782  for (let y = 0; y + tall - 1 <= bottom; y++) {
783    // Every other column, and both bounds.
784    for (let x = lo; x <= hi + 1; x += 2) {
785      const xx = Math.min(x, hi)
786      const c = cost(xx, y)
787      if (c < bestCost) {
788        bestCost = c
789        best = { x: xx, y }
790      }
791    }
792  }
793  if (last && last.x >= 0 && last.x <= cols - wide && last.y >= 0 && last.y + tall - 1 <= bottom && cost(last.x, last.y) <= bestCost + 3) best = last
794  const { x, y } = best
795  const tailCol = Math.max(x + 2, Math.min(x + wide - 3, Math.round(cx)))
796  const tailRow = Math.max(y + 1, Math.min(y + tall - 2, Math.round((speaker.y0 + speaker.y1) / 2)))
797  const tail =
798    y + tall - 1 < speaker.y0 ? { col: tailCol, row: y + tall - 1, char: '┬' }
799    : y > speaker.y1 ? { col: tailCol, row: y, char: '┴' }
800    : x + wide - 1 < speaker.x0 ? { col: x + wide - 1, row: tailRow, char: '├' }
801    : x > speaker.x1 ? { col: x, row: tailRow, char: '┤' }
802    : undefined
803  // The bubble's cells count as drawn for the next bubble.
804  for (let row = y; row < y + tall; row++) for (let col = x; col < x + wide; col++) covered[row * cols + col] = 1
805
806  return { x, y, tail }
807}
808
809export type Stage = {
810  cols: number
811  rows: number
812  // The buddy's clock, in seconds: the backdrop's.
813  t: number
814  script: Script
815  previous?: Script
816  // Milliseconds since this script arrived (its actors' clock), and since
817  // the previous one did, so its code keeps its own time while it dissolves.
818  since: number
819  previousSince?: number
820  reveal: number
821}
822
823const BUBBLE_MS = 32
824// Where each speaker's bubble sat last frame.
825const lastBubble = new WeakMap<Actor, { x: number; y: number }>()
826const OLD_MS = 500
827
828// Words wrapped to lines no wider than `width`.
829function wrap(text: string, width: number) {
830  const lines: string[] = []
831  let line = ''
832  for (const word of text.split(' ')) {
833    const next = line ? `${line} ${word}` : word
834    if (next.length <= width) {
835      line = next
836    } else {
837      if (line) lines.push(line)
838      // A word too long for a line is broken across lines.
839      let rest = word
840      while (rest.length > width) {
841        lines.push(rest.slice(0, width))
842        rest = rest.slice(width)
843      }
844      line = rest
845    }
846  }
847  if (line) lines.push(line)
848
849  return lines
850}
851
852type Box = { x0: number; y0: number; x1: number; y1: number }
853type Placed = { actor: Actor; lines: string[]; x: number; y: number; width: number; alpha: number; look?: ClawdLook }
854
855// Draws one frame of a script: backdrop, swarms, actors, then speech on top,
856// with the backdrop cleared around everything that reads.
857export function stage(frame: Stage): string {
858  const { cols, rows, t, since, reveal } = frame
859  // A scene whose code broke with nothing else to show keeps the last scene
860  // on until the narrator's fixed one arrives.
861  const isBlank = (s: Script) => Boolean(s.code?.error) && s.actors.length === 0 && s.swarms.length === 0
862  const script = isBlank(frame.script) && frame.previous && !isBlank(frame.previous) ? frame.previous : frame.script
863  const previous = script === frame.script ? frame.previous : undefined
864  const words = new Uint32Array(cols * rows * 3)
865  paintBackground(words, {
866    cols,
867    rows,
868    t,
869    background: script.background,
870    previous: previous?.background,
871    fade: clamp(since / 1500),
872    reveal,
873    strength: 0.4,
874  })
875  // Rows above the band's risen front stay empty while it grows in.
876  const shown = (row: number) => row >= Math.floor(rows * (1 - clamp(reveal * 1.3)))
877  const clear = (box: Box) => {
878    for (let row = Math.max(0, box.y0); row <= Math.min(rows - 1, box.y1); row++) {
879      for (let col = Math.max(0, box.x0); col <= Math.min(cols - 1, box.x1); col++) {
880        if (gate && !gate(col, row)) continue
881        const at = (row * cols + col) * 3
882        words[at] = 0x20
883        words[at + 1] = CLEAR
884        words[at + 2] = CLEAR
885      }
886    }
887  }
888  const pack = (c: Rgb) => (Math.round(c[0]) << 16) | (Math.round(c[1]) << 8) | Math.round(c[2])
889  // While scenes dissolve, which cells this pass may touch.
890  let gate: ((col: number, row: number) => boolean) | undefined
891  const put = (col: number, row: number, char: string, color: Rgb, back?: Rgb) => {
892    if (col < 0 || col >= cols || row < 0 || row >= rows || !shown(row) || (gate && !gate(col, row))) return
893    const at = (row * cols + col) * 3
894    words[at] = char.codePointAt(0) ?? 0x20
895    words[at + 1] = pack(color)
896    words[at + 2] = back ? pack(back) : CLEAR
897  }
898
899  // Where every actor stands this frame, the outgoing scene's flickering out.
900  const placed: Placed[] = []
901  const scenes: [Script, number, number][] =
902    previous && since < OLD_MS ? [[previous, since / 1000 + 30, 1 - since / OLD_MS], [script, since / 1000, 1]] : [[script, since / 1000, 1]]
903  for (const [play, sceneT, alpha] of scenes) {
904    play.actors.forEach((actor, i) => {
905      const env = { t: sceneT, w: cols, h: rows, k: i, n: play.actors.length }
906      if (actor.show && actor.show(env) <= 0) return
907      const index = actor.frame ? Math.floor(actor.frame(env)) : Math.floor(sceneT * actor.fps)
908      const lines = actor.frames[mod(index, actor.frames.length)] ?? []
909      const width = Math.max(1, ...lines.map(line => line.trimEnd().length))
910      const x = actor.x(env)
911      let look: ClawdLook | undefined
912      if (actor.kind === 'clawd') {
913        // Clawd steps as far as it has walked, one frame each 1.5 columns, so
914        // its feet never slide; standing, it blinks now and then.
915        const dx = x - actor.x({ ...env, t: sceneT - 0.12 })
916        const isWalking = Math.abs(dx) >= 0.2
917        look = {
918          ...LOOK,
919          pose: isWalking ? 'walk' : 'stand',
920          stride: isWalking ? mod(Math.floor(x / 1.5), 4) : 0,
921          facing: isWalking ? Math.sign(dx) : 0,
922          isBlinking: !isWalking && hash(Math.floor(sceneT * 0.5), i) > 0.6 && fract(sceneT * 0.5) < 0.06,
923          color: parseHex(actor.color),
924        }
925      }
926      placed.push({ actor, lines, x: Math.round(x), y: Math.round(actor.y(env)), width, alpha, look })
927    })
928  }
929
930  // Each actor is solid inside its outline: on every line, the cells from its
931  // first drawn character to its last hide whatever lies behind them.
932  const spans = (one: Placed) =>
933    one.lines.map(line => {
934      const start = line.search(/\S/)
935
936      return start < 0 ? undefined : { x0: one.x + start, x1: one.x + line.trimEnd().length - 1 }
937    })
938  const covered = new Uint8Array(cols * rows)
939  for (const one of placed) {
940    spans(one).forEach((span, dy) => {
941      const row = one.y + dy
942      if (!span || row < 0 || row >= rows) return
943      for (let col = Math.max(0, span.x0); col <= Math.min(cols - 1, span.x1); col++) covered[row * cols + col] = 1
944    })
945  }
946
947  // Speech: a bubble in the clearest spot near its speaker.
948  const sceneT = since / 1000
949  const bubbles: Bubble[] = []
950  const saidNow = new Set<string>()
951  for (const one of placed) {
952    const { actor } = one
953    if (one.alpha < 1 || !actor.say || sceneT < actor.sayAt) continue
954    const typed = Math.floor(((sceneT - actor.sayAt) * 1000) / BUBBLE_MS)
955    const all = wrap(actor.say, Math.max(10, Math.min(30, cols - 6)))
956    let left = typed
957    const lines = all.map(line => {
958      const part = line.slice(0, Math.max(0, left))
959      left -= line.length + 1
960
961      return part
962    })
963    // The text plus a border and a space of padding either side. A bubble
964    // the strip cannot hold whole (while the band grows in) waits.
965    const wide = Math.max(...all.map(line => line.length)) + 4
966    const tall = all.length + 2
967    if (tall > rows || wide > cols) continue
968    const bottom = rows - 1
969    const outline = spans(one)
970    const drawn = outline.flatMap((span, dy) => (span ? [{ ...span, row: one.y + dy }] : []))
971    // An actor with nothing drawn (a blank narrator) speaks from its box.
972    const speaker: Box =
973      drawn.length > 0
974        ? {
975            x0: Math.min(...drawn.map(d => d.x0)),
976            x1: Math.max(...drawn.map(d => d.x1)),
977            y0: Math.min(...drawn.map(d => d.row)),
978            y1: Math.max(...drawn.map(d => d.row)),
979          }
980        : { x0: one.x, x1: one.x + Math.max(0, ...one.lines.map(l => l.length - 1)), y0: one.y, y1: one.y + one.lines.length - 1 }
981    const { x, y, tail } = placeBubble({ speaker, wide, tall, cols, rows, covered, last: lastBubble.get(actor) })
982    lastBubble.set(actor, { x, y })
983    bubbles.push({ lines, x, y, wide, tall, base: parseHex(actor.color), tail })
984  }
985
986  // Room to read: the backdrop gives way around the actors.
987  for (const one of placed) {
988    spans(one).forEach((span, dy) => span && clear({ x0: span.x0 - 1, y0: one.y + dy, x1: span.x1 + 1, y1: one.y + dy }))
989  }
990
991  // Swarms first, behind everything.
992  for (const [play, playT, alpha] of scenes) {
993    const env: Env = { t: playT, w: cols, h: rows, k: 0, n: 0 }
994    for (const swarm of play.swarms) {
995      const color = parseHex(swarm.color)
996      for (let k = 0; k < swarm.count; k++) {
997        if (alpha < 1 && hash(k, Math.floor(since / 60)) > alpha) continue
998        const at = { ...env, k, n: swarm.count }
999        const col = Math.round(swarm.x(at))
1000        const row = Math.round(swarm.y(at))
1001        put(col, row, swarm.glyphs[k % swarm.glyphs.length] ?? '*', color)
1002      }
1003    }
1004  }
1005  // One pixel, two to a cell: the cell keeps whatever its other half holds
1006  // (a backdrop pixel, an earlier pixel) and shows the new one in its half.
1007  const plotPixel = (col: number, py: number, color: Rgb) => {
1008    const row = Math.floor(py / 2)
1009    if (col < 0 || col >= cols || row < 0 || row >= rows || !shown(row) || (gate && !gate(col, row))) return
1010    const at = (row * cols + col) * 3
1011    const ch = words[at]
1012    const fg = words[at + 1]!
1013    const bg = words[at + 2]!
1014    let top: number | undefined
1015    let bottom: number | undefined
1016    if (ch === 0x2580) {
1017      top = fg
1018      bottom = bg === CLEAR ? undefined : bg
1019    } else if (ch === 0x2584) {
1020      bottom = fg
1021      top = bg === CLEAR ? undefined : bg
1022    } else if (ch === 0x2588) {
1023      top = bottom = fg
1024    } else if (ch === 0x20 && bg !== CLEAR) {
1025      top = bottom = bg
1026    } else if (ch !== undefined && ch >= 0x2596 && ch <= 0x259f) {
1027      // A quarter-block edge from the 3D world: each half counts as the
1028      // glyph's color where the glyph has anything in it.
1029      const q = QUARTER_HALVES[ch - 0x2596] ?? 0
1030      if (q & 1) top = fg
1031      if (q & 2) bottom = fg
1032    }
1033    if (py % 2 === 0) top = pack(color)
1034    else bottom = pack(color)
1035    if (top !== undefined) {
1036      words[at] = 0x2580
1037      words[at + 1] = top
1038      words[at + 2] = bottom ?? CLEAR
1039    } else {
1040      words[at] = 0x2584
1041      words[at + 1] = bottom ?? CLEAR
1042      words[at + 2] = CLEAR
1043    }
1044  }
1045
1046  // The cells the code has drawn characters and sprites on this frame (not
1047  // fills), and the last Clawd it drew: what its speech bubbles stay off.
1048  const inked = new Uint8Array(cols * rows)
1049  let isFilling = false
1050  const ink = (col: number, row: number) => {
1051    if (!isFilling && col >= 0 && col < cols && row >= 0 && row < rows) inked[row * cols + col] = 1
1052  }
1053  let lastClawd: Box | undefined
1054  const mark = (box: Box) => {
1055    lastClawd = box
1056    for (let row = Math.max(0, box.y0); row <= Math.min(rows - 1, box.y1); row++) for (let col = Math.max(0, box.x0); col <= Math.min(cols - 1, box.x1); col++) ink(col, row)
1057  }
1058  // The cells speech bubbles have taken this frame.
1059  const bubbled = new Uint8Array(cols * rows)
1060
1061  // The frame's 3D samples, made when the code first draws in 3D, and drawn
1062  // as glyphs before any 2D drawing that follows, so that sits on top.
1063  let frame3d: Frame | undefined
1064  let isDirty3d = false
1065  const frameOf = () => (frame3d ??= Object.assign(createFrame(cols, rows), { tick: () => spend() }))
1066  const flush3d = () => {
1067    if (frame3d && isDirty3d) composite(frame3d, put)
1068    isDirty3d = false
1069  }
1070  // A frame that draws for too long (a fill the size of the world, thousands
1071  // of meshes) stops the scene the way a runaway loop does.
1072  const deadline = performance.now() + FRAME_MS
1073  let calls = 0
1074  const spend = () => {
1075    if ((calls += 1) % 256 === 0 && performance.now() > deadline) throw new ScriptError(`a frame took more than ${FRAME_MS}ms to draw`)
1076  }
1077
1078  // Clawd at a column and a pixel row, some of its pixels held back while it
1079  // fades in.
1080  const paintClawd = (x: number, py: number, look: ClawdLook, isHidden?: (px: { x: number; y: number }) => boolean) => {
1081    for (const px of clawdPixels(look)) {
1082      if (isHidden?.(px)) continue
1083      plotPixel(x + px.x, py + px.y, px.c)
1084    }
1085
1086    return clawdAnchors(look)
1087  }
1088
1089  // A scene's code draws over the backdrop and swarms, under the actors:
1090  // the outgoing scene's for a moment, dissolving cell by cell into the new
1091  // one's, then the new one's. Each cell flips once, at its own moment.
1092  const runCode = (play: Script, playT: number, isOutgoing: boolean) => {
1093    const code = play.code
1094    if (!code?.program || code.error) return
1095    frame3d = undefined
1096    isDirty3d = false
1097    const dt = code.isStarted ? clamp(playT - code.lastT, 0, 0.25) : 0
1098    code.lastT = playT
1099    const globals = { t: playT, dt, w: cols, h: rows }
1100    let scene3d: { frame: Frame; view: View; light: Light; fog: Fog } | undefined
1101    canvas = {
1102      cols,
1103      rows,
1104      code,
1105      put: (col, row, char, color, back) => {
1106        spend()
1107        flush3d()
1108        put(col, row, char, color, back)
1109        ink(col, row)
1110      },
1111      fill: (on: boolean) => {
1112        isFilling = on
1113      },
1114      clear: (col, row) => {
1115        spend()
1116        flush3d()
1117        clear({ x0: col, y0: row, x1: col, y1: row })
1118      },
1119      pixel: (col, py, color) => {
1120        spend()
1121        flush3d()
1122        plotPixel(col, py, color)
1123        ink(col, Math.floor(py / 2))
1124      },
1125      clawd: (x, py, look) => {
1126        spend()
1127        flush3d()
1128        const a = paintClawd(x, py, look)
1129        mark({ x0: x - 1, y0: Math.floor(py / 2), x1: x + a.w, y1: Math.floor((py + a.h - 1) / 2) })
1130
1131        return a
1132      },
1133      pixel3d: (col, py, color, depth) => {
1134        spend()
1135        flush3d()
1136        if (!claimPixel(frameOf(), col, py, depth)) return
1137        plotPixel(col, py, color)
1138        ink(col, Math.floor(py / 2))
1139      },
1140      mark,
1141      scene3d: (isDrawing = true) => {
1142        if (isDrawing) {
1143          spend()
1144          isDirty3d = true
1145        }
1146        if (!scene3d) {
1147          scene3d = {
1148            frame: frameOf(),
1149            view: resolve(code.cam ?? DEFAULT_CAMERA, cols * SX, rows * SY),
1150            light: code.light ?? DEFAULT_LIGHT,
1151            fog: code.fog ?? DEFAULT_FOG,
1152          }
1153        }
1154
1155        return scene3d
1156      },
1157      say: (text, x, y, color) => {
1158        spend()
1159        if (isOutgoing) return
1160        // The same line twice in a frame is one bubble.
1161        if (saidNow.has(text)) return
1162        saidNow.add(text)
1163        flush3d()
1164        // A new line types out; one that only changed from a line already
1165        // typed (a live count, a timer) shows whole rather than restarting.
1166        let first = code.said.get(text)
1167        if (first === undefined) {
1168          const isTyped = (line: string, at: number) => (playT - at) * 1000 >= line.length * BUBBLE_MS
1169          first = [...code.said].some(([line, at]) => isTyped(line, at)) ? -Infinity : playT
1170          if (code.said.size >= 32) code.said.clear()
1171          code.said.set(text, first)
1172        }
1173        const { lines, wide, tall } = typedBubble(text, playT - first, cols)
1174        if (wide > cols || tall > rows) return
1175        // The speaker is the Clawd last drawn when the point is on or beside
1176        // it; otherwise the point itself.
1177        const near = lastClawd && x >= lastClawd.x0 - 2 && x <= lastClawd.x1 + 2 && y >= lastClawd.y0 - 1 && y <= lastClawd.y1 + 1
1178        const speaker: Box = near && lastClawd ? lastClawd : { x0: x, y0: y, x1: x, y1: y }
1179        // The bubble keeps clear of what is drawn in runs (Clawd, labels,
1180        // props) and of earlier bubbles, not of a scattering of rain, snow
1181        // or sparks: a drawn cell counts when the cells on both sides of it,
1182        // or above and below it, are drawn too.
1183        const covered = new Uint8Array(cols * rows)
1184        const drawn = (col: number, row: number) => col >= 0 && col < cols && row >= 0 && row < rows && inked[row * cols + col] === 1
1185        for (let row = 0; row < rows; row++) {
1186          for (let col = 0; col < cols; col++) {
1187            const i = row * cols + col
1188            if (bubbled[i] || (inked[i] && ((drawn(col - 1, row) && drawn(col + 1, row)) || (drawn(col, row - 1) && drawn(col, row + 1))))) covered[i] = 1
1189          }
1190        }
1191        const placed3 = placeBubble({ speaker, wide, tall, cols, rows, covered, last: code.bubblesAt.get(text) })
1192        bubbled.set(covered)
1193        if (code.bubblesAt.size >= 32) code.bubblesAt.clear()
1194        code.bubblesAt.set(text, { x: placed3.x, y: placed3.y })
1195        bubbles.push({ lines, x: placed3.x, y: placed3.y, wide, tall, base: color, tail: placed3.tail })
1196      },
1197    }
1198    try {
1199      if (!code.isStarted) {
1200        code.isStarted = true
hooks/effects.ts 267 lines
1// Procedural effects for the spinner: each one a field from a pixel to a
2// value in [0, 1], mapped through the scene's palette and packed into a
3// Raster's cells (upper half blocks, two pixels a row).
4
5export const EFFECTS = [
6  'plasma', 'fire', 'starfield', 'rain', 'tunnel', 'lava', 'glitch', 'aurora', 'waves', 'pulse', 'fireworks',
7] as const
8export type Effect = (typeof EFFECTS)[number]
9
10// The ambient layer behind a scene: one effect, faint and see-through.
11export type Background = {
12  effect: Effect
13  palette: string[]
14  speed: number
15  intensity: number
16}
17
18type Field = (x: number, y: number, t: number, w: number, h: number) => number
19
20const fract = (v: number) => v - Math.floor(v)
21const clamp = (v: number, lo = 0, hi = 1) => Math.min(hi, Math.max(lo, v))
22const hash = (x: number, y: number) => fract(Math.sin(x * 12.9898 + y * 78.233) * 43758.5453)
23
24function noise(x: number, y: number) {
25  const ix = Math.floor(x)
26  const iy = Math.floor(y)
27  const fx = x - ix
28  const fy = y - iy
29  const sx = fx * fx * (3 - 2 * fx)
30  const sy = fy * fy * (3 - 2 * fy)
31  const a = hash(ix, iy)
32  const b = hash(ix + 1, iy)
33  const c = hash(ix, iy + 1)
34  const d = hash(ix + 1, iy + 1)
35
36  return a + (b - a) * sx + (c - a) * sy + (a - b - c + d) * sx * sy
37}
38
39// Cells are about twice as tall as wide, so x distances count half.
40const polar = (x: number, y: number, w: number, h: number) => {
41  const dx = (x - w / 2) * 0.5
42  const dy = y - h / 2
43
44  return { r: Math.hypot(dx, dy), a: Math.atan2(dy, dx) }
45}
46
47const FIELDS: Record<Effect, Field> = {
48  plasma: (x, y, t) =>
49    (Math.sin(x * 0.08 + t) + Math.sin(y * 0.3 + t * 1.3) + Math.sin((x + y) * 0.05 + t * 0.7) +
50      Math.sin(Math.hypot(x * 0.5, y) * 0.2 - t)) / 8 + 0.5,
51  fire: (x, y, t, _w, h) => clamp(noise(x * 0.15, y * 0.45 + t * 3) * 1.5 * Math.pow(y / h, 1.3) + noise(x * 0.4, t * 5) * 0.15 * (y / h)),
52  starfield: (x, y, t, w, h) => {
53    const { r, a } = polar(x, y, w, h)
54    const ray = Math.floor((a + Math.PI) * 24)
55    const z = fract(hash(ray, 3) + t * 0.25)
56    const reach = Math.hypot(w * 0.25, h * 0.5)
57
58    return clamp(Math.exp(-Math.abs(r - z * reach) * 1.2) * z * 1.6)
59  },
60  rain: (x, y, t, _w, h) => {
61    const head = fract(hash(x, 2) + t * (0.3 + hash(x, 1) * 0.5)) * (h + 12)
62    const d = head - y
63
64    return d >= 0 && d < 10 ? 1 - d / 10 : hash(x, Math.floor(y + t * 4)) * 0.08
65  },
66  tunnel: (x, y, t, w, h) => {
67    const { r, a } = polar(x, y, w, h)
68
69    return 0.5 + 0.5 * Math.sin(30 / (r + 2) + t * 4) * Math.cos(a * 3 + t)
70  },
71  lava: (x, y, t, w, h) => {
72    let sum = 0
73    for (let i = 0; i < 6; i++) {
74      const bx = (0.5 + 0.45 * Math.sin(t * (0.3 + i * 0.11) + i * 2.1)) * w
75      const by = (0.5 + 0.45 * Math.cos(t * (0.4 + i * 0.07) + i * 1.3)) * h
76      sum += 6 / (Math.hypot((x - bx) * 0.5, y - by) + 0.5)
77    }
78
79    return clamp((sum - 2.2) / 2)
80  },
81  glitch: (x, y, t) => {
82    const frame = Math.floor(t * 9)
83    const shift = hash(Math.floor(y / 2), frame) > 0.75 ? Math.floor(hash(y, frame) * 24) : 0
84    const block = hash(Math.floor((x + shift) / 5), Math.floor(y / 3) + frame * 7)
85
86    return block > 0.6 ? block : 0.5 + 0.5 * Math.sin((x + shift) * 0.1 + t * 2) * 0.4
87  },
88  aurora: (x, y, t, _w, h) => {
89    const center = h * (0.45 + 0.25 * Math.sin(x * 0.04 + t * 0.6) + 0.12 * Math.sin(x * 0.11 - t * 1.4))
90    const band = (y - center) / (2.5 + 3 * noise(x * 0.05, t * 0.5))
91
92    return clamp(Math.exp(-band * band) * (0.6 + 0.4 * noise(x * 0.2, t * 2)))
93  },
94  waves: (x, y, t, _w, h) => {
95    const surface = h * (0.5 + 0.3 * Math.sin(x * 0.09 - t * 2) * Math.sin(x * 0.023 + t * 0.7))
96
97    return y > surface ? clamp(0.55 + 0.45 * Math.sin(x * 0.2 + y * 0.6 - t * 3)) : 0.1 * noise(x * 0.1, y * 0.3 + t)
98  },
99  pulse: (x, y, t, w, h) => {
100    const { r } = polar(x, y, w, h)
101    const ring = fract(t * 0.6) * Math.hypot(w * 0.25, h * 0.5)
102
103    return clamp(Math.exp(-Math.abs(r - ring) * 0.6) + 0.15 * Math.sin(r * 0.5 - t * 3))
104  },
105  fireworks: (x, y, t, w, h) => {
106    let v = 0
107    for (let k = 0; k < 4; k++) {
108      const cycle = Math.floor(t / 1.6 + k * 0.37)
109      const age = fract(t / 1.6 + k * 0.37)
110      const cx = hash(k, cycle) * w
111      const cy = (0.2 + hash(cycle, k) * 0.6) * h
112      const r = Math.hypot((x - cx) * 0.5, y - cy)
113      const sparkle = hash(Math.floor(Math.atan2(y - cy, x - cx) * 6), cycle) > 0.4 ? 1 : 0.3
114      v += Math.exp(-Math.abs(r - age * 14) * 1.3) * (1 - age) * sparkle
115    }
116
117    return clamp(v)
118  },
119}
120
121function parseHex(hex: string): [number, number, number] {
122  const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim())
123  if (!m || m[1] === undefined) return [128, 128, 128]
124  const s = m[1].length === 3 ? [...m[1]].map(c => c + c).join('') : m[1]
125
126  return [parseInt(s.slice(0, 2), 16), parseInt(s.slice(2, 4), 16), parseInt(s.slice(4, 6), 16)]
127}
128
129type Rgb = [number, number, number]
130
131function shade(stops: Rgb[], v: number): Rgb {
132  const p = clamp(v) * (stops.length - 1)
133  const i = Math.min(stops.length - 2, Math.floor(p))
134  const k = p - i
135  const a = stops[i] ?? [0, 0, 0]
136  const b = stops[i + 1] ?? a
137
138  return [a[0] + (b[0] - a[0]) * k, a[1] + (b[1] - a[1]) * k, a[2] + (b[2] - a[2]) * k]
139}
140
141const pack = ([r, g, b]: Rgb) => (Math.round(r) << 16) | (Math.round(g) << 8) | Math.round(b)
142const mix = (a: Rgb, b: Rgb, k: number): Rgb => [a[0] + (b[0] - a[0]) * k, a[1] + (b[1] - a[1]) * k, a[2] + (b[2] - a[2]) * k]
143const dim = (c: Rgb, k: number): Rgb => [c[0] * k, c[1] * k, c[2] * k]
144
145const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
146
147function base64(bytes: Uint8Array) {
148  let out = ''
149  let i = 0
150  for (; i + 2 < bytes.length; i += 3) {
151    const v = ((bytes[i] ?? 0) << 16) | ((bytes[i + 1] ?? 0) << 8) | (bytes[i + 2] ?? 0)
152    out += (B64[(v >> 18) & 63] ?? '') + (B64[(v >> 12) & 63] ?? '') + (B64[(v >> 6) & 63] ?? '') + (B64[v & 63] ?? '')
153  }
154  if (i < bytes.length) {
155    const v = ((bytes[i] ?? 0) << 16) | ((bytes[i + 1] ?? 0) << 8)
156    out += (B64[(v >> 18) & 63] ?? '') + (B64[(v >> 12) & 63] ?? '')
157    out += i + 1 < bytes.length ? (B64[(v >> 6) & 63] ?? '') + '=' : '=='
158  }
159
160  return out
161}
162
163// The terminal's own color: what shows through wherever nothing is drawn.
164export const CLEAR = 0x01000000
165
166// A 4x4 ordered dither: a pixel of opacity a shows where a beats its cell, so
167// a fade reads as a thinning pattern rather than a color against a background
168// the oracle cannot know.
169const BAYER = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5].map(v => (v + 0.5) / 16)
170const bayer = (x: number, y: number) => BAYER[(y & 3) * 4 + (x & 3)] ?? 0.5
171
172export const smooth = (lo: number, hi: number, v: number) => {
173  const k = clamp((v - lo) / (hi - lo))
174
175  return k * k * (3 - 2 * k)
176}
177
178export type Paint = {
179  cols: number
180  rows: number
181  t: number
182  background: Background
183  // The background being faded out, and how far the fade has gone (0 to 1).
184  previous?: Background
185  fade: number
186  // How far the band has risen into view, 0 to 1, from the bottom up.
187  reveal: number
188  // How strongly the backdrop shows, 0 to 1: kept low behind a scene.
189  strength: number
190}
191
192// Paints the backdrop into `words` (a Raster's cells): half blocks where the
193// effect is dense enough, the terminal's own color everywhere else, thinning
194// toward the band's edges and its rising front.
195export function paintBackground(words: Uint32Array, paint: Paint) {
196  const { cols, rows, t, background, previous, fade, reveal, strength } = paint
197  const w = cols
198  const h = rows * 2
199  const stops = background.palette.map(parseHex)
200  const oldStops = previous ? previous.palette.map(parseHex) : stops
201  const pixel = (x: number, y: number): { c: Rgb; a: number } => {
202    const v = FIELDS[background.effect](x, y, t * background.speed, w, h)
203    let c = shade(stops, 0.15 + 0.85 * v)
204    let a = smooth(0.2, 0.75, v) * (0.55 + 0.45 * background.intensity)
205    if (previous && fade < 1) {
206      const pv = FIELDS[previous.effect](x, y, t * previous.speed, w, h)
207      c = mix(shade(oldStops, 0.15 + 0.85 * pv), c, fade)
208      a = a * fade + smooth(0.2, 0.75, pv) * (0.55 + 0.45 * previous.intensity) * (1 - fade)
209    }
210    const edge = smooth(0, w * 0.14, Math.min(x, w - 1 - x)) * smooth(0, 2.5, Math.min(y + 0.5, h - 0.5 - y))
211    const rise = (h - 1 - y) / Math.max(1, h - 1)
212    const front = reveal * 1.35 - 0.3 * noise(x * 0.18, t * 1.5)
213    a *= strength * edge * smooth(0, 0.25, front - rise)
214
215    return { c, a }
216  }
217  for (let row = 0; row < rows; row++) {
218    for (let col = 0; col < cols; col++) {
219      const at = (row * cols + col) * 3
220      const top = pixel(col, row * 2)
221      const bottom = pixel(col, row * 2 + 1)
222      const isTop = top.a > bayer(col, row * 2)
223      const isBottom = bottom.a > bayer(col, row * 2 + 1)
224      words[at] = isTop || !isBottom ? (isTop ? 0x2580 : 0x20) : 0x2584
225      words[at + 1] = isTop ? pack(top.c) : isBottom ? pack(bottom.c) : CLEAR
226      words[at + 2] = isTop && isBottom ? pack(bottom.c) : CLEAR
227    }
228  }
229}
230
231export { parseHex, shade, mix, dim, hash, clamp, type Rgb }
232
233export function encode(words: Uint32Array) {
234  return base64(new Uint8Array(words.buffer))
235}
236
237export function cleanBackground(raw: unknown): Background | undefined {
238  if (typeof raw !== 'object' || raw === null) return undefined
239  const s = raw as Record<string, unknown>
240  const effect = EFFECTS.find(name => name === s.effect)
241  if (!effect) return undefined
242  const palette = Array.isArray(s.palette)
243    ? s.palette.filter((c): c is string => typeof c === 'string' && /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.test(c.trim())).slice(0, 6)
244    : []
245  if (palette.length < 2) return undefined
246
247  return {
248    effect,
249    palette,
250    speed: clamp(typeof s.speed === 'number' ? s.speed : 1, 0.1, 4),
251    intensity: clamp(typeof s.intensity === 'number' ? s.intensity : 0.6),
252  }
253}
254
255// Text the Raster can draw: one printable, single-width character a cell.
256export function cleanText(text: string, most: number) {
257  return text
258    .replace(/[‘’]/g, "'")
259    .replace(/[“”]/g, '"')
260    .replace(/[–—]/g, '-')
261    .replace(/…/g, '...')
262    .replace(/[^\x20-\x7e]/g, '')
263    .replace(/\s+/g, ' ')
264    .trim()
265    .slice(0, most)
266}
267
hooks/clawd.ts 264 lines
1// Clawd, the mascot, in pixel art: a rounded orange block with two eyes and
2// four little feet, 14 pixels wide and 8 tall at scale 1, which is 4 rows of
3// cells (two pixels to a cell). Scenes draw it with a pose, a facing, eyes,
4// a color and a scale, and get back where its head, sides and feet are, so
5// they can hang a hat on it or put a prop in its hand.
6
7import type { Rgb } from './effects'
8import type { Mesh } from './render3d'
9
10export const CLAWD_W = 14
11export const CLAWD_H = 8
12
13// The body: B is the body, E an eye (its resting place, 1 wide and 2 tall).
14// The claws are drawn per pose beside it.
15const BODY = [
16  '..BBBBBBBBBB..',
17  '..BBBBBBBBBB..',
18  '.BBBEBBBBBEBB.',
19  '.BBBEBBBBBEBB.',
20  '.BBBBBBBBBBBB.',
21  '..BBBBBBBBBB..',
22]
23
24export type Arm = 'up' | 'out' | 'down'
25export const ARMS: Arm[] = ['up', 'out', 'down']
26
27// A claw's pixels for each pose, for the left side (x from the body's left
28// edge at 1, so 0 is just outside it); the right side mirrors them. Each
29// ends at its tip, where things are held.
30const CLAWS: Record<Arm, { pixels: [number, number][]; tip: [number, number] }> = {
31  up: { pixels: [[0, 1], [0, 2]], tip: [0, 0] },
32  out: { pixels: [[-1, 2], [0, 2]], tip: [-2, 2] },
33  down: { pixels: [[0, 3], [0, 4]], tip: [0, 5] },
34}
35const BODY_H = BODY.length
36// The feet: two pairs under the body, each foot a pixel wide and two tall.
37const FEET = [3, 5, 8, 10]
38const EYES = [4, 9]
39const EYE_ROW = 2
40
41export const CLAWD_ORANGE: Rgb = [217, 119, 87]
42const EYE_DARK: Rgb = [42, 22, 16]
43
44export type Pose = 'stand' | 'walk' | 'jump' | 'sit'
45export type Eyes = 'open' | 'closed' | 'wide'
46
47export type ClawdLook = {
48  pose: Pose
49  // Which walk frame (0 to 3) while walking: a step cycle, so a walker's
50  // feet match its travel when this comes from its x.
51  stride: number
52  // -1 left, 0 ahead, 1 right: where the eyes look, and which way the feet
53  // swing.
54  facing: number
55  eyes: Eyes
56  // The eyes shifted from where they rest, in pixels: a glance.
57  look: { x: number; y: number }
58  isBlinking: boolean
59  color: Rgb
60  eyeColor: Rgb
61  // Whole pixels per pixel: 1 is 14x8, 2 is 28x16.
62  scale: number
63  // Where each claw is held.
64  arms: { left: Arm; right: Arm }
65}
66
67export const LOOK: ClawdLook = {
68  pose: 'stand',
69  stride: 0,
70  facing: 0,
71  eyes: 'open',
72  look: { x: 0, y: 0 },
73  isBlinking: false,
74  color: CLAWD_ORANGE,
75  eyeColor: EYE_DARK,
76  scale: 1,
77  arms: { left: 'up', right: 'up' },
78}
79
80export type Pixel = { x: number; y: number; c: Rgb }
81
82// Where things attach, in pixels from the sprite's top-left: above the head,
83// at either side, at the feet, and at the eyes.
84export type Anchors = {
85  w: number
86  h: number
87  top: { x: number; y: number }
88  left: { x: number; y: number }
89  right: { x: number; y: number }
90  feet: { x: number; y: number }
91  eyes: { x: number; y: number; w: number }
92}
93
94// The sprite's pixels at scale 1, body first and eyes last so a body pixel
95// never covers an eye that glanced aside.
96function pixelsAtOne(look: ClawdLook): Pixel[] {
97  const body: Pixel[] = []
98  const eyes: Pixel[] = []
99  // Sitting, the body rests on the ground where the feet would be.
100  const drop = look.pose === 'sit' ? CLAWD_H - BODY_H : 0
101  BODY.forEach((row, y) => {
102    for (let x = 0; x < row.length; x++) if (row[x] !== '.') body.push({ x, y: y + drop, c: look.color })
103  })
104  for (const [px, py] of CLAWS[look.arms.left].pixels) body.push({ x: px, y: py + drop, c: look.color })
105  for (const [px, py] of CLAWS[look.arms.right].pixels) body.push({ x: CLAWD_W - 1 - px, y: py + drop, c: look.color })
106  const ex = Math.max(-1, Math.min(1, Math.round(look.look.x))) || look.facing
107  const ey = Math.max(-1, Math.min(1, Math.round(look.look.y)))
108  for (const x0 of EYES) {
109    const x = x0 + ex
110    const y = EYE_ROW + ey + drop
111    if (look.eyes === 'closed' || look.isBlinking) {
112      // A line where the eye's lower pixel was.
113      eyes.push({ x, y: y + 1, c: look.eyeColor }, { x: x + (x0 === EYES[0] ? -1 : 1), y: y + 1, c: look.eyeColor })
114    } else if (look.eyes === 'wide') {
115      for (let dx = -1; dx <= 0; dx++) for (let dy = 0; dy <= 1; dy++) eyes.push({ x: x + dx, y: y + dy, c: look.eyeColor })
116    } else {
117      eyes.push({ x, y, c: look.eyeColor }, { x, y: y + 1, c: look.eyeColor })
118    }
119  }
120  const feet: Pixel[] = []
121  if (look.pose !== 'sit') {
122    const top = BODY_H
123    const ground = CLAWD_H - 1
124    const dir = look.facing || 1
125    FEET.forEach((x0, i) => {
126      // The left pair and the right pair swing against each other.
127      const isFront = i < 2
128      let swing = 0
129      let isLifted = false
130      if (look.pose === 'walk') {
131        // A four-beat step: the pairs swing past each other, and the pair
132        // mid-swing is off the ground.
133        const beat = ((Math.round(look.stride) % 4) + 4) % 4
134        if (beat === 0) swing = isFront ? 1 : -1
135        else if (beat === 2) swing = isFront ? -1 : 1
136        else isLifted = beat === 1 ? !isFront : isFront
137      } else if (look.pose === 'jump') {
138        isLifted = true
139      }
140      const x = x0 + swing * dir
141      for (let y = top; y <= (isLifted ? ground - 1 : ground); y++) feet.push({ x, y, c: look.color })
142    })
143  }
144  // A claw held out reaches a pixel past the box on either side.
145  const inside = (p: Pixel) => p.x >= -1 && p.x <= CLAWD_W && p.y >= 0 && p.y < CLAWD_H
146
147  return [...body, ...feet, ...eyes].filter(inside)
148}
149
150// The sprite's pixels at its scale, each pixel of the small sprite drawn as
151// a block of scale by scale.
152export function clawdPixels(look: ClawdLook): Pixel[] {
153  const scale = Math.max(1, Math.min(4, Math.round(look.scale)))
154  const small = pixelsAtOne(look)
155  if (scale === 1) return small
156  const out: Pixel[] = []
157  for (const p of small) {
158    for (let dy = 0; dy < scale; dy++) for (let dx = 0; dx < scale; dx++) out.push({ x: p.x * scale + dx, y: p.y * scale + dy, c: p.c })
159  }
160
161  return out
162}
163
164export function clawdAnchors(look: ClawdLook): Anchors {
165  const s = Math.max(1, Math.min(4, Math.round(look.scale)))
166  const drop = look.pose === 'sit' ? CLAWD_H - BODY_H : 0
167  // The claws' tips, where things are held.
168  const leftTip = CLAWS[look.arms.left].tip
169  const rightTip = CLAWS[look.arms.right].tip
170
171  return {
172    w: CLAWD_W * s,
173    h: CLAWD_H * s,
174    top: { x: Math.floor((CLAWD_W / 2) * s), y: drop * s - 1 },
175    left: { x: leftTip[0] * s - (s - 1), y: (leftTip[1] + drop) * s },
176    right: { x: (CLAWD_W - 1 - rightTip[0]) * s + (s - 1), y: (rightTip[1] + drop) * s },
177    feet: { x: Math.floor((CLAWD_W / 2) * s), y: CLAWD_H * s },
178    eyes: { x: EYES[0]! * s, y: (EYE_ROW + drop) * s, w: (EYES[1]! - EYES[0]! + 1) * s },
179  }
180}
181
182// Clawd as a solid for 3D scenes, in the sprite's pixel units: x across its
183// middle, y up from its feet, z toward the viewer. The body is the sprite's
184// outline as a block as deep as it is tall, with feet at its corners and the
185// claws beside it. The eyes are not part of it: a scene draws them flat on
186// its face, where clawdPoints puts them.
187const meshes = new Map<string, Mesh>()
188const BODY_DEPTH = 3
189
190export function clawdMesh(look: ClawdLook): Mesh {
191  const drop = look.pose === 'sit' ? CLAWD_H - BODY_H : 0
192  const beat = look.pose === 'walk' ? ((Math.round(look.stride) % 4) + 4) % 4 : -1
193  const key = `${look.pose}|${beat}|${look.arms.left}|${look.arms.right}`
194  const done = meshes.get(key)
195  if (done) return done
196  const verts: number[] = []
197  const faces: number[] = []
198  const vert = (x: number, y: number, z: number) => {
199    verts.push(x, y, z)
200
201    return verts.length / 3 - 1
202  }
203  const poly = (ids: number[]) => {
204    for (let i = 1; i + 1 < ids.length; i++) faces.push(ids[0]!, ids[i]!, ids[i + 1]!)
205  }
206  const block = (x0: number, x1: number, y0: number, y1: number, z0: number, z1: number) => {
207    const c = [vert(x0, y0, z0), vert(x1, y0, z0), vert(x1, y1, z0), vert(x0, y1, z0), vert(x0, y0, z1), vert(x1, y0, z1), vert(x1, y1, z1), vert(x0, y1, z1)]
208    for (const q of [[0, 1, 2, 3], [5, 4, 7, 6], [4, 0, 3, 7], [1, 5, 6, 2], [3, 2, 6, 7], [4, 5, 1, 0]]) poly(q.map(i => c[i]!))
209  }
210  // The body: 12 wide, 10 at its top and bottom rows, as a chamfered prism.
211  const bottom = CLAWD_H - BODY_H - drop
212  const top = CLAWD_H - drop
213  const outline: [number, number][] = [[-5, bottom], [5, bottom], [6, bottom + 1], [6, top - 1], [5, top], [-5, top], [-6, top - 1], [-6, bottom + 1]]
214  const front = outline.map(([x, y]) => vert(x, y, BODY_DEPTH))
215  const back = outline.map(([x, y]) => vert(x, y, -BODY_DEPTH))
216  poly(front)
217  poly([...back].reverse())
218  for (let i = 0; i < outline.length; i++) {
219    const j = (i + 1) % outline.length
220    poly([front[i]!, back[i]!, back[j]!, front[j]!])
221  }
222  // Feet at the corners; walking lifts one diagonal pair, then the other.
223  if (look.pose !== 'sit') {
224    for (const [fx, fz, diagonal] of [[-2.5, 1.5, 0], [2.5, -1.5, 0], [-2.5, -1.5, 1], [2.5, 1.5, 1]] as const) {
225      const isLifted = look.pose === 'jump' || (beat === 1 && diagonal === 0) || (beat === 3 && diagonal === 1)
226      block(fx - 0.5, fx + 0.5, isLifted ? 1 : 0, bottom, fz - 0.5, fz + 0.5)
227    }
228  }
229  // The claws, each pixel a block, mirrored for the right side.
230  for (const [side, arm] of [[-1, look.arms.left], [1, look.arms.right]] as const) {
231    for (const [px, py] of CLAWS[arm].pixels) {
232      const col = side < 0 ? px : CLAWD_W - 1 - px
233      block(col - CLAWD_W / 2, col - CLAWD_W / 2 + 1, CLAWD_H - py - 1 - drop, CLAWD_H - py - drop, -0.5, 0.5)
234    }
235  }
236  const mesh = { verts, faces }
237  meshes.set(key, mesh)
238
239  return mesh
240}
241
242// Where things attach on the solid, in its pixel units (z toward the viewer):
243// above the head, the claw tips, under the feet, and the eyes on its face,
244// each eye's center with how it looks (open, wide, or shut as a line), and
245// the span the two eyes cover.
246export function clawdPoints(look: ClawdLook) {
247  const drop = look.pose === 'sit' ? CLAWD_H - BODY_H : 0
248  const ex = Math.max(-1, Math.min(1, Math.round(look.look.x))) || look.facing
249  const ey = Math.max(-1, Math.min(1, Math.round(look.look.y)))
250  const half = CLAWD_W / 2
251  const tipAt = (side: number, tip: [number, number]) => ({ x: (side < 0 ? tip[0] : CLAWD_W - 1 - tip[0]) + 0.5 - half + (side < 0 ? -0.5 : 0.5), y: CLAWD_H - tip[1] - 0.5 - drop, z: 0 })
252  const eyeY = CLAWD_H - EYE_ROW - ey - 1 - drop
253  const shut = look.eyes === 'closed' || look.isBlinking
254
255  return {
256    top: { x: 0, y: CLAWD_H - drop, z: 0 },
257    left: tipAt(-1, CLAWS[look.arms.left].tip),
258    right: tipAt(1, CLAWS[look.arms.right].tip),
259    feet: { x: 0, y: 0, z: 0 },
260    eyes: EYES.map(col => ({ x: col + ex + 0.5 - half, y: eyeY, z: BODY_DEPTH, kind: shut ? ('shut' as const) : look.eyes === 'wide' ? ('wide' as const) : ('open' as const) })),
261    eyeSpan: { x: EYES[0]! + ex - half, y: eyeY, z: BODY_DEPTH, w: EYES[1]! - EYES[0]! + 1 },
262  }
263}
264
hooks/lang.ts 1698 lines
1// A small JavaScript-like language for scenes the buddy writes as code. The
2// module's environment has no eval, and the code comes from a model reading
3// the developer's repo, so it runs here, in an interpreter of its own: it sees
4// only its own values and the functions handed to it, and every step spends
5// fuel, so a runaway loop stops the scene rather than the terminal.
6//
7// What it knows: let/const/var, functions and arrow functions (closures),
8// if/else, for, for-of, for-in, while, break/continue/return, numbers,
9// strings and template literals, arrays, plain objects, destructuring, the
10// usual operators, Math, and a whitelist of array and string methods.
11
12// A value of the language: a number, string, boolean, null, undefined, array,
13// plain object, or function.
14// eslint-disable-next-line @typescript-eslint/no-explicit-any
15type V = any
16
17export class ScriptError extends Error {}
18
19// ---------------------------------------------------------------- tokens
20
21type Tok =
22  | { k: 'num'; v: number; at: number }
23  | { k: 'str'; v: string; at: number }
24  | { k: 'tpl'; strs: string[]; exprs: string[]; at: number }
25  | { k: 'id' | 'op' | 'eof'; v: string; at: number }
26
27const OPS = [
28  '>>>=', '===', '!==', '**=', '>>>', '<<=', '>>=', '...', '=>', '==', '!=', '<=', '>=', '&&', '||', '??', '?.', '++', '--',
29  '+=', '-=', '*=', '/=', '%=', '**', '<<', '>>', '&=', '|=', '^=', '&&=', '||=', '??=',
30].sort((a, b) => b.length - a.length)
31const SINGLE = '{}()[];,.?:+-*/%<>=!&|^~'
32
33function lex(src: string): Tok[] {
34  const toks: Tok[] = []
35  let i = 0
36  const fail = (what: string) => {
37    throw new ScriptError(`${what} at line ${src.slice(0, i).split('\n').length}`)
38  }
39  while (i < src.length) {
40    const c = src[i] as string
41    if (/\s/.test(c)) {
42      i++
43    } else if (src.startsWith('//', i)) {
44      while (i < src.length && src[i] !== '\n') i++
45    } else if (src.startsWith('/*', i)) {
46      const end = src.indexOf('*/', i + 2)
47      i = end < 0 ? src.length : end + 2
48    } else if (/[0-9]/.test(c) || (c === '.' && /[0-9]/.test(src[i + 1] ?? ''))) {
49      const m = /^(0x[0-9a-f_]+|0b[01_]+|(\d[\d_]*\.?[\d_]*|\.\d[\d_]*)(e[+-]?\d+)?)/i.exec(src.slice(i))
50      const text = m?.[0] ?? c
51      toks.push({ k: 'num', v: Number(text.replace(/_/g, '')), at: i })
52      i += text.length
53    } else if (/[A-Za-z_$]/.test(c)) {
54      const m = /^[A-Za-z_$][A-Za-z0-9_$]*/.exec(src.slice(i))
55      const text = m?.[0] ?? c
56      toks.push({ k: 'id', v: text, at: i })
57      i += text.length
58    } else if (c === '"' || c === "'") {
59      const at = i
60      let v = ''
61      i++
62      while (i < src.length && src[i] !== c) {
63        if (src[i] === '\\') {
64          const [ch, next] = escapeAt(src, i + 1)
65          v += ch
66          i = next
67        } else {
68          v += src[i]
69          i++
70        }
71      }
72      if (i >= src.length) fail('unclosed string')
73      i++
74      toks.push({ k: 'str', v, at })
75    } else if (c === '`') {
76      const at = i
77      const strs: string[] = []
78      const exprs: string[] = []
79      let v = ''
80      i++
81      while (i < src.length && src[i] !== '`') {
82        if (src[i] === '\\') {
83          const [ch, next] = escapeAt(src, i + 1)
84          v += ch
85          i = next
86        } else if (src.startsWith('${', i)) {
87          strs.push(v)
88          v = ''
89          const start = i + 2
90          i = closeOf(src, start)
91          exprs.push(src.slice(start, i))
92          i++
93        } else {
94          v += src[i]
95          i++
96        }
97      }
98      if (i >= src.length) fail('unclosed template')
99      i++
100      strs.push(v)
101      toks.push({ k: 'tpl', strs, exprs, at })
102    } else {
103      // "?." before a digit is a ternary and a number: t > 0 ? .5 : 1
104      const op = OPS.find(o => src.startsWith(o, i) && !(o === '?.' && /\d/.test(src[i + 2] ?? ''))) ?? (SINGLE.includes(c) ? c : undefined)
105      if (!op) fail(`unexpected "${c}"`)
106      toks.push({ k: 'op', v: op as string, at: i })
107      i += (op as string).length
108    }
109  }
110  toks.push({ k: 'eof', v: '', at: src.length })
111
112  return toks
113}
114
115const SIMPLE_ESCAPES: Record<string, string> = { n: '\n', t: '\t', r: '\r', b: '\b', f: '\f', v: '\v', '0': '\0' }
116
117// The character an escape at i (just after its backslash) stands for, and
118// where the escape ends.
119function escapeAt(src: string, i: number): [string, number] {
120  const c = src[i] ?? ''
121  const simple = SIMPLE_ESCAPES[c]
122  if (simple !== undefined) return [simple, i + 1]
123  if (c === 'x' || c === 'u') {
124    const m = (c === 'x' ? /^[0-9a-f]{2}/i : /^([0-9a-f]{4}|\{[0-9a-f]{1,6}\})/i).exec(src.slice(i + 1, i + 9))
125    const code = m ? parseInt(m[0].replace(/[{}]/g, ''), 16) : NaN
126    if (m && code <= 0x10ffff) return [String.fromCodePoint(code), i + 1 + m[0].length]
127  }
128
129  return [c, i + 1]
130}
131
132// From just after a template's "${", the index of the "}" that closes it,
133// stepping over strings and nested templates so their braces do not count.
134function closeOf(src: string, i: number): number {
135  let depth = 1
136  while (i < src.length) {
137    const c = src[i]
138    if (c === '"' || c === "'" || c === '`') {
139      i = skipString(src, i)
140      continue
141    }
142    if (c === '{') depth++
143    else if (c === '}' && --depth === 0) return i
144    i++
145  }
146
147  return i
148}
149
150// The index just past the string literal that opens at i.
151function skipString(src: string, i: number): number {
152  const q = src[i]
153  i++
154  while (i < src.length && src[i] !== q) {
155    if (src[i] === '\\') i += 2
156    else if (q === '`' && src.startsWith('${', i)) i = closeOf(src, i + 2) + 1
157    else i++
158  }
159
160  return i + 1
161}
162
163// ---------------------------------------------------------------- syntax
164
165type Node = { t: string; [key: string]: V }
166
167const ASSIGN = new Set(['=', '+=', '-=', '*=', '/=', '%=', '**=', '<<=', '>>=', '>>>=', '&=', '|=', '^=', '&&=', '||=', '??='])
168const BINARY: Record<string, number> = {
169  '??': 1, '||': 2, '&&': 3, '|': 4, '^': 5, '&': 6,
170  '==': 7, '!=': 7, '===': 7, '!==': 7,
171  '<': 8, '>': 8, '<=': 8, '>=': 8, in: 8,
172  '<<': 9, '>>': 9, '>>>': 9,
173  '+': 10, '-': 10, '*': 11, '/': 11, '%': 11, '**': 12,
174}
175
176function parse(src: string): Node[] {
177  const toks = lex(src)
178  let p = 0
179  const peek = (o = 0) => toks[Math.min(p + o, toks.length - 1)] as Tok
180  const fail = (what: string): never => {
181    throw new ScriptError(`${what} at line ${src.slice(0, peek().at).split('\n').length}`)
182  }
183  const is = (v: string, o = 0) => {
184    const tok = peek(o)
185
186    return (tok.k === 'op' || tok.k === 'id') && tok.v === v
187  }
188  const eat = (v: string) => {
189    if (!is(v)) fail(`expected "${v}" but found "${'v' in peek() ? String((peek() as { v: unknown }).v) : peek().k}"`)
190    p++
191  }
192  const maybe = (v: string) => (is(v) ? (p++, true) : false)
193  const name = () => {
194    const tok = peek()
195    if (tok.k !== 'id') fail('expected a name')
196    p++
197
198    return (tok as { v: string }).v
199  }
200  const semi = () => {
201    maybe(';')
202  }
203
204  function program(): Node[] {
205    const body: Node[] = []
206    while (peek().k !== 'eof') body.push(statement())
207
208    return body
209  }
210
211  function block(): Node[] {
212    eat('{')
213    const body: Node[] = []
214    while (!is('}')) {
215      if (peek().k === 'eof') fail('missing "}"')
216      body.push(statement())
217    }
218    eat('}')
219
220    return body
221  }
222
223  function pattern(): Node {
224    let target: Node
225    if (maybe('[')) {
226      const items: (Node | null)[] = []
227      while (!is(']')) {
228        if (is(',')) items.push(null)
229        else items.push(restOr(pattern))
230        if (!is(']')) eat(',')
231      }
232      eat(']')
233      target = { t: 'arrPat', items }
234    } else if (maybe('{')) {
235      const props: [string, Node][] = []
236      while (!is('}')) {
237        if (maybe('...')) {
238          props.push(['...', { t: 'rest', target: pattern() }])
239        } else {
240          const key = name()
241          props.push([key, maybe(':') ? pattern() : withDefault({ t: 'id', name: key })])
242        }
243        if (!is('}')) eat(',')
244      }
245      eat('}')
246      target = { t: 'objPat', props }
247    } else {
248      target = { t: 'id', name: name() }
249    }
250
251    return withDefault(target)
252  }
253  function withDefault(target: Node): Node {
254    return maybe('=') ? { t: 'default', target, def: assign() } : target
255  }
256  // A "...rest" item in a list pattern, or an ordinary one.
257  function restOr(item: () => Node): Node {
258    return maybe('...') ? { t: 'rest', target: pattern() } : item()
259  }
260
261  // An array or object literal on the left of "=" is a pattern: [a, b] = [b, a].
262  function toPattern(e: Node): Node {
263    switch (e.t) {
264      case 'id':
265      case 'member':
266        return e
267      case 'spread':
268        return { t: 'rest', target: toPattern(e.e) }
269      case 'assign':
270        if (e.op === '=') return { t: 'default', target: toPattern(e.target), def: e.value }
271        break
272      case 'arr':
273        return { t: 'arrPat', items: (e.items as Node[]).map(toPattern) }
274      case 'obj':
275        return {
276          t: 'objPat',
277          props: (e.props as [Node | string, Node][]).map(([k, v]) => (typeof k === 'string' ? [k, k === '...' ? { t: 'rest', target: toPattern(v) } : toPattern(v)] : fail('cannot assign to that'))),
278        }
279    }
280
281    return fail('cannot assign to that')
282  }
283
284  function declaration(): Node {
285    const kind = name()
286    const decls: [Node, Node | undefined][] = []
287    do {
288      const target = pattern()
289      decls.push(target.t === 'default' ? [target.target, target.def] : [target, undefined])
290    } while (maybe(','))
291
292    return { t: 'decl', kind, decls }
293  }
294
295  function params(): Node[] {
296    eat('(')
297    const list: Node[] = []
298    while (!is(')')) {
299      list.push(restOr(pattern))
300      if (!is(')')) eat(',')
301    }
302    eat(')')
303
304    return list
305  }
306
307  function statement(): Node {
308    const tok = peek()
309    if (is('{')) return { t: 'block', body: block() }
310    if (is(';')) {
311      p++
312
313      return { t: 'empty' }
314    }
315    if (tok.k === 'id') {
316      switch (tok.v) {
317        case 'let':
318        case 'const':
319        case 'var': {
320          const d = declaration()
321          semi()
322
323          return d
324        }
325        case 'function': {
326          p++
327          const fnName = name()
328
329          return { t: 'fnDecl', name: fnName, fn: { t: 'fn', name: fnName, params: params(), body: block() } }
330        }
331        case 'if': {
332          p++
333          eat('(')
334          const test = expression()
335          eat(')')
336          const then = statement()
337
338          return { t: 'if', test, then, otherwise: maybe('else') ? statement() : undefined }
339        }
340        case 'while': {
341          p++
342          eat('(')
343          const test = expression()
344          eat(')')
345
346          return { t: 'while', test, body: statement() }
347        }
348        case 'do': {
349          p++
350          const body = statement()
351          eat('while')
352          eat('(')
353          const test = expression()
354          eat(')')
355          semi()
356
357          return { t: 'doWhile', test, body }
358        }
359        case 'for': {
360          p++
361          eat('(')
362          const isDecl = is('let') || is('const') || is('var')
363          if (isDecl && (is('of', 2) || is('in', 2) || peek(1).k === 'op')) {
364            // for (const x of xs), for (const [a, b] of xs), for (const k in o)
365            const save = p
366            const kind = name()
367            const target = pattern()
368            if (is('of') || is('in')) {
369              const each = name()
370              const iter = expression()
371              eat(')')
372
373              return { t: each === 'of' ? 'forOf' : 'forIn', kind, target, iter, body: statement() }
374            }
375            p = save
376          }
377          const init = is(';') ? undefined : isDecl ? declaration() : { t: 'expr', e: expression() }
378          eat(';')
379          const test = is(';') ? undefined : expression()
380          eat(';')
381          const update = is(')') ? undefined : expression()
382          eat(')')
383
384          return { t: 'for', init, test, update, body: statement() }
385        }
386        case 'switch': {
387          p++
388          eat('(')
389          const disc = expression()
390          eat(')')
391          eat('{')
392          const cases: { test?: Node; body: Node[] }[] = []
393          while (!is('}')) {
394            let test: Node | undefined
395            if (maybe('case')) test = expression()
396            else eat('default')
397            eat(':')
398            const body: Node[] = []
399            while (!is('case') && !is('default') && !is('}')) {
400              if (peek().k === 'eof') fail('missing "}"')
401              body.push(statement())
402            }
403            cases.push({ test, body })
404          }
405          eat('}')
406
407          return { t: 'switch', disc, cases }
408        }
409        case 'return': {
410          p++
411          const value = is(';') || is('}') ? undefined : expression()
412          semi()
413
414          return { t: 'return', value }
415        }
416        case 'break':
417        case 'continue':
418          p++
419          semi()
420
421          return { t: tok.v }
422      }
423    }
424    const e = expression()
425    semi()
426
427    return { t: 'expr', e }
428  }
429
430  function expression(): Node {
431    let e = assign()
432    while (maybe(',')) e = { t: 'seq', a: e, b: assign() }
433
434    return e
435  }
436
437  // An arrow function starts here: x =>, () =>, (a, b) =>, ([a, b]) =>.
438  function isArrow(): boolean {
439    if (peek().k === 'id' && is('=>', 1)) return true
440    if (!is('(')) return false
441    let depth = 0
442    for (let o = 0; ; o++) {
443      const tok = peek(o)
444      if (tok.k === 'eof') return false
445      if (tok.k === 'op' && ['(', '[', '{'].includes(tok.v)) depth++
446      if (tok.k === 'op' && [')', ']', '}'].includes(tok.v)) depth--
447      if (depth === 0) return is('=>', o + 1)
448    }
449  }
450
451  function assign(): Node {
452    if (isArrow()) {
453      const list = peek().k === 'id' ? [{ t: 'id', name: name() }] : params()
454      eat('=>')
455      if (is('{')) return { t: 'fn', arrow: true, params: list, body: block() }
456
457      return { t: 'fn', arrow: true, params: list, body: [{ t: 'return', value: assign() }] }
458    }
459    const left = conditional()
460    const tok = peek()
461    if (tok.k === 'op' && ASSIGN.has(tok.v)) {
462      const target = tok.v === '=' && (left.t === 'arr' || left.t === 'obj') ? toPattern(left) : left
463      if (!['id', 'member', 'arrPat', 'objPat'].includes(target.t)) fail('cannot assign to that')
464      p++
465
466      return { t: 'assign', op: tok.v, target, value: assign() }
467    }
468
469    return left
470  }
471
472  function conditional(): Node {
473    const test = binary(1)
474    if (!maybe('?')) return test
475    const a = assign()
476    eat(':')
477
478    return { t: 'cond', test, a, b: assign() }
479  }
480
481  function binary(min: number): Node {
482    let left = unary()
483    for (;;) {
484      const tok = peek()
485      const op = tok.k === 'op' || (tok.k === 'id' && tok.v === 'in') ? tok.v : undefined
486      const prec = op === undefined ? undefined : BINARY[op]
487      if (op === undefined || prec === undefined || prec < min) return left
488      p++
489      // ** binds to the right; the rest to the left.
490      const right = binary(op === '**' ? prec : prec + 1)
491      left = { t: ['&&', '||', '??'].includes(op) ? 'logic' : 'bin', op, l: left, r: right }
492    }
493  }
494
495  function unary(): Node {
496    const tok = peek()
497    if (tok.k === 'op' && ['-', '+', '!', '~'].includes(tok.v)) {
498      p++
499
500      return { t: 'unary', op: tok.v, arg: unary() }
501    }
502    if (tok.k === 'op' && (tok.v === '++' || tok.v === '--')) {
503      p++
504
505      return { t: 'update', op: tok.v, prefix: true, target: unary() }
506    }
507    if (tok.k === 'id' && tok.v === 'typeof') {
508      p++
509
510      return { t: 'typeof', arg: unary() }
511    }
512    const e = postfix()
513    const after = peek()
514    if (after.k === 'op' && (after.v === '++' || after.v === '--')) {
515      p++
516
517      return { t: 'update', op: after.v, prefix: false, target: e }
518    }
519
520    return e
521  }
522
523  function args(): Node[] {
524    eat('(')
525    const list: Node[] = []
526    while (!is(')')) {
527      list.push(maybe('...') ? { t: 'spread', e: assign() } : assign())
528      if (!is(')')) eat(',')
529    }
530    eat(')')
531
532    return list
533  }
534
535  function postfix(): Node {
536    let e: Node
537    if (maybe('new')) {
538      const callee = primary()
539      e = { t: 'call', callee, args: is('(') ? args() : [] }
540    } else {
541      e = primary()
542    }
543    // Past a "?.", the whole rest of the chain is optional: o?.a.b() is
544    // undefined when o is, as in JavaScript.
545    let optional = false
546    for (;;) {
547      if (maybe('?.')) {
548        optional = true
549        if (is('(')) {
550          e = { t: 'call', callee: e, args: args(), optional }
551          continue
552        }
553        if (!is('[')) {
554          e = { t: 'member', obj: e, prop: { t: 'lit', v: name() }, optional }
555          continue
556        }
557      }
558      if (maybe('.')) e = { t: 'member', obj: e, prop: { t: 'lit', v: name() }, optional }
559      else if (maybe('[')) {
560        const prop = expression()
561        eat(']')
562        e = { t: 'member', obj: e, prop, optional }
563      } else if (is('(')) e = { t: 'call', callee: e, args: args(), optional }
564      else return e
565    }
566  }
567
568  function primary(): Node {
569    const tok = peek()
570    p++
571    if (tok.k === 'num') return { t: 'lit', v: tok.v }
572    if (tok.k === 'str') return { t: 'lit', v: tok.v }
573    if (tok.k === 'tpl') return { t: 'tpl', strs: tok.strs, exprs: tok.exprs.map(source => parseExpression(source)) }
574    if (tok.k === 'id') {
575      if (tok.v === 'true') return { t: 'lit', v: true }
576      if (tok.v === 'false') return { t: 'lit', v: false }
577      if (tok.v === 'null') return { t: 'lit', v: null }
578      if (tok.v === 'undefined') return { t: 'lit', v: undefined }
579      if (tok.v === 'function') {
580        const fnName = peek().k === 'id' ? name() : undefined
581
582        // A function expression's name is its own to use inside; a
583        // declaration's is the enclosing scope's.
584        return { t: 'fn', name: fnName, self: fnName !== undefined, params: params(), body: block() }
585      }
586
587      return { t: 'id', name: tok.v }
588    }
589    if (tok.k === 'op' && tok.v === '(') {
590      const e = expression()
591      eat(')')
592
593      return e
594    }
595    if (tok.k === 'op' && tok.v === '[') {
596      const items: Node[] = []
597      while (!is(']')) {
598        items.push(maybe('...') ? { t: 'spread', e: assign() } : assign())
599        if (!is(']')) eat(',')
600      }
601      eat(']')
602
603      return { t: 'arr', items }
604    }
605    if (tok.k === 'op' && tok.v === '{') {
606      const props: [Node | string, Node][] = []
607      while (!is('}')) {
608        if (maybe('...')) {
609          props.push(['...', assign()])
610        } else {
611          const key = peek()
612          let k: Node | string
613          if (maybe('[')) {
614            k = expression()
615            eat(']')
616          } else {
617            p++
618            k = key.k === 'num' || key.k === 'str' || key.k === 'id' ? String((key as { v: unknown }).v) : fail('bad property name')
619          }
620          if (typeof k === 'string' && (is(',') || is('}'))) props.push([k, { t: 'id', name: k }])
621          else if (typeof k === 'string' && is('(')) props.push([k, { t: 'fn', name: k, params: params(), body: block() }])
622          else {
623            eat(':')
624            props.push([k, assign()])
625          }
626        }
627        if (!is('}')) eat(',')
628      }
629      eat('}')
630
631      return { t: 'obj', props }
632    }
633
634    return fail(`unexpected "${(tok as { v?: unknown }).v ?? (tok as { k: string }).k}"`)
635  }
636
637  function parseExpression(source: string): Node {
638    const [first] = parse(`(${source});`)
639
640    return (first as Node).e as Node
641  }
642
643  return program()
644}
645
646// ---------------------------------------------------------------- running
647
648// The fuel left for the current run; every statement, loop turn and call
649// spends one.
650let fuel = 0
651// When the run must be over by the clock: what a host method does on one
652// unit of fuel (copying a huge object, reading a long string as a number)
653// is caught here when fuel alone would not catch it.
654let deadline = Infinity
655const late = () => {
656  if (performance.now() > deadline) throw new ScriptError('the scene code took too long to run')
657}
658const burn = () => {
659  if (--fuel < 0) throw new ScriptError('the scene code took too long to run')
660  if ((fuel & 255) === 0) late()
661}
662// Work over n elements inside one host method spends fuel too, so a loop of
663// slices over a huge array cannot run for seconds on one unit each.
664const cost = (n: number) => {
665  fuel -= Math.max(0, Math.min(n, 2 ** 31 - 1)) >> 5
666  if (fuel < 0) throw new ScriptError('the scene code took too long to run')
667  if (n >= 1024) late()
668}
669// The keys of an object about to be copied or walked, paid for.
670const keysOf = (v: Record<string, V>) => sized(Object.keys(v))
671
672// The largest array the code may build, and the longest string.
673const MAX_LENGTH = 100_000
674
675class Scope {
676  vars = new Map<string, V>()
677  consts?: Set<string>
678  // A function's own scope, where its var declarations land.
679  constructor(readonly parent?: Scope, readonly isFunction = false) {}
680  declare(name: string, value: V, isConst = false) {
681    this.vars.set(name, value)
682    if (isConst) (this.consts ??= new Set()).add(name)
683    else this.consts?.delete(name)
684  }
685  lookup(name: string): V {
686    for (let s: Scope | undefined = this; s; s = s.parent) if (s.vars.has(name)) return s.vars.get(name)
687    throw new ScriptError(`${name} is not defined`)
688  }
689  assign(name: string, value: V) {
690    for (let s: Scope | undefined = this; s; s = s.parent) {
691      if (s.vars.has(name)) {
692        if (s.consts?.has(name)) throw new ScriptError(`${name} is a const`)
693        s.vars.set(name, value)
694
695        return
696      }
697    }
698    throw new ScriptError(`${name} is not defined`)
699  }
700}
701
702// A function the code made.
703class Fn {
704  constructor(
705    readonly params: Node[],
706    readonly body: Exec,
707    readonly scope: Scope,
708    readonly name?: string,
709    readonly self = false,
710    // An arrow function keeps the `this` of where it was written.
711    readonly isArrow = false,
712  ) {}
713}
714
715type Eval = (s: Scope) => V
716type Signal = undefined | 'break' | 'continue' | { value: V }
717type Exec = (s: Scope) => Signal
718
719export const obj = (entries: Record<string, V> = {}) => Object.assign(Object.create(null) as Record<string, V>, entries)
720const isObj = (v: V): v is Record<string, V> => typeof v === 'object' && v !== null && !Array.isArray(v) && Object.getPrototypeOf(v) === null
721const describe = (v: V) => (v === null ? 'null' : v === undefined ? 'undefined' : typeof v === 'string' ? `"${v.slice(0, 20)}"` : Array.isArray(v) ? 'an array' : typeof v)
722
723// What a value reads as in a string: objects have no prototype here, so the
724// host's String would throw on them.
725const str = (v: V, depth = 0): string => {
726  if (Array.isArray(v)) return depth > 8 ? '' : joinAll(v, ',', depth + 1)
727  if (isObj(v)) return '[object Object]'
728  if (v instanceof Fn || typeof v === 'function') return `function ${v.name ?? ''}() {}`
729
730  return String(v)
731}
732const joinAll = (a: V[], sep: string, depth = 0): string => {
733  cost(a.length)
734  let out = ''
735  for (let i = 0; i < a.length; i++) {
736    if (i > 0) out += sep
737    const v = a[i]
738    if (v !== null && v !== undefined) out += str(v, depth)
739    checkLength(out.length)
740  }
741
742  return out
743}
744
745// `self` is what `this` means inside: the holder of a method call.
746export function call(f: V, args: V[], self?: V): V {
747  burn()
748  if (f instanceof Fn) {
749    const scope = new Scope(f.scope, true)
750    if (f.self) scope.declare(f.name as string, f)
751    if (!f.isArrow) scope.declare('this', self)
752    bindList(f.params, args, scope, 'let')
753    scope.declare('arguments', args)
754    const signal = f.body(scope)
755
756    return typeof signal === 'object' ? signal.value : undefined
757  }
758  if (typeof f === 'function') return (f as (...a: V[]) => V)(...args)
759  throw new ScriptError(`${describe(f)} is not a function`)
760}
761
762const checkLength = (n: number) => {
763  if (n > MAX_LENGTH) throw new ScriptError('an array or string grew too big')
764
765  return n
766}
767
768// The length of the array a method over a would build or walk.
769const sized = (a: V[]) => (cost(a.length), a)
770
771const ARRAY: Record<string, (a: V[], ...args: V[]) => V> = {
772  push: (a, ...v) => (checkLength(a.length + v.length), a.push(...v)),
773  pop: a => a.pop(),
774  shift: a => sized(a).shift(),
775  unshift: (a, ...v) => (checkLength(a.length + v.length), sized(a).unshift(...v)),
776  slice: (a, s, e) => sized(a).slice(s, e),
777  splice: (a, s, n, ...v) => (s === undefined ? [] : (checkLength(a.length + v.length), sized(a).splice(s, n === undefined ? a.length : n, ...v))),
778  concat: (a, ...v) => {
779    let n = a.length
780    for (const x of v) n += Array.isArray(x) ? x.length : 1
781    cost(checkLength(n))
782
783    return a.concat(...v)
784  },
785  indexOf: (a, v, from) => sized(a).indexOf(v, from),
786  lastIndexOf: (a, v, from) => (from === undefined ? sized(a).lastIndexOf(v) : sized(a).lastIndexOf(v, from)),
787  includes: (a, v, from) => sized(a).includes(v, from),
788  join: (a, sep) => joinAll(a, sep === undefined ? ',' : str(sep)),
789  reverse: a => sized(a).reverse(),
790  fill: (a, v, s, e) => sized(a).fill(v, s, e),
791  at: (a, i) => a.at(i),
792  map: (a, f) => a.map((v, i) => call(f, [v, i, a])),
793  filter: (a, f) => a.filter((v, i) => call(f, [v, i, a])),
794  forEach: (a, f) => void a.forEach((v, i) => call(f, [v, i, a])),
795  some: (a, f) => a.some((v, i) => call(f, [v, i, a])),
796  every: (a, f) => a.every((v, i) => call(f, [v, i, a])),
797  find: (a, f) => a.find((v, i) => call(f, [v, i, a])),
798  findIndex: (a, f) => a.findIndex((v, i) => call(f, [v, i, a])),
799  reduce: (a, f, ...init) =>
800    init.length > 0 ? a.reduce((acc, v, i) => call(f, [acc, v, i, a]), init[0]) : a.reduce((acc, v, i) => call(f, [acc, v, i, a])),
801  sort: (a, f) => {
802    cost(a.length * Math.ceil(Math.log2(a.length + 1)))
803
804    const byString = (x: V, y: V) => {
805      const sx = str(x)
806      const sy = str(y)
807
808      return sx < sy ? -1 : sx > sy ? 1 : 0
809    }
810
811    return a.sort(f === undefined ? byString : (x, y) => Number(call(f, [x, y])) || 0)
812  },
813  flat: a => {
814    let n = 0
815    for (const x of a) n += Array.isArray(x) ? x.length : 1
816    cost(checkLength(n))
817
818    return a.flat()
819  },
820}
821
822const text = (s: string) => (cost(s.length), s)
823const padTo = (n: V) => (cost(checkLength(Number(n) || 0)), n)
824
825const STRING: Record<string, (s: string, ...args: V[]) => V> = {
826  slice: (s, a, b) => text(s).slice(a, b),
827  substring: (s, a, b) => text(s).substring(a, b),
828  charAt: (s, i) => s.charAt(i),
829  charCodeAt: (s, i) => s.charCodeAt(i),
830  codePointAt: (s, i) => s.codePointAt(i),
831  indexOf: (s, v, from) => text(s).indexOf(str(v), from),
832  lastIndexOf: (s, v, from) => text(s).lastIndexOf(str(v), from),
833  includes: (s, v, from) => text(s).includes(str(v), from),
834  startsWith: (s, v, from) => s.startsWith(str(v), from),
835  endsWith: (s, v, end) => s.endsWith(str(v), end),
836  split: (s, sep, limit) => text(s).split(sep === undefined ? (undefined as unknown as string) : str(sep), limit),
837  repeat: (s, n) => {
838    const times = Math.max(0, Math.floor(Number(n) || 0))
839    cost(checkLength(s.length * times))
840
841    return s.repeat(times)
842  },
843  padStart: (s, n, f) => s.padStart(padTo(n), f === undefined ? undefined : str(f)),
844  padEnd: (s, n, f) => s.padEnd(padTo(n), f === undefined ? undefined : str(f)),
845  toUpperCase: s => text(s).toUpperCase(),
846  toLowerCase: s => text(s).toLowerCase(),
847  trim: s => text(s).trim(),
848  replace: (s, a, b) => {
849    const out = text(s).replace(str(a), str(b))
850    checkLength(out.length)
851
852    return out
853  },
854  replaceAll: (s, a, b) => {
855    const parts = text(s).split(str(a))
856    const glue = str(b)
857    checkLength(s.length + (parts.length - 1) * glue.length)
858
859    return parts.join(glue)
860  },
861  at: (s, i) => s.at(i),
862}
863
864const own = <T>(table: Record<string, T>, key: V): T | undefined =>
865  Object.prototype.hasOwnProperty.call(table, str(key)) ? table[str(key)] : undefined
866
867// Names hung on a library function, as Array.from hangs on Array.
868const STATICS = new WeakMap<object, Record<string, V>>()
869
870function get(target: V, key: V): V {
871  if (target === null || target === undefined) throw new ScriptError(`cannot read "${str(key)}" of ${target}`)
872  if (Array.isArray(target)) {
873    if (typeof key === 'number') return target[key]
874    if (key === 'length') return target.length
875    const m = own(ARRAY, key)
876    if (m) return (...a: V[]) => m(target, ...a)
877    if (/^\d+$/.test(str(key))) return target[Number(key)]
878
879    return undefined
880  }
881  if (typeof target === 'string') {
882    if (typeof key === 'number' || /^\d+$/.test(str(key))) return target[Number(key)]
883    if (key === 'length') return target.length
884    const m = own(STRING, key)
885
886    return m ? (...a: V[]) => m(target, ...a) : undefined
887  }
888  if (typeof target === 'number') {
889    if (key === 'toFixed') return (d: V) => target.toFixed(Math.max(0, Math.min(20, Number(d) || 0)))
890
891    return undefined
892  }
893  if (isObj(target)) return own(target, key)
894  if (typeof target === 'function') {
895    const statics = STATICS.get(target)
896
897    return statics ? own(statics, key) : undefined
898  }
899
900  return undefined
901}
902
903function set(target: V, key: V, value: V): V {
904  if (Array.isArray(target)) {
905    if (key === 'length') {
906      target.length = checkLength(Math.max(0, Math.floor(Number(value) || 0)))
907
908      return value
909    }
910    const i = Number(key)
911    if (!Number.isInteger(i) || i < 0) throw new ScriptError(`bad array index ${str(key)}`)
912    checkLength(i + 1)
913    target[i] = value
914
915    return value
916  }
917  if (isObj(target)) {
918    if (Object.isFrozen(target)) throw new ScriptError(`cannot set "${str(key)}": built-in objects are read-only`)
919    target[str(key)] = value
920
921    return value
922  }
923  throw new ScriptError(`cannot set "${str(key)}" on ${describe(target)}`)
924}
925
926// An operand read as a number or compared loosely: a long string costs its
927// length to scan, and a plain object (which has no toString here) or an array
928// reads as the string it would in JavaScript, instead of throwing.
929const operand = (v: V): V => {
930  if (typeof v === 'string') {
931    cost(v.length)
932
933    return v
934  }
935
936  return isObj(v) || Array.isArray(v) ? str(v) : v
937}
938
939function binop(op: string, a: V, b: V): V {
940  if (op === 'in') return isObj(b) ? Object.prototype.hasOwnProperty.call(b, str(a)) : Array.isArray(b) ? Number(a) in b : false
941  if (op === '==' || op === '!=') {
942    // Two objects are the same only when they are one object.
943    const same = isPrimitive(a) || isPrimitive(b) ? operand(a) == operand(b) : a === b // eslint-disable-line eqeqeq
944
945    return op === '==' ? same : !same
946  }
947  if (op !== '+' && op !== '===' && op !== '!==') {
948    a = operand(a)
949    b = operand(b)
950  }
951  switch (op) {
952    case '+': {
953      if (typeof a === 'number' && typeof b === 'number') return a + b
954      const v = isPrimitive(a) && isPrimitive(b) ? a + b : str(a) + str(b)
955      if (typeof v === 'string') checkLength(v.length)
956
957      return v
958    }
959    case '-': return a - b
960    case '*': return a * b
961    case '/': return a / b
962    case '%': return a % b
963    case '**': return a ** b
964    case '===': return a === b
965    case '!==': return a !== b
966    case '<': return a < b
967    case '>': return a > b
968    case '<=': return a <= b
969    case '>=': return a >= b
970    case '&': return a & b
971    case '|': return a | b
972    case '^': return a ^ b
973    case '<<': return a << b
974    case '>>': return a >> b
975    case '>>>': return a >>> b
976  }
977  throw new ScriptError(`unknown operator ${op}`)
978}
979
980const isPrimitive = (v: V) => v === null || (typeof v !== 'object' && typeof v !== 'function')
981
982// Binds a pattern (a name, [a, b], {x, y}, with defaults and ...rest) to a
983// value: kind is let, const or var to declare, or "assign" to assign.
984function bind(pattern: Node, value: V, scope: Scope, kind: string) {
985  switch (pattern.t) {
986    case 'id': {
987      if (kind === 'assign') return scope.assign(pattern.name, value)
988      let target = scope
989      if (kind === 'var') while (!target.isFunction && target.parent) target = target.parent
990      target.declare(pattern.name, value, kind === 'const')
991
992      return
993    }
994    case 'member':
995      set(evalNode(pattern.obj)(scope), evalNode(pattern.prop)(scope), value)
996
997      return
998    case 'default':
999      bind(pattern.target, value === undefined ? evalNode(pattern.def)(scope) : value, scope, kind)
1000
1001      return
1002    case 'arrPat':
1003      bindList(pattern.items, value, scope, kind)
1004
1005      return
1006    case 'objPat': {
1007      const taken = new Set<string>()
1008      for (const [key, target] of pattern.props as [string, Node][]) {
1009        if (key !== '...') {
1010          taken.add(key)
1011          bind(target, get(value, key), scope, kind)
1012          continue
1013        }
1014        const rest = obj()
1015        if (isObj(value)) for (const k of keysOf(value)) if (!taken.has(k)) rest[k] = value[k]
1016        bind(target.target, rest, scope, kind)
1017      }
1018
1019      return
1020    }
1021  }
1022  throw new ScriptError('cannot assign to that')
1023}
1024
1025// Binds the items of a list pattern (or parameters) to a value's elements.
1026function bindList(items: (Node | null)[], value: V, scope: Scope, kind: string) {
1027  for (let i = 0; i < items.length; i++) {
1028    const item = items[i]
1029    if (!item) continue
1030    if (item.t === 'rest') {
1031      const list = Array.isArray(value) ? value : toList(value)
1032      cost(list.length)
1033      bind(item.target, list.slice(i), scope, kind)
1034
1035      return
1036    }
1037    bind(item, get(value, i), scope, kind)
1038  }
1039}
1040
1041// Calls visit on node and everything under it, stopping at functions unless
1042// told to go in.
1043function walk(node: V, visit: (n: Node) => void, intoFns: boolean) {
1044  if (Array.isArray(node)) {
1045    for (const n of node) walk(n, visit, intoFns)
1046
1047    return
1048  }
1049  if (node === null || typeof node !== 'object') return
1050  if (typeof node.t === 'string') {
1051    visit(node)
1052    if (node.t === 'fn' && !intoFns) return
1053  }
1054  for (const k in node) if (k !== 't') walk(node[k], visit, intoFns)
1055}
1056
1057// The names a pattern binds.
1058function patternNames(p: Node, out: string[]) {
1059  switch (p.t) {
1060    case 'id':
1061      out.push(p.name)
1062      break
1063    case 'default':
1064    case 'rest':
1065      patternNames(p.target, out)
1066      break
1067    case 'arrPat':
1068      for (const item of p.items as (Node | null)[]) if (item) patternNames(item, out)
1069      break
1070    case 'objPat':
1071      for (const [, target] of p.props as [string, Node][]) patternNames(target, out)
1072  }
1073}
1074
1075// The var names declared anywhere in a function body, which exist throughout it.
1076function varNames(body: Node[]): string[] {
1077  const out: string[] = []
1078  walk(body, n => {
1079    if (n.t === 'decl' && n.kind === 'var') for (const [target] of n.decls as [Node, Node | undefined][]) patternNames(target, out)
1080    if ((n.t === 'forOf' || n.t === 'forIn') && n.kind === 'var') patternNames(n.target, out)
1081  }, false)
1082
1083  return out
1084}
1085
1086const hasFn = (node: V): boolean => {
1087  let found = false
1088  walk(node, n => {
1089    if (n.t === 'fn') found = true
1090  }, true)
1091
1092  return found
1093}
1094
1095// Compiled closures, cached per node so a function body compiles once.
1096const compiled = new WeakMap<Node, Eval | Exec>()
1097function evalNode(node: Node): Eval {
1098  let f = compiled.get(node) as Eval | undefined
1099  if (!f) {
1100    f = compileExpr(node)
1101    compiled.set(node, f)
1102  }
1103
1104  return f
1105}
1106function execNode(node: Node): Exec {
1107  let f = compiled.get(node) as Exec | undefined
1108  if (!f) {
1109    f = compileStmt(node)
1110    compiled.set(node, f)
1111  }
1112
1113  return f
1114}
1115
1116function compileExpr(node: Node): Eval {
1117  switch (node.t) {
1118    case 'lit': {
1119      const v = node.v
1120
1121      return () => v
1122    }
1123    case 'tpl': {
1124      const strs = node.strs as string[]
1125      const exprs = (node.exprs as Node[]).map(evalNode)
1126
1127      return s => {
1128        let out = strs[0] ?? ''
1129        exprs.forEach((e, i) => {
1130          out += str(e(s)) + (strs[i + 1] ?? '')
1131          checkLength(out.length)
1132        })
1133
1134        return out
1135      }
1136    }
1137    case 'id': {
1138      const id = node.name as string
1139
1140      return s => s.lookup(id)
1141    }
1142    case 'arr': {
1143      const items = (node.items as Node[]).map(item => (item.t === 'spread' ? { spread: evalNode(item.e) } : { one: evalNode(item) }))
1144
1145      return s => {
1146        const out: V[] = []
1147        for (const item of items) {
1148          if (item.spread) out.push(...toList(item.spread(s)))
1149          else out.push(item.one(s))
1150        }
1151
1152        return out
1153      }
1154    }
1155    case 'obj': {
1156      const props = (node.props as [Node | string, Node][]).map(([k, v]) => ({ key: typeof k === 'string' ? k : evalNode(k), value: evalNode(v) }))
1157
1158      return s => {
1159        const out = obj()
1160        for (const { key, value } of props) {
1161          if (key === '...') {
1162            const v = value(s)
1163            if (isObj(v)) {
1164              keysOf(v)
1165              Object.assign(out, v)
1166            }
1167          }
1168          else out[typeof key === 'string' ? key : String(key(s))] = value(s)
1169        }
1170
1171        return out
1172      }
1173    }
1174    case 'fn': {
1175      const params = node.params as Node[]
1176      const body = compileBody(node.body as Node[], true)
1177      const name = node.name as string | undefined
1178      const self = node.self === true
1179
1180      return s => new Fn(params, body, s, name, self, node.arrow === true)
1181    }
1182    case 'seq': {
1183      const a = evalNode(node.a)
1184      const b = evalNode(node.b)
1185
1186      return s => (a(s), b(s))
1187    }
1188    case 'unary': {
1189      const arg = evalNode(node.arg)
1190      switch (node.op) {
1191        case '-': return s => -operand(arg(s))
1192        case '+': return s => +operand(arg(s))
1193        case '!': return s => !arg(s)
1194        default: return s => ~operand(arg(s))
1195      }
1196    }
1197    case 'typeof': {
1198      const arg = evalNode(node.arg)
1199      // typeof of a name never declared is "undefined"; any other error stands.
1200      const isName = (node.arg as Node).t === 'id'
hooks/render3d.ts 765 lines
1// A small 3D renderer for scene code. The world is sampled at a finer grid
2// than the cells (2 across and 4 down per cell, so the samples are square),
3// each sample is lit with a smoothly interpolated normal, and each cell is
4// then drawn from its samples: a cell a surface covers whole is a solid cell
5// in the lit color, a cell covered in part is the block glyph that matches
6// which quarters are covered (so edges come out anti-aliased), and a mesh
7// drawn `ascii` is instead the glyph whose density matches its brightness,
8// the classic terminal look, which suits curved things.
9
10import type { Rgb } from './effects'
11
12export type Vec3 = [number, number, number]
13
14// Triangles over shared vertices: verts is x, y, z per vertex; faces is
15// three vertex indices per triangle.
16export type Mesh = { verts: number[]; faces: number[] }
17
18// `fov` is the angle the strip shows across, in degrees (see NOMINAL_COLS).
19export type Camera = { eye: Vec3; target: Vec3; fov: number }
20export type Light = { dir: Vec3; ambient: number }
21export type Fog = { near: number; far: number; color: Rgb }
22
23export type Transform = {
24  at: Vec3
25  // Rotations in radians, applied about x, then y, then z.
26  rot: Vec3
27  scale: Vec3
28}
29
30export type Style = { color: Rgb; wire: boolean; unlit: boolean; ascii?: boolean }
31
32export const DEFAULT_CAMERA: Camera = { eye: [0, 2.5, 9], target: [0, 1, 0], fov: 90 }
33export const DEFAULT_LIGHT: Light = { dir: [0.4, 1, 0.6], ambient: 0.25 }
34export const DEFAULT_FOG: Fog = { near: 8, far: 40, color: [0, 0, 0] }
35
36// The strip is wide and only 9 rows tall, so the field of view is the
37// angle across it, measured over this many columns whatever the terminal's
38// width: things stay the same size on a wider terminal, which shows more.
39const NOMINAL_COLS = 120
40
41// Samples per cell, across and down.
42export const SX = 2
43export const SY = 4
44
45// Nothing nearer than this draws: the near plane, where geometry that
46// crosses behind the camera is cut.
47const NEAR = 0.05
48
49// The most triangles one frame draws, over every mesh.
50export const MOST_TRIANGLES = 20_000
51
52// A frame's samples: depth, the lit color, and what owns each (nothing, a
53// surface, or a sprite drawn in cells that the glyphs must leave alone).
54export type Frame = {
55  cols: number
56  rows: number
57  w: number
58  h: number
59  depth: Float32Array
60  color: Float32Array
61  owner: Uint8Array
62  // Cells with surface samples drawn since the last composite.
63  dirty: Uint8Array
64  triangles: number
65  // Called for every triangle or edge drawn, so the caller can keep time.
66  tick?: () => void
67}
68
69export const createFrame = (cols: number, rows: number): Frame => {
70  const w = cols * SX
71  const h = rows * SY
72
73  return {
74    cols,
75    rows,
76    w,
77    h,
78    depth: new Float32Array(w * h).fill(Infinity),
79    color: new Float32Array(w * h * 3),
80    owner: new Uint8Array(w * h),
81    dirty: new Uint8Array(cols * rows),
82    triangles: 0,
83  }
84}
85
86const SURFACE = 1
87const SPRITE = 2
88const ASCII = 3
89const isSurface = (owner: number) => owner === SURFACE || owner === ASCII
90
91const sub = (a: Vec3, b: Vec3): Vec3 => [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
92const dot = (a: Vec3, b: Vec3) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
93const cross = (a: Vec3, b: Vec3): Vec3 => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
94const norm = (a: Vec3): Vec3 => {
95  const l = Math.hypot(a[0], a[1], a[2]) || 1
96
97  return [a[0] / l, a[1] / l, a[2] / l]
98}
99const clamp01 = (v: number) => Math.min(1, Math.max(0, v))
100const smooth = (lo: number, hi: number, v: number) => {
101  const k = clamp01(hi === lo ? (v >= hi ? 1 : 0) : (v - lo) / (hi - lo))
102
103  return k * k * (3 - 2 * k)
104}
105
106// ---------------------------------------------------------------- camera
107
108export type View = { eye: Vec3; right: Vec3; up: Vec3; forward: Vec3; f: number; w: number; h: number }
109
110export function resolve(cam: Camera, w: number, h: number): View {
111  const forward = norm(sub(cam.target, cam.eye))
112  const worldUp: Vec3 = Math.abs(forward[1]) > 0.99 ? [0, 0, -1] : [0, 1, 0]
113  const right = norm(cross(forward, worldUp))
114  const up = cross(right, forward)
115  const fov = (Math.max(10, Math.min(170, cam.fov)) * Math.PI) / 180
116  // The focal length in samples: pixels over the nominal width, times the
117  // samples per column.
118  const f = ((NOMINAL_COLS / 2) * SX) / Math.tan(fov / 2)
119
120  return { eye: cam.eye, right, up, forward, f, w, h }
121}
122
123// A point in the camera's frame: across, up, and its depth ahead.
124type ViewPoint = { r: number; u: number; d: number }
125
126const toView = (view: View, p: Vec3): ViewPoint => {
127  const d = sub(p, view.eye)
128
129  return { r: dot(d, view.right), u: dot(d, view.up), d: dot(d, view.forward) }
130}
131
132// On the sample grid; `x` and `y` in samples.
133export type Projected = { x: number; y: number; depth: number }
134
135const toScreen = (view: View, v: ViewPoint): Projected => ({
136  x: (v.r * view.f) / v.d + view.w / 2,
137  y: view.h / 2 - (v.u * view.f) / v.d,
138  depth: v.d,
139})
140
141// A world point on the sample grid, or undefined behind the camera.
142export function project(view: View, p: Vec3): Projected | undefined {
143  const v = toView(view, p)
144
145  return v.d < NEAR ? undefined : toScreen(view, v)
146}
147
148// The part of a polygon in front of the near plane (Sutherland-Hodgman
149// against one plane), each vertex carrying its attributes along.
150type Vertex = { v: ViewPoint; p: Vec3; n: Vec3 }
151
152function clipNear(poly: Vertex[]): Vertex[] {
153  if (poly.every(x => x.v.d >= NEAR)) return poly
154  const out: Vertex[] = []
155  const mixv = (a: Vec3, b: Vec3, k: number): Vec3 => [a[0] + (b[0] - a[0]) * k, a[1] + (b[1] - a[1]) * k, a[2] + (b[2] - a[2]) * k]
156  for (let i = 0; i < poly.length; i++) {
157    const a = poly[i]!
158    const b = poly[(i + 1) % poly.length]!
159    const aIn = a.v.d >= NEAR
160    const bIn = b.v.d >= NEAR
161    if (aIn) out.push(a)
162    if (aIn !== bIn) {
163      const k = (NEAR - a.v.d) / (b.v.d - a.v.d)
164      out.push({
165        v: { r: a.v.r + (b.v.r - a.v.r) * k, u: a.v.u + (b.v.u - a.v.u) * k, d: NEAR },
166        p: mixv(a.p, b.p, k),
167        n: mixv(a.n, b.n, k),
168      })
169    }
170  }
171
172  return out
173}
174
175// ---------------------------------------------------------------- meshes
176
177export function transformPoint(t: Transform, p: Vec3): Vec3 {
178  return placed(t, rotation(t.rot), p)
179}
180
181const placed = (t: Transform, m: Mat3, p: Vec3): Vec3 => {
182  const [x, y, z] = apply(m, [p[0] * t.scale[0], p[1] * t.scale[1], p[2] * t.scale[2]])
183
184  return [x + t.at[0], y + t.at[1], z + t.at[2]]
185}
186
187// A rotation as a row-major 3x3 matrix, built once per mesh rather than
188// six trig calls per point.
189type Mat3 = [number, number, number, number, number, number, number, number, number]
190
191// Rz * Ry * Rx, so x applies first.
192const rotation = (rot: Vec3): Mat3 => {
193  const [cx, sx, cy, sy, cz, sz] = [Math.cos(rot[0]), Math.sin(rot[0]), Math.cos(rot[1]), Math.sin(rot[1]), Math.cos(rot[2]), Math.sin(rot[2])]
194
195  return [cz * cy, cz * sy * sx - sz * cx, cz * sy * cx + sz * sx, sz * cy, sz * sy * sx + cz * cx, sz * sy * cx - cz * sx, -sy, cy * sx, cy * cx]
196}
197
198const apply = (m: Mat3, p: Vec3): Vec3 => [
199  m[0] * p[0] + m[1] * p[1] + m[2] * p[2],
200  m[3] * p[0] + m[4] * p[1] + m[5] * p[2],
201  m[6] * p[0] + m[7] * p[1] + m[8] * p[2],
202]
203
204// A normal under a transform: the inverse of the scale (a squashed sphere's
205// normals lean out, not along the squash), then the rotation.
206const transformNormal = (t: Transform, m: Mat3, n: Vec3): Vec3 =>
207  apply(m, norm([n[0] / (t.scale[0] || 1), n[1] / (t.scale[1] || 1), n[2] / (t.scale[2] || 1)]))
208
209// A mesh with a normal for each corner of each face: faces meeting at a
210// gentle angle share smoothed normals (a sphere looks round), faces meeting
211// at a crease keep their own (a cube stays sharp). And its edges to draw as
212// wire, each once: two vertex indices per edge, without the seams inside
213// flat polygons (the diagonal of a triangulated quad).
214type Prepared = { faceNormals: Vec3[]; cornerNormals: Vec3[]; edges: number[] }
215const prepared = new WeakMap<Mesh, Prepared>()
216const CREASE = Math.cos((50 * Math.PI) / 180)
217const FLAT = 0.999
218// The most faces around one vertex that smoothing considers: a real mesh has
219// a handful; a mesh of thousands of faces on three vertices would otherwise
220// cost the square of its size to prepare.
221const MOST_AROUND = 24
222
223export function prepare(mesh: Mesh): Prepared {
224  const done = prepared.get(mesh)
225  if (done) return done
226  const { verts, faces } = mesh
227  const at = (i: number): Vec3 => [verts[i * 3] ?? 0, verts[i * 3 + 1] ?? 0, verts[i * 3 + 2] ?? 0]
228  const count = Math.floor(faces.length / 3)
229  const faceNormals: Vec3[] = []
230  const around = new Map<number, number[]>()
231  for (let f = 0; f < count; f++) {
232    const [a, b, c] = [faces[f * 3]!, faces[f * 3 + 1]!, faces[f * 3 + 2]!]
233    faceNormals.push(norm(cross(sub(at(b), at(a)), sub(at(c), at(a)))))
234    for (const i of [a, b, c]) {
235      const list = around.get(i) ?? []
236      if (list.length < MOST_AROUND) list.push(f)
237      around.set(i, list)
238    }
239  }
240  const cornerNormals: Vec3[] = []
241  for (let f = 0; f < count; f++) {
242    const mine = faceNormals[f]!
243    for (let k = 0; k < 3; k++) {
244      const i = faces[f * 3 + k]!
245      const sum: Vec3 = [0, 0, 0]
246      for (const g of around.get(i) ?? []) {
247        const other = faceNormals[g]!
248        if (dot(mine, other) >= CREASE) {
249          sum[0] += other[0]
250          sum[1] += other[1]
251          sum[2] += other[2]
252        }
253      }
254      cornerNormals.push(norm(sum))
255    }
256  }
257  // Edges keyed by their two indices as one number (the index count is
258  // bounded, so this is exact), each with the faces along it.
259  const span = Math.floor(verts.length / 3) + 1
260  const edgeFaces = new Map<number, number[]>()
261  for (let f = 0; f < count; f++) {
262    for (let k = 0; k < 3; k++) {
263      const a = faces[f * 3 + k]!
264      const b = faces[f * 3 + ((k + 1) % 3)]!
265      const key = Math.min(a, b) * span + Math.max(a, b)
266      const along = edgeFaces.get(key)
267      if (along) along.push(f)
268      else edgeFaces.set(key, [f])
269    }
270  }
271  const edges: number[] = []
272  for (const [key, along] of edgeFaces) {
273    if (along.length === 2 && dot(faceNormals[along[0]!]!, faceNormals[along[1]!]!) > FLAT) continue
274    edges.push(Math.floor(key / span), key % span)
275  }
276  const result = { faceNormals, cornerNormals, edges }
277  prepared.set(mesh, result)
278
279  return result
280}
281
282// ---------------------------------------------------------------- drawing
283
284const fogged = (c: Rgb, fog: Fog, depth: number): Rgb => {
285  const k = smooth(fog.near, fog.far, depth)
286
287  return [c[0] + (fog.color[0] - c[0]) * k, c[1] + (fog.color[1] - c[1]) * k, c[2] + (fog.color[2] - c[2]) * k]
288}
289
290function plot(frame: Frame, x: number, y: number, depth: number, c: Rgb, owner = SURFACE) {
291  const at = y * frame.w + x
292  frame.depth[at] = depth
293  frame.owner[at] = owner
294  frame.color[at * 3] = c[0]
295  frame.color[at * 3 + 1] = c[1]
296  frame.color[at * 3 + 2] = c[2]
297  frame.dirty[Math.floor(y / SY) * frame.cols + Math.floor(x / SX)] = 1
298}
299
300// Lights a point of a surface: ambient, diffuse by the normal against the
301// light, and a small highlight where the light reflects toward the eye.
302function lit(color: Rgb, n: Vec3, p: Vec3, view: View, l: Vec3, ambient: number): Rgb {
303  const toEye = norm(sub(view.eye, p))
304  // Two-sided: a face seen from behind shades as if it faced the eye.
305  const nn: Vec3 = dot(n, toEye) < 0 ? [-n[0], -n[1], -n[2]] : n
306  const diffuse = Math.max(0, dot(nn, l))
307  const half = norm([l[0] + toEye[0], l[1] + toEye[1], l[2] + toEye[2]])
308  const spec = Math.pow(Math.max(0, dot(nn, half)), 24) * 0.35
309  const k = ambient + (1 - ambient) * diffuse
310
311  return [Math.min(255, color[0] * k + 255 * spec), Math.min(255, color[1] * k + 255 * spec), Math.min(255, color[2] * k + 255 * spec)]
312}
313
314// Fills one triangle on the sample grid, interpolating depth (linear in
315// 1/depth), world position and normal, lighting each sample.
316function fillTriangle(frame: Frame, view: View, fog: Fog, l: Vec3, ambient: number, tri: [Vertex, Vertex, Vertex], style: Style) {
317  const [A, B, C] = tri
318  const a = toScreen(view, A.v)
319  const b = toScreen(view, B.v)
320  const c = toScreen(view, C.v)
321  const area = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)
322  // A triangle too thin to show, or one with a corner at infinity (its
323  // weights would be NaN and own every sample).
324  if (!(Math.abs(area) >= 1e-9) || !Number.isFinite(a.x + a.y + b.x + b.y + c.x + c.y)) return
325  const x0 = Math.max(0, Math.floor(Math.min(a.x, b.x, c.x)))
326  const x1 = Math.min(frame.w - 1, Math.ceil(Math.max(a.x, b.x, c.x)))
327  const y0 = Math.max(0, Math.floor(Math.min(a.y, b.y, c.y)))
328  const y1 = Math.min(frame.h - 1, Math.ceil(Math.max(a.y, b.y, c.y)))
329  if (x1 < x0 || y1 < y0) return
330  const ia = 1 / a.depth
331  const ib = 1 / b.depth
332  const ic = 1 / c.depth
333  for (let y = y0; y <= y1; y++) {
334    const py = y + 0.5
335    for (let x = x0; x <= x1; x++) {
336      const px = x + 0.5
337      let wa = ((b.x - px) * (c.y - py) - (b.y - py) * (c.x - px)) / area
338      let wb = ((c.x - px) * (a.y - py) - (c.y - py) * (a.x - px)) / area
339      let wc = 1 - wa - wb
340      if (wa < -1e-6 || wb < -1e-6 || wc < -1e-6) continue
341      const depth = 1 / (wa * ia + wb * ib + wc * ic)
342      const at = y * frame.w + x
343      if (depth >= frame.depth[at]!) continue
344      // Perspective-correct weights for the attributes.
345      wa *= ia * depth
346      wb *= ib * depth
347      wc *= ic * depth
348      let color = style.color
349      if (!style.unlit) {
350        const n = norm([
351          wa * A.n[0] + wb * B.n[0] + wc * C.n[0],
352          wa * A.n[1] + wb * B.n[1] + wc * C.n[1],
353          wa * A.n[2] + wb * B.n[2] + wc * C.n[2],
354        ])
355        const p: Vec3 = [
356          wa * A.p[0] + wb * B.p[0] + wc * C.p[0],
357          wa * A.p[1] + wb * B.p[1] + wc * C.p[1],
358          wa * A.p[2] + wb * B.p[2] + wc * C.p[2],
359        ]
360        color = lit(color, n, p, view, l, ambient)
361      }
362      plot(frame, x, y, depth, fogged(color, fog, depth), style.ascii ? ASCII : SURFACE)
363    }
364  }
365}
366
367// A line between two world points, a cell's width thick, cut at the near
368// plane and depth tested along its length.
369export function drawLine(frame: Frame, view: View, fog: Fog, p0: Vec3, p1: Vec3, color: Rgb) {
370  const zero: Vec3 = [0, 0, 0]
371  const ends = clipNear([
372    { v: toView(view, p0), p: p0, n: zero },
373    { v: toView(view, p1), p: p1, n: zero },
374  ])
375  if (ends.length < 2) return
376  const a = toScreen(view, ends[0]!.v)
377  const b = toScreen(view, ends[1]!.v)
378  // Only the part on screen is stepped, a sample at a time, with the step
379  // still a share of the whole segment so depth follows it correctly: an
380  // end cut at the near plane can project miles off-screen, and spreading
381  // a fixed count of steps over that leaves gaps in the part that shows.
382  const span = clipSpan(a.x, a.y, b.x, b.y, frame.w, frame.h)
383  if (!span) return
384  const [k0, k1] = span
385  const steps = Math.min(frame.w + frame.h, Math.ceil(Math.max(Math.abs(b.x - a.x), Math.abs(b.y - a.y)) * (k1 - k0)) || 1)
386  for (let i = 0; i <= steps; i++) {
387    const k = k0 + ((k1 - k0) * i) / steps
388    const x = Math.floor(a.x + (b.x - a.x) * k)
389    const y = Math.floor(a.y + (b.y - a.y) * k)
390    const depth = 1 / ((1 - k) / a.depth + k / b.depth)
391    // A little nearer than the surface it lies on, so edges show on faces.
392    dab(frame, x, y, depth - 0.03, fogged(color, fog, depth))
393  }
394}
395
396// The share of a segment inside the frame (Liang-Barsky), as the parameters
397// where it enters and leaves, or undefined when it misses. A sample of
398// margin keeps the dabs that straddle the frame's edge.
399function clipSpan(x0: number, y0: number, x1: number, y1: number, w: number, h: number): [number, number] | undefined {
400  let k0 = 0
401  let k1 = 1
402  for (const [p, dp, hi] of [[x0, x1 - x0, w], [y0, y1 - y0, h]] as const) {
403    if (dp === 0) {
404      if (p < -1 || p > hi) return undefined
405      continue
406    }
407    const ka = (-1 - p) / dp
408    const kb = (hi - p) / dp
409    k0 = Math.max(k0, Math.min(ka, kb))
410    k1 = Math.min(k1, Math.max(ka, kb))
411  }
412
413  return k0 <= k1 ? [k0, k1] : undefined
414}
415
416export function drawPoint(frame: Frame, view: View, fog: Fog, p: Vec3, color: Rgb) {
417  const a = project(view, p)
418  if (!a) return
419  dab(frame, Math.floor(a.x), Math.floor(a.y), a.depth, fogged(color, fog, a.depth))
420}
421
422// A 2 by 2 sample dab: what a line or a point needs to read as a glyph.
423function dab(frame: Frame, x: number, y: number, depth: number, color: Rgb) {
424  for (let dy = 0; dy < 2; dy++) {
425    for (let dx = 0; dx < 2; dx++) {
426      const sx = x + dx
427      const sy = y + dy
428      if (sx < 0 || sx >= frame.w || sy < 0 || sy >= frame.h) continue
429      const at = sy * frame.w + sx
430      if (depth >= frame.depth[at]!) continue
431      plot(frame, sx, sy, depth, color)
432    }
433  }
434}
435
436// Draws a mesh placed by a transform: filled and lit, or as wire edges.
437export function drawMesh(frame: Frame, view: View, light: Light, fog: Fog, mesh: Mesh, t: Transform, style: Style) {
438  const { verts, faces } = mesh
439  const { cornerNormals, edges } = prepare(mesh)
440  const m = rotation(t.rot)
441  const n = Math.floor(verts.length / 3)
442  const world: Vec3[] = []
443  const eyed: ViewPoint[] = []
444  for (let i = 0; i < n; i++) {
445    const p = placed(t, m, [verts[i * 3]!, verts[i * 3 + 1]!, verts[i * 3 + 2]!])
446    world.push(p)
447    eyed.push(toView(view, p))
448  }
449  if (style.wire) {
450    // Each edge once; an edge counts against the triangle budget.
451    for (let e = 0; e + 1 < edges.length && frame.triangles < MOST_TRIANGLES; e += 2) {
452      const a = world[edges[e]!]
453      const b = world[edges[e + 1]!]
454      if (!a || !b) continue
455      frame.triangles += 1
456      frame.tick?.()
457      drawLine(frame, view, fog, a, b, style.color)
458    }
459
460    return
461  }
462  const l = norm(light.dir)
463  const faceCount = Math.floor(faces.length / 3)
464  for (let f = 0; f < faceCount && frame.triangles < MOST_TRIANGLES; f++) {
465    const ids = [faces[f * 3]!, faces[f * 3 + 1]!, faces[f * 3 + 2]!]
466    const corners: Vertex[] = []
467    for (let k = 0; k < 3; k++) {
468      const v = eyed[ids[k]!]
469      const p = world[ids[k]!]
470      if (!v || !p) break
471      corners.push({ v, p, n: transformNormal(t, m, cornerNormals[f * 3 + k] ?? [0, 1, 0]) })
472    }
473    if (corners.length < 3) continue
474    if (corners.every(c => c.v.d < NEAR)) continue
475    frame.triangles += 1
476    frame.tick?.()
477    const poly = clipNear(corners)
478    for (let j = 1; j + 1 < poly.length; j++) fillTriangle(frame, view, fog, l, light.ambient, [poly[0]!, poly[j]!, poly[j + 1]!], style)
479  }
480}
481
482// Claims a sprite's pixel (a column and a pixel row, two to a cell) at a
483// depth: true when it is nearer than what is there, in which case the
484// glyphs leave that pixel to the sprite.
485export function claimPixel(frame: Frame, col: number, py: number, depth: number) {
486  const x0 = col * SX
487  const y0 = py * (SY / 2)
488  if (x0 < 0 || x0 >= frame.w || y0 < 0 || y0 >= frame.h) return false
489  let nearest = Infinity
490  for (let dy = 0; dy < SY / 2; dy++) for (let dx = 0; dx < SX; dx++) nearest = Math.min(nearest, frame.depth[(y0 + dy) * frame.w + x0 + dx]!)
491  if (depth >= nearest) return false
492  for (let dy = 0; dy < SY / 2; dy++) {
493    for (let dx = 0; dx < SX; dx++) {
494      const at = (y0 + dy) * frame.w + x0 + dx
495      frame.depth[at] = depth
496      frame.owner[at] = SPRITE
497    }
498  }
499
500  return true
501}
502
503// ---------------------------------------------------------------- glyphs
504
505// Glyphs by brightness, sparse to dense, for `ascii` meshes.
506const RAMP = [...' .,:;-=+*#%@']
507
508// The block glyph for each set of covered quarters: bit 1 top-left, 2
509// top-right, 4 bottom-left, 8 bottom-right.
510const QUARTERS = [' ', '▘', '▝', '▀', '▖', '▌', '▞', '▛', '▗', '▚', '▐', '▜', '▄', '▙', '▟', '█']
511
512// How far a sample's depth must jump from a neighbor's to count as an edge
513// (as a share of its depth), and how much the farther side darkens there:
514// a dark rim on the far side of every overlap, so shapes read as separate.
515const EDGE = 0.06
516const RIM = 0.45
517
518// Two groups closer than this (squared distance over the channels: about
519// 20 levels each) are one color: the cell stays solid rather than split.
520const ALIKE = 20 * 20 * 3
521
522// Draws the cells drawn on since the last composite. Each cell's four
523// quarters (two samples each) are split into two groups by color, and the
524// cell is the quarter-block glyph of that split with a color for each
525// group, so a cell can hold an edge, or a gradient, with two colors.
526export function composite(frame: Frame, put: (col: number, row: number, char: string, color: Rgb, back?: Rgb) => void) {
527  const per = SX * SY
528  const half = SY / 2
529  for (let row = 0; row < frame.rows; row++) {
530    for (let col = 0; col < frame.cols; col++) {
531      const cell = row * frame.cols + col
532      if (!frame.dirty[cell]) continue
533      frame.dirty[cell] = 0
534      // Per quarter: samples covered, their summed color; and the cell's.
535      const qCount = [0, 0, 0, 0]
536      const qSum: Rgb[] = [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]
537      let covered = 0
538      let sprite = 0
539      let ascii = 0
540      const sum: Rgb = [0, 0, 0]
541      for (let dy = 0; dy < SY; dy++) {
542        for (let dx = 0; dx < SX; dx++) {
543          const x = col * SX + dx
544          const y = row * SY + dy
545          const at = y * frame.w + x
546          const owner = frame.owner[at]!
547          if (owner === SPRITE) sprite += 1
548          if (!isSurface(owner)) continue
549          if (owner === ASCII) ascii += 1
550          const c = rimmed(frame, x, y, at)
551          const q = (dy < half ? 0 : 2) + (dx < SX / 2 ? 0 : 1)
552          qCount[q] = (qCount[q] ?? 0) + 1
553          qSum[q]![0] += c[0]
554          qSum[q]![1] += c[1]
555          qSum[q]![2] += c[2]
556          covered += 1
557          sum[0] += c[0]
558          sum[1] += c[1]
559          sum[2] += c[2]
560        }
561      }
562      // A cell a sprite holds is the sprite's, unless a surface drawn since
563      // covers more of it (something passing in front of Clawd).
564      if (covered === 0 || sprite >= covered) continue
565      const mean: Rgb = [sum[0] / covered, sum[1] / covered, sum[2] / covered]
566      const coverage = covered / per
567      if (ascii > 0) {
568        // The classic look: a glyph as dense as the brightness, thinned by
569        // coverage, in ink that keeps the hue, over a dark tint when solid.
570        // Mid-tones lean toward the denser glyphs (a gamma of 0.6).
571        const brightness = (0.299 * mean[0] + 0.587 * mean[1] + 0.114 * mean[2]) / 255
572        const level = Math.min(RAMP.length - 1, Math.round(Math.pow(brightness, 0.6) * coverage * (RAMP.length - 1)))
573        const back: Rgb | undefined = coverage >= 0.5 ? [mean[0] * 0.18, mean[1] * 0.18, mean[2] * 0.18] : undefined
574        put(col, row, RAMP[Math.max(1, level)] ?? '.', lift(mean), back)
575        continue
576      }
577      const qMean: (Rgb | undefined)[] = qCount.map((n, q) => (n > 0 ? [qSum[q]![0] / n, qSum[q]![1] / n, qSum[q]![2] / n] : undefined))
578      const onMask = qMean.reduce((m, c, q) => (c ? m | (1 << q) : m), 0)
579      // The split of the covered quarters into two color groups that keeps
580      // each group most alike; the uncovered quarters always sit in the
581      // second group, which draws as the background.
582      let best = onMask
583      let bestCost = Infinity
584      for (let mask = 1; mask <= onMask; mask++) {
585        if ((mask & onMask) !== mask) continue
586        const a = groupMean(qMean, mask)
587        const b = groupMean(qMean, onMask & ~mask)
588        let cost = 0
589        for (let q = 0; q < 4; q++) {
590          const c = qMean[q]
591          if (!c) continue
592          const g = mask & (1 << q) ? a : b
593          cost += g ? dist(c, g) : 0
594        }
595        if (cost < bestCost - 1e-9) {
596          bestCost = cost
597          best = mask
598        }
599      }
600      const fg = groupMean(qMean, best) ?? mean
601      const rest = onMask & ~best
602      const bg = groupMean(qMean, rest)
603      if (onMask === 15 && (best === 15 || (bg && dist(fg, bg) < ALIKE))) {
604        // Four quarters of much the same color: a solid cell.
605        put(col, row, ' ', mean, mean)
606      } else if (best === 15) {
607        // Four quarters of one color: a solid cell.
608        put(col, row, ' ', fg, fg)
609      } else if (bg && bitCount(rest) > bitCount(~onMask & 15)) {
610        // The second group covers more than the uncovered part: two colors.
611        put(col, row, QUARTERS[best] ?? '█', fg, bg)
612      } else {
613        put(col, row, QUARTERS[best] ?? '█', fg)
614      }
615    }
616  }
617}
618
619// A sample's color, darkened where its depth jumps away from a neighbor's
620// (the far side of an overlap). Depth is compared as 1/depth, which runs
621// straight across a flat surface on screen, so only a kink counts, not a
622// slope: a floor receding steadily toward the horizon gets no rim, a nearer
623// shape in front of it does. Along each axis the kink is how far this
624// sample sits beyond where its neighbors' trend puts it; at a surface's
625// border the trend comes from the two samples on the one side it has, so a
626// limb curving away still rims where a flat floor's far edge does not.
627function rimmed(frame: Frame, x: number, y: number, at: number): Rgb {
628  const c: Rgb = [frame.color[at * 3]!, frame.color[at * 3 + 1]!, frame.color[at * 3 + 2]!]
629  const i = 1 / frame.depth[at]!
630  // 1/depth of the surface sample k steps along an axis, if there is one.
631  const along = (dx: number, dy: number, k: number) => {
632    const sx = x + dx * k
633    const sy = y + dy * k
634    if (sx < 0 || sx >= frame.w || sy < 0 || sy >= frame.h) return undefined
635    const n = sy * frame.w + sx
636
637    return isSurface(frame.owner[n]!) ? 1 / frame.depth[n]! : undefined
638  }
639  const kink = (dx: number, dy: number) => {
640    const a = along(dx, dy, 1)
641    const b = along(dx, dy, -1)
642    if (a !== undefined && b !== undefined) return a + b - 2 * i
643    const side = a !== undefined ? 1 : b !== undefined ? -1 : 0
644    if (side === 0) return 0
645    const near = along(dx, dy, side)!
646    const beyond = along(dx, dy, 2 * side)
647    // Two samples wide there is no trend to go by: the jump alone decides.
648    if (beyond === undefined) return near - i > EDGE * near ? Infinity : 0
649
650    return 2 * near - beyond - i
651  }
652  const isRim = kink(1, 0) > EDGE * i || kink(0, 1) > EDGE * i
653
654  return isRim ? [c[0] * RIM, c[1] * RIM, c[2] * RIM] : c
655}
656
657const bitCount = (m: number) => ((m & 1) + ((m >> 1) & 1) + ((m >> 2) & 1) + ((m >> 3) & 1))
658
659const dist = (a: Rgb, b: Rgb) => (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2 + (a[2] - b[2]) ** 2
660
661function groupMean(q: (Rgb | undefined)[], mask: number): Rgb | undefined {
662  const sum: Rgb = [0, 0, 0]
663  let n = 0
664  for (let i = 0; i < 4; i++) {
665    const c = q[i]
666    if (!(mask & (1 << i)) || !c) continue
667    sum[0] += c[0]
668    sum[1] += c[1]
669    sum[2] += c[2]
670    n += 1
671  }
672
673  return n > 0 ? [sum[0] / n, sum[1] / n, sum[2] / n] : undefined
674}
675
676// Ink that stays visible: the color brightened toward its hue when dark.
677function lift(c: Rgb): Rgb {
678  const max = Math.max(c[0], c[1], c[2], 1)
679  const k = Math.max(1, 90 / max)
680
681  return [Math.min(255, c[0] * k), Math.min(255, c[1] * k), Math.min(255, c[2] * k)]
682}
683
684// ---------------------------------------------------------------- shapes
685
686const quad = (faces: number[], a: number, b: number, c: number, d: number) => faces.push(a, b, c, a, c, d)
687
688// A box centered on the origin.
689export function box(w: number, h: number, d: number): Mesh {
690  const x = w / 2
691  const y = h / 2
692  const z = d / 2
693  const verts = [-x, -y, -z, x, -y, -z, x, y, -z, -x, y, -z, -x, -y, z, x, -y, z, x, y, z, -x, y, z]
694  const faces: number[] = []
695  quad(faces, 0, 1, 2, 3)
696  quad(faces, 5, 4, 7, 6)
697  quad(faces, 4, 0, 3, 7)
698  quad(faces, 1, 5, 6, 2)
699  quad(faces, 3, 2, 6, 7)
700  quad(faces, 4, 5, 1, 0)
701
702  return { verts, faces }
703}
704
705// A sphere of latitude rings and longitude segments.
706export function sphere(r: number, segments = 12): Mesh {
707  const seg = Math.max(3, Math.min(48, Math.round(segments)))
708  const rings = Math.max(2, Math.round(seg / 2))
709  const verts: number[] = []
710  const faces: number[] = []
711  for (let i = 0; i <= rings; i++) {
712    const phi = (i / rings) * Math.PI
713    for (let j = 0; j < seg; j++) {
714      const theta = (j / seg) * Math.PI * 2
715      verts.push(r * Math.sin(phi) * Math.cos(theta), r * Math.cos(phi), r * Math.sin(phi) * Math.sin(theta))
716    }
717  }
718  for (let i = 0; i < rings; i++) {
719    for (let j = 0; j < seg; j++) {
720      const a = i * seg + j
721      const b = i * seg + ((j + 1) % seg)
722      const c = (i + 1) * seg + ((j + 1) % seg)
723      const d = (i + 1) * seg + j
724      if (i > 0) faces.push(a, b, c)
725      if (i < rings - 1) faces.push(a, c, d)
726    }
727  }
728
729  return { verts, faces }
730}
731
732// A cylinder (or a cone when the top radius is 0) standing on the origin,
733// its base at y 0 and its top at y h.
734export function cylinder(rBottom: number, rTop: number, h: number, segments = 12): Mesh {
735  const seg = Math.max(3, Math.min(48, Math.round(segments)))
736  const verts: number[] = []
737  const faces: number[] = []
738  for (let j = 0; j < seg; j++) {
739    const theta = (j / seg) * Math.PI * 2
740    verts.push(rBottom * Math.cos(theta), 0, rBottom * Math.sin(theta))
741    verts.push(rTop * Math.cos(theta), h, rTop * Math.sin(theta))
742  }
743  // The centers of the two caps.
744  const cb = seg * 2
745  const ct = seg * 2 + 1
746  verts.push(0, 0, 0, 0, h, 0)
747  for (let j = 0; j < seg; j++) {
748    const a = j * 2
749    const b = ((j + 1) % seg) * 2
750    quad(faces, a, b, b + 1, a + 1)
751    faces.push(cb, b, a)
752    if (rTop > 0) faces.push(ct, a + 1, b + 1)
753  }
754
755  return { verts, faces }
756}
757
758// A flat rectangle on the ground (the x-z plane), centered on the origin.
759export function plane(w: number, d: number): Mesh {
760  const x = w / 2
761  const z = d / 2
762
763  return { verts: [-x, 0, -z, x, 0, -z, x, 0, z, -x, 0, z], faces: [0, 1, 2, 0, 2, 3] }
764}
765