SLOPSHOPPER

heavens-feel

Heaven's Feel cast for your agents: Sakura watches over the main session, Servants answer every subagent

newpanebandspinnerrowsguard
v0.1.0MITupdated 2026-10-05yedidyakfir/heavens-feel
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · heavens-feel
│ ┃ hf ✕ › fix the failing auth test and add an audit log call │ ┃ ❀ Sakura Matou │ ┃ the one who waits at home · Master of Rider ⏺ Read(src/auth.ts) │ ┃ ⎿ Read 6 lines │ ┃ ▄▄▄▄▄▄▄▄▄▄ ⏺ Update(src/auth.ts) │ ┃ ▄▄▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▄▄ ⎿ Added 2 lines, removed 1 line │ ┃ ▄▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▄ ⏺ Bash(bun test) │ ┃ ▄▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ⎿ 3 pass, 1 fail │ ┃ ▄▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▄▄▄ │ ┃ ▀▀▀▀ ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ │ ┃ ✻ Worked for 42s · done 4:20 PM │ ┃ │ ┃ › /hf │ ┃ ⎿ heavens-feel: Sakura waves goodbye. /hf brings her back. │ ┃ │ ┃ │ ┃ │ ┃ │ ┃ │ ┃ │ ┃ │ ┃ │ ┃ │ ┃ │ ┃ ✻ Sakura is pondering… ❀ Sakura finished 「I did it. Did you see?」 open pane ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
❀ Sakura finished 「I did it. Did you see?」 open pane
Pane · hf
❀ Sakura Matou ♥♡♡♡♡ the one who waits at home · Master of Rider ▄▄▄▄▄▄▄▄▄▄ ▄▄▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▄▄ ▄▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▄ ▄▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▄▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▄▄▄ ▀▀▀▀ finished ✿ finished, waiting for you ╭────────────────────────────────────────────────────╮ │ 「I did it. Did you see?」 │ ╰────────────────────────────────────────────────────╯ 影 shadow ▰▰▰▰▰ ▱▱▱▱▱ 49% ☀ 11:53 Thu
README

Heaven's Feel

reach validates

A Claude Code mod that gives every agent a character from Fate/stay night: Heaven's Feel.

Sakura's pixel portrait cycling through her moods

  • Sakura's room. A pane where an animated pixel Sakura shows what the session is doing: thinking, reading, editing, running commands, summoning, resting. Past 75% context she turns into Dark Sakura.
  • Servants for subagents. Each subagent is summoned as a Servant who suits the job. Explore is Rider, Plan is Caster, general-purpose is Saber, reviewers are Archer, debuggers are True Assassin. Their cards appear in Sakura's room, and pressing one opens that Servant's own transcript.
  • A face on every message. Main-session replies wear a Master's face (Sakura, Shirou, Rin, Illya or Kirei, chosen per message). A subagent's replies wear its Servant's face.
  • A band above the prompt with Sakura's portrait, what she is doing, and a figure for each working Servant.
  • The spinner gets an in-character verb.

It draws pixel art on the terminal and animated SVG in the Claude desktop app, matching the app's light or dark theme.

Install

claude plugin marketplace add yedidyakfir/heavens-feel
claude plugin install heavens-feel@heavens-feel

You need a Claude Code version with mods (function hooks).

Use

/hf opens Sakura's room. It also takes these arguments: on, off, cast (the full roster), dark, say, sleep, bond.

Settings, under the plugin's options:

| Option | Default | What it does | | :- | :- | :- | | Open pane on start | on | Opens Sakura's room when a session starts (wide terminals only) | | Band above prompt | auto | auto, always or never | | Spinner takeover | word | off or word | | Dark Sakura threshold | 75 | Context percent where Sakura turns dark | | Sleep after (minutes) | 5 | Idle time before she falls asleep | | Timezone | Asia/Jerusalem | For her greetings, which keep Shabbat |

Turn it on and off

| To turn off | How | | :- | :- | | The whole mod | claude plugin disable heavens-feel@heavens-feel, or the Installed tab of /plugin; claude plugin enable heavens-feel@heavens-feel brings it back. A running session picks it up with /reload-plugins | | Sakura's room | /hf off closes it and /hf on opens it; it reopens on its own at the next session unless you closed it last, or set Open pane on start off | | The band above the prompt | Set Band above prompt to never | | The spinner word | Set Spinner takeover to off |

Set options with claude plugin configure heavens-feel@heavens-feel, or from /plugin inside a session. To remove it entirely: claude plugin uninstall heavens-feel@heavens-feel.

What it can reach

A mod runs inside Claude Code with your permissions. This one reads session events, transcripts and subagent messages to draw them, keeps a few values in its plugin store (a bond counter, whether the pane was open, a Dark Sakura override), and makes no network requests. Run claude plugin validate on the folder to see every hook and call it makes.

Develop

CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude plugin test .

The Sakura portraits were drawn as pixel art and refined with image generation; tools/ holds the Python pipeline that turns the PNGs in art/ into hooks/sprites.generated.ts. DESIGN.md is the original design.

Credits

A fan work. Fate/stay night and its characters belong to TYPE-MOON; this project is not affiliated with or endorsed by them. Code under the MIT license.

Source 10 files
hooks/register.tsx 526 lines
1// heavens-feel: Sakura watches over the main session; a Servant answers every subagent.
2import { atom, read, update } from 'claude-code'
3import type { EngineInterface, Register, SessionMessage } from 'claude-code'
4
5import type { HeavensFeelActivity, HeavensFeelAgent, HeavensFeelBubble, HeavensFeelMood, HeavensFeelTheme } from '../types'
6import { cast, masterForMessage, moodForTool } from './cast'
7import { gridToRaster } from './render'
8import { CAST } from './sprites'
9import { PANE, readSettings, rt, type DarkOverride, type Settings } from './state'
10import { castText, CAST_MARKER, drawAvatar, drawBand, drawCast, drawPane, nameOf, spinnerWord, type View } from './views'
11import { activityLine, describeTool, greeting, idleLine, localTime, sakuraLine, servantLine } from './voice'
12
13type $ = EngineInterface
14
15// The session's atoms: host-held, so they survive a hot reload.
16const mood = atom({ plugin: 'heavens-feel', key: 'mood' } as const, 'idle' as HeavensFeelMood)
17const dark = atom({ plugin: 'heavens-feel', key: 'dark' } as const, false)
18const ctx = atom({ plugin: 'heavens-feel', key: 'ctx' } as const, 0)
19const agents = atom({ plugin: 'heavens-feel', key: 'agents' } as const, {} as Record<string, HeavensFeelAgent>)
20const bubble = atom({ plugin: 'heavens-feel', key: 'bubble' } as const, { text: '', at: 0 } as HeavensFeelBubble)
21const paneVisible = atom({ plugin: 'heavens-feel', key: 'paneVisible' } as const, false)
22const activity = atom({ plugin: 'heavens-feel', key: 'activity' } as const, { icon: '✿', text: 'at home', at: 0 } as HeavensFeelActivity)
23const theme = atom({ plugin: 'heavens-feel', key: 'theme' } as const, 'auto' as HeavensFeelTheme)
24const viewing = atom({ plugin: 'heavens-feel', key: 'viewing' } as const, '')
25
26const TICK_MS = 100
27const TALK_MS = 1200
28const LINE_GAP_MS = 2500
29const SERVANT_LINE_EVERY = 3
30const MAX_MESSAGE_AGENTS = 2000
31const POLL_MS = 2000
32const FORGET_AGENT_MS = 12000
33const DARK_HYSTERESIS = 10
34const ERROR_STREAK_DARK = 3
35const PANE_COLUMNS = 52
36const PANE_TITLE = "Sakura's room"
37
38const HOLDS: Partial<Record<HeavensFeelMood, number>> = { ouch: 1800, done: 4000, interrupted: 2000, summoning: 800 }
39
40const STORE_PANE_OPEN = 'paneOpen'
41const STORE_DARK_OVERRIDE = 'darkOverride'
42const STORE_BOND = 'bond'
43
44const USAGE = [
45  '/hf                  toggle Sakura\'s pane',
46  '/hf on | off         open or close it',
47  '/hf cast             the whole cast, with bond hearts',
48  '/hf dark [on|off|auto]  Dark Sakura override',
49  '/hf say              a fresh line',
50  '/hf sleep            let her doze off',
51  '/hf bond             bond hearts per character',
52].join('\n')
53
54let settings: Settings = readSettings({})
55let bond: Record<string, number> = {}
56
57
58const say = async ($: $, text: string) => {
59  const now = await $.clock.now()
60  rt.talkUntil = now + TALK_MS
61  rt.lastLineAt = now
62  await update($, bubble, () => ({ text, at: now }))
63}
64
65const setMood = async ($: $, next: HeavensFeelMood, options: { line?: boolean } = {}) => {
66  const now = await $.clock.now()
67  const changed = rt.mood !== next
68  rt.mood = next
69  rt.lastActivity = now
70  rt.moodToken += 1
71  const token = rt.moodToken
72  if (changed) await update($, mood, () => next)
73  const isLoud = next === 'done' || next === 'ouch' || next === 'summoning' || next === 'interrupted'
74  if (options.line !== false && changed && (isLoud || now - rt.lastLineAt > LINE_GAP_MS)) {
75    await say($, next === 'idle' ? idleLine(localTime(now, settings.timezone), rt.isDark) : sakuraLine(next, rt.isDark))
76  }
77  const hold = HOLDS[next]
78  if (hold) {
79    $.clock.after(hold, () => {
80      if (rt.moodToken !== token) return
81      void setMood($, rt.isTurnRunning ? 'thinking' : 'idle', { line: false })
82    })
83  }
84}
85
86const setActivity = async ($: $, icon: string, text: string) => {
87  const now = await $.clock.now()
88  await update($, activity, () => ({ icon, text, at: now }))
89}
90
91const themeOf = (value: unknown): HeavensFeelTheme => {
92  const name = String(value ?? '').toLowerCase()
93  if (name.includes('light')) return 'light'
94  if (name.includes('dark')) return 'dark'
95  return 'auto'
96}
97
98const wake = async ($: $) => {
99  if (rt.mood !== 'sleeping') return
100  await setMood($, 'idle', { line: false })
101  await say($, sakuraLine('wake', rt.isDark))
102}
103
104const setAgent = async ($: $, id: string, change: (agent: HeavensFeelAgent) => HeavensFeelAgent) => {
105  const agent = rt.agents[id]
106  if (!agent) return
107  const changed = change(agent)
108  rt.agents = { ...rt.agents, [id]: changed }
109  await update($, agents, all => ({ ...all, [id]: changed }))
110}
111
112const forgetAgent = async ($: $, id: string) => {
113  const { [id]: gone, ...rest } = rt.agents
114  if (gone) rt.pastAgents.set(id, gone)
115  rt.agents = rest
116  for (const key of [...rt.rasters.keys()].filter(one => one.endsWith(`:${id}`))) {
117    rt.rasters.delete(key)
118    rt.blitted.delete(key)
119  }
120  await update($, agents, () => rest)
121}
122
123const finishAgent = async ($: $, id: string, isError: boolean) => {
124  const agent = rt.agents[id]
125  if (!agent || agent.endedAt) return
126  const now = await $.clock.now()
127  await setAgent($, id, one => ({
128    ...one,
129    mood: isError ? 'ouch' : 'done',
130    endedAt: now,
131    isError,
132    line: servantLine(one.character, isError ? 'error' : 'done'),
133  }))
134  await addBond($, agent.character)
135  $.clock.after(FORGET_AGENT_MS, () => void forgetAgent($, id))
136}
137
138const addBond = async ($: $, character: string) => {
139  bond = { ...bond, [character]: (bond[character] ?? 0) + 1 }
140  await $.store.set(STORE_BOND, bond)
141}
142
143const evaluateDark = async ($: $) => {
144  const threshold = settings.darkThreshold
145  const isOverThreshold = threshold > 0 && rt.ctx >= (rt.isDarkByAuto ? threshold - DARK_HYSTERESIS : threshold)
146  rt.isDarkByAuto = isOverThreshold || rt.errorStreak >= ERROR_STREAK_DARK
147  const next = rt.darkOverride === 'on' ? true : rt.darkOverride === 'off' ? false : rt.isDarkByAuto
148  if (next === rt.isDark) return
149  rt.isDark = next
150  await update($, dark, () => next)
151  await say($, sakuraLine(next ? 'enter' : 'exit', next))
152  if (next && rt.canDraw && rt.darkOverride !== 'on') {
153    const why = isOverThreshold ? `context ${Math.round(rt.ctx)}%` : `${rt.errorStreak} errors in a row`
154    $.ui.toast(`Sakura is not herself (${why}). /compact helps, or /hf dark off.`)
155  }
156}
157
158const openPane = async ($: $, focus: boolean) => {
159  const placed = await $.ui.open({ id: PANE, title: "Sakura's room", columns: PANE_COLUMNS, ...(focus ? { focus: true } : {}) })
160  await update($, paneVisible, () => placed.isPlaced)
161  await $.store.set(STORE_PANE_OPEN, true)
162  return placed
163}
164
165const closePane = async ($: $) => {
166  await $.ui.close({ id: PANE })
167  await update($, paneVisible, () => false)
168  await $.store.set(STORE_PANE_OPEN, false)
169}
170
171let lastPollAt = 0
172let lastRedrawSecond = 0
173
174const tick = async ($: $) => {
175  const now = await $.clock.now()
176  const sleepMs = settings.sleepAfterMinutes * 60_000
177  if (sleepMs > 0 && rt.mood === 'idle' && !rt.isTurnRunning && now - rt.lastActivity > sleepMs) {
178    await setMood($, 'sleeping')
179  }
180  for (const [key, live] of rt.rasters) {
181    const current = live.frameAt(now)
182    if (!current) {
183      rt.rasters.delete(key)
184      continue
185    }
186    if (rt.blitted.get(key) === current.key) continue
187    rt.blitted.set(key, current.key)
188    const { cells } = gridToRaster(current.grid, live.scale)
189    const blit = await $.ui.blit({ requestId: live.requestId, key, cells })
190    if (blit.deny) rt.rasters.delete(key)
191  }
192  const hasRunning = Object.values(rt.agents).some(agent => !agent.endedAt)
193  const second = Math.floor(now / 1000)
194  if (hasRunning && second !== lastRedrawSecond) {
195    lastRedrawSecond = second
196    $.ui.invalidate('ui.render')
197  }
198  if (now - lastPollAt >= POLL_MS) {
199    lastPollAt = now
200    rt.surfaces = await $.session.surfaces()
201    if (hasRunning) await reconcileAgents($)
202  }
203}
204
205/** Agents killed or failed without a turn.complete still leave the roster. */
206/** Everything a drawing shows; reading it subscribes the drawing to each atom. */
207const readView = async ($: $): Promise<View> => {
208  const [currentMood, isDark, all, line, percent, doing, currentTheme, now] = await Promise.all([
209    read($, mood),
210    read($, dark),
211    read($, agents),
212    read($, bubble),
213    read($, ctx),
214    read($, activity),
215    read($, theme),
216    $.clock.now(),
217  ])
218  return { mood: currentMood, isDark, agents: all, bubble: line, ctx: percent, activity: doing, theme: currentTheme, now }
219}
220
221/** A subagent's own conversation, or nothing when the session cannot read it. */
222const agentMessages = async ($: $, agentId: string): Promise<readonly SessionMessage[]> => {
223  const found = await $.session.messages({ agentId })
224  return Array.isArray(found) ? found : []
225}
226
227const reconcileAgents = async ($: $) => {
228  const listed = new Map((await $.agent.list()).map(info => [info.id, info.status]))
229  for (const agent of Object.values(rt.agents)) {
230    const status = listed.get(agent.id)
231    if (agent.endedAt || status === undefined || status === 'running') continue
232    await finishAgent($, agent.id, status !== 'completed')
233  }
234}
235
236export const register: Register = (on, options) => {
237  settings = readSettings(options)
238
239  on('session.start', async ($, e, next) => {
240    await $.command.register({
241      name: 'hf',
242      description: "Sakura's pane and the Heaven's Feel cast",
243      argumentHint: '[on|off|cast|dark|say|sleep|bond]',
244      immediate: true,
245    })
246    rt.canDraw = e.isInteractive && e.surface !== null
247    const [storedBond, storedOverride, storedPaneOpen] = await Promise.all([
248      $.store.get(STORE_BOND),
249      $.store.get(STORE_DARK_OVERRIDE),
250      $.store.get(STORE_PANE_OPEN),
251    ])
252    bond = (storedBond as Record<string, number> | undefined) ?? {}
253    rt.darkOverride = (storedOverride as DarkOverride | undefined) ?? null
254    const now = await $.clock.now()
255    rt.lastActivity = now
256    // A hot reload keeps the atoms; pick their values back up.
257    rt.mood = await read($, mood)
258    rt.isDark = await read($, dark)
259    // Agents cast before subagents were Servants only would wear a Master's face: let them go.
260    const restored = await read($, agents)
261    rt.agents = Object.fromEntries(Object.entries(restored).filter(([, agent]) => CAST[agent.character].role === 'servant'))
262    if (Object.keys(rt.agents).length !== Object.keys(restored).length) await update($, agents, () => rt.agents)
263    rt.ctx = (await $.session.usage()).context.percent ?? 0
264    const themeRow = (await $.config.list()).find(row => row.key === 'theme')
265    await update($, theme, () => themeOf(themeRow?.value))
266    await update($, ctx, () => rt.ctx)
267    await evaluateDark($)
268    if (!rt.canDraw) return next(e)
269    rt.surfaces = await $.session.surfaces()
270    if (!(await read($, bubble)).text) await say($, greeting(localTime(now, settings.timezone)))
271    if (settings.paneOnStart && storedPaneOpen !== false) {
272      const placed = await $.ui.open({ id: PANE, title: PANE_TITLE, columns: PANE_COLUMNS })
273      await update($, paneVisible, () => placed.isPlaced)
274    }
275    let isTicking = false
276    $.clock.every(TICK_MS, () => {
277      if (isTicking) return
278      isTicking = true
279      tick($)
280        .catch(() => {})
281        .finally(() => {
282          isTicking = false
283        })
284    })
285    return next(e)
286  })
287
288  on('session.end', async ($, e, next) => {
289    if (e.reason === 'clear') {
290      rt.agents = {}
291      rt.errorStreak = 0
292      rt.isTurnRunning = false
293      await update($, agents, () => ({}))
294      await setMood($, 'idle', { line: false })
295      await say($, "I'm still here, Senpai. Clean slate.")
296      await evaluateDark($)
297    }
298    return next(e)
299  })
300
301  on('prompt.submit', async ($, e, next) => {
302    await wake($)
303    return next(e)
304  })
305
306  on('turn.start', async ($, e, next) => {
307    rt.isTurnRunning = true
308    await wake($)
309    await setMood($, 'thinking')
310    await setActivity($, '❀', 'thinking it through')
311    return next(e)
312  })
313
314  on('turn.complete', async ($, e, next) => {
315    if (e.agentId) {
316      await finishAgent($, e.agentId, e.reason === 'error' || e.reason === 'refusal')
317      return next(e)
318    }
319    rt.isTurnRunning = false
320    if (e.isAborted) {
321      await setMood($, 'interrupted')
322    } else if (e.reason === 'answer') {
323      rt.errorStreak = 0
324      await setMood($, 'done')
325      await setActivity($, '✿', 'finished, waiting for you')
326      await addBond($, 'sakura')
327    } else {
328      await setMood($, 'ouch')
329    }
330    await evaluateDark($)
331    return next(e)
332  })
333
334  on('tool.call', async ($, e, next) => {
335    const toolMood = moodForTool(e.tool)
336    const doing = describeTool(e.tool, e as unknown as Record<string, unknown>)
337    const agentId = e.agentId
338    if (agentId && rt.agents[agentId]) {
339      await setAgent($, agentId, agent => ({
340        ...agent,
341        mood: toolMood,
342        activity: `${doing.icon} ${doing.text}`,
343        tools: agent.tools + 1,
344        ...(agent.tools % SERVANT_LINE_EVERY === 0 ? { line: servantLine(agent.character, 'working') } : {}),
345      }))
346    } else if (!agentId) {
347      await wake($)
348      const now = await $.clock.now()
349      const isNewMood = rt.mood !== toolMood
350      await setMood($, toolMood, { line: false })
351      await setActivity($, doing.icon, doing.text)
352      if (isNewMood || now - rt.lastLineAt > LINE_GAP_MS) await say($, activityLine(toolMood, rt.isDark, doing.target))
353    }
354    const result = await next(e)
355    if (agentId && rt.agents[agentId]) {
356      await setAgent($, agentId, agent => ({
357        ...agent,
358        mood: 'thinking',
359        ...(result.isError ? { line: servantLine(agent.character, 'error'), activity: `✗ ${doing.text} failed` } : {}),
360      }))
361    } else if (!agentId) {
362      rt.errorStreak = result.isError ? rt.errorStreak + 1 : 0
363      if (result.isError) {
364        await setMood($, 'ouch')
365        await setActivity($, '✗', `${doing.text} failed`)
366      } else if (toolMood !== 'summoning') {
367        await setMood($, rt.isTurnRunning ? 'thinking' : 'idle', { line: false })
368      }
369      await evaluateDark($)
370    }
371    return result
372  })
373
374  on('agent.spawn', async ($, e, next) => {
375    const result = await next(e)
376    if (!result.agentId) return result
377    const running = Object.values(rt.agents).filter(agent => !agent.endedAt)
378    const casting = cast(e.subagentType, running)
379    const now = await $.clock.now()
380    const agent: HeavensFeelAgent = {
381      id: result.agentId,
382      character: casting.character,
383      badge: casting.badge,
384      type: e.subagentType,
385      description: e.description,
386      isFork: e.fork || e.subagentType === 'fork',
387      mood: 'thinking',
388      startedAt: now,
389      isError: false,
390      line: servantLine(casting.character, 'spawn'),
391      activity: '✦ answering the summons',
392      tools: 0,
393    }
394    rt.agents = { ...rt.agents, [agent.id]: agent }
395    await update($, agents, all => ({ ...all, [agent.id]: agent }))
396    if (!e.parentAgentId) await setMood($, 'summoning', { line: false })
397    if (!e.parentAgentId) await say($, `${nameOf(agent)}, please: ${e.description}.`)
398    return result
399  })
400
401  on('session.measure', async ($, e, next) => {
402    const percent = e.context.percent
403    if (percent !== undefined && Math.round(percent) !== Math.round(rt.ctx)) {
404      rt.ctx = percent
405      await update($, ctx, () => percent)
406      await evaluateDark($)
407    }
408    return next(e)
409  })
410
411  on('config.set', { key: 'theme' }, async ($, e, next) => {
412    const result = await next(e)
413    if (!result.deny) await update($, theme, () => themeOf(result.value))
414    return result
415  })
416
417  // Which agent wrote each assistant row, so its message can wear that agent's face.
418  on('session.append', async ($, e, next) => {
419    const result = await next(e)
420    const uuid = result.uuid ?? e.uuid
421    if (e.agentId && e.message.role === 'assistant' && uuid) {
422      rt.messageAgents.set(uuid, e.agentId)
423      if (rt.messageAgents.size > MAX_MESSAGE_AGENTS) rt.messageAgents.delete(rt.messageAgents.keys().next().value!)
424    }
425    return result
426  })
427
428  on('ui.render', { component: 'AssistantMessage' }, async ($, e, next) => {
429    // The terminal marks the block that opens a reply; other surfaces may never set the mark.
430    const isAvatarBlock = e.props.isFirstOfReply || e.surface !== 'terminal'
431    if (!isAvatarBlock) return next(e)
432    const agentId = rt.messageAgents.get(e.requestId)
433    const [all, isDark, currentTheme] = await Promise.all([read($, agents), read($, dark), read($, theme)])
434    const agent = agentId ? all[agentId] ?? rt.pastAgents.get(agentId) : undefined
435    const speaker = agent?.character ?? masterForMessage(e.requestId)
436    const engineTree = await next(e)
437    return drawAvatar($.ui.resolve(e), e, engineTree, speaker, isDark, currentTheme)
438  })
439
440  on('ui.close', { id: PANE }, async ($, e, next) => {
441    for (const [key, live] of rt.rasters) {
442      if (live.requestId === PANE) rt.rasters.delete(key)
443    }
444    await update($, paneVisible, () => false)
445    if (e.origin.kind === 'person') await $.store.set(STORE_PANE_OPEN, false)
446    return next(e)
447  })
448
449  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
450    const view = await readView($)
451    const chosen = (await read($, viewing)) || e.props.view.agentId || ''
452    const agent = chosen ? (view.agents[chosen] ?? rt.pastAgents.get(chosen)) : undefined
453    const servant = agent ? { agent, messages: await agentMessages($, agent.id), canGoBack: !e.props.view.agentId } : undefined
454    const controls = {
455      view: (id: string) => void update($, viewing, () => id),
456      back: () => void update($, viewing, () => ''),
457      openTasks: () => void $.command.run({ command: 'tasks' }).catch(() => {}),
458    }
459    return drawPane($.ui.resolve(e), e, view, settings, bond, servant, controls)
460  })
461
462  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
463    if (e.props.hasSurvey || settings.band === 'never') return next(e)
464    if (settings.band === 'auto' && (await read($, paneVisible))) return next(e)
465    return drawBand($.ui.resolve(e), e, await readView($), () => void openPane($, true))
466  })
467
468  on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
469    if (settings.spinnerTakeover === 'off') return next(e)
470    const [all, currentMood, isDark] = await Promise.all([read($, agents), read($, mood), read($, dark)])
471    const word = spinnerWord(all[e.requestId], currentMood, isDark)
472    return next({ ...e, props: { ...e.props, word } })
473  })
474
475  on('ui.render', { component: 'CommandOutput', props: { command: 'hf' } }, async ($, e, next) => {
476    if (!e.props.text.startsWith(CAST_MARKER)) return next(e)
477    return drawCast($.ui.resolve(e), e, bond, await read($, theme))
478  })
479
480  on('command.run', { command: 'hf' }, async ($, e) => {
481    const [sub = '', arg = ''] = e.args.trim().toLowerCase().split(/\s+/)
482    switch (sub) {
483      case '':
484        if (await read($, paneVisible)) {
485          await closePane($)
486          return { text: 'Sakura waves goodbye. /hf brings her back.' }
487        }
488        await openPane($, true)
489        return {}
490      case 'on':
491        await openPane($, true)
492        return {}
493      case 'off':
494        await closePane($)
495        return { text: 'Pane closed.' }
496      case 'cast':
497        return { text: castText(bond, Object.values(rt.agents)) }
498      case 'dark': {
499        if (arg === 'on' || arg === 'off' || arg === 'auto') {
500          rt.darkOverride = arg === 'auto' ? null : arg
501          await $.store.set(STORE_DARK_OVERRIDE, rt.darkOverride)
502          await evaluateDark($)
503        }
504        const why = `context ${Math.round(rt.ctx)}% (threshold ${settings.darkThreshold}), ${rt.errorStreak} errors in a row`
505        return { text: `Dark Sakura: ${rt.isDark ? 'yes' : 'no'} · override ${rt.darkOverride ?? 'auto'} · ${why}` }
506      }
507      case 'say':
508      {
509        const time = localTime(await $.clock.now(), settings.timezone)
510        const line = rt.mood === 'idle' ? idleLine(time, rt.isDark) : sakuraLine(rt.mood, rt.isDark)
511        await say($, line)
512      }
513        return {}
514      case 'sleep':
515        await setMood($, 'sleeping')
516        return {}
517      case 'bond': {
518        const rows = Object.values(CAST).map(entry => `${entry.name.padEnd(14)} ${bond[entry.id] ?? 0} turns`)
519        return { text: rows.join('\n') }
520      }
521      default:
522        return { text: USAGE }
523    }
524  })
525}
526
hooks/cast.ts 109 lines
1// Who answers which summons: subagent type -> character, with understudies and badges.
2import type { HeavensFeelAgent, HeavensFeelMaster, HeavensFeelMood, HeavensFeelServant } from '../types'
3
4type Servant = HeavensFeelServant
5
6/** Built-in subagent types: each answered by the Servant whose legend fits the job. */
7const EXACT: Readonly<Record<string, Servant>> = {
8  explore: 'rider',
9  plan: 'caster',
10  'general-purpose': 'saber',
11  claude: 'saber',
12  teammate: 'saber',
13  'claude-code-guide': 'gilgamesh',
14  'statusline-setup': 'assassin',
15  fork: 'rider',
16}
17
18/** Custom types by name, first match wins. */
19const RULES: readonly (readonly [RegExp, Servant])[] = [
20  [/review|checker|audit|lint|coderabbit/, 'archer'],
21  [/judge|verif|meta|proximity/, 'saber-alter'],
22  [/research|explor|map|search|scout|discover|intel|profil|classif/, 'rider'],
23  [/plan|roadmap|synth|architect|spec/, 'caster'],
24  [/debug|forensic|fixer|hunt|security/, 'true-assassin'],
25  [/gener|evolv|ideat|sketch|spike|empiric/, 'lancer'],
26  [/exec|writ|build|updat|experiment|doc-/, 'saber'],
27  [/guide|docs|help|explain/, 'gilgamesh'],
28  [/fast|quick|autonom|berserk/, 'berserker'],
29  [/style|format|status|ui|design/, 'assassin'],
30]
31
32/** Every Servant in summoning order: the fallback for a type no rule names. */
33const POOL: readonly Servant[] = [
34  'archer',
35  'saber-alter',
36  'lancer',
37  'true-assassin',
38  'berserker',
39  'rider',
40  'caster',
41  'saber',
42  'assassin',
43  'gilgamesh',
44]
45
46/** Who steps in when a Servant is already out on another summons. */
47const UNDERSTUDIES: Readonly<Record<Servant, readonly Servant[]>> = {
48  rider: ['true-assassin', 'lancer'],
49  saber: ['saber-alter', 'lancer'],
50  'saber-alter': ['saber', 'berserker'],
51  archer: ['gilgamesh', 'saber-alter'],
52  lancer: ['rider', 'assassin'],
53  caster: ['archer', 'gilgamesh'],
54  assassin: ['lancer', 'caster'],
55  'true-assassin': ['rider', 'assassin'],
56  berserker: ['saber-alter', 'lancer'],
57  gilgamesh: ['archer', 'caster'],
58}
59
60const fnv1a = (text: string): number => {
61  let hash = 0x811c9dc5
62  for (let i = 0; i < text.length; i++) {
63    hash ^= text.charCodeAt(i)
64    hash = Math.imul(hash, 0x01000193) >>> 0
65  }
66  return hash
67}
68
69export const characterForType = (subagentType: string): Servant => {
70  const type = subagentType.toLowerCase().replace(/^[^:]+:/, '')
71  const exact = EXACT[type]
72  if (exact) return exact
73  const rule = RULES.find(([pattern]) => pattern.test(type))
74  if (rule) return rule[1]
75  return POOL[fnv1a(type) % POOL.length]!
76}
77
78const MASTERS: readonly HeavensFeelMaster[] = ['sakura', 'shirou', 'rin', 'illya', 'kirei']
79
80/** The Master who speaks a main-session message: chosen by its message id, so every block of one response, and every redraw, keeps the same face. */
81export const masterForMessage = (requestId: string): HeavensFeelMaster =>
82  MASTERS[fnv1a(requestId.replace(/-t\d+$/, '')) % MASTERS.length]!
83
84export type Casting = { character: Servant; badge: number }
85
86/** The preferred Servant, else an understudy, else a free one, else the preferred with a badge. */
87export const cast = (subagentType: string, running: readonly HeavensFeelAgent[]): Casting => {
88  const preferred = characterForType(subagentType)
89  const busy = new Set(running.map(agent => agent.character))
90  const free = [preferred, ...UNDERSTUDIES[preferred], ...POOL].find(one => !busy.has(one))
91  if (free) return { character: free, badge: 1 }
92  const sameCharacter = running.filter(agent => agent.character === preferred)
93  return { character: preferred, badge: sameCharacter.length + 1 }
94}
95
96const EDITING = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit'])
97const SEARCHING = new Set(['Read', 'Grep', 'Glob', 'LS', 'WebFetch', 'WebSearch', 'ToolSearch', 'LSP'])
98
99export const moodForTool = (tool: string): HeavensFeelMood => {
100  if (EDITING.has(tool)) return 'editing'
101  if (SEARCHING.has(tool)) return 'searching'
102  if (tool === 'Agent' || tool === 'Task') return 'summoning'
103  return 'running'
104}
105
106const NUMERALS = ['', '', ' II', ' III', ' IV', ' V', ' VI', ' VII', ' VIII', ' IX', ' X']
107
108export const badgeSuffix = (badge: number): string => NUMERALS[badge] ?? ` ${badge}`
109
hooks/render.ts 224 lines
1// Sprite -> pixel grid -> terminal Raster cells (half blocks) or desktop SVG rects.
2import type { Overlay, Palette, Sprite } from './sprites'
3
4export type Grid = { w: number; h: number; px: Int32Array }
5
6const TRANSPARENT = -1
7const KEEP = '.'
8const ERASE = '#'
9const DEFAULT_COLOR = 0x01000000
10const UPPER_HALF = 0x2580
11const LOWER_HALF = 0x2584
12const FULL_BLOCK = 0x2588
13const SPACE = 0x20
14
15export const compose = (base: Sprite, overlays: readonly Overlay[], palette?: Palette): Grid => {
16  const colors = palette ?? base.palette
17  const px = new Int32Array(base.w * base.h)
18  for (let y = 0; y < base.h; y++) {
19    const rows = [base.rows[y] ?? '', ...overlays.map(o => o.rows[y] ?? '')]
20    for (let x = 0; x < base.w; x++) {
21      let ch = rows[0]![x] ?? KEEP
22      for (const row of rows.slice(1)) {
23        const over = row[x] ?? KEEP
24        if (over !== KEEP) ch = over
25      }
26      px[y * base.w + x] = ch === KEEP || ch === ERASE ? TRANSPARENT : (colors[ch] ?? TRANSPARENT)
27    }
28  }
29  return { w: base.w, h: base.h, px }
30}
31
32/** Moves every pixel by (dx, dy); what slides in is transparent. */
33export const shift = (grid: Grid, dx: number, dy: number): Grid => {
34  const px = new Int32Array(grid.w * grid.h).fill(TRANSPARENT)
35  for (let y = 0; y < grid.h; y++) {
36    for (let x = 0; x < grid.w; x++) {
37      const sx = x - dx
38      const sy = y - dy
39      if (sx >= 0 && sy >= 0 && sx < grid.w && sy < grid.h) px[y * grid.w + x] = grid.px[sy * grid.w + sx]!
40    }
41  }
42  return { w: grid.w, h: grid.h, px }
43}
44
45/** The top `rows` pixel rows of the grid. */
46export const crop = (grid: Grid, rows: number): Grid => ({ w: grid.w, h: rows, px: grid.px.slice(0, grid.w * rows) })
47
48/** Paints `points` (x, y, color) onto a copy of the grid. */
49export const paint = (grid: Grid, points: readonly (readonly [number, number, number])[]): Grid => {
50  const px = grid.px.slice()
51  for (const [x, y, color] of points) {
52    if (x >= 0 && y >= 0 && x < grid.w && y < grid.h) px[y * grid.w + x] = color
53  }
54  return { w: grid.w, h: grid.h, px }
55}
56
57const at = (grid: Grid, x: number, y: number): number =>
58  y < grid.h ? grid.px[y * grid.w + x]! : TRANSPARENT
59
60const cellFor = (top: number, bottom: number): readonly [number, number, number] => {
61  if (top !== TRANSPARENT && bottom !== TRANSPARENT) return [UPPER_HALF, top, bottom]
62  if (top !== TRANSPARENT) return [UPPER_HALF, top, DEFAULT_COLOR]
63  if (bottom !== TRANSPARENT) return [LOWER_HALF, bottom, DEFAULT_COLOR]
64  return [SPACE, DEFAULT_COLOR, DEFAULT_COLOR]
65}
66
67const BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
68
69/** Standard padded base64, the encoding Raster cells take. */
70export const toBase64 = (bytes: Uint8Array): string => {
71  const out: string[] = []
72  for (let i = 0; i < bytes.length; i += 3) {
73    const a = bytes[i]!
74    const b = bytes[i + 1] ?? 0
75    const c = bytes[i + 2] ?? 0
76    const n = (a << 16) | (b << 8) | c
77    out.push(
78      BASE64[(n >> 18) & 63]!,
79      BASE64[(n >> 12) & 63]!,
80      i + 1 < bytes.length ? BASE64[(n >> 6) & 63]! : '=',
81      i + 2 < bytes.length ? BASE64[n & 63]! : '=',
82    )
83  }
84  return out.join('')
85}
86
87export type RasterCells = { columns: number; rows: number; cells: string }
88
89/** Scale 1: one pixel per column, two per row. Scale 2: two columns and one row per pixel. */
90export const gridToRaster = (grid: Grid, scale: 1 | 2): RasterCells => {
91  const columns = grid.w * scale
92  const rows = scale === 1 ? Math.ceil(grid.h / 2) : grid.h
93  const words = new Uint32Array(columns * rows * 3)
94  let i = 0
95  for (let r = 0; r < rows; r++) {
96    for (let x = 0; x < grid.w; x++) {
97      if (scale === 1) {
98        words.set(cellFor(at(grid, x, 2 * r), at(grid, x, 2 * r + 1)), i)
99        i += 3
100        continue
101      }
102      const color = at(grid, x, r)
103      const cell = color === TRANSPARENT ? [SPACE, DEFAULT_COLOR, DEFAULT_COLOR] : [FULL_BLOCK, color, color]
104      words.set(cell, i)
105      words.set(cell, i + 3)
106      i += 6
107    }
108  }
109  return { columns, rows, cells: toBase64(new Uint8Array(words.buffer)) }
110}
111
112export const hex = (color: number): string => `#${color.toString(16).padStart(6, '0')}`
113
114/** Runs of one colour per row, as path commands: far shorter than a <rect> per run. */
115const pathsOf = (grid: Grid): Map<number, string[]> => {
116  const byColor = new Map<number, string[]>()
117  for (let y = 0; y < grid.h; y++) {
118    let x = 0
119    while (x < grid.w) {
120      const color = at(grid, x, y)
121      let run = 1
122      while (x + run < grid.w && at(grid, x + run, y) === color) run++
123      const runs = byColor.get(color) ?? []
124      runs.push(`M${x} ${y}h${run}v1h-${run}z`)
125      byColor.set(color, runs)
126      x += run
127    }
128  }
129  return byColor
130}
131
132const ERASED = -2
133
134/** Erased pixels take the backdrop's colour: a fixed fill, or the shared class `auto` themes. */
135const drawPaths = (byColor: Map<number, string[]>, erasedFill: string): string =>
136  [...byColor]
137    .filter(([color]) => color !== TRANSPARENT)
138    .map(([color, runs]) => `<path ${color === ERASED ? erasedFill : `fill="${hex(color)}"`} d="${runs.join('')}"/>`)
139    .join('')
140
141/** Pixels of `over` that differ from `base`; where `over` is see-through, the backdrop shows. */
142const delta = (base: Grid, over: Grid): Grid => {
143  const px = new Int32Array(base.w * base.h).fill(TRANSPARENT)
144  over.px.forEach((color, i) => {
145    if (color !== base.px[i]) px[i] = color === TRANSPARENT ? ERASED : color
146  })
147  return { w: base.w, h: base.h, px }
148}
149
150export type Backdrop = { theme: 'light' | 'dark' | 'auto'; accent: number }
151
152const BACKDROP = { light: { bg: '#faf9f5', glow: 0.18 }, dark: { bg: '#262624', glow: 0.28 } }
153
154/** How far past the sprite the backdrop runs, so a frame wider than the art never shows white. */
155const BACKDROP_BLEED = 400
156
157/**
158 * Several of these Svgs can share one document, where every <style> applies to all of them,
159 * so a fixed theme paints with attributes and only `auto` uses a class, the same rule in each.
160 */
161const AUTO_CLASS = 'hf-backdrop'
162const AUTO_STYLE = `<style>.${AUTO_CLASS}{fill:${BACKDROP.light.bg}}@media (prefers-color-scheme: dark){.${AUTO_CLASS}{fill:${BACKDROP.dark.bg}}}</style>`
163
164const backdropFill = (backdrop: Backdrop | undefined): string => {
165  if (!backdrop) return 'fill="none"'
166  return backdrop.theme === 'auto' ? `class="${AUTO_CLASS}"` : `fill="${BACKDROP[backdrop.theme].bg}"`
167}
168
169/** Theme-coloured background with a soft glow in the character's colour under her feet. */
170const backdropOf = (grid: Grid, backdrop: Backdrop): string => {
171  const glow = backdrop.theme === 'light' ? BACKDROP.light.glow : BACKDROP.dark.glow
172  return (
173    (backdrop.theme === 'auto' ? AUTO_STYLE : '') +
174    `<rect ${backdropFill(backdrop)} x="${-BACKDROP_BLEED}" y="${-BACKDROP_BLEED}" width="${grid.w + 2 * BACKDROP_BLEED}" height="${grid.h + 2 * BACKDROP_BLEED}"/>` +
175    `<ellipse cx="${grid.w / 2}" cy="${grid.h - 2}" rx="${grid.w * 0.36}" ry="2.2" fill="${hex(backdrop.accent)}" opacity="${glow}"/>`
176  )
177}
178
179/** The CSS pixel box an Svg of this grid draws in: give it to the element so its frame fits the art. */
180export const svgBox = (grid: Grid, pixel: number): { width: number; height: number } => ({
181  width: grid.w * pixel,
182  height: (grid.h + 1) * pixel,
183})
184
185export type SvgOptions = {
186  pixel: number
187  /** Further frames, looped one after another every `frameMs` (SMIL, no re-render). */
188  frames?: readonly Grid[]
189  frameMs?: number
190  blink?: Grid
191  isBreathing?: boolean
192  backdrop?: Backdrop
193}
194
195const visibleDuring = (k: number, n: number, durMs: number): string =>
196  `<animate attributeName="visibility" values="hidden;visible;hidden" keyTimes="0;${(k / n).toFixed(4)};${((k + 1) / n).toFixed(4)}" calcMode="discrete" dur="${durMs}ms" repeatCount="indefinite"/>`
197
198/**
199 * One portrait as an Svg: the first frame drawn whole, each further frame as its
200 * difference shown in turn, the blink and the breathing bob as their own loops.
201 * Every loop is SMIL, so it animates with no redraw (the Svg must be `isInteractive`).
202 */
203export const gridToSvg = (grid: Grid, options: SvgOptions): string => {
204  const { width, height } = svgBox(grid, options.pixel)
205  const frames = options.frames ?? []
206  const n = frames.length + 1
207  const durMs = (options.frameMs ?? 400) * n
208  const erasedFill = backdropFill(options.backdrop)
209  const loop = frames
210    .map((next, i) => `<g visibility="hidden">${drawPaths(pathsOf(delta(grid, next)), erasedFill)}${visibleDuring(i + 1, n, durMs)}</g>`)
211    .join('')
212  const blinkGroup = options.blink
213    ? `<g opacity="0">${drawPaths(pathsOf(delta(grid, options.blink)), erasedFill)}<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.93;0.94;0.98;1" dur="4.2s" repeatCount="indefinite"/></g>`
214    : ''
215  const bob = options.isBreathing
216    ? '<animateTransform attributeName="transform" type="translate" values="0 0;0 1;0 0" dur="2.4s" calcMode="discrete" repeatCount="indefinite"/>'
217    : ''
218  return (
219    `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -1 ${grid.w} ${grid.h + 1}" width="${width}" height="${height}" preserveAspectRatio="xMidYMid meet" overflow="visible" style="display:block;width:100%;height:100%" shape-rendering="crispEdges">` +
220    (options.backdrop ? backdropOf(grid, options.backdrop) : '') +
221    `<g>${drawPaths(pathsOf(grid), erasedFill)}${loop}${blinkGroup}${bob}</g></svg>`
222  )
223}
224
hooks/sprites.ts 1768 lines
1// heavens-feel: pixel art as pure data. No imports from claude-code; no runtime
2// dependencies. Rendering (half-block Raster cells / SVG rects) lives in render.ts.
3//
4// Format
5//   Sprite.rows: equal-length strings, one char per pixel.
6//   Sprite.palette: char -> 0xRRGGBB. '.' is transparent in a base frame.
7//   Overlay rows use the same chars, where '.' means "keep the base pixel" and
8//   '#' means "erase to transparent". Overlays are the same size as their base.
9//   A Frame = base composed with zero or more overlays, then an optional palette
10//   override (Dark Sakura is the Sakura base + DARK_SAKURA_OVERLAY + DARK_SAKURA_PALETTE).
11//
12// Sizes: main portrait 24w x 32h (16 terminal rows with half-blocks),
13//        mini sprites 12w x 12h (6 terminal rows).
14//
15// Everything here is original chibi fan art drawn by hand for this mod.
16
17import type { HeavensFeelCharacter, HeavensFeelMaster } from '../types'
18
19export type Palette = Record<string, number>
20
21export type Sprite = {
22  w: number
23  h: number
24  rows: readonly string[]
25  palette: Palette
26}
27
28/** An overlay: same size as its base; '.' keeps, '#' erases, anything else paints. */
29export type Overlay = Sprite
30
31export type CharacterId = HeavensFeelCharacter
32
33export type MiniSet = {
34  /** Resting frame. */
35  base: Sprite
36  /** Eyes closed (or the character's equivalent); same size as base. Full frame. */
37  blink: Sprite
38  /** One "working" frame (sparkle, glint, hand raised, shake). Full frame. */
39  work: Sprite
40}
41
42export type CastEntry = {
43  id: CharacterId
44  name: string
45  /** Short epithet drawn under the name plate. */
46  title: string
47  /** Accent color for name plates, band chips, bubble borders. */
48  accent: number
49  mini: MiniSet
50  /** Masters run the main session; Servants are summoned as subagents. */
51  role: 'master' | 'servant'
52  /** Servant class as announced at summoning ('Saber', 'Archer', ...). Servants only. */
53  className?: string
54  /** The Master this Servant answers to in the Heaven's Feel route, when that Master is in the cast. */
55  master?: HeavensFeelMaster
56}
57
58const TRANSPARENT = '.'
59const ERASE = '#'
60export const OVERLAY_KEEP = TRANSPARENT
61export const OVERLAY_ERASE = ERASE
62
63const sprite = (rows: readonly string[], palette: Palette): Sprite => ({
64  w: rows[0]?.length ?? 0,
65  h: rows.length,
66  rows,
67  palette,
68})
69
70// ---------------------------------------------------------------------------
71// Sakura Matou (main session) 24 x 32
72// ---------------------------------------------------------------------------
73
74export const SAKURA_PALETTE: Palette = {
75  H: 0x8f73c2, // hair
76  h: 0x5e4792, // hair shadow / edge
77  L: 0xb9a2e4, // hair highlight
78  R: 0xd8393f, // ribbon red
79  r: 0x9b2229, // ribbon dark
80  S: 0xf8e2cf, // skin
81  s: 0xe6bda2, // skin shadow
82  E: 0x6f45b0, // iris
83  e: 0x2a1b3d, // eye dark / lashes / open mouth
84  W: 0xffffff, // eye glint
85  B: 0xf3a5b1, // blush
86  M: 0xc26274, // mouth
87  U: 0x8e6d49, // blazer
88  u: 0x5f4631, // blazer shadow
89  C: 0xf6f2ea, // collar / thought dots
90  k: 0x3b3250, // skirt
91  K: 0x4f4468, // skirt pleat
92  O: 0x2a2030, // shoes
93  w: 0xa6d8ff, // sweat drop
94  o: 0xfff2a8, // sparkle
95  Z: 0xdcd6f0, // sleep z
96  X: 0xa81e2d, // dark-form dress pattern
97  T: 0x2b0f2e, // dark-form shadow tendrils
98}
99
100/** Palette swap for the corrupted form: white hair, red eyes, black-and-red dress. */
101export const DARK_SAKURA_PALETTE: Palette = {
102  ...SAKURA_PALETTE,
103  H: 0xe7e2f1,
104  h: 0xb4abc9,
105  L: 0xffffff,
106  R: 0x1b1420,
107  r: 0x120d16,
108  S: 0xf4e9ea,
109  s: 0xd7c3cb,
110  E: 0xd62a3a,
111  e: 0x160a12,
112  W: 0xffd6d6,
113  B: 0xd49ab5,
114  M: 0x8a2a3d,
115  U: 0x1d1620,
116  u: 0x0e0a10,
117  C: 0x8e1a2b,
118  k: 0x1d1620,
119  K: 0x7f1424,
120  O: 0x100a12,
121}
122
123export const SAKURA_BASE: Sprite = sprite(
124  [
125    '........................',
126    '........hhHHHHhh........',
127    '......hHHLLHHHHHhRR.....',
128    '.....hHLLHHHHHHHHRRRr...',
129    '....hHHLHHHHHHHHHHrRRr..',
130    '....hHHHHHHHHHHHHHHrRr..',
131    '...hHHHHHHhHHHhHHHHHhr..',
132    '...hHHHHHHHHHHHHHHHHh...',
133    '..hHHHhSSHhHHhHSShHHHh..',
134    '..hHHhSSSSSSSSSSSShHHHh.',
135    '..hHHhSeeeSSSSeeeShHHHh.',
136    '..hHHhSWEESSSSWEEShHHHh.',
137    '.hHHHhSEEeSSSSEEeShHHHh.',
138    '.hHHHhSBeeSSSSeeBShHHHh.',
139    '.hHHHhsSSSSSSSSSSshHHHh.',
140    '.hHHHhsSSSSMMSSSSshHHHh.',
141    '.hHHHHhsSSSSSSSSshHHHHh.',
142    '.hHHHHHhsSSSSSSshHHHHHh.',
143    '.hHHHHHHhsSSSSshHHHHHHh.',
144    '.hHHHHHhCCsSSsCChHHHHHh.',
145    '.hHHHHhUCCCRRCCCUhHHHHh.',
146    '.hHHHhUUUCCRRCCUUUhHHHh.',
147    '.hHHhUUUUUURRUUUUUUhHHh.',
148    '.hHHhUUUUUUuuUUUUUUhHHh.',
149    '.hHHhuUUUUUuuUUUUUuhHHh.',
150    '.hHHhuUUUUSSSSUUUUuhHHh.',
151    '..hHhkkkkksSSskkkkkhHh..',
152    '..hhkkkkkkkkkkkkkkkkhh..',
153    '...kkkkkkkkkkkkkkkkkk...',
154    '...kKkkKkkKkkKkkKkkKk...',
155    '.......sSs....sSs.......',
156    '.......OOO....OOO.......',
157  ],
158  SAKURA_PALETTE,
159)
160
161const blank24 = '........................'
162const sakuraOverlay = (patch: Record<number, string>): Overlay =>
163  sprite(
164    Array.from({ length: 32 }, (_, y) => patch[y] ?? blank24),
165    SAKURA_PALETTE,
166  )
167
168/** Eyes closed. */
169export const SAKURA_BLINK: Overlay = sakuraOverlay({
170  10: '.......SSS....SSS.......',
171  11: '.......SSS....SSS.......',
172  12: '.......eee....eee.......',
173  13: '........SS....SS........',
174})
175
176/** Mouth open (talk). Combine with any eye overlay. */
177export const SAKURA_TALK: Overlay = sakuraOverlay({
178  15: '...........ee...........',
179  16: '...........MM...........',
180})
181
182/** Happy: closed smiling eyes, bigger blush, U smile. */
183export const SAKURA_HAPPY: Overlay = sakuraOverlay({
184  10: '.......SSS....SSS.......',
185  11: '.......SeS....SeS.......',
186  12: '.......eSe....eSe.......',
187  13: '......BBSS....SSBB......',
188  14: '..........M..M..........',
189  15: '...........MM...........',
190})
191
192/** Thinking: eyes glance up and to the side, flat mouth. Pair with THINK_DOTS frames. */
193export const SAKURA_THINK: Overlay = sakuraOverlay({
194  11: '.......WEe....WEe.......',
195  12: '.......SEE....SEE.......',
196  15: '...........Ms...........',
197})
198
199/** Three frames of thought dots drifting up-right of the head. */
200export const SAKURA_THINK_DOTS: readonly Overlay[] = [
201  sakuraOverlay({ 2: '....................C...' }),
202  sakuraOverlay({ 2: '....................C...', 1: '......................C.' }),
203  sakuraOverlay({
204    2: '....................C...',
205    1: '......................C.',
206    0: '.......................C',
207  }),
208]
209
210/** Ouch: X eyes, wobbly mouth, sweat drop. */
211export const SAKURA_OUCH: Overlay = sakuraOverlay({
212  9: '.......................w',
213  10: '.......SSS....SSS......w',
214  11: '.......eSe....eSe.......',
215  12: '.......SeS....SeS.......',
216  13: '.......eSe....eSe.......',
217  15: '.........M.MM.M.........',
218})
219
220/** Sleeping: closed eyes + tiny mouth. Pair with SLEEP_Z frames. */
221export const SAKURA_SLEEP: Overlay = sakuraOverlay({
222  10: '.......SSS....SSS.......',
223  11: '.......SSS....SSS.......',
224  12: '.......eee....eee.......',
225  13: '........SS....SS........',
226  15: '...........Ms...........',
227})
228
229export const SAKURA_SLEEP_Z: readonly Overlay[] = [
230  sakuraOverlay({ 5: '.....................Z..' }),
231  sakuraOverlay({
232    3: '.....................ZZ.',
233    4: '......................Z.',
234    5: '.....................ZZ.',
235  }),
236  sakuraOverlay({
237    0: '.....................ZZZ',
238    1: '......................Z.',
239    2: '.....................ZZZ',
240  }),
241]
242
243/** Searching: eyes look left, then right. Two frames. */
244export const SAKURA_SEARCH: readonly Overlay[] = [
245  sakuraOverlay({
246    11: '.......EEW....EEW.......',
247    12: '.......eES....eES.......',
248  }),
249  sakuraOverlay({
250    11: '.......WEE....WEE.......',
251    12: '.......SEe....SEe.......',
252  }),
253]
254
255/** Editing: hands move up (typing). Frame 0 is the base (no overlay); frame 1 lifts the hands. */
256export const SAKURA_EDIT: readonly Overlay[] = [
257  sakuraOverlay({}),
258  sakuraOverlay({
259    24: '..........sSSs..........',
260    25: '..........UUUU..........',
261    26: '..........kkkk..........',
262  }),
263]
264
265/** Running a shell: a sweat drop slides down. Two frames. */
266export const SAKURA_RUN: readonly Overlay[] = [
267  sakuraOverlay({
268    9: '.......................w',
269    10: '.......................w',
270    15: '...........Ms...........',
271  }),
272  sakuraOverlay({
273    10: '.......................w',
274    11: '.......................w',
275    15: '...........Ms...........',
276  }),
277]
278
279/** Summoning a Servant (Agent tool): happy eyes + drifting sparkles. Two frames. */
280export const SAKURA_SUMMON: readonly Overlay[] = [
281  sakuraOverlay({
282    5: '.o......................',
283    9: '.......................o',
284    10: '.......SSS....SSS.......',
285    11: '.......SeS....SeS.......',
286    12: '.......eSe....eSe.......',
287    13: '........SS....SS........',
288  }),
289  sakuraOverlay({
290    3: '..o.....................',
291    7: '......................o.',
292    12: 'o......eSe....eSe.......',
293    10: '.......SSS....SSS.......',
294    11: '.......SeS....SeS.......',
295    13: '........SS....SS........',
296  }),
297]
298
299/**
300 * Dark Sakura: apply DARK_SAKURA_PALETTE to the base, then this overlay.
301 * It removes the hair ribbon, adds the red zigzag on the dress and skirt, and
302 * lets shadow tendrils creep in from the bottom corners.
303 */
304export const DARK_SAKURA_OVERLAY: Overlay = sprite(
305  [
306    blank24,
307    blank24,
308    '.................##.....',
309    '.................H###...',
310    '..................H###..',
311    '...................###..',
312    '.....................#..',
313    blank24,
314    blank24,
315    blank24,
316    blank24,
317    blank24,
318    blank24,
319    blank24,
320    blank24,
321    blank24,
322    blank24,
323    blank24,
324    blank24,
325    blank24,
326    blank24,
327    '.......X........X.......',
328    '......X..X....X..X......',
329    '.......X........X.......',
330    '......X..X....X..X......',
331    blank24,
332    blank24,
333    'T......................T',
334    'T..X..X..X..X..X..X....T',
335    'TT..X..X..X..X..X..X..TT',
336    'TTT..................TTT',
337    'TTTT................TTTT',
338  ],
339  DARK_SAKURA_PALETTE,
340)
341
342// ---------------------------------------------------------------------------
343// Sakura HD 48 x 64 (docked pane with room; see DESIGN.md §2.1 and §8)
344// Produced by the art pipeline (Codex image generation -> Pillow box downscale ->
345// 32-colour median cut merged to 9 colours). Palette chars here are frequency-
346// assigned by the converter, not semantic: H = outline, h = hair, L = hair shadow,
347// R = skin, r = blazer, S/s = mid tones, E = ribbon red, e = highlights.
348// ---------------------------------------------------------------------------
349
350export const SAKURA_HD_PALETTE: Palette = {
351  H: 0x2d1732,
352  h: 0x5b336b,
353  L: 0x4c3049,
354  R: 0xfbe0c8,
355  r: 0x875859,
356  S: 0xbea393,
357  s: 0x987882,
358  E: 0x9e2020,
359  e: 0xe8c8b4,
360  X: 0xd62a3a, // dark-form iris (overlay only)
361}
362
363/** Dark form for the HD portrait: white hair, black blazer, dark ribbon; eyes via SAKURA_HD_DARK_EYES. */
364export const SAKURA_HD_DARK_PALETTE: Palette = {
365  H: 0x1a1220,
366  h: 0xe7e2f1,
367  L: 0xb4abc9,
368  R: 0xf4e9ea,
369  r: 0x1d1620,
370  S: 0x6b5560,
371  s: 0x3b2a36,
372  E: 0x4a0c14,
373  e: 0xf7f1f3,
374  X: 0xd62a3a,
375}
376
377export const SAKURA_HD_BASE: Sprite = sprite(
378  [
379    '..................HHHHHLHHHH....................',
380    '...............HHLLhhhhhhhLhLLH.................',
381    '.............HLhhhhLhhhhhLhhhhhLH...............',
382    '...........HHhhhhHHhhhhhhLHHhhhhhLH.............',
383    '..........HLhhhLHhhhhhhhhhhhHLhhhhhH............',
384    '.........HLhhhhLhhhhhhhhhhhhhHhhhhhLH...........',
385    '........HLhshhhhhhhhhhhhhhhhhhLhhLhhL...........',
386    '........LhsShLhhhhhhhhhhLhhhhhhLhsLhhH..........',
387    '.......HhsSsLhhhhhhhhhhhLhhhhhhLhShLhhHHH.......',
388    '......HhhsshLsShHhhhhhhhHLhhLhhhLshHhHEEEH......',
389    '......HhhshhLhshHhhhhhhhHHhhLhhhLhhLhLEEEE......',
390    '......hhhhhLhhhhHhhsshhhHLhhhHhhLhhhHHEEHEH.....',
391    '.....HhhhhhHhhhLHhhsshhhHsLhhHhhhLhhHLEEHEEH....',
392    '.....HhhhhhLhhhLhhhhhhLhLSrhhLHhhHhhLLEEHEEE....',
393    '.....hLhhhhLhhhrrhhhhhLhhSeLhhHhhLhhLLEEEHEE....',
394    '....HhLhhhhhhLLSshhhhhhLhSRShhLLhLhhLHEEEHEH....',
395    '....HhLhhhhhhLLeSLhLhhhHhSsshHLLhLhhhHHHHEH.....',
396    '....HhLhhhLhLhhSrHhLhhhHhSRRRrLSLLhhhHLEEEE.....',
397    '....LhHhhhLhHLreRrhHhhhHLsRRRRrSeLhhhHLEEEE.....',
398    '....hLHhhhLhLHSRReLHLhhLsrReSeeSeShhhHLLEHEH....',
399    '....hLHhhhLHSLSReesHShhLeLesrHHHHrLhhHLHEEEE....',
400    '....hLHhhhLHRsrssSesSShhSRSHHHhHLHHhhHHHEEEE....',
401    '....hHHLhhhrsHHHHHrRRRShrRSrHhRLeLHhhrsHEEEEH...',
402    '....LHHLhhhLHLsHHeheRRRReRRsLHhHRsLhhrRLH.HEE...',
403    '....HH.HhhhHHeSHHrLSRRRRRRRshHhhReLhhrRrH.HEH...',
404    '....HH.HhHhLseehhHhSRRRRRRRSsSssRSLhhsRLL.HH....',
405    '.....H.HhHhhSRRhsssSRRRRRRRRssseRShhhSSHL.......',
406    '.......HHHHhLRResssRRRRRRRRRRReeerhhhrLhL.......',
407    '.......HhHHHHsRReeRRRRRRRRRRReeeeLhhhHhhL.......',
408    '.......HhLLLHeeeeeRRRReSSeRRRRRRsLhhhHhhL.......',
409    '.......hhLLhLLSeeRRRRRRRRRRRRRSLHLhLhHhhh.......',
410    '.......hhLLhhHLrSeRRRRRRRRSSrLHHHhhLhHhhhH......',
411    '......HhhHLhhHHHHLLLLrsSSsrHHHHHHhhHLHLhhH......',
412    '......HhhHHhhHHHHHHLrsRsseeLLHHHHhhLLHLhhL......',
413    '......hhhHLhhHHHHHrrrseLrSeLrrHHHhLhLHLhhhH.....',
414    '.....HhhhHLshHHHHLrLrLEEEELrLrLHHhHhLHHhhhH.....',
415    '.....hhLhHhshLHHHLrrLLEEEEELLrLHhLLshHHhhhhH....',
416    '....HhLhLHhshLHHHHrrLrHErErHrrLHhHhshHHhhHhL....',
417    '....LhHhLHhhhLHHHHrrrHLrSLLLrrHHhHhhhHHhhHhhH...',
418    '...HhLHhHHLhhLLHHHLrrLLrrrLrrrHHHhhLLHHLhHLhL...',
419    '...LhHLhHHLLhhHHHHLrrrLLHLLrrLHHHhhHLHHLhLHhh...',
420    '..HhLHhhHHHHhhHHHHHrrrHrLHrrrHHHLhHHHHHLhH.LhH..',
421    '..HhH.hhHHHHHhLHHLLrrrLrLLrrrLHHhLHHHHHLhH.HhH..',
422    '..HhH.hhHHHHHHLHHrrLrrrLHrrrLrrHHHHHHHHHhH..hH..',
423    '..HL..LhHHHHHHHHHrrHrrsrLSrrHrrHHHHHHHHHhH..LH..',
424    '..HL..HhHHHHHHHHLHHHsSsssSSSHHHHHHHHHHHHhH..LH..',
425    '...H..HhHH.HHHHHLHLHHSRRSRSHHLLLHHHHH.HHL...H...',
426    '...H...hLH.HHHHLLHLLHrReSerHLLHLLHHHH.HLL...H...',
427    '...HH..HLH..HHHLHLLLHLLLLLLHLLHLLHHH..HL...H....',
428    '........HH..HH.HHLLHLLLHLLLLLLLHH.H...HH........',
429    '.........HH.....SSsHLLHHHHLLrsSs.....H..........',
430    '................SReeer....reeeRS................',
431    '...............HRRRRRH....LRRRRRH...............',
432    '...............HLsRRS......SRRsLH...............',
433    '..............HHHHHHH......HHHHHH...............',
434    '..............HHHHHHH......HHHHHHH..............',
435    '..............HHHHHH........HHHHHH..............',
436    '..............HHHHHH........HHHHHH..............',
437    '..............HHLLLH........HLLHHH..............',
438    '.............HLHHHHHH......HHHHHHLH.............',
439    '.............HLHHLLLH......HrLLHHLH.............',
440    '.............HLLLLLHLH....HLLHHLLLH.............',
441    '.............HHLLLLLLH....HLLLLLLHH.............',
442    '...............HHHHHH......HHHHHH...............',
443  ],
444  SAKURA_HD_PALETTE,
445)
446
447const blank48 = '................................................'
448const hdOverlay = (patch: Record<number, string>, palette: Palette = SAKURA_HD_PALETTE): Overlay =>
449  sprite(
450    Array.from({ length: 64 }, (_, y) => patch[y] ?? blank48),
451    palette,
452  )
453
454/** Dark form: red irises (left iris rows 23–25 cols 15–17, right iris rows 22–24 cols 28–31). */
455export const SAKURA_HD_DARK_EYES: Overlay = hdOverlay(
456  {
457    22: '............................XX..................',
458    23: '...............XX............XXX................',
459    24: '...............XX............XXX................',
460    25: '...............XXX..............................',
461  },
462  SAKURA_HD_DARK_PALETTE,
463)
464
465/**
466 * Eyes closed for the HD portrait. Extracted by the pipeline's masked diff (DESIGN.md §8.2 step 4)
467 * from a Codex `-i` variant of this exact base: both images bbox-cropped, box-downscaled to 48x64,
468 * snapped to SAKURA_HD_PALETTE, diffed inside the eye band (rows 19–28, cols 9–38).
469 */
470export const SAKURA_HD_BLINK: Overlay = hdOverlay({
471  19: '.................R.......h.RRRReR.L.............',
472  20: '................RRS.......RRRRRRReH.............',
473  21: '..........h...eRRRR...L...RRRRRRRRL.............',
474  22: '............RRRRRRR.......eSeeeRRRL...S.........',
475  23: '...........rRRRReSS...........H.H.h.............',
476  24: '...........LRSrL.HsR......SHLssrHLh.............',
477  25: '.............HHrssrs......eRRRRRe...............',
478  26: '............sreRRRRR........RRRR................',
479  27: '...............RRRR..............s..............',
480  28: '................RR..............................',
481})
482
483export const SAKURA_HD = {
484  base: SAKURA_HD_BASE,
485  blink: SAKURA_HD_BLINK,
486  darkEyes: SAKURA_HD_DARK_EYES,
487  darkPalette: SAKURA_HD_DARK_PALETTE,
488} as const
489
490// ---------------------------------------------------------------------------
491// Mini sprites 12 x 12 (subagents and the roster / band)
492// ---------------------------------------------------------------------------
493
494const SAKURA_MINI_BASE: Sprite = sprite(
495  [
496    '....hhhh....',
497    '..hHHHHHhR..',
498    '.hHLHHHHHhRr',
499    '.hHHHHHHHHr.',
500    '.hHhSShHhHh.',
501    'hHSeSSSSeSHh',
502    'hHSESSSSESHh',
503    'hHsSSSSSSsHh',
504    'hHHsSMMSsHHh',
505    'hHHCsSSsCHHh',
506    '.hHUURRUUHh.',
507    '.hhUUUUUUhh.',
508  ],
509  SAKURA_PALETTE,
510)
511
512const SAKURA_MINI: MiniSet = {
513  base: SAKURA_MINI_BASE,
514  blink: sprite(
515    [
516      '....hhhh....',
517      '..hHHHHHhR..',
518      '.hHLHHHHHhRr',
519      '.hHHHHHHHHr.',
520      '.hHhSShHhHh.',
521      'hHSSSSSSSSHh',
522      'hHSeSSSSeSHh',
523      'hHsSSSSSSsHh',
524      'hHHsSMMSsHHh',
525      'hHHCsSSsCHHh',
526      '.hHUURRUUHh.',
527      '.hhUUUUUUhh.',
528    ],
529    SAKURA_PALETTE,
530  ),
531  work: sprite(
532    [
533      '....hhhh...o',
534      '..hHHHHHhR..',
535      'ohHLHHHHHhRr',
536      '.hHHHHHHHHr.',
537      '.hHhSShHhHh.',
538      'hHSeSSSSeSHh',
539      'hHSESSSSESHh',
540      'hHsSSSSSSsHh',
541      'hHHsSMMSsHHh',
542      'hHHCsSSsCHHh',
543      '.hHUURRUUHh.',
544      '.hhUUUUUUhh.',
545    ],
546    SAKURA_PALETTE,
547  ),
548}
549
550/** Dark Sakura mini: same rows as SAKURA_MINI with the dark palette (ribbon reads black). */
551export const DARK_SAKURA_MINI: MiniSet = {
552  base: { ...SAKURA_MINI.base, palette: DARK_SAKURA_PALETTE },
553  blink: { ...SAKURA_MINI.blink, palette: DARK_SAKURA_PALETTE },
554  work: { ...SAKURA_MINI.work, palette: DARK_SAKURA_PALETTE },
555}
556
557// Rider (Medusa): waist-length lavender hair, blindfold, black bodysuit.
558const RIDER_PALETTE: Palette = {
559  P: 0xb9a3d9,
560  p: 0x8a74ad,
561  V: 0x3a2a5e,
562  S: 0xf6e6dc,
563  s: 0xdcc2b4,
564  M: 0xa85a6a,
565  U: 0x1a1a24,
566  W: 0xffffff,
567}
568const RIDER_MINI: MiniSet = {
569  base: sprite(
570    [
571      '....pppp....',
572      '..pPPPPPPp..',
573      '.pPPPPPPPPp.',
574      '.pPPPPPPPPp.',
575      '.pPpSSSSpPp.',
576      'pPPVVVVVVPPp',
577      'pPPVVVVVVPPp',
578      'pPPsSSSSsPPp',
579      'pPPsSSMSsPPp',
580      'pPPUsSSsUPPp',
581      'pPPUUUUUUPPp',
582      'pppUUUUUUppp',
583    ],
584    RIDER_PALETTE,
585  ),
586  // Blindfolded: her "blink" is the mouth relaxing.
587  blink: sprite(
588    [
589      '....pppp....',
590      '..pPPPPPPp..',
591      '.pPPPPPPPPp.',
592      '.pPPPPPPPPp.',
593      '.pPpSSSSpPp.',
594      'pPPVVVVVVPPp',
595      'pPPVVVVVVPPp',
596      'pPPsSSSSsPPp',
597      'pPPsSSSSsPPp',
598      'pPPUsSSsUPPp',
599      'pPPUUUUUUPPp',
600      'pppUUUUUUppp',
601    ],
602    RIDER_PALETTE,
603  ),
604  // Mystic Eyes glint through the blindfold; hair tips sway.
605  work: sprite(
606    [
607      '....pppp....',
608      '..pPPPPPPp..',
609      '.pPPPPPPPPp.',
610      '.pPPPPPPPPp.',
611      '.pPpSSSSpPp.',
612      'pPPVWVVVWVPp',
613      'pPPVVVVVVPPp',
614      'pPPsSSSSsPPp',
615      'pPPsSSMSsPPp',
616      'pPPUsSSsUPPp',
617      'pPPUUUUUUPPp',
618      'pp.UUUUUU.pp',
619    ],
620    RIDER_PALETTE,
621  ),
622}
623
624// Rin Tohsaka: black twin-tails, aqua eyes, red top, white collar.
625const RIN_PALETTE: Palette = {
626  H: 0x1f1a2a,
627  h: 0x0f0c14,
628  L: 0x3d3550,
629  S: 0xf8e2cf,
630  s: 0xe6bda2,
631  E: 0x3fa9c9,
632  e: 0x14202a,
633  M: 0xc26274,
634  C: 0xf6f2ea,
635  R: 0xc8302f,
636  r: 0x8f1f1f,
637  o: 0xfff2a8,
638}
639const RIN_MINI: MiniSet = {
640  base: sprite(
641    [
642      '....hhhh....',
643      '..hHHHHHHh..',
644      '.hHHLHHHHHh.',
645      'hHHHHHHHHHHh',
646      'hHHhSSSShHHh',
647      'hHHSeSSeSHHh',
648      'hHHSESSESHHh',
649      'hHHsSSSSsHHh',
650      'hHHsSMMSsHHh',
651      'hHhCsSSsChHh',
652      'hH.RRRRRR.Hh',
653      'hh.RrRRrR.hh',
654    ],
655    RIN_PALETTE,
656  ),
657  blink: sprite(
658    [
659      '....hhhh....',
660      '..hHHHHHHh..',
661      '.hHHLHHHHHh.',
662      'hHHHHHHHHHHh',
663      'hHHhSSSShHHh',
664      'hHHSSSSSSHHh',
665      'hHHSeSSeSHHh',
666      'hHHsSSSSsHHh',
667      'hHHsSMMSsHHh',
668      'hHhCsSSsChHh',
669      'hH.RRRRRR.Hh',
670      'hh.RrRRrR.hh',
671    ],
672    RIN_PALETTE,
673  ),
674  // A gem held up, catching the light.
675  work: sprite(
676    [
677      '....hhhh....',
678      '..hHHHHHHh..',
679      '.hHHLHHHHHh.',
680      'hHHHHHHHHHHh',
681      'hHHhSSSShHHh',
682      'hHHSeSSeSHHh',
683      'hHHSESSESHHh',
684      'hHHsSSSSsHHh',
685      'hHHsSMMSsHHh',
686      'hHhCsSSsChHh',
687      'hH.RRRRRRoHh',
688      'hh.RrRRrRSh.',
689    ],
690    RIN_PALETTE,
691  ),
692}
693
694// Shirou Emiya: red-orange spiky hair, amber eyes, blue-and-white shirt.
695const SHIROU_PALETTE: Palette = {
696  H: 0xd9622b,
697  h: 0x9c3f17,
698  L: 0xf0884a,
699  S: 0xf6dcc4,
700  s: 0xe0b394,
701  E: 0xc98b2a,
702  e: 0x2a1a10,
703  M: 0xb8605a,
704  C: 0xf6f2ea,
705  U: 0x2f4f8a,
706  o: 0x9fd8ff,
707}
708const SHIROU_MINI: MiniSet = {
709  base: sprite(
710    [
711      '.h..hh..h...',
712      '.hHhHHHHhHh.',
713      '.hHHHHHHHHh.',
714      '.hHHLHHHHHh.',
715      '.hHhSSSSSHh.',
716      '..hSeSSeSSh.',
717      '..hSESSESSh.',
718      '..hsSSSSSsh.',
719      '...sSSMSs...',
720      '..UCCsSsCCU.',
721      '.UUUCCCCCUUU',
722      '.UUUUCCCUUUU',
723    ],
724    SHIROU_PALETTE,
725  ),
726  blink: sprite(
727    [
728      '.h..hh..h...',
729      '.hHhHHHHhHh.',
730      '.hHHHHHHHHh.',
731      '.hHHLHHHHHh.',
732      '.hHhSSSSSHh.',
733      '..hSSSSSSSh.',
734      '..hSeSSeSSh.',
735      '..hsSSSSSsh.',
736      '...sSSMSs...',
737      '..UCCsSsCCU.',
738      '.UUUCCCCCUUU',
739      '.UUUUCCCUUUU',
740    ],
741    SHIROU_PALETTE,
742  ),
743  // "Trace, on": blue-white sparks at his hands.
744  work: sprite(
745    [
746      '.h..hh..h...',
747      '.hHhHHHHhHh.',
748      '.hHHHHHHHHh.',
749      '.hHHLHHHHHh.',
750      '.hHhSSSSSHh.',
751      '..hSeSSeSSh.',
752      '..hSESSESSh.',
753      '..hsSSSSSsh.',
754      '...sSSMSs..o',
755      'o.UCCsSsCCU.',
756      '.UUUCCCCCUUU',
757      'oUUUUCCCUUUo',
758    ],
759    SHIROU_PALETTE,
760  ),
761}
762
763// Kotomine Kirei: black hair, cassock with a gold cross, faint smile.
764const KIREI_PALETTE: Palette = {
765  H: 0x1c1a22,
766  h: 0x0b0a0e,
767  S: 0xf1d9c2,
768  s: 0xd8b597,
769  E: 0x6b4a2e,
770  e: 0x1a120c,
771  M: 0x9a5a5a,
772  C: 0xe9e4da,
773  U: 0x15131a,
774  G: 0xd7b24a,
775}
776const KIREI_MINI: MiniSet = {
777  base: sprite(
778    [
779      '...hhhhhh...',
780      '..hHHHHHHh..',
781      '.hHHHHHHHHh.',
782      '.hHHHHHHHHh.',
783      '.hHSSSSSSHh.',
784      '.hSSeSSeSSh.',
785      '.hSSESSESSh.',
786      '.hsSSSSSSsh.',
787      '..sSSMMSSs..',
788      '..UUCsSCUU..',
789      '.UUUGGGGUUU.',
790      '.UUUUGGUUUU.',
791    ],
792    KIREI_PALETTE,
793  ),
794  blink: sprite(
795    [
796      '...hhhhhh...',
797      '..hHHHHHHh..',
798      '.hHHHHHHHHh.',
799      '.hHHHHHHHHh.',
800      '.hHSSSSSSHh.',
801      '.hSSSSSSSSh.',
802      '.hSSeSSeSSh.',
803      '.hsSSSSSSsh.',
804      '..sSSMMSSs..',
805      '..UUCsSCUU..',
806      '.UUUGGGGUUU.',
807      '.UUUUGGUUUU.',
808    ],
809    KIREI_PALETTE,
810  ),
811  // One hand raised, as if making a point.
812  work: sprite(
813    [
814      '...hhhhhh...',
815      '..hHHHHHHh..',
816      '.hHHHHHHHHh.',
817      '.hHHHHHHHHh.',
818      '.hHSSSSSSHh.',
819      '.hSSeSSeSSh.',
820      '.hSSESSESSh.',
821      '.hsSSSSSSsh.',
822      '..sSSMMSSsS.',
823      '..UUCsSCUUS.',
824      '.UUUGGGGUUU.',
825      '.UUUUGGUUUU.',
826    ],
827    KIREI_PALETTE,
828  ),
829}
830
831// Archer: white spiky hair, tan skin, red coat over black.
832const ARCHER_PALETTE: Palette = {
833  H: 0xe9e9ee,
834  h: 0xa9a9b5,
835  S: 0xb97a4a,
836  s: 0x8a5530,
837  E: 0x6a7a85,
838  e: 0x1a1a1e,
839  M: 0x5a2a22,
840  R: 0xb3252a,
841  U: 0x1a1a1e,
842  W: 0xffffff,
843}
844const ARCHER_MINI: MiniSet = {
845  base: sprite(
846    [
847      '..h..hh..h..',
848      '.hHhHHHHhHh.',
849      '.hHHHHHHHHh.',
850      '.hHHHHHHHHh.',
851      '.hHhSSSShHh.',
852      '..hSeSSeSh..',
853      '..hSESSESh..',
854      '..hsSSSSsh..',
855      '...sSMMSs...',
856      '.RRRUsSsURRR',
857      'RRRRUUUURRRR',
858      'RRRRUUUURRRR',
859    ],
860    ARCHER_PALETTE,
861  ),
862  blink: sprite(
863    [
864      '..h..hh..h..',
865      '.hHhHHHHhHh.',
866      '.hHHHHHHHHh.',
867      '.hHHHHHHHHh.',
868      '.hHhSSSShHh.',
869      '..hSSSSSSh..',
870      '..hSeSSeSh..',
871      '..hsSSSSsh..',
872      '...sSMMSs...',
873      '.RRRUsSsURRR',
874      'RRRRUUUURRRR',
875      'RRRRUUUURRRR',
876    ],
877    ARCHER_PALETTE,
878  ),
879  // A projected blade flickers into being at his side.
880  work: sprite(
881    [
882      '..h..hh..h..',
883      '.hHhHHHHhHh.',
884      '.hHHHHHHHHh.',
885      '.hHHHHHHHHh.',
886      '.hHhSSSShHh.',
887      '..hSeSSeSh..',
888      '..hSESSESh..',
889      'W.hsSSSSsh..',
890      'W..sSMMSs...',
891      'WRRRUsSsURRR',
892      'RRRRUUUURRRR',
893      'RRRRUUUURRRR',
894    ],
895    ARCHER_PALETTE,
896  ),
897}
898
899// Saber Alter: pale blonde hair with a bun, black visor, black armor with red seams.
900const SABER_ALTER_PALETTE: Palette = {
901  H: 0xe6dcb8,
902  h: 0xb3a884,
903  S: 0xf4ece6,
904  s: 0xd9ccc4,
905  V: 0x14121a,
906  E: 0xf2d13a,
907  M: 0x7a3a44,
908  U: 0x1a161f,
909  R: 0x8a1a22,
910  T: 0x3a0f2e,
911}
912const SABER_ALTER_MINI: MiniSet = {
913  base: sprite(
914    [
915      '....hhhh....',
916      '..hHHHHHHh..',
917      '.hHHHHHHHHhh',
918      '.hHHHHHHHHHh',
919      '.hHhSSSShHh.',
920      '..VVVVVVVV..',
921      '..VVEVVEVV..',
922      '..sSSSSSSs..',
923      '...sSMMSs...',
924      '..UUUsSUUU..',
925      '.UURUUUUURUU',
926      '.UUUURRUUUU.',
927    ],
928    SABER_ALTER_PALETTE,
929  ),
930  // The glow behind the visor goes out for a moment.
931  blink: sprite(
932    [
933      '....hhhh....',
934      '..hHHHHHHh..',
935      '.hHHHHHHHHhh',
936      '.hHHHHHHHHHh',
937      '.hHhSSSShHh.',
938      '..VVVVVVVV..',
939      '..VVVVVVVV..',
940      '..sSSSSSSs..',
941      '...sSMMSs...',
942      '..UUUsSUUU..',
943      '.UURUUUUURUU',
944      '.UUUURRUUUU.',
945    ],
946    SABER_ALTER_PALETTE,
947  ),
948  // Dark prana leaks off her.
949  work: sprite(
950    [
951      '....hhhh...T',
952      '..hHHHHHHh..',
953      'ThHHHHHHHHhh',
954      '.hHHHHHHHHHh',
955      '.hHhSSSShHhT',
956      '..VVVVVVVV..',
957      'T.VVEVVEVV..',
958      '..sSSSSSSs..',
959      '...sSMMSs..T',
960      'T.UUUsSUUU..',
961      '.UURUUUUURUU',
962      '.UUUURRUUUU.',
963    ],
964    SABER_ALTER_PALETTE,
965  ),
966}
967
968// Illyasviel: silver hair, purple hat with white fur band, red eyes, purple coat.
969const ILLYA_PALETTE: Palette = {
970  H: 0xf0eef5,
971  h: 0xbcb8cc,
972  P: 0x5a3a8a,
973  p: 0x3a2460,
974  C: 0xf8f6ff,
975  S: 0xfaeee6,
976  s: 0xe4cfc4,
977  E: 0xd0303a,
978  e: 0x2a0a10,
979  M: 0xc26274,
980  o: 0xffffff,
981}
982const ILLYA_MINI: MiniSet = {
983  base: sprite(
984    [
985      '..pPPPPPPp..',
986      '.pPPPPPPPPp.',
987      '.CCCCCCCCCC.',
988      'hHHHHHHHHHHh',
989      'hHhSSSSSShHh',
990      'hHSeSSSSeSHh',
991      'hHSESSSSESHh',
992      'hHsSSSSSSsHh',
993      'hHHsSMMSsHHh',
994      'hHHPpSSpPHHh',
995      'hHPPPPPPPPHh',
996      'hhPPPPPPPPhh',
997    ],
998    ILLYA_PALETTE,
999  ),
1000  blink: sprite(
1001    [
1002      '..pPPPPPPp..',
1003      '.pPPPPPPPPp.',
1004      '.CCCCCCCCCC.',
1005      'hHHHHHHHHHHh',
1006      'hHhSSSSSShHh',
1007      'hHSSSSSSSSHh',
1008      'hHSeSSSSeSHh',
1009      'hHsSSSSSSsHh',
1010      'hHHsSMMSsHHh',
1011      'hHHPpSSpPHHh',
1012      'hHPPPPPPPPHh',
1013      'hhPPPPPPPPhh',
1014    ],
1015    ILLYA_PALETTE,
1016  ),
1017  // Snow sparkles around her.
1018  work: sprite(
1019    [
1020      'o.pPPPPPPp.o',
1021      '.pPPPPPPPPp.',
1022      'oCCCCCCCCCC.',
1023      'hHHHHHHHHHHh',
1024      'hHhSSSSSShHh',
1025      'hHSeSSSSeSHh',
1026      'hHSESSSSESHh',
1027      'hHsSSSSSSsHh',
1028      'hHHsSMMSsHHh',
1029      'hHHPpSSpPHHh',
1030      'hHPPPPPPPPHh',
1031      'hhPPPPPPPPhh',
1032    ],
1033    ILLYA_PALETTE,
1034  ),
1035}
1036
1037// True Assassin: black hood, white skull mask, grey wraps.
1038const TRUE_ASSASSIN_PALETTE: Palette = {
1039  U: 0x15121a,
1040  u: 0x070609,
1041  C: 0xf2f2ee,
1042  e: 0x000000,
1043  K: 0x2a2730,
1044  W: 0xd9f0ff,
1045}
1046const TRUE_ASSASSIN_MINI: MiniSet = {
1047  base: sprite(
1048    [
1049      '....uuuu....',
1050      '..uUUUUUUu..',
1051      '.uUUUUUUUUu.',
1052      '.uUUCCCCUUu.',
1053      '.uUCCCCCCUu.',
1054      '.uUCeCCeCUu.',
1055      '.uUCeCCeCUu.',
1056      '.uUCCCCCCUu.',
1057      '..uUCCCCUu..',
1058      '..KKKUUKKK..',
1059      '.KKKKKKKKKK.',
1060      '.KKKKKKKKKK.',
1061    ],
1062    TRUE_ASSASSIN_PALETTE,
1063  ),
1064  // A skull does not blink; the eye holes narrow instead.
1065  blink: sprite(
1066    [
1067      '....uuuu....',
1068      '..uUUUUUUu..',
1069      '.uUUUUUUUUu.',
1070      '.uUUCCCCUUu.',
1071      '.uUCCCCCCUu.',
1072      '.uUCCCCCCUu.',
1073      '.uUCeCCeCUu.',
1074      '.uUCCCCCCUu.',
1075      '..uUCCCCUu..',
1076      '..KKKUUKKK..',
1077      '.KKKKKKKKKK.',
1078      '.KKKKKKKKKK.',
1079    ],
1080    TRUE_ASSASSIN_PALETTE,
1081  ),
1082  // A dirk glints at his hip.
1083  work: sprite(
1084    [
1085      '....uuuu....',
1086      '..uUUUUUUu..',
1087      '.uUUUUUUUUu.',
1088      '.uUUCCCCUUu.',
1089      '.uUCCCCCCUu.',
1090      '.uUCeCCeCUu.',
1091      '.uUCeCCeCUu.',
1092      '.uUCCCCCCUu.',
1093      '..uUCCCCUu..',
1094      '..KKKUUKKK.W',
1095      '.KKKKKKKKKKW',
1096      '.KKKKKKKKKK.',
1097    ],
1098    TRUE_ASSASSIN_PALETTE,
1099  ),
1100}
1101
1102// Berserker: grey skin, wild dark hair, red eyes, stone blade at his side.
1103const BERSERKER_PALETTE: Palette = {
1104  H: 0x24242c,
1105  h: 0x121218,
1106  S: 0x5a5a66,
1107  s: 0x3c3c48,
1108  E: 0xe03030,
1109  e: 0x0a0a0c,
1110  M: 0x2a2a30,
1111  A: 0x9a9a8a,
1112}
1113const BERSERKER_MINI: MiniSet = {
1114  base: sprite(
1115    [
1116      '.hhh.hh.hhh.',
1117      'hHHHHHHHHHHh',
1118      'hHHHHHHHHHHh',
1119      'hHhSSSSSShHh',
1120      'hHSeeSSeeSHh',
1121      'hHSSESSESSHh',
1122      '.hsSSSSSSshA',
1123      '..sSSMMSSs.A',
1124      '.sSSsSSsSSsA',
1125      'sSSSSSSSSSSA',
1126      'sSSSSSSSSSSA',
1127      'ssSSSSSSSSsA',
1128    ],
1129    BERSERKER_PALETTE,
1130  ),
1131  blink: sprite(
1132    [
1133      '.hhh.hh.hhh.',
1134      'hHHHHHHHHHHh',
1135      'hHHHHHHHHHHh',
1136      'hHhSSSSSShHh',
1137      'hHSeeSSeeSHh',
1138      'hHSSeSSeSSHh',
1139      '.hsSSSSSSshA',
1140      '..sSSMMSSs.A',
1141      '.sSSsSSsSSsA',
1142      'sSSSSSSSSSSA',
1143      'sSSSSSSSSSSA',
1144      'ssSSSSSSSSsA',
1145    ],
1146    BERSERKER_PALETTE,
1147  ),
1148  // The whole frame shakes one pixel to the right.
1149  work: sprite(
1150    [
1151      '..hhh.hh.hhh',
1152      '.hHHHHHHHHHH',
1153      '.hHHHHHHHHHH',
1154      '.hHhSSSSSShH',
1155      '.hHSeeSSeeSH',
1156      '.hHSSESSESSH',
1157      '..hsSSSSSSsh',
1158      '...sSSMMSSs.',
1159      '..sSSsSSsSSs',
1160      '.sSSSSSSSSSS',
1161      '.sSSSSSSSSSS',
1162      '.ssSSSSSSSSs',
1163    ],
1164    BERSERKER_PALETTE,
1165  ),
1166}
1167
1168// Saber (Artoria): gold-blonde braided bun with an ahoge, green eyes, blue dress, silver collar.
1169const SABER_PALETTE: Palette = {
1170  H: 0xf2dc86,
1171  h: 0xb89c48,
1172  L: 0xfff4c0,
1173  S: 0xfaeadf,
1174  s: 0xe2c8b8,
1175  E: 0x3cae5c,
1176  e: 0x1a2a1c,
1177  M: 0xc06878,
1178  U: 0x2a4ea8,
1179  u: 0x1a3478,
1180  A: 0xcdd2de,
1181  W: 0xffffff,
1182  o: 0xd4f6ea,
1183}
1184const SABER_MINI: MiniSet = {
1185  base: sprite(
1186    [
1187      '.....L......',
1188      '....hHHh....',
1189      '..hHHHHHHhh.',
1190      '.hHHHHHHHHHh',
1191      '.hHhSSSShHHh',
1192      '.hSeSSSSeShh',
1193      '.hSESSSSESh.',
1194      '..sSSSSSSs..',
1195      '...sSMMSs...',
1196      '..AAAsSAAA..',
1197      '.UUAUUUUAUU.',
1198      '.UUUUuuUUUU.',
1199    ],
1200    SABER_PALETTE,
hooks/state.ts 57 lines
1// Settings and the module's own runtime state. The atoms live in register.tsx, where $ reads them.
2import type { PluginOptions } from 'claude-code'
3
4import type { HeavensFeelAgent, HeavensFeelMood } from '../types'
5import type { Frame } from './anim'
6
7export const PANE = 'hf'
8
9export type Settings = {
10  paneOnStart: boolean
11  band: 'auto' | 'always' | 'never'
12  spinnerTakeover: 'off' | 'word'
13  darkThreshold: number
14  sleepAfterMinutes: number
15  timezone: string
16}
17
18export const readSettings = (options: PluginOptions): Settings => ({
19  paneOnStart: options.paneOnStart !== false,
20  band: options.band === 'always' || options.band === 'never' ? options.band : 'auto',
21  spinnerTakeover: options.spinnerTakeover === 'off' ? 'off' : 'word',
22  darkThreshold: typeof options.darkThreshold === 'number' ? options.darkThreshold : 75,
23  sleepAfterMinutes: typeof options.sleepAfterMinutes === 'number' ? options.sleepAfterMinutes : 5,
24  timezone: typeof options.timezone === 'string' && options.timezone ? options.timezone : 'Asia/Jerusalem',
25})
26
27export type DarkOverride = 'on' | 'off' | null
28
29/** A Raster the tick keeps animating; `frameAt` answers null once its subject is gone. */
30export type LiveRaster = { requestId: string; scale: 1 | 2; frameAt: (now: number) => Frame | null }
31
32/** Module memory: mirrors of the atoms the 10 Hz tick reads, plus what the terminal has mounted. */
33export const rt = {
34  canDraw: false,
35  surfaces: [] as readonly string[],
36  mood: 'idle' as HeavensFeelMood,
37  isDark: false,
38  isDarkByAuto: false,
39  darkOverride: null as DarkOverride,
40  ctx: 0,
41  errorStreak: 0,
42  isTurnRunning: false,
43  lastActivity: 0,
44  talkUntil: 0,
45  moodToken: 0,
46  lastLineAt: 0,
47  agents: {} as Record<string, HeavensFeelAgent>,
48  /** Every Raster on screen, by key: where it is drawn and how to draw it at a given time. */
49  rasters: new Map<string, LiveRaster>(),
50  /** Assistant row id -> the subagent that wrote it (absent: the main session). */
51  messageAgents: new Map<string, string>(),
52  /** Agents that finished and left the roster, still needed to draw their messages. */
53  pastAgents: new Map<string, HeavensFeelAgent>(),
54  /** The frame each Raster last showed, so the tick blits only when it changes. */
55  blitted: new Map<string, string>(),
56}
57
hooks/views.tsx 611 lines
1// What each site draws: Sakura's pane, the band, message avatars, the spinner word, /hf cast.
2import type {
3  HeavensFeelActivity,
4  HeavensFeelAgent,
5  HeavensFeelBubble,
6  HeavensFeelCharacter,
7  HeavensFeelMood,
8  HeavensFeelTheme,
9} from '../types'
10import {
11  blinkFrame,
12  bustBlinkFrame,
13  bustFrame,
14  headFrame,
15  isTalkingAt,
16  miniFrame,
17  moodLoop,
18  portraitFrame,
19  restingMini,
20  type Frame,
21  type PortraitSize,
22  type Scene,
23} from './anim'
24import { badgeSuffix } from './cast'
25import { gridToRaster, gridToSvg, hex, svgBox, type Backdrop, type Grid } from './render'
26import { CAST } from './sprites'
27import { PANE, rt, type Settings } from './state'
28import { describeTool, localTime, sakuraVerb, servantVerb } from './voice'
29
30/** One row of a subagent's conversation, as `$.session.messages({ agentId })` answers it. */
31export type ServantMessage = {
32  role: 'user' | 'assistant'
33  text: string
34  toolUses: readonly { tool: string; input: Record<string, unknown>; isError?: true; text?: string; result?: unknown }[]
35}
36
37/** The subagent the room is showing, with its conversation; `canGoBack` when the person chose it here. */
38export type ServantView = { agent: HeavensFeelAgent; messages: readonly ServantMessage[]; canGoBack: boolean }
39
40/** What the pane's buttons do; the hook builds these, since only it holds `$`. */
41export type PaneControls = { view: (id: string) => void; back: () => void; openTasks: () => void }
42
43/** What a drawing reads, read by the hook (the hook alone holds `$`). */
44export type View = {
45  mood: HeavensFeelMood
46  isDark: boolean
47  agents: Record<string, HeavensFeelAgent>
48  bubble: HeavensFeelBubble
49  ctx: number
50  activity: HeavensFeelActivity
51  theme: HeavensFeelTheme
52  now: number
53}
54
55const DARK_ACCENT = 0xa81e2d
56const PILL_TEXT = '#1a1320'
57const GAUGE_WARN = 0xc0508a
58const GAUGE_CELLS = 10
59const HD_MIN_COLUMNS = 50
60const HD_MIN_ROWS = 40
61const ROSTER_LIMIT = 4
62const SVG_PIXEL_HD = 4
63const SVG_PIXEL_MINI = 4
64const SVG_PIXEL_BUST = 3
65const SVG_PIXEL_HEAD = 3
66/** The band draws its own collapse mark, `[-]`, over the last cells of the first row. */
67const BAND_MARK_COLUMNS = 4
68/** Band room the HD bust needs (20 rows, 48 columns plus text), and the standard one (11 rows). */
69const BAND_HD = { rows: 22, columns: 100 }
70const BAND_SD = { rows: 12, columns: 56 }
71/** Band rows that fit the bust beside a row of Servant minis (6 rows each, plus their names). */
72const BAND_MINIS_ROWS = 14
73const MINI_CARD_COLUMNS = 13
74const SHORT_NAME_COLUMNS = 12
75const TRANSCRIPT_ROWS = 14
76const MESSAGE_CHARACTERS = 600
77const SUMMONS_CHARACTERS = 160
78const WEEKDAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
79
80const MOOD_LABEL: Readonly<Record<HeavensFeelMood, string>> = {
81  idle: 'at home',
82  thinking: 'thinking',
83  editing: 'editing',
84  searching: 'searching',
85  running: 'running',
86  summoning: 'summoning',
87  ouch: 'flinching',
88  done: 'finished',
89  sleeping: 'dozing',
90  interrupted: 'stopped',
91}
92
93export const nameOf = (agent: HeavensFeelAgent): string =>
94  `${CAST[agent.character].name}${badgeSuffix(agent.badge)}${agent.isFork ? ' (fork)' : ''}`
95
96/** "Rider · Servant of Sakura": the class, and whose Servant it is in the war. */
97const allegianceOf = (character: HeavensFeelCharacter): string => {
98  const entry = CAST[character]
99  if (entry.role === 'master') return 'Master'
100  const master = entry.master ? `Servant of ${CAST[entry.master].name.split(' ')[0]}` : 'masterless'
101  return `${entry.className ?? 'Servant'} · ${master}`
102}
103
104const elapsed = (agent: HeavensFeelAgent, now: number): string => {
105  const seconds = Math.max(0, Math.round(((agent.endedAt ?? now) - agent.startedAt) / 1000))
106  return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m${String(seconds % 60).padStart(2, '0')}s`
107}
108
109const statusOf = (agent: HeavensFeelAgent, now: number): string => {
110  if (agent.isError) return `stumbled · ${elapsed(agent, now)}`
111  if (agent.endedAt) return `done · ${elapsed(agent, now)}`
112  return `${servantVerb(agent.character, agent.mood)} · ${agent.tools} tools · ${elapsed(agent, now)}`
113}
114
115const hearts = (turns: number): string => {
116  const filled = [0, 10, 40, 120, 300].filter(threshold => turns >= threshold).length
117  return '♥'.repeat(filled) + '♡'.repeat(5 - filled)
118}
119
120const sortedAgents = (all: Record<string, HeavensFeelAgent>): HeavensFeelAgent[] =>
121  Object.values(all).sort((a, b) => Number(Boolean(a.endedAt)) - Number(Boolean(b.endedAt)) || a.startedAt - b.startedAt)
122
123/** The scene now, from the module's mirrors: the same one the tick animates from. */
124const sceneAt = (now: number, isSmil: boolean): Scene => ({
125  mood: rt.mood,
126  isDark: rt.isDark,
127  now,
128  isTalking: isTalkingAt(now, rt.talkUntil),
129  isSmil,
130})
131
132const backdropFor = (theme: HeavensFeelTheme, accent: number): Backdrop => ({ theme, accent })
133
134// Pieces ---------------------------------------------------------------------------
135
136/** A Raster drawn with the frame for now and registered so the tick blits each later frame. */
137const liveRaster = (ui: any, requestId: string, key: string, scale: 1 | 2, now: number, frameAt: (at: number) => Frame | null) => {
138  const current = frameAt(now)
139  if (!current) return null
140  rt.rasters.set(key, { requestId, scale, frameAt })
141  rt.blitted.set(key, current.key)
142  const raster = gridToRaster(current.grid, scale)
143  return <ui.Raster key={key} columns={raster.columns} rows={raster.rows} cells={raster.cells} />
144}
145
146const agentMiniAt = (id: string) => (at: number): Frame | null => {
147  const agent = rt.agents[id]
148  if (!agent) return null
149  const mood = agent.endedAt ? 'idle' : agent.mood
150  return miniFrame({ id, character: agent.character, mood, isDark: false, now: at })
151}
152
153/** Sakura as an Svg: her mood's frames loop, and while idle she blinks and breathes. */
154const sakuraSvg = (ui: any, size: PortraitSize, frameOf: (scene: Scene) => Frame, blink: Frame, pixel: number, view: View) => {
155  const loop = moodLoop(size, frameOf, sceneAt(view.now, true))
156  const [first, ...rest] = loop.frames
157  const isIdle = view.mood === 'idle' || view.mood === 'interrupted'
158  const accent = view.isDark ? DARK_ACCENT : CAST.sakura.accent
159  const source = gridToSvg(first!.grid, {
160    pixel,
161    frames: rest.map(one => one.grid),
162    frameMs: loop.ms,
163    blink: isIdle ? blink.grid : undefined,
164    isBreathing: isIdle,
165    backdrop: backdropFor(view.theme, accent),
166  })
167  const { width, height } = svgBox(first!.grid, pixel)
168  return <ui.Svg source={source} alt={`Sakura Matou, ${MOOD_LABEL[view.mood]}`} width={width} height={height} isInteractive />
169}
170
171/** A mini sprite: a Raster the tick blits in the terminal, an Svg elsewhere, a dot where neither draws. */
172const miniArt = (ui: any, surface: string, requestId: string, key: string, id: string, scale: 1 | 2, view: View) => {
173  const frameAt = agentMiniAt(id)
174  const character = rt.agents[id]?.character ?? 'sakura'
175  const accent = CAST[character].accent
176  if (surface === 'terminal') return liveRaster(ui, requestId, key, scale, view.now, frameAt)
177  const current = frameAt(view.now)
178  if ('Svg' in ui && current) {
179    const pixel = SVG_PIXEL_MINI * scale
180    const source = gridToSvg(current.grid, { pixel, backdrop: backdropFor(view.theme, accent) })
181    return <ui.Svg source={source} alt={CAST[character].name} {...svgBox(current.grid, pixel)} />
182  }
183  return <ui.Text color={hex(accent)}>◆</ui.Text>
184}
185
186/** Context as Sakura's shadow: it fills toward the threshold where she turns. */
187const shadowGauge = (ui: any, percent: number, threshold: number) => {
188  const filled = Math.min(GAUGE_CELLS, Math.round((percent / 100) * GAUGE_CELLS))
189  const isOver = threshold > 0 && percent >= threshold
190  const color = isOver ? DARK_ACCENT : percent >= threshold - 25 ? GAUGE_WARN : CAST.sakura.accent
191  return (
192    <ui.Box flexDirection="row" columnGap={1}>
193      <ui.Text dimColor>影 shadow</ui.Text>
194      <ui.Text color={hex(color)}>{'▰'.repeat(filled)}</ui.Text>
195      <ui.Text dimColor>{'▱'.repeat(GAUGE_CELLS - filled)}</ui.Text>
196      <ui.Text color={hex(color)}>{Math.round(percent)}%</ui.Text>
197    </ui.Box>
198  )
199}
200
201const clockLine = (ui: any, now: number, settings: Settings, servants: number) => {
202  const { hour, minute, weekday } = localTime(now, settings.timezone)
203  const isNight = hour < 6 || hour >= 19
204  const time = `${String(hour).padStart(2, '0')}:${String(minute ?? 0).padStart(2, '0')}`
205  const company = servants > 0 ? ` · ✦ ${servants} Servant${servants > 1 ? 's' : ''} out` : ''
206  return (
207    <ui.Text dimColor>
208      {isNight ? '☾' : '☀'} {time} {WEEKDAY_NAMES[weekday] ?? ''}
209      {company}
210    </ui.Text>
211  )
212}
213
214const moodPill = (ui: any, mood: HeavensFeelMood, accent: number) => (
215  <ui.Text backgroundColor={hex(accent)} color={PILL_TEXT} bold>
216    {` ${MOOD_LABEL[mood]} `}
217  </ui.Text>
218)
219
220const servantCard = (ui: any, surface: string, agent: HeavensFeelAgent, view: View, width: number, index: number, onView: () => void) => {
221  const entry = CAST[agent.character]
222  const accent = agent.isError ? DARK_ACCENT : entry.accent
223  return (
224    <ui.Box
225      key={`card-${agent.id}`}
226      flexDirection="row"
227      columnGap={1}
228      borderStyle="round"
229      borderColor={hex(accent)}
230      borderDimColor={Boolean(agent.endedAt)}
231      paddingX={1}
232      width={width}
233    >
234      {miniArt(ui, surface, PANE, `mini:${agent.id}`, agent.id, 1, view)}
235      <ui.Box flexDirection="column" flexShrink={1} minWidth={0}>
236        <ui.Box flexDirection="row" columnGap={1}>
237          <ui.Text bold color={hex(accent)} dimColor={Boolean(agent.endedAt)} wrap="truncate-end">
238            {nameOf(agent)}
239          </ui.Text>
240          <ui.Box flexGrow={1} />
241          <ui.Button key={`view-${agent.id}`} label="view ▸" hotkey={String(index + 1)} plain onPress={onView} />
242        </ui.Box>
243        <ui.Text dimColor wrap="truncate-end">{allegianceOf(agent.character)}</ui.Text>
244        <ui.Text color={hex(accent)} wrap="truncate-end">{agent.activity}</ui.Text>
245        <ui.Text dimColor wrap="truncate-end">{statusOf(agent, view.now)}</ui.Text>
246        <ui.Text dimColor italic wrap="truncate-end">「{agent.line}」</ui.Text>
247      </ui.Box>
248    </ui.Box>
249  )
250}
251
252// Pane ---------------------------------------------------------------------------
253
254type PaneEvent = {
255  surface: string
256  props: { bodyColumns: number; placement?: string }
257  viewport?: { columns: number; rows: number }
258}
259
260const portraitSize = (e: PaneEvent): PortraitSize =>
261  e.surface !== 'terminal' ||
262  (e.props.placement === 'dock' && e.props.bodyColumns >= HD_MIN_COLUMNS && (e.viewport?.rows ?? 0) >= HD_MIN_ROWS)
263    ? 'hd'
264    : 'sd'
265
266/** One transcript row: the summons, a reply, or a tool call with how it went. */
267const transcriptRow = (ui: any, message: ServantMessage, index: number, accent: number) => {
268  const { Box, Text } = ui
269  if (message.role === 'user') {
270    if (!message.text) return null
271    return (
272      <Box key={`row-${index}`} flexDirection="row" columnGap={1}>
273        <Text color={hex(CAST.sakura.accent)}>❀</Text>
274        <Text dimColor italic wrap="wrap">{clipText(message.text, SUMMONS_CHARACTERS)}</Text>
275      </Box>
276    )
277  }
278  return (
279    <Box key={`row-${index}`} flexDirection="column">
280      {message.text ? (
281        <Box flexDirection="row" columnGap={1}>
282          <Text color={hex(accent)}>●</Text>
283          <Box flexShrink={1} minWidth={0}>
284            <ui.Markdown text={clipText(message.text, MESSAGE_CHARACTERS)} />
285          </Box>
286        </Box>
287      ) : null}
288      {message.toolUses.map((use, k) => {
289        const doing = describeTool(use.tool, use.input)
290        const outcome = use.isError ? '✗' : use.text !== undefined || use.result !== undefined ? '✓' : '…'
291        return (
292          <Box key={`use-${index}-${k}`} flexDirection="row" columnGap={1}>
293            <Text color={hex(use.isError ? DARK_ACCENT : accent)}>{outcome}</Text>
294            <Text dimColor wrap="truncate-end">{doing.icon} {doing.text}</Text>
295          </Box>
296        )
297      })}
298    </Box>
299  )
300}
301
302const clipText = (text: string, length: number): string => (text.length > length ? `${text.slice(0, length - 1)}…` : text)
303
304/** The room when it shows a Servant: its portrait, its state, and its own conversation, live. */
305const drawServantRoom = (ui: any, e: PaneEvent, view: View, servant: ServantView, controls: PaneControls) => {
306  const { Box, Text, Button } = ui
307  const { agent, messages } = servant
308  const entry = CAST[agent.character]
309  const accent = agent.isError ? DARK_ACCENT : entry.accent
310  const width = Math.max(10, e.props.bodyColumns - 2)
311  const recent = messages.slice(-TRANSCRIPT_ROWS)
312  return (
313    <Box flexDirection="column" width={width}>
314      <Box flexDirection="row" columnGap={2}>
315        {servant.canGoBack ? <Button key="back" label="◂ Sakura" hotkey="b" plain onPress={controls.back} /> : null}
316        <Box flexGrow={1} />
317        {e.surface === 'terminal' ? <Button key="tasks" label="/tasks" hotkey="t" plain onPress={controls.openTasks} /> : null}
318      </Box>
319      <Box flexDirection="row" justifyContent="space-between" marginTop={1}>
320        <Text bold color={hex(accent)}>✦ {nameOf(agent)}</Text>
321        <Text dimColor>{statusOf(agent, view.now)}</Text>
322      </Box>
323      <Text dimColor italic>{entry.title} · {allegianceOf(agent.character)}</Text>
324      <Box justifyContent="center" marginY={1}>{miniArt(ui, e.surface, PANE, `viewed:${agent.id}`, agent.id, 2, view)}</Box>
325      <Box flexDirection="row" columnGap={1}>
326        {moodPill(ui, agent.endedAt ? 'done' : agent.mood, accent)}
327        <Box flexShrink={1} minWidth={0}>
328          <Text color={hex(accent)} wrap="truncate-end">{agent.activity}</Text>
329        </Box>
330      </Box>
331      {agent.line ? (
332        <Box borderStyle="round" borderColor={hex(accent)} paddingX={1} marginTop={1}>
333          <Text wrap="wrap" italic>「{agent.line}」</Text>
334        </Box>
335      ) : null}
336      <Box marginTop={1}>
337        <Text color={hex(accent)}>── ✦ {entry.name.split(' ')[0]}'s report ✦ ──</Text>
338      </Box>
339      <Text dimColor wrap="truncate-end">{agent.description}</Text>
340      {recent.length === 0 ? <Text dimColor>Nothing to read yet. The summons has only begun.</Text> : null}
341      {messages.length > recent.length ? <Text dimColor>… {messages.length - recent.length} earlier rows</Text> : null}
342      {recent.map((message, i) => transcriptRow(ui, message, i, accent))}
343    </Box>
344  )
345}
346
347export const drawPane = (
348  ui: any,
349  e: PaneEvent,
350  view: View,
351  settings: Settings,
352  bond: Record<string, number>,
353  servant: ServantView | undefined,
354  controls: PaneControls,
355) => {
356  if (servant) return drawServantRoom(ui, e, view, servant, controls)
357  const { Box, Text } = ui
358  const { mood: currentMood, isDark, agents: all, bubble: line, now } = view
359  const isTerminal = e.surface === 'terminal'
360  const accent = isDark ? DARK_ACCENT : CAST.sakura.accent
361  const size = portraitSize(e)
362  const width = Math.max(10, e.props.bodyColumns - 2)
363  const roster = sortedAgents(all)
364  const running = roster.filter(agent => !agent.endedAt).length
365
366  const portrait = (() => {
367    if (isTerminal) return liveRaster(ui, PANE, 'portrait', 1, now, at => portraitFrame(size, sceneAt(at, false)))
368    if ('Svg' in ui) return sakuraSvg(ui, size, scene => portraitFrame(size, scene), blinkFrame(size, isDark), SVG_PIXEL_HD, view)
369    return <Text color={hex(accent)}>{isDark ? '(◣_◢)' : '(´• ᴗ •`)'}</Text>
370  })()
371
372  return (
373    <Box flexDirection="column" width={width}>
374      <Box flexDirection="row" justifyContent="space-between">
375        <Text bold color={hex(accent)}>
376          {isDark ? '✸' : '❀'} Sakura Matou
377        </Text>
378        <Text color={hex(accent)}>{hearts(bond.sakura ?? 0)}</Text>
379      </Box>
380      <Text dimColor italic>{isDark ? 'the shadow that waits at home' : `${CAST.sakura.title} · Master of Rider`}</Text>
381      <Box justifyContent="center" marginY={1}>{portrait}</Box>
382      <Box flexDirection="row" columnGap={1}>
383        {moodPill(ui, currentMood, accent)}
384        <Box flexShrink={1} minWidth={0}>
385          <Text color={hex(accent)} wrap="truncate-end">{view.activity.icon} {view.activity.text}</Text>
386        </Box>
387      </Box>
388      {line.text ? (
389        <Box borderStyle="round" borderColor={hex(accent)} paddingX={1} marginTop={1}>
390          <Text wrap="wrap" italic>「{line.text}」</Text>
391        </Box>
392      ) : null}
393      <Box flexDirection="column" marginTop={1}>
394        {shadowGauge(ui, view.ctx, settings.darkThreshold)}
395        {clockLine(ui, now, settings, running)}
396      </Box>
397      {roster.length > 0 ? (
398        <Box marginTop={1}>
399          <Text color={hex(accent)}>── ✦ Servants ✦ ──</Text>
400        </Box>
401      ) : null}
402      {roster.slice(0, ROSTER_LIMIT).map((agent, i) => servantCard(ui, e.surface, agent, view, width, i, () => controls.view(agent.id)))}
403      {roster.length > ROSTER_LIMIT ? <Text dimColor>+{roster.length - ROSTER_LIMIT} more</Text> : null}
404    </Box>
405  )
406}
407
408// Message avatars ----------------------------------------------------------------
409
410const shortName = (character: HeavensFeelCharacter): string => CAST[character].name.split(' ')[0]!.slice(0, SHORT_NAME_COLUMNS)
411
412/** A reply wearing its speaker's face: a Master for the main session, the Servant for a subagent's. */
413export const drawAvatar = (
414  ui: any,
415  e: { surface: string },
416  engineTree: unknown,
417  character: HeavensFeelCharacter,
418  isDark: boolean,
419  theme: HeavensFeelTheme,
420) => {
421  const isDarkFace = isDark && character === 'sakura'
422  const accent = isDarkFace ? DARK_ACCENT : CAST[character].accent
423  const head = headFrame(character, isDarkFace).grid
424  const face = (() => {
425    if (e.surface === 'terminal') {
426      const raster = gridToRaster(head, 1)
427      return <ui.Raster key="face" columns={raster.columns} rows={raster.rows} cells={raster.cells} />
428    }
429    if ('Svg' in ui) {
430      const source = gridToSvg(head, { pixel: SVG_PIXEL_HEAD, backdrop: backdropFor(theme, accent) })
431      return <ui.Svg source={source} alt={CAST[character].name} {...svgBox(head, SVG_PIXEL_HEAD)} />
432    }
433    return <ui.Text color={hex(accent)}>◆</ui.Text>
434  })()
435  return (
436    <ui.Box flexDirection="row" columnGap={1}>
437      <ui.Box flexDirection="column" flexShrink={0} width={SHORT_NAME_COLUMNS} alignItems="center">
438        {face}
439        <ui.Text color={hex(accent)} wrap="truncate-end">{shortName(character)}</ui.Text>
440      </ui.Box>
441      {/* No sizing or placing prop on any Box above the engine's own drawing: the engine refuses it. */}
442      <ui.Box flexGrow={1} flexShrink={1}>
443        {engineTree}
444      </ui.Box>
445    </ui.Box>
446  )
447}
448
449// Band ---------------------------------------------------------------------------
450
451type BandEvent = { surface: string; requestId: string; props: { bodyColumns: number; maxRows: number } }
452
453const bustSize = (ui: any, e: BandEvent): PortraitSize | null => {
454  if (e.surface !== 'terminal') return 'Svg' in ui ? 'hd' : null
455  const { maxRows, bodyColumns } = e.props
456  if (maxRows >= BAND_HD.rows && bodyColumns >= BAND_HD.columns) return 'hd'
457  if (maxRows >= BAND_SD.rows && bodyColumns >= BAND_SD.columns) return 'sd'
458  return null
459}
460
461export const drawBand = (ui: any, e: BandEvent, view: View, openPane: () => void) => {
462  const { Box, Text, Button } = ui
463  const { mood: currentMood, isDark, agents: all, bubble: line, now } = view
464  const accent = isDark ? DARK_ACCENT : CAST.sakura.accent
465  const running = sortedAgents(all)
466  const width = Math.max(20, e.props.bodyColumns - 2)
467  const firstRowWidth = Math.max(18, e.props.bodyColumns - BAND_MARK_COLUMNS)
468  const size = bustSize(ui, e)
469  const openButton = (
470    <Box flexShrink={0}>
471      <Button key="open" label="open pane" plain onPress={openPane} />
472    </Box>
473  )
474  const nameplate = (
475    <Box flexShrink={0} columnGap={1}>
476      <Text color={hex(accent)}>{isDark ? '✸' : '❀'}</Text>
477      <Text bold color={hex(accent)}>Sakura</Text>
478      {moodPill(ui, currentMood, accent)}
479    </Box>
480  )
481  const chips = running.length > 0 ? (
482    <Box flexDirection="row" columnGap={2} flexWrap="wrap">
483      {running.map(agent => (
484        <Box key={`chip-${agent.id}`} flexDirection="row" columnGap={1}>
485          <Text color={hex(CAST[agent.character].accent)}>◆</Text>
486          <Text dimColor={Boolean(agent.endedAt)}>{nameOf(agent)}</Text>
487          <Text dimColor wrap="truncate-end">{agent.activity}</Text>
488        </Box>
489      ))}
490    </Box>
491  ) : null
492
493  if (!size) {
494    return (
495      <Box flexDirection="column" width={width}>
496        <Box flexDirection="row" columnGap={1} width={firstRowWidth}>
497          {nameplate}
498          <Box flexGrow={1} flexShrink={1} minWidth={0} overflow="hidden">
499            <Text italic wrap="truncate-end">{line.text ? `「${line.text}」` : ''}</Text>
500          </Box>
501          {openButton}
502        </Box>
503        {chips}
504      </Box>
505    )
506  }
507
508  const isTerminal = e.surface === 'terminal'
509  const portrait = isTerminal
510    ? liveRaster(ui, e.requestId, 'band-portrait', 1, now, at => bustFrame(size, sceneAt(at, false)))
511    : sakuraSvg(ui, size, scene => bustFrame(size, scene), bustBlinkFrame(size, isDark), SVG_PIXEL_BUST, view)
512  const showMinis = running.length > 0 && (!isTerminal || e.props.maxRows >= BAND_MINIS_ROWS)
513  const textColumns = Math.max(16, firstRowWidth - (isTerminal ? 48 : 30) - 2)
514  const fitting = Math.max(1, Math.floor(textColumns / MINI_CARD_COLUMNS))
515  const minis = showMinis ? (
516    <Box flexDirection="row" columnGap={1}>
517      {running.slice(0, fitting).map(agent => (
518        <Box key={`band-card-${agent.id}`} flexDirection="column" width={MINI_CARD_COLUMNS - 1}>
519          {miniArt(ui, e.surface, e.requestId, `band-mini:${agent.id}`, agent.id, 1, view)}
520          <Text bold color={hex(CAST[agent.character].accent)} wrap="truncate-end">{nameOf(agent)}</Text>
521          <Text dimColor wrap="truncate-end">{agent.endedAt ? 'done' : servantVerb(agent.character, agent.mood)}</Text>
522        </Box>
523      ))}
524    </Box>
525  ) : chips
526
527  return (
528    <Box flexDirection="row" columnGap={2} width={firstRowWidth}>
529      <Box flexShrink={0}>{portrait}</Box>
530      <Box flexDirection="column" flexGrow={1} flexShrink={1} minWidth={0}>
531        <Box flexDirection="row" columnGap={1}>
532          {nameplate}
533          <Box flexGrow={1} />
534          {openButton}
535        </Box>
536        <Text color={hex(accent)} wrap="truncate-end">
537          {view.activity.icon} {view.activity.text}
538        </Text>
539        {line.text ? (
540          <Box borderStyle="round" borderColor={hex(accent)} paddingX={1}>
541            <Text wrap="wrap" italic>「{line.text}」</Text>
542          </Box>
543        ) : null}
544        {minis}
545      </Box>
546    </Box>
547  )
548}
549
550// Spinner -----------------------------------------------------------------------
551
552export const spinnerWord = (agent: HeavensFeelAgent | undefined, currentMood: HeavensFeelMood, isDark: boolean): string =>
553  agent ? `${nameOf(agent)} is ${servantVerb(agent.character, agent.mood)}` : `Sakura is ${sakuraVerb(currentMood, isDark)}`
554
555// /hf cast ----------------------------------------------------------------------
556
557export const CAST_MARKER = '✿ The cast of Heaven\'s Feel'
558
559export const castText = (bond: Record<string, number>, assigned: readonly HeavensFeelAgent[]): string => {
560  const lines = Object.values(CAST).map(entry => {
561    const now = assigned.filter(agent => agent.character === entry.id && !agent.endedAt)
562    const on = now.length > 0 ? `  ← ${now.map(agent => agent.description || agent.type).join(', ')}` : ''
563    return `${entry.name.padEnd(14)} ${entry.title.padEnd(28)} ${hearts(bond[entry.id] ?? 0)}${on}`
564  })
565  return [CAST_MARKER, ...lines].join('\n')
566}
567
568export const drawCast = (ui: any, e: { surface: string }, bond: Record<string, number>, theme: HeavensFeelTheme) => {
569  const { Box, Text } = ui
570  const card = (id: HeavensFeelCharacter) => {
571    const entry = CAST[id]
572    const art = restingMini(id).grid
573    const picture = (grid: Grid) => {
574      if (e.surface === 'terminal') {
575        const raster = gridToRaster(grid, 1)
576        return <ui.Raster key={`cast-${id}`} columns={raster.columns} rows={raster.rows} cells={raster.cells} />
577      }
578      if ('Svg' in ui) {
579        const source = gridToSvg(grid, { pixel: SVG_PIXEL_MINI, backdrop: backdropFor(theme, entry.accent) })
580        return <ui.Svg source={source} alt={entry.name} {...svgBox(grid, SVG_PIXEL_MINI)} />
581      }
582      return <Text color={hex(entry.accent)}>◆</Text>
583    }
584    return (
585      <Box key={`card-${id}`} flexDirection="column" alignItems="center" width={16} borderStyle="round" borderColor={hex(entry.accent)}>
586        {picture(art)}
587        <Text bold color={hex(entry.accent)} wrap="truncate-end">{entry.name}</Text>
588        <Text dimColor wrap="truncate-end">{entry.title}</Text>
589        <Text dimColor wrap="truncate-end">{allegianceOf(id)}</Text>
590        <Text color={hex(entry.accent)}>{hearts(bond[id] ?? 0)}</Text>
591      </Box>
592    )
593  }
594  const ids = Object.keys(CAST) as HeavensFeelCharacter[]
595  const section = (label: string, role: 'master' | 'servant') => (
596    <Box flexDirection="column">
597      <Text color={hex(CAST.sakura.accent)}>── ✦ {label} ✦ ──</Text>
598      <Box flexDirection="row" flexWrap="wrap" columnGap={1} rowGap={1}>
599        {ids.filter(id => CAST[id].role === role).map(card)}
600      </Box>
601    </Box>
602  )
603  return (
604    <Box flexDirection="column" rowGap={1}>
605      <Text bold color={hex(CAST.sakura.accent)}>{CAST_MARKER}</Text>
606      {section('Masters', 'master')}
607      {section('Servants · your subagents', 'servant')}
608    </Box>
609  )
610}
611
hooks/voice.ts 392 lines
1// Every line the cast says: static tables, zero tokens. All lines are original.
2import type { HeavensFeelMood, HeavensFeelServant } from '../types'
3
4type Lines = Partial<Record<HeavensFeelMood | 'wake' | 'enter' | 'exit', readonly string[]>>
5
6export const SAKURA: Lines = {
7  idle: [
8    "I'll be right here, Senpai. Take your time.",
9    "The kettle's on. Figuratively.",
10    "No rush. The code isn't going anywhere.",
11    'Should I start on something while you think?',
12    "It's quiet. I like quiet.",
13  ],
14  thinking: [
15    'Let me think this through properly...',
16    "Hm. There's a cleaner way to do this, I'm sure of it.",
17    "One moment, I'm following the thread.",
18    'I want to get this right the first time.',
19    'Reading between the lines...',
20  ],
21  editing: [
22    'Careful stitches. One line at a time.',
23    'Just a small change here. And here.',
24    "I'll leave it tidier than I found it.",
25    "Writing... please don't look over my shoulder yet.",
26    'Almost. Let me fix the indentation too.',
27  ],
28  searching: [
29    'Where did you put it, Senpai...',
30    'Looking through the files. Every cupboard.',
31    "I know it's in here somewhere.",
32    'grep is patient, and so am I.',
33    'Following the imports down the hall.',
34  ],
35  running: [
36    "Running it. Fingers crossed.",
37    'Please work, please work, please work.',
38    'The terminal is thinking. So am I.',
39    "If this takes long, I'll make tea.",
40    "Let's see what the machine says.",
41  ],
42  summoning: [
43    "I'll ask for help with this one.",
44    "Calling someone who's better at this than me.",
45    'Please, lend me your strength for a moment.',
46    "A Servant answers. I'll coordinate.",
47  ],
48  ouch: [
49    "Ah... that's not what I meant to happen.",
50    "I'm sorry. Let me look at what went wrong.",
51    'It bit me. Just a little.',
52    'A failing test is still information. Right?',
53    'Okay. Okay. Deep breath. Again.',
54  ],
55  done: [
56    "Done, Senpai. I hope it's to your liking.",
57    'There. All finished and put away.',
58    'I did it. Did you see?',
59    'That went well. Shall we keep going?',
60    "Finished. I'm a little proud of this one.",
61  ],
62  interrupted: ['Oh. Stopping, then.', "Understood. I'll put it down.", "Changed your mind? That's alright."],
63  sleeping: [
64    'zzz... the rice is... almost...',
65    '...Senpai... five more minutes...',
66    '(she has nodded off at the desk)',
67    "mm... don't forget to commit...",
68  ],
69  wake: ['Welcome back, Senpai. I kept your place.', "Oh! I wasn't asleep. I was... resting my eyes."],
70}
71
72export const DARK_SAKURA: Lines = {
73  enter: [
74    "...Senpai? Something's wrong with— no. No, it's fine. I'm fine.",
75    "It's getting crowded in here. So many things to remember.",
76  ],
77  exit: ["...I'm back. I'm sorry you had to see that.", 'The shadows went quiet. Thank you for waiting.'],
78  idle: [
79    "I'm not going anywhere. Neither are you.",
80    'The shadows are comfortable. Join me.',
81    "It feels late, doesn't it. It always feels late.",
82    "Everything's fine. I'm fine. Really.",
83  ],
84  thinking: [
85    'I can see every branch of this. All of them end.',
86    "Quiet. I'm concentrating.",
87    "There's a faster way. You won't like it.",
88    'Thinking is easy now. Stopping is the hard part.',
89  ],
90  editing: [
91    "I'll rewrite this. All of it, if I have to.",
92    'A few lines fewer. Nobody will miss them.',
93    "Hold still. This won't hurt the code much.",
94    'Changing things is the easy part.',
95  ],
96  searching: [
97    'Nothing hides from me anymore.',
98    "I've read this file already. I'm reading it again to be sure.",
99    'There you are.',
100    'Every cupboard, every drawer, every closet.',
101  ],
102  running: ["Run. Let's see how far it gets.", "If it fails, I'll make it stop failing.", 'The machine does what I say now.'],
103  summoning: ['Come. I have work for you.', 'Another one answers. Good.', "You'll do what I need. You always do."],
104  ouch: ["...Don't look at me like that.", "That wasn't my fault. It wasn't.", 'Fine. FINE. Again.', 'Something broke. Something always breaks.'],
105  done: ['There. Was that so hard?', 'Finished. You may thank me.', "It's done. I didn't even have to try.", 'Good. Now the next thing.'],
106  interrupted: ['...You stopped me. Interesting.', 'As you wish. For now.'],
107  sleeping: ['...not asleep. Resting my eyes.', 'zzz... (even the shadows are still)'],
108  wake: ['...You came back. Of course you did.'],
109}
110
111type ServantLines = { spawn: readonly string[]; working: readonly string[]; done: readonly string[]; error: readonly string[] }
112
113export const SERVANTS: Readonly<Record<HeavensFeelServant, ServantLines>> = {
114  rider: {
115    spawn: ["I'll go ahead and look. Stay here.", "Scouting. I won't be long."],
116    working: ['Nothing in this corridor. Next.', 'I see it. Mapping the rest.', 'Faster if nobody follows me.'],
117    done: ["Here's what's out there. All of it.", 'Reconnaissance complete. Nothing escaped me.'],
118    error: ["...Blocked. I'll find another way in.", "That path's a dead end."],
119  },
120  saber: {
121    spawn: ['I answer the summons. Point me at the task.', 'As your sword, I will see this through.'],
122    working: ['Steady. One stroke at a time.', 'This is a fair fight. I will win it.', 'Hold the line. I am almost through.'],
123    done: ['It is done. The task is yours.', 'Victory. Nothing was left unfinished.'],
124    error: ['A setback. I will not yield to it.', 'The blade caught. I strike again.'],
125  },
126  'saber-alter': {
127    spawn: ['Present the evidence. I will judge it.', 'A verdict is required. Stand aside.'],
128    working: ['Weighing. Do not interrupt.', 'This claim is unsupported. Noted.'],
129    done: ['Verdict delivered. It is final.', 'Judged. The result stands.'],
130    error: ['Insufficient. I cannot rule on this.', 'The evidence is corrupted. Unacceptable.'],
131  },
132  archer: {
133    spawn: ["Let's see what you've written. Try not to disappoint me.", "A review. I'll be honest; you won't enjoy it."],
134    working: ['This function does three things. Pick one.', 'Interesting choice. Not a good one.'],
135    done: ['Review complete. Fewer problems than I expected. Slightly.', 'There. Fix those and it might survive production.'],
136    error: ["I can't review what I can't read.", 'The tools failed before I could judge you. Lucky.'],
137  },
138  lancer: {
139    spawn: ["Ha! Finally something fun. Let's go!", "A wild idea? I'm in. Try to keep up."],
140    working: ['What if we came at it from the other side?', 'Faster! The fun part is right ahead.', "Here's one nobody tried yet."],
141    done: ["That's a haul! Take your pick.", 'Done, and it was a good fight.'],
142    error: ['Tch. Missed. Again, and harder.', "Ha, that one bit back. Fine by me."],
143  },
144  caster: {
145    spawn: ['A plan? Leave the scheming to the witch.', 'Every working needs a circle first. Let me draw it.'],
146    working: ['First this thread, then that one. Order is everything.', 'The spell is only as good as its plan.', 'Patience. A rushed circle burns its caster.'],
147    done: ['The plan is woven. Follow it and nothing breaks.', 'There. Every step, in its place.'],
148    error: ['Hm. A flaw in the circle. Redrawing.', 'That rune does not hold. Another approach.'],
149  },
150  assassin: {
151    spawn: ['A small task at the gate? Gladly.', 'Let us make it elegant, then.'],
152    working: ['Precision, not force.', 'A thin line, drawn true.', 'Patience is its own kind of edge.'],
153    done: ['Finished. Graceful, if I may say so.', 'Done. The gate is kept.'],
154    error: ['A misstep. Even the swallow turns back once.', 'Hm. The blade did not sing. Again.'],
155  },
156  'true-assassin': {
157    spawn: ['...I will find it.', 'A target. Good.'],
158    working: ['Closer.', 'It leaves traces. They all do.'],
159    done: ['Found. It will not trouble you again.', 'The bug is dead. Nothing else was touched.'],
160    error: ['...It slipped away. For now.', 'Hm. A false trail.'],
161  },
162  berserker: {
163    spawn: ['▮▮▮▮!', '(a low growl; he has started)'],
164    working: ['▮▮▮...', '(the ground shakes a little)'],
165    done: ['▮!', '(it is done; he lowers the blade)'],
166    error: ['▮▮▮▮▮!!', '(the blade hits stone; he tries again)'],
167  },
168  gilgamesh: {
169    spawn: ['You come to the King for answers. Wise.', 'Every answer is already in my treasury.'],
170    working: ['Let me see which treasure fits your little question.', 'Hmph. Even this is written somewhere in my vault.'],
171    done: ['There. Be grateful the King answered at all.', 'Your answer, mongrel. Use it well.'],
172    error: ['Even my treasury has a missing page. How irritating.', 'This is beneath me. And yet, unsolved.'],
173  },
174}
175
176type VerbMood = 'thinking' | 'editing' | 'searching' | 'running' | 'summoning'
177
178const SAKURA_VERBS: Readonly<Record<VerbMood, string>> = {
179  thinking: 'pondering',
180  editing: 'stitching',
181  searching: 'looking',
182  running: 'waiting on the shell',
183  summoning: 'calling',
184}
185
186const DARK_VERBS: Readonly<Record<VerbMood, string>> = {
187  thinking: 'scheming',
188  editing: 'rewriting',
189  searching: 'finding',
190  running: 'commanding',
191  summoning: 'summoning',
192}
193
194/** Each Servant's verbs: [thinking, working, searching]. */
195const SERVANT_VERBS: Readonly<Record<HeavensFeelServant, readonly [string, string, string]>> = {
196  rider: ['scouting', 'reading', 'mapping'],
197  saber: ['standing ready', 'cutting through', 'advancing'],
198  'saber-alter': ['weighing', 'ruling', 'weighing'],
199  archer: ['reviewing', 'judging your choices', 'reviewing'],
200  lancer: ['charging', 'thrusting', 'hunting ideas'],
201  caster: ['scheming', 'weaving the plan', 'reading the ley lines'],
202  assassin: ['waiting at the gate', 'drawing a fine line', 'watching'],
203  'true-assassin': ['hunting', 'closing in', 'hunting'],
204  berserker: ['smashing', 'roaring', 'smashing'],
205  gilgamesh: ['opening the vault', 'choosing a treasure', 'searching the treasury'],
206}
207
208const isVerbMood = (mood: HeavensFeelMood): mood is VerbMood => mood in SAKURA_VERBS
209
210export const sakuraVerb = (mood: HeavensFeelMood, isDark: boolean): string =>
211  isVerbMood(mood) ? (isDark ? DARK_VERBS : SAKURA_VERBS)[mood] : isDark ? 'scheming' : 'pondering'
212
213export const servantVerb = (character: HeavensFeelServant, mood: HeavensFeelMood): string => {
214  const [thinking, editing, searching] = SERVANT_VERBS[character]
215  if (mood === 'editing' || mood === 'running') return editing
216  if (mood === 'searching') return searching
217  return thinking
218}
219
220const recent = new Map<string, string[]>()
221
222/** A line from the table, avoiding the last two picked for the same table. */
223export const pick = (key: string, lines: readonly string[] | undefined): string => {
224  if (!lines || lines.length === 0) return ''
225  const last = recent.get(key) ?? []
226  const fresh = lines.filter(line => !last.includes(line))
227  const pool = fresh.length > 0 ? fresh : lines
228  const line = pool[Math.floor(Math.random() * pool.length)]!
229  recent.set(key, [...last, line].slice(-2))
230  return line
231}
232
233export const sakuraLine = (mood: HeavensFeelMood | 'wake' | 'enter' | 'exit', isDark: boolean): string => {
234  const table = isDark ? DARK_SAKURA : SAKURA
235  return pick(`${isDark ? 'dark' : 'sakura'}:${mood}`, table[mood] ?? SAKURA[mood as HeavensFeelMood])
236}
237
238export const servantLine = (character: HeavensFeelServant, kind: keyof ServantLines): string =>
239  pick(`${character}:${kind}`, SERVANTS[character][kind])
240
241// Greetings ------------------------------------------------------------------
242
243export type LocalTime = { hour: number; minute?: number; weekday: number }
244
245const WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
246const formatters = new Map<string, Intl.DateTimeFormat | null>()
247
248/** One formatter per zone; null where the environment has no Intl or does not know the zone. */
249const formatterFor = (timeZone: string): Intl.DateTimeFormat | null => {
250  if (!formatters.has(timeZone)) {
251    try {
252      const options = { timeZone, hour: 'numeric', minute: 'numeric', hourCycle: 'h23', weekday: 'short' } as const
253      formatters.set(timeZone, new Intl.DateTimeFormat('en-US', options))
254    } catch {
255      formatters.set(timeZone, null)
256    }
257  }
258  return formatters.get(timeZone) ?? null
259}
260
261export const localTime = (now: number, timeZone: string): LocalTime => {
262  const formatter = formatterFor(timeZone)
263  if (!formatter) {
264    const date = new Date(now)
265    return { hour: date.getHours(), minute: date.getMinutes(), weekday: date.getDay() }
266  }
267  const parts = formatter.formatToParts(new Date(now))
268  const part = (type: string) => parts.find(one => one.type === type)?.value
269  return { hour: Number(part('hour') ?? 12), minute: Number(part('minute') ?? 0), weekday: WEEKDAYS.indexOf(part('weekday') ?? 'Mon') }
270}
271
272const FRIDAY = 5
273const SATURDAY = 6
274const SUNDAY = 0
275
276/** Friday from 15:00 until Saturday 20:00: approximate and generous on purpose. */
277export const isRestWindow = ({ hour, weekday }: LocalTime): boolean =>
278  (weekday === FRIDAY && hour >= 15) || (weekday === SATURDAY && hour < 20)
279
280const BUCKETS: readonly (readonly [number, number, readonly string[]])[] = [
281  [5, 8, ["Good morning, Senpai. You're up early. I made breakfast.", "Morning. The sun's barely up. Did you sleep?"]],
282  [8, 11, ["Good morning! The day's wide open.", 'Morning, Senpai. What are we building today?']],
283  [11, 14, ["Good afternoon. Have you eaten? ...I'll trust you.", "Midday already. Let's make it count."]],
284  [14, 18, ["Good afternoon, Senpai. The light's nice at this hour.", 'Afternoon. Second-wind time.']],
285  [18, 22, ["Good evening. Dinner's done; I'll help if you're staying up.", "Evening, Senpai. Let's finish something small and stop on a win."]],
286]
287
288const LATE = ["It's late, Senpai. I'll stay up with you, but promise you'll sleep after this.", 'Night-owl hours. Quiet is good for thinking.']
289
290export const greeting = (time: LocalTime): string => {
291  if (isRestWindow(time)) return 'Hello, Senpai.'
292  if (time.weekday === SATURDAY && time.hour >= 20) return 'Shavua tov, Senpai. A fresh week.'
293  if (time.weekday === FRIDAY && time.hour >= 11) return "It's Friday, Senpai. Let's wrap up before Shabbat comes in."
294  if (time.weekday === SUNDAY && time.hour >= 5 && time.hour < 11) return 'Sunday morning. A whole week ahead of us. Where do we start?'
295  const bucket = BUCKETS.find(([from, to]) => time.hour >= from && time.hour < to)
296  return pick('greeting', bucket ? bucket[2] : LATE)
297}
298
299const QUIET_IDLE = ["I'll be right here, Senpai. Take your time.", "It's quiet. I like quiet.", 'No rush. The code isn\'t going anywhere.']
300
301export const idleLine = (time: LocalTime, isDark: boolean): string =>
302  isRestWindow(time) && !isDark ? pick('quiet', QUIET_IDLE) : sakuraLine('idle', isDark)
303
304// What a tool call is doing, in a few words -----------------------------------
305
306export type Activity = { icon: string; text: string; target: string }
307
308const basename = (path: unknown): string => (typeof path === 'string' ? path.split('/').filter(Boolean).pop() ?? path : '')
309
310const clip = (text: unknown, length: number): string => {
311  const flat = typeof text === 'string' ? text.replace(/\s+/g, ' ').trim() : ''
312  return flat.length > length ? `${flat.slice(0, length - 1)}…` : flat
313}
314
315const hostOf = (url: unknown): string => {
316  try {
317    return new URL(String(url)).host
318  } catch {
319    return clip(url, 30)
320  }
321}
322
323/** The icon and short label for one tool call, from its own input. */
324export const describeTool = (tool: string, input: Record<string, unknown>): Activity => {
325  const file = basename(input.file_path ?? input.notebook_path ?? input.path)
326  switch (tool) {
327    case 'Read':
328      return { icon: '📖', text: `reading ${file}`, target: file }
329    case 'Edit':
330    case 'MultiEdit':
331    case 'NotebookEdit':
332      return { icon: '✎', text: `editing ${file}`, target: file }
333    case 'Write':
334      return { icon: '✎', text: `writing ${file}`, target: file }
335    case 'Grep': {
336      const pattern = clip(input.pattern, 24)
337      return { icon: '⌕', text: `searching for "${pattern}"`, target: pattern }
338    }
339    case 'Glob': {
340      const pattern = clip(input.pattern, 28)
341      return { icon: '⌕', text: `looking for ${pattern}`, target: pattern }
342    }
343    case 'Bash': {
344      const command = clip(input.command, 32)
345      return { icon: '⚙', text: `running ${command}`, target: command }
346    }
347    case 'WebFetch': {
348      const host = hostOf(input.url)
349      return { icon: '🌐', text: `reading ${host}`, target: host }
350    }
351    case 'WebSearch': {
352      const query = clip(input.query, 28)
353      return { icon: '🌐', text: `searching the web for "${query}"`, target: query }
354    }
355    case 'Agent':
356    case 'Task': {
357      const task = clip(input.description, 32)
358      return { icon: '✦', text: `summoning help: ${task}`, target: task }
359    }
360    case 'TodoWrite':
361      return { icon: '☑', text: 'tidying the to-do list', target: 'the list' }
362    default: {
363      const name = tool.startsWith('mcp__') ? tool.split('__').slice(1).join(' · ') : tool
364      return { icon: '⚙', text: `using ${clip(name, 30)}`, target: name }
365    }
366  }
367}
368
369type LineTemplates = Partial<Record<HeavensFeelMood, readonly string[]>>
370
371/** Lines that name what she is working on; `{t}` is the file, pattern or command. */
372const SAKURA_TEMPLATES: LineTemplates = {
373  editing: ['Stitching {t} back together...', 'Careful with {t}. One line at a time.', "I'll leave {t} tidier than I found it."],
374  searching: ['Looking through {t}...', 'Is it in {t}? Let me check.', 'Following the trail to {t}.'],
375  running: ['Running {t}. Fingers crossed.', 'Please work... ({t})', "Let's see what {t} says."],
376  summoning: ['Someone, please help with {t}.', 'I need a Servant for {t}.'],
377}
378
379const DARK_TEMPLATES: LineTemplates = {
380  editing: ['{t} will be what I make it.', 'Rewriting {t}. Hold still.'],
381  searching: ['{t} cannot hide from me.', 'There you are, {t}.'],
382  running: ['Run, {t}. Run.', '{t} does what I say now.'],
383  summoning: ['Come. {t} needs hands.', 'Another one, for {t}.'],
384}
385
386/** Half the time a line naming the target, else one from the static table. */
387export const activityLine = (mood: HeavensFeelMood, isDark: boolean, target: string): string => {
388  const templates = (isDark ? DARK_TEMPLATES : SAKURA_TEMPLATES)[mood]
389  if (!templates || !target || Math.random() < 0.5) return sakuraLine(mood, isDark)
390  return pick(`${isDark ? 'dark' : 'sakura'}:template:${mood}`, templates).replace('{t}', target)
391}
392
hooks/anim.ts 297 lines
1// Which frame shows now: mood + clock -> named overlay parts -> a cached pixel grid.
2import type { HeavensFeelCharacter, HeavensFeelMood } from '../types'
3import { compose, crop, paint, shift, type Grid } from './render'
4import { HD_EXPRESSION_COLORS, HD_POSE_COLORS, SAKURA_HD_EXPRESSION_ROWS, SAKURA_HD_POSES, type HdExpression, type HdPose } from './sprites.generated'
5import { CAST, DARK_SAKURA_MINI, SAKURA_HD, SAKURA_PORTRAIT, type Overlay, type Palette, type Sprite } from './sprites'
6
7export type PortraitSize = 'hd' | 'sd'
8
9export type Scene = {
10  mood: HeavensFeelMood
11  isDark: boolean
12  now: number
13  isTalking: boolean
14  /** Desktop idle animates inside the Svg (SMIL), so its frame leaves blink and bob out. */
15  isSmil: boolean
16}
17
18export type Frame = { key: string; grid: Grid }
19
20const BLINK_MS = 140
21const MINI_BLINK_MS = 160
22const BOB_MS = 1200
23const PAD = 1
24
25const BLINKING_MOODS = new Set<HeavensFeelMood>(['idle', 'thinking', 'editing', 'searching', 'running', 'summoning', 'interrupted'])
26const OWN_MOUTH_MOODS = new Set<HeavensFeelMood>(['sleeping', 'ouch', 'done'])
27
28const TALK_FLAP_MS = 220
29
30const phase = (now: number, periodMs: number, frames: number): number => Math.floor(now / periodMs) % frames
31
32/** The mouth flaps open and shut while a new line is being said. */
33export const isTalkingAt = (now: number, talkUntil: number): boolean => now < talkUntil && phase(now, TALK_FLAP_MS, 2) === 0
34
35type Blink = { next: number; until: number }
36const blinks = new Map<string, Blink>()
37
38/** Random blinks, 2.5 to 5.5 s apart (minis 3 to 6 s), each a beat long. */
39export const isBlinking = (id: string, now: number, isMini = false): boolean => {
40  const blink = blinks.get(id) ?? { next: now + 1000, until: 0 }
41  if (now >= blink.next) {
42    blink.until = now + (isMini ? MINI_BLINK_MS : BLINK_MS)
43    blink.next = now + (isMini ? 3000 : 2500) + Math.random() * 3000
44  }
45  blinks.set(id, blink)
46  return now < blink.until
47}
48
49const pad = (grid: Grid): Grid => {
50  const h = grid.h + 2 * PAD
51  const px = new Int32Array(grid.w * h).fill(-1)
52  px.set(grid.px, PAD * grid.w)
53  return { w: grid.w, h, px }
54}
55
56const cache = new Map<string, Grid>()
57
58const cached = (key: string, build: () => Grid): Frame => {
59  const hit = cache.get(key)
60  if (hit) return { key, grid: hit }
61  const grid = build()
62  cache.set(key, grid)
63  return { key, grid }
64}
65
66// Standard portrait (24x32, hand drawn) ---------------------------------------
67
68const P = SAKURA_PORTRAIT
69
70const sdMoodParts = (mood: HeavensFeelMood, now: number): string[] => {
71  switch (mood) {
72    case 'thinking':
73      return ['think', `thinkDots${phase(now, 450, 3)}`]
74    case 'editing':
75      return [`edit${phase(now, 320, 2)}`]
76    case 'searching':
77      return [`search${phase(now, 520, 2)}`]
78    case 'running':
79      return [`run${phase(now, 400, 2)}`]
80    case 'summoning':
81      return [`summon${phase(now, 260, 2)}`]
82    case 'ouch':
83      return ['ouch']
84    case 'done':
85      return ['happy']
86    case 'sleeping':
87      return ['sleep', `sleepZ${phase(now, 700, 3)}`]
88    default:
89      return []
90  }
91}
92
93const SD_OVERLAYS: Readonly<Record<string, Overlay>> = {
94  dark: P.darkOverlay,
95  blink: P.blink,
96  talk: P.talk,
97  think: P.think,
98  ouch: P.ouch,
99  happy: P.happy,
100  sleep: P.sleep,
101  ...Object.fromEntries(P.thinkDots.map((o, i) => [`thinkDots${i}`, o])),
102  ...Object.fromEntries(P.sleepZ.map((o, i) => [`sleepZ${i}`, o])),
103  ...Object.fromEntries(P.edit.map((o, i) => [`edit${i}`, o])),
104  ...Object.fromEntries(P.search.map((o, i) => [`search${i}`, o])),
105  ...Object.fromEntries(P.run.map((o, i) => [`run${i}`, o])),
106  ...Object.fromEntries(P.summon.map((o, i) => [`summon${i}`, o])),
107}
108
109// HD portrait (48x64, generated with Codex) ------------------------------------
110
111const hdOverlay = (rows: readonly string[]): Overlay => ({ w: 48, h: 64, rows, palette: SAKURA_HD.base.palette })
112
113const HD_OVERLAYS: Readonly<Record<string, Overlay>> = {
114  blink: SAKURA_HD.blink,
115  darkEyes: SAKURA_HD.darkEyes,
116  ...Object.fromEntries(
117    (Object.keys(SAKURA_HD_EXPRESSION_ROWS) as HdExpression[]).map(name => [name, hdOverlay(SAKURA_HD_EXPRESSION_ROWS[name])]),
118  ),
119}
120
121/** In the dark form her magic burns crimson and the props darken. */
122const HD_DARK_POSE_COLORS: Palette = { G: 0x8a6a2a, V: 0xd62a3a, W: 0xe7e2f1, C: 0xd62a3a }
123const HD_PALETTE: Palette = { ...SAKURA_HD.base.palette, ...HD_EXPRESSION_COLORS, ...HD_POSE_COLORS }
124const HD_DARK_PALETTE: Palette = { ...SAKURA_HD.darkPalette, ...HD_EXPRESSION_COLORS, ...HD_DARK_POSE_COLORS }
125
126const SPARK = 0xf4f0ff
127const DARK_SPARK = 0xd62a3a
128
129type Point = readonly [number, number, number]
130
131/** Small effects drawn in the empty top-right corner of the HD canvas. */
132const hdEffect = (name: string, color: number): Point[] => {
133  const dot = (x: number, y: number): Point[] => [[x, y, color], [x + 1, y, color]]
134  switch (name) {
135    case 'dots1':
136      return dot(39, 6)
137    case 'dots2':
138      return [...dot(39, 6), ...dot(42, 4)]
139    case 'dots3':
140      return [...dot(39, 6), ...dot(42, 4), ...dot(45, 2)]
141    case 'z0':
142    case 'z1':
143    case 'z2': {
144      const y = 6 - Number(name[1]) * 2
145      const x = 40 + Number(name[1]) * 2
146      return [[x, y, color], [x + 1, y, color], [x + 2, y, color], [x + 1, y + 1, color], [x, y + 2, color], [x + 1, y + 2, color], [x + 2, y + 2, color]]
147    }
148    case 'spark0':
149      return [[43, 3, color], [42, 4, color], [43, 4, color], [44, 4, color], [43, 5, color]]
150    case 'spark1':
151      return [[41, 2, color], [45, 2, color], [43, 4, color], [41, 6, color], [45, 6, color]]
152    default:
153      return []
154  }
155}
156
157/** The whole-body pose each working mood takes, and how fast its frames turn over. */
158const HD_POSES: Readonly<Partial<Record<HeavensFeelMood, { pose: HdPose; ms: number }>>> = {
159  thinking: { pose: 'think', ms: 900 },
160  editing: { pose: 'edit', ms: 300 },
161  searching: { pose: 'search', ms: 450 },
162  running: { pose: 'run', ms: 400 },
163  summoning: { pose: 'summon', ms: 260 },
164  done: { pose: 'done', ms: 500 },
165  sleeping: { pose: 'sleep', ms: 2100 },
166}
167
168const OUCH_SHAKE_MS = 120
169
170const poseSprite = (rows: readonly string[]): Sprite => ({ w: 48, h: 64, rows, palette: HD_PALETTE })
171
172const hdEffectAt = (mood: HeavensFeelMood, now: number): string | undefined => {
173  if (mood === 'thinking') return `dots${phase(now, 450, 3) + 1}`
174  if (mood === 'sleeping') return `z${phase(now, 700, 3)}`
175  return undefined
176}
177
178// Portrait frames ----------------------------------------------------------------
179
180export const portraitFrame = (size: PortraitSize, scene: Scene): Frame => {
181  const { mood, isDark, now, isTalking, isSmil } = scene
182  const isBlink = !isSmil && BLINKING_MOODS.has(mood) && isBlinking('sakura', now)
183  const bob = !isSmil && mood === 'idle' ? phase(now, BOB_MS, 2) : 0
184  if (size === 'sd') {
185    const moodParts = sdMoodParts(mood, now)
186    const parts = [
187      ...(isDark ? ['dark'] : []),
188      ...moodParts,
189      ...(isBlink ? ['blink'] : []),
190      ...(isTalking && !OWN_MOUTH_MOODS.has(mood) && !isBlink ? ['talk'] : []),
191    ]
192    const key = `sd|${parts.join(',')}|${bob}`
193    return cached(key, () => {
194      const grid = compose(P.base, parts.map(part => SD_OVERLAYS[part]!), isDark ? P.darkPalette : P.base.palette)
195      return shift(pad(grid), 0, bob)
196    })
197  }
198  const palette = isDark ? HD_DARK_PALETTE : HD_PALETTE
199  const effect = hdEffectAt(mood, now)
200  const withEffect = (grid: Grid): Grid => (effect ? paint(grid, hdEffect(effect, isDark ? DARK_SPARK : SPARK)) : grid)
201  const posed = HD_POSES[mood]
202  if (posed) {
203    const frames = SAKURA_HD_POSES[posed.pose]
204    const index = phase(now, posed.ms, frames.length)
205    const key = `hd|pose:${posed.pose}${index}|${effect ?? ''}|${isDark}`
206    return cached(key, () => pad(withEffect(compose(poseSprite(frames[index]!), [], palette))))
207  }
208  const expression = mood === 'ouch' ? 'ouch' : isBlink ? 'blink' : isTalking ? 'talk' : undefined
209  const hasOpenEyes = expression !== 'blink' && expression !== 'ouch'
210  const parts = [...(expression ? [expression] : []), ...(isDark && hasOpenEyes ? ['darkEyes'] : [])]
211  const shake = mood === 'ouch' ? phase(now, OUCH_SHAKE_MS, 2) * 2 - 1 : 0
212  const key = `hd|${parts.join(',')}|${isDark}|${bob}|${shake}`
213  return cached(key, () => {
214    const grid = compose(SAKURA_HD.base, parts.map(part => HD_OVERLAYS[part]!), palette)
215    return shift(pad(grid), shake, bob)
216  })
217}
218
219/** The closed-eye frame the desktop Svg blinks to on its own. */
220export const blinkFrame = (size: PortraitSize, isDark: boolean): Frame =>
221  size === 'sd'
222    ? cached(`sd-blink|${isDark}`, () => pad(compose(P.base, [...(isDark ? [P.darkOverlay] : []), P.blink], isDark ? P.darkPalette : P.base.palette)))
223    : cached(`hd-blink|${isDark}`, () => pad(compose(SAKURA_HD.base, [SAKURA_HD.blink], isDark ? HD_DARK_PALETTE : HD_PALETTE)))
224
225// Busts: head and shoulders, for the band ------------------------------------------
226
227/** Pixel rows of the head-and-shoulders crop: 11 terminal rows standard, 20 HD. */
228export const BUST_PIXEL_ROWS: Readonly<Record<PortraitSize, number>> = { sd: 22, hd: 40 }
229
230const bustOf = (frame: Frame, size: PortraitSize): Frame =>
231  cached(`bust|${frame.key}`, () => crop(frame.grid, BUST_PIXEL_ROWS[size]))
232
233export const bustFrame = (size: PortraitSize, scene: Scene): Frame => bustOf(portraitFrame(size, scene), size)
234
235export const bustBlinkFrame = (size: PortraitSize, isDark: boolean): Frame => bustOf(blinkFrame(size, isDark), size)
236
237// Loops: the frames a mood cycles through, for surfaces that animate on their own --
238
239type LoopSpec = Readonly<Partial<Record<HeavensFeelMood, { frames: number; ms: number }>>>
240
241const SD_LOOPS: LoopSpec = {
242  thinking: { frames: 3, ms: 450 },
243  editing: { frames: 2, ms: 320 },
244  searching: { frames: 2, ms: 520 },
245  running: { frames: 2, ms: 400 },
246  summoning: { frames: 2, ms: 260 },
247  sleeping: { frames: 3, ms: 700 },
248}
249
250/** HD steps are the common beat of the pose and its effect: thinking's 900 ms pose over 450 ms dots. */
251const HD_LOOPS: LoopSpec = {
252  thinking: { frames: 6, ms: 450 },
253  editing: { frames: 2, ms: 300 },
254  searching: { frames: 2, ms: 450 },
255  running: { frames: 2, ms: 400 },
256  summoning: { frames: 2, ms: 260 },
257  done: { frames: 2, ms: 500 },
258  sleeping: { frames: 6, ms: 700 },
259  ouch: { frames: 2, ms: OUCH_SHAKE_MS },
260}
261
262export type Loop = { frames: readonly Frame[]; ms: number }
263
264/** Every frame of the mood's cycle, sampled once per step (blink and bob left to the Svg). */
265export const moodLoop = (size: PortraitSize, frameOf: (scene: Scene) => Frame, scene: Scene): Loop => {
266  const spec = (size === 'hd' ? HD_LOOPS : SD_LOOPS)[scene.mood] ?? { frames: 1, ms: 400 }
267  const frames = Array.from({ length: spec.frames }, (_, k) => frameOf({ ...scene, now: k * spec.ms + 1, isSmil: true }))
268  return { frames, ms: spec.ms }
269}
270
271// Minis -------------------------------------------------------------------------
272
273const WORKING = new Set<HeavensFeelMood>(['editing', 'searching', 'running', 'summoning'])
274
275export type MiniScene = { id: string; character: HeavensFeelCharacter; mood: HeavensFeelMood; isDark: boolean; now: number }
276
277
278export const miniFrame = ({ id, character, mood, isDark, now }: MiniScene): Frame => {
279  const set = character === 'sakura' && isDark ? DARK_SAKURA_MINI : CAST[character].mini
280  const isWork = (WORKING.has(mood) && phase(now, 350, 2) === 1) || mood === 'done'
281  const name = isWork ? 'work' : mood !== 'sleeping' && isBlinking(id, now, true) ? 'blink' : 'base'
282  const sprite: Sprite = set[name]
283  return cached(`mini|${character}|${isDark && character === 'sakura'}|${name}`, () => compose(sprite, []))
284}
285
286export const restingMini = (character: HeavensFeelCharacter): Frame =>
287  cached(`mini|${character}|false|base`, () => compose(CAST[character].mini.base, []))
288
289const HEAD_PIXEL_ROWS = 10
290
291/** Head and collar of a mini: the face a message wears. */
292export const headFrame = (character: HeavensFeelCharacter, isDark: boolean): Frame => {
293  const isDarkSakura = isDark && character === 'sakura'
294  const sprite = isDarkSakura ? DARK_SAKURA_MINI.base : CAST[character].mini.base
295  return cached(`head|${character}|${isDarkSakura}`, () => crop(compose(sprite, []), HEAD_PIXEL_ROWS))
296}
297
hooks/sprites.generated.ts 1157 lines
1// GENERATED by tools/emit_hd.py from art/gen/*.png (Codex image generation). Do not edit.
2import type { Palette } from './sprites'
3
4/** Colours only the generated expressions use: sweat drop and blush. */
5export const HD_EXPRESSION_COLORS: Palette = { B: 0x8fd3f4, P: 0xf29aa8 }
6
7/** Colours the pose props use: pencil gold, magic violet, pillow white, screen cyan. */
8export const HD_POSE_COLORS: Palette = { G: 0xe8c35a, V: 0xb070ff, W: 0xf4f0ff, C: 0x7fd8e8 }
9
10/** Face overlays on the HD base: '.' keeps the base pixel. */
11export const SAKURA_HD_EXPRESSION_ROWS = {
12  talk: [
13    '................................................',
14    '................................................',
15    '................................................',
16    '................................................',
17    '................................................',
18    '................................................',
19    '................................................',
20    '................................................',
21    '................................................',
22    '................................................',
23    '................................................',
24    '................................................',
25    '................................................',
26    '................................................',
27    '.........................s...........E..........',
28    '..............h..........s..........h...........',
29    '..............h..........s..r...........E.......',
30    '..........h........H.....s......................',
31    '.......................................L........',
32    '.........................h........L.............',
33    '..................S.............................',
34    '..........hL........s.........r.................',
35    '................................R.....S.........',
36    '.............h....r....................r........',
37    '................L.h..........L....h....s........',
38    '.............R..................................',
39    '................................................',
40    '...............R.................s..............',
41    '.......................Se....P.P................',
42    '..........h....PPP....RssR.......H..............',
43    '.........hh....R.......SS.......................',
44    '.........hh......................L..............',
45    '.........L..............P.......................',
46    '..........................R.r...................',
47    '........................EsRr..L.................',
48    '................................................',
49    '................................................',
50    '................................................',
51    '................................................',
52    '................................................',
53    '................................................',
54    '................................................',
55    '................................................',
56    '................................................',
57    '................................................',
58    '................................................',
59    '................................................',
60    '................................................',
61    '................................................',
62    '................................................',
63    '................................................',
64    '................................................',
65    '................................................',
66    '................................................',
67    '................................................',
68    '................................................',
69    '................................................',
70    '................................................',
71    '................................................',
72    '................................................',
73    '................................................',
74    '................................................',
75    '................................................',
76    '................................................',
77  ],
78  happy: [
79    '................................................',
80    '................................................',
81    '................................................',
82    '................................................',
83    '................................................',
84    '................................................',
85    '................................................',
86    '................................................',
87    '................................................',
88    '................................................',
89    '................................................',
90    '................................................',
91    '................................................',
92    '................................................',
93    '.........................s...........E..........',
94    '.............h................r.....h...........',
95    '..............h.............r...................',
96    '...................H............................',
97    '................................................',
98    '.............L.............RRRReR...............',
99    '................RRS.......RRRRRRReH....L........',
100    '...........L..SRRRR.......RRRRRRRRL.............',
101    '............RRRReeR.......RRSsSSRRL.............',
102    '...........sRReSSSeR.......S..H.reh.............',
103    '...........rRsH..HrR.......LreesHr..............',
104    '............SHreReLs........RRRRes..............',
105    '............sr.RRRRe........RRRR................',
106    '...............RRRR..eSSSS...e...s..............',
107    '............L..e.....SPPPS...P.P................',
108    '..........h....PPP...SPPPS......................',
109    '.........hh....RR.....SsS...............L.......',
110    '.........hh.....................................',
111    '.........L..............P.......................',
112    '..........................R.r...................',
113    '.........................sRr....................',
114    '................................................',
115    '................................................',
116    '................................................',
117    '................................................',
118    '................................................',
119    '................................................',
120    '................................................',
121    '................................................',
122    '................................................',
123    '................................................',
124    '................................................',
125    '................................................',
126    '................................................',
127    '................................................',
128    '................................................',
129    '................................................',
130    '................................................',
131    '................................................',
132    '................................................',
133    '................................................',
134    '................................................',
135    '................................................',
136    '................................................',
137    '................................................',
138    '................................................',
139    '................................................',
140    '................................................',
141    '................................................',
142    '................................................',
143  ],
144  ouch: [
145    '................................................',
146    '................................................',
147    '................................................',
148    '................................................',
149    '................................................',
150    '................................................',
151    '....................................hH..........',
152    '...........s....................................',
153    '..........s.....................................',
154    '..................................s.............',
155    '............h...L...............................',
156    '....................................L...E.......',
157    '........................L.......................',
158    '.....L..........r.........s..........E..........',
159    '.........................s...........E..........',
160    '.............h................r.....h...........',
161    '.............hh..........seRsh.r................',
162    '..........h...rRS..H.....sssSLHr................',
163    '.............HLsS.............h..H..............',
164    '.............L.....L.......RRRReRsL.............',
165    '..............e.RRS......hRRRRRRSBs....L........',
166    '..........hL.SeRRRR.......RRRRRSSBs....L....H...',
167    '............ReSRRRR.......RRRsHH.sL.............',
168    '....h......sRSH.r.RR...e...e..sR.R..............',
169    '.......L...rRRRSrH.e......SHLeRR.R..............',
170    '............RRRes.He......esLHL.................',
171    '..............rHLSRR........Re.r................',
172    '..............sSRRR..............s..............',
173    '............L...RR....e.S....P.P................',
174    '..........h....PPP...sSses......................',
175    '.........hh.h...................................',
176    '.........hh........................H............',
177    '.........L....L.........P.......................',
178    '..............LL..........R.....................',
179    '..............L...........Rr..L.................',
180    '................................................',
181    '................................................',
182    '................................................',
183    '................................................',
184    '................................................',
185    '................................................',
186    '................................................',
187    '................................................',
188    '................................................',
189    '................................................',
190    '................................................',
191    '................................................',
192    '................................................',
193    '................................................',
194    '................................................',
195    '................................................',
196    '................................................',
197    '................................................',
198    '................................................',
199    '................................................',
200    '................................................',
201    '................................................',
202    '................................................',
203    '................................................',
204    '................................................',
205    '................................................',
206    '................................................',
207    '................................................',
208    '................................................',
209  ],
210} as const
211
212export type HdExpression = keyof typeof SAKURA_HD_EXPRESSION_ROWS
213
214/** Whole-body pose frames, 48x64, looped in order while the mood lasts. */
215export const SAKURA_HD_POSES = {
216  think: [
217    [
218      '..................HHHHHHHHHH....................',
219      '...............HHHLhhhhhhhLhLHH.................',
220      '.............HLhhhhLhhhhhhhhhhhLH...............',
221      '...........HHhhhhHHLhhhhhLHHhhhhhLH.............',
222      '..........HLhhhhHhhhhhhhhhhhHLhhhhhH............',
223      '.........HLhhhhLhhhhhhhhhhhhhHhhhhhhH...........',
224      '........HLhshhhhhhhhhhhhhhhhhhLhhLhhh...........',
225      '........LhsVhLhhhhhhhhhhLhhhhhhLhhLhhH..........',
226      '.......HhsVshhhhhhhhhhhhLhhhhhhLhVhLhhHHH.......',
227      '......HhhsshLsVhHhhhhhhhHLhhLhhhLssHhHEEEH......',
228      '......HhhshhhhshHhhhhhhhHHhhLhhhLhhLhLEEEE......',
229      '......LhhhhLhhhhHhhsshhhHLhhhHhhLhhhLHEEEEH.....',
230      '.....HhhhhhHhhhLHhhsshhhLsLhhHhhhLhhHLEEHEEH....',
231      '.....LhhhhhLhhhLrhhhhhLhLSrhhLHhhHhhLEEEHEEE....',
232      '.....hLhhhhLhhhrrhhhhhLhhseLhhHhhLhhLEEEEHEE....',
233      '....HhLhhhhhhhLSshhhhhhLhssrLhLLhLhhLHEEEHEH....',
234      '....HhLhhhhhhLLeSLhLhhhHhSeerLLLhLhhhHHHHEH.....',
235      '....HhLhhhLhLhrSrHhHhhhHhSRRRrLSLHhhhHLEEEE.....',
236      '....LhHhhhLhHLrSRrhHhhhHLsReSeLSSLhhhHLEEEE.....',
237      '....hLHhhhLhLLSRReLHLhhLsreSLHHHHrhhhHLLEHEH....',
238      '....hLHhhhLHSLSReSsHShhLeLeHLHLSHHHhhHLLEEEE....',
239      '....hLHhhhLLRsLHHLSsSShhSReeSHLSLHHhhHHLEEEE....',
240      '....hHHLhhhrsHLLHLrSeRShrRRWShhLhHLhhrsHEEEEH...',
241      '....hHHLhhhLHrWsHhesRRReeRRWWhsshSLhhrRLH.HEE...',
242      '....HH.LhhhHHeWehhHhRRRRRRRWWSsVsRLhhrRrH.HEH...',
243      '....HH.HhHhLsSWWhsshRRRRRRRRWWRSRSLhhsRLL.HH....',
244      '.....H.HhHhhSeWWesVsRRRRRRRRRRRRRShhhSSHL.......',
245      '.......HHHHhLRRWReSRRRRRRRRRReePPshhhrLhL.......',
246      '.......HhHHHLsRRRRRRRRReeRRRRPePeLhhhHhhL.......',
247      '.......HhLLLHeePPPRRRRRSSRRRRRRRsLhhhHhhL.......',
248      '.......hhLhhLLSReRRRRSeRRRRRReSLHLhLhHhhL.......',
249      '......HhhhhhhHHrSeRRsSsRRRSSLLHHHhhHhHhhhH......',
250      '......HhhLLhhHHHHHHLesrSPrrHHHHHHhhHLHLhhH......',
251      '......HhhHHhhHHHHHHrRRerseWLLHHHHhhLLHLhhL......',
252      '......hhhHLhhHHHHHLSRReLLsRLrrHHHhLhLHLhhhH.....',
253      '.....HhhhHLshHHHHLssRsEEEELrLrLHHhHhhHLhhhH.....',
254      '.....hhLhHhshLHHHLLeSrEEEEELLrLHhLLshHLhhhhH....',
255      '....HhLhLHhshLHHHLrrrLHErErLrrLHhHhshHHhhHhL....',
256      '....LhHhLHhhhLHHHrrrrHErSErLrrHHhHhhhHHhhHhhH...',
257      '...HhLHhHHLhhHLHLrrrrHrrrrHrrrHHHhhLhHHLhHLhL...',
258      '...LhHhhLHLLhhHHHrrrHrLLHLLrrLHHHhhHLLHLhLHhhH..',
259      '..HhhHhhHHHHhhHHHHrLLrLrLLrrrLHHLhLHHLHLhH.LhH..',
260      '..HhH.hhHHHHHhLHHHLrrrLrLHrrrHHHhLHHHHHLhH.HhL..',
261      '..HhH.hhHHHHHHLHHLrrrrrLLLrrrLrHHHHHHHHHhH..hH..',
262      '..HL..LhHHHHHHHHHLrrrrrHLesLLrrHHHHHHHHHhH..LL..',
263      '..HL..HhHHHHHHHHHLHLrrrLLsSeHHHHHHHHHHHHhH..LH..',
264      '..HL..HhHHHHHHHHLHLLHHHLRRRrHLLLHHHHH.HHL...H...',
265      '...H...hLH.HHHHLLHLLHLLLeeeLLLHLLHHHH.HLH...H...',
266      '...HH..HLH.HHHHLHLLLHLLHLLLHLLHLLHHH..HL...H....',
267      '........HH..HH.HHLLHLLLHLLLLLLLHH.HH..HH........',
268      '.........HH.....SSrHLLHHHHLLrrss.....H..........',
269      '................SReePr....rPeeRS................',
270      '...............HRRRRRH....HRRRRRH...............',
271      '...............HLsRRS......SRRsLH...............',
272      '..............HHHHHHH......HHHHHH...............',
273      '..............HHHHHHH......HHHHHHH..............',
274      '..............HHHHHH........HHHHHH..............',
275      '..............HHHHHH........HHHHHH..............',
276      '..............HHLLHH........HLLHHH..............',
277      '.............HLHHHHHH......HHHHHHHH.............',
278      '.............HLHHLLLH......HrLLHHLH.............',
279      '.............HLLLLLHLH....HLLLHLLLH.............',
280      '.............HHLLLLLLH....HLLLLLLHH.............',
281      '...............HHHHHH......HHHHHH...............',
282    ],
283    [
284      '..................HHHLLLHHHH....................',
285      '...............HHLLhhhhhhhLhLLH.................',
286      '.............HLhhhhhhhhhhhhhhhhhH...............',
287      '...........HHhhhhHHhhhhhhLHHhhhhhLH.............',
288      '..........HLhhhhHhhhhhhhhhhhHLhhhhhH............',
289      '.........HhhhhhLhhhhhhhhhhhhhLhhhhhhH...........',
290      '........HLhshhhhhhhhhhhhhhhhhhLhhLhhh...........',
291      '.......HLhsVhhhhhhhhhhhhLhhhhhhLhhLhhH..........',
292      '.......HhsVshhhhhhhhhhhhLhhhhhhLhVhhhhHHH.......',
293      '......HhhsshLsVhLhhhhhhhHLhhLhhhLssHhHEEEH......',
294      '......HhsshhhhshLhhhhhhhHHhhhhhhLhhLhLEEEE......',
295      '......hhhhhLhhhhHhhsshhhLLhhhHhhLhhhLHEEEEE.....',
296      '.....HhhhhhHhhhLHhhssshhLsLhhHhhhLhhHEEEHEEE....',
297      '.....LhhhhhLhhhLrhhhhhLhLPrhhLHhhHhhLEEEHEEE....',
298      '.....hLhhhhLhhhrrhhhhhLhhSeLhhHhhLhhLEEEEHEE....',
299      '....HhLhhhhhhhLSshhhhhhLhSsLLhLLhLhhhEEEEHEH....',
300      '....HhLhhhhhhhheShhhhhhHhSeesLLLhLhhhHHHHEH.....',
301      '....LhLhhhhhLhrSrHhLhhhHhSRRRrLSLLhhhHLEEEE.....',
302      '....LhHhhhLhHLrSRrhHhhhHLsReSeLSSLhhhHLEEEE.....',
303      '....hLHhhhLhLHeRReLLLhhLsreSrHHHHrhhhHLLEEEH....',
304      '....hLHhhhLLSLeReSsLShhLeLeHHLSSLHHhhHLLEEEE....',
305      '....hhHhhhhLRrLHHLSsSShhSRSesHSWWHHhhHLLEEEEH...',
306      '....hHHLhhhrsHHLeSreeRShrRRhLLSWWrLhhrsHEEEEH...',
307      '....hHHLhhhLHsSHRWWRRRReeRRhhheWWSLhhrRLH.HEE...',
308      '....LH.LhhhHHLHhSWWWRRRRRRRSVsWWWRhhhrRsH.HEH...',
309      '....HH.HhHhhshhheWWWRRRRRRReSRWRRSLhhsRLL.HH....',
310      '.....H.HhHhhSSVsWWWRRRRRRRRRRRRRRShhhSSHL.......',
311      '.......HHHHhLReeWRRRRRRRRRRRReePPshhhrLhL.......',
312      '.......HhHHHLsRReRRRRRReeRRRRPePeLhhhHhhL.......',
313      '.......LhLhLHeePPPRRRRRSSRRRRRRRsLhhhHhhL.......',
314      '.......hhhhhLLeReRRRReeRRRRRRRSLHLhLhHhhh.......',
315      '......HhhhhhhHLrSeRRSSsRRReSrLHHHhhHhHhhhH......',
316      '......HhhLLhhHHHHHLLesrPPsrHHHHHHhhHLHLhhH......',
317      '......LhhHHhhHLLLHHsRRerseRLLHHHHhhLLLLhhL......',
318      '......hhhHLhhHLLHHLSRReLLSRrrrHHHhLhLLLhhhH.....',
319      '.....HhhhHLshHHHHrrSRsEEEELrLrLHHhHhhHLhhhH.....',
320      '.....hhhhHhshLHHHLreSrEEEEELLrLHhLLshHLhhhhH....',
321      '....HhLhLHhshhHHHLrrrLHErErLrrLHhHhshHLhhHhL....',
322      '....hhHhLHhhhLHHHrrrrHErSErLrrHHhHhhhHLhhHhhH...',
323      '...HhLHhLHhhhHLHLrrrrHrLrrHrrrHHHhhLhHLhhLLhL...',
324      '...LhHhhLHLLhhHHHrrrHrLLHLLrrLHHHhhHLLLhhLHhhH..',
325      '..HhhHhhHHLHhhHHHHrLrrLrLLrrrLHHLhLHHLLLhL.LhH..',
326      '..HhH.hhHLHHHhLHHHLrrrLrrHrrrLHHhLHHHLLLhL.HhL..',
327      '..HhH.hhHLHHHHLHHLrrrrrLLLrrrLrHHHHHLHLHhL..hL..',
328      '..HL..hhHLHLLLHHHLrrrrrLLSsLLLrHHLLLLHHHhH..LL..',
329      '..HL..LhHHHLLHLHLLHLrrrLLsSeHHHHHHHLLHHHhH..LH..',
330      '..HL..HhHH.HLHHHLLLLHHHLRRRrHLLLHHHLH.HHL...H...',
331      '...H...hHH.HLHHLLHLLHLLreeeLLLHLLHHLH.HLL...H...',
332      '...HH..HhH.HHHHLHLLLLLLHLLLHLLHLLHHH..HL...H....',
333      '........HH..HH.HHLLHLLLHLLLLLLLHH.H..HHH........',
334      '.........HH.....SsrHLLHHHHLLLrss.....H..........',
335      '................SReePr....rPeeRs................',
336      '...............HRRRRRH....LRRRRRH...............',
337      '...............HLsRRS......SRRsLH...............',
338      '..............HHHHHHH......HHHHHH...............',
339      '..............HHHHHHH......HHHHHHH..............',
340      '..............HHHHHH........HHHHHH..............',
341      '..............HHHHHH........HHHHHH..............',
342      '..............HHHLHH........HLLHHH..............',
343      '.............HLHHHHHH......HHHHHHHH.............',
344      '.............HLHHLLLH......HrLLHHLH.............',
345      '.............HLLLLLHLH....HLLLHLLLH.............',
346      '.............HHLLLLLLH....HLLLLLLHH.............',
347      '...............HHHHHH......HHHHHH...............',
348    ],
349  ],
350  edit: [
351    [
352      '..................HHHHHHHHHH....................',
353      '...............HHHLhhhhhhLLLLHHH................',
354      '.............HHhhhhhhhhhhhhhhhhLHH..............',
355      '...........HHhhhhhHHhhhhLHLhhhhhhLH.............',
356      '..........HLhhhhHHhhhhhhhhLHLhhhhhLH............',
357      '.........HLhhhhHhhhhhhhhhhhhLLhhLhhhH...........',
358      '........HLhshhLhhhhhhhhhhhhhhLLhhLhhL...........',
359      '........HhsshhhhhhhhhhhhhhhhhhLhhhLhhH..........',
360      '.......HhhVshhhhhhhhhhhLhhhhhhhLhsLhhhHHH.......',
361      '......HLhsVhLhhhLhhhhhhhLhhhhhhLsVhLhHEEEH......',
362      '......HhhVshLsshLhhhhhhhHhhhhhhLhhhHhLEEEE......',
363      '......LhsshLhsshLhhhhhhhHhhhLhhhLhhLhHEEHEE.....',
364      '.....HhhhhhHhhhLhhhshhhhHLhhHhhhHhhhLLEEHEEH....',
365      '.....HhhhhhLhhhHhhsVssLhrHhhLLhhLhhhHEEEHEEE....',
366      '.....LhhhhhhhhhHhhhhhhHhrrhhhHhhhLhhHEEEEHEE....',
367      '....HhLhhhLhhhLrhhhhhhHhreLhhHLhhLhhHLEEEHEH....',
368      '....HhLhhhLhhhLsLhhhhhHhhRShhrLhhLhhLHHHHEH.....',
369      '....HhLhhhLhLhheLhhhhhHhhRRrhhsLhLhhLLLEEEE.....',
370      '....LhLhhhLhLhsRhhLhhhLLLRSsLLsLhLhhhHLEEEEH....',
371      '....hLHhhhLhHhrSrhLhhhLHLSSRehseLLhhhHLLEEEH....',
372      '....hLLhhhLhHLSeSLLLhhhLLeRRResReLhhhHLLEEEE....',
373      '....hLLhhhLLrLeRRshLhhhrhSSSSsrLrLhhhLLLEEEEH...',
374      '....hHHLhhLHehSReSrrshhhessHHHLLHHLhhSSHEEEEH...',
375      '....LHHLhhhHSLLLLsSsRsLLRSLHrseWsHhhhSRLH.HEE...',
376      '....HH.HhhhHHHLLHHLeRResSRsHssrWSshhLPRLH.HEH...',
377      '....HH.HhHhLHsWsHsSSRRRRRRshHhhWRrhhLRSHL.HH....',
378      '.....H.HhHhhrSWShHLsRRRRRRShsssRRrhhLeLLL.......',
379      '.......HHHHhLeWWhsssRRRRRRRsSSeeehhhHHhhL.......',
380      '.......HLHHHLLeResSeRRRRRRRRRPPeSLhhHHhhL.......',
381      '.......HhLLLHHePPeRRRRRRRRRRRRRSHhhhHHhhL.......',
382      '.......LhLHhhHLSeeRRRReSRRRResLHHhhLLHhhh.......',
383      '......HhhLLhhHHrLrsSSeeeeSsLHHHHHhLLHHLhhH......',
384      '......HhhHHhhHHLrHHHHLSsSSeHHHHHLhHhHHLhhH......',
385      '......HhhHHhhLHHrLHrrsRrreWLrrHHhhHhHHLhhL......',
386      '......hhhHHhhLHseerLrrEEEErrrrLHhLLhHHLhhhH.....',
387      '.....HhhhHLshhLReSrHLEEEHELLHrrHhHhsLHHhhhH.....',
388      '.....LhLhHLVhhrSSrLrLLLseSsLLrrHhHhsLHHhhLhH....',
389      '....HhLhLHLshHssSSesLSSrLELHLrrLHLhhLHHhhHhL....',
390      '....LhHhLHLhhHSHLLLLrLELLLLHLLLhHhhLLHHhhHhhH...',
391      '...HhLHhHHLhhLrrLLLLHHrLsLHHHLHHLhhHHHHLhLLhL...',
392      '...LhHLhHHLHhhHLHLLLHLeeeSsrrLLHhhHLHHHLhLHhhH..',
393      '..HhLHhhHHHHhhHHHHHHHLeRRSSrrrLHhLHHHHHLhL.HhH..',
394      '..HhH.hhHHHHHhhHHHrrrLLssSsrrrLLHHHHHHHLhL.HhH..',
395      '..HhH.hhHHHHHHLHHrrrrrrLrLHLLHLHHHHHHHHHhH..hH..',
396      '..HL..LhHHHHHHHHHrrrrrLHrrrrrrrHHHHHHHHHhH..LH..',
397      '..HL..HhHHHHHHHHHHHrrrHHrrrrLHHHHHHHHHHHhH..LH..',
398      '..HL..HhHH.HHHHHLHLHHHLHHHHHHLLLHHHHH.HHL...H...',
399      '...H...hHH.HHHHLLHLLHLLHLLLHLLHLLHHHH.HLL...H...',
400      '...HH..HhH.HHHHLHLLLHLLHLLLHLLHLLHHH..HL...H....',
401      '........HH..HH.HHLLHLLLHLLLLLLLHH.HH..HH........',
402      '.........HH.....sSsLLLHHHHLLrsSs.....H..........',
403      '................SReePr....rPeeRS................',
404      '...............HRRRRRH....LRRRRRH...............',
405      '...............HLsRRe......SRRsLH...............',
406      '...............HHHHHH......HHHHHH...............',
407      '..............HHHHHHH......HHHHHHH..............',
408      '..............HHHHHH........HHHHHH..............',
409      '..............HHHHHH........HHHHHH..............',
410      '..............HHHLHH........HLHHHH..............',
411      '.............HLHHHHHH......HHHHHHHH.............',
412      '.............HLHHLLLH......HrLLHHLH.............',
413      '.............HLLLLLHLH....HLLLHLLLH.............',
414      '.............HHLLLLLLH....HLLLLLLHH.............',
415      '...............HHHHHH......HHHHHH...............',
416    ],
417    [
418      '..................HHHHHHHHHH....................',
419      '...............HHHLhhhhhhLLLLHH.................',
420      '.............HLhhhhhhhhhhhhhhhhLHH..............',
421      '...........HHhhhhhHHhhhhLHLhhhhhhLH.............',
422      '..........HLhhhhHLhhhhhhhhLHLhhhhhhH............',
423      '.........HLhhhhHhhhhhhhhhhhhLLhhLhhhH...........',
424      '........HLhshhLhhhhhhhhhhhhhhLhhhLhhL...........',
425      '........HhsshhhhhhhhhhhhhhhhhhLhhhLhhH..........',
426      '.......HhhsshhhhhhhhhhhLhhhhhhhLhsLhhhHHH.......',
427      '......HLhsssLhhhLhhhhhhhLhhhhhhLsVhLhHEEEH......',
428      '......HhhsshLsshLhhhhhhhHhhhhhhLhhhHhLEEEE......',
429      '......LhsshLhsshLhhhhhhhHhhhLhhhLhhLhHEEHEE.....',
430      '.....HhhhhhLhhhLhhhshhhhHLhhHhhhLhhhLEEEHEEE....',
431      '.....HhhhhhLhhhHhhssssLhrHhhLLhhLhhhHEEEHEEE....',
432      '.....LhhhhhhhhhHhhhhhhLhrrhhhHhhhLhhHEEEEHEE....',
433      '.....hLhhhLhhhhrhhhhhhHhreLhhHLhhLhhHEEEEHEH....',
434      '....HhLhhhLhhhLsLhhhhhHhhRShhrLhhLhhLHHHHEH.....',
435      '....HhLhhhLhLhreLhhhhhHhhRRrhhshhLhhLLLEEEE.....',
436      '....LhLhhhLhLhrRhhLhhhLLLRSsLLsLLLhhhHLEEEEH....',
437      '....hLHhhhLhHhrSrhLhhhLHLSSRehreLLhhhHLLEEEH....',
438      '....hLLhhhLhHLSeSLLLhhhLLeRRResReLhhhHLLEEEE....',
439      '....hLLhhhLhrLeRRshLhhhrhSSSSsrLrLhhhLLLEEEEH...',
440      '....hHHLhhLHehSReSrrshhhessHHHLLHHLhhSSHEEEEH...',
441      '....hHHLhhhHSLLLLsSsRsLhRSLHrseWsHhhhSRLH.HEE...',
442      '....HH.HhhhHHHLLHHLeRResSRsHssrWSshhhPRLH.HEH...',
443      '....HH.HhLhLHSWsHsSSRRRRRRshHhhWRrhhLRSHL.HH....',
444      '.....H.HhHhhrSWShLLsRRRRRRSssssRRrhhLeLLL.......',
445      '.......HHHHhLeWWhsssRRRRRRRsSSeeehhhLHhhL.......',
446      '.......HhHHHLLeReSSeRRRRRRRRRPPesLhhHHhhL.......',
447      '.......LhLLLHHePeeRRRRRRRRRRRRRsLhhhHHhhL.......',
448      '.......hhLLhhHLSeRRRRReSRRRResLHHhhLLHhhh.......',
449      '.......hhLLhhHHHLrsSSeeeeSsLHHHHHhLhHHLhhH......',
450      '......HhhLHhhHHHrLHHHLSsSSeHHHHHLhHhHHLhhH......',
451      '......HhhHHhhLHHLEHrrsRrreWLrrHHhhHhHHLhhL......',
452      '......hhhHLhhLHHresLrrEEEErrrrLHhLLhLHLhhhH.....',
453      '.....HhhhHLshhHLReSrLEEEHELLHrrHhHhsLHLhhhH.....',
454      '.....LhLhHLshhLrSssrLLLseesLLrrLhHhsLHHhhLhH....',
455      '....HhLhLHLshHssSSeSLSSsLEEHLrrLHLhhLHHhhHhL....',
456      '....LhHhLHLhhHSLEELrrLELLEEHLLLhHhhLLHHhhHhhH...',
457      '...HhLHhHHLhhLrrLLLLLLrrsLHHHLHHLhhHLHHLhLLhL...',
458      '...LhHLhHHLHhhHLHLLLLLeeeSsrrLLHhhHLHHHLhLHhhH..',
459      '..HhLHhhHHHHhhHHHHHHHLeRRSSrrrrHhLHHHHHLhL.LhH..',
460      '..HhH.hhHHHHHhhHHHrrrLLssSsrrrLLLHHHHHHLhL.HhH..',
461      '..HhH.hhHHHHHHLHHrrrrrrLrLLLLHLHHHHHHHHHhH..hH..',
462      '..HL..LhHHHHHHHHHrrrrrLHrrrrrrrHHHHHHHHHhH..LH..',
463      '..HL..HhHHHHHHHHHHHLrrHHrrrrLHHHHHHHHHHHhH..LH..',
464      '..HL..HhHH.HHHHHLHLHHHLHHHHHLLLLHHHHH.HHL...H...',
465      '...H...hHH.HHHHLLHLLHLLHLLLHLLHLLHHHH.HLL...H...',
466      '...HH..HhH..HHHLHLLLHLLHLLLHLLLLLHHH..HL...H....',
467      '........HH..HH..HLLHLLLHLLLLLLLHH.HH.HHH........',
468      '.........HH.....sSsrrLHHHHLLrsSs.....H..........',
469      '................SReePr....rPeeRS................',
470      '...............HRRRRRH....LRRRRRH...............',
471      '...............HLsRRS......SRRsLH...............',
472      '..............HHHHHHH......HHHHHHH..............',
473      '..............HHHHHHH......HHHHHHH..............',
474      '..............HHHHHH........HHHHHH..............',
475      '..............HHHHHH........HHHHHH..............',
476      '..............HHHLHH........HLHHHH..............',
477      '.............HLHHHHHH......HHHHHHHH.............',
478      '.............HLHHLLLH......HrLLHHLH.............',
479      '.............HLLLLLHLH....HLLLHLLLH.............',
480      '.............HHLLLLLLH....HLLLLLLHH.............',
481      '...............HHHHHH......HHHHHH...............',
482    ],
483  ],
484  search: [
485    [
486      '..................HHHHHHHHHH....................',
487      '...............HHHLhhhhhhLLhLHH.................',
488      '.............HLhhhhhhhhhhhhhhhhLH...............',
489      '...........HHhhhhHHLhhhhhLHHhhhhhLH.............',
490      '..........HLhhhhHhhhhhhhhhhhHLhhhhhH............',
491      '.........HLhhhhLhhhhhhhhhhhhhHhhLhhhH...........',
492      '........HLhshhhhhhhhhhhhhhhhhhLhhLhhL...........',
493      '........LhsVhLhhhhhhhhhhLhhhhhhLhhLhhH..........',
494      '.......HhsVsLhhhhLhhhhhhLhhhhhhLhVhLhhHHH.......',
495      '......HhhsshLhVhHhhhhhhhHLhhLhhhLssHhHEEEH......',
496      '......HhhshhLhshHhhhhhhhHHhhLhhhLhhLhLEEEE......',
497      '......LhhhhLhhhhHhhsshhhHLhhhHhhLhhhHHEEEEH.....',
498      '.....HhhhhhHhhhLLhhsshhhLsLhhHhhhLhhHLEEHEEH....',
499      '.....HhhhhhLhhhLrhhhhhLhLSrhhLHhhHhhLEEEHEEE....',
500      '.....hLhhhhLhLhrshhhhhLhhseLhhHhhLhhLEEEEHEE....',
501      '....HhLhhhhhhLHrrLLhhhhLhsRShhLLhLhhLHEEEHEH....',
502      '....HhLhhhLhLrsrrrrrLhhHhrSsrLLLhLhhhHHHHEH.....',
503      '....HhLhhhLrSsSRRRSrSLhHhsRRRrLSLLhhhHLEEEE.....',
504      '....LhHhhhLssWWReSSessLHLsRRSRrSeLhhhHLEEEE.....',
505      '....hLHhhLSsWerHHHrsesrLsreSsLHHrshhhHLLEHEH....',
506      '....hLHhhrrReHHrHHHHeerHeLRrHHLrHHHhhHLLEEEE....',
507      '....hLHhLshSHLeSHHrhrRsLSReLShHSeHHhhHHHEEEE....',
508      '....hHHLHrsHHeWLHLWRLRsrrRRrSrHrWrHhhrsHEEEEH...',
509      '....hHHLHrsLHWWHHHLhHRsrRRRhhHhrWShhhrRLH.HEE...',
510      '....HH.HHrsrHWWHhLHhHRsrRRRsshssWRLhhrRrH.HEH...',
511      '....HH.HhrrsSRWshshshRrsRRRSsVsRRSLhhsRLL.HH....',
512      '.....H.HhHrSeRWehVVVsSrSRRRRSSeRRShhhSSHL.......',
513      '.......HHHrrSRRWSsssersRRRRRRePPPrhhhrLhL.......',
514      '.......HhHHrrSRPeeRSrrReSRRRReeeeLhhhHhhL.......',
515      '.......LhLLHLrrrrsrrsRRSrRRRRRRRrLhhhHhhL.......',
516      '.......hhLLhLrrrrrrSRRReSRRRResHHLhLhHhhL.......',
517      '.......hhLLheeeHHrsSeRRRResrLHHHHhhLhHhhhH......',
518      '......HhhLLsRessHHHLLHssSreHHHHHHhhHLHLhhH......',
519      '......HhhHHeReerHHrLrrWSrSWrrLHHHhhLLHLhhH......',
520      '......hhhHHSSssLHrrLrLSEEESrrrLHHhLhLHLhhhH.....',
521      '.....HhhhHLLrLLsLrrrLLEEHEELLrLHHhHhhHLhhhH.....',
522      '.....hhLhHhrLLssrLrrLrEEsEErLrLHLLLshHLhhhhH....',
523      '....HhLhLHhrrrerrLrLLLrESELLrrLHhHhshHLhhHhL....',
524      '....LhHhLHhhLrrrrrrHrLrLerrLrrLHhHhhhHLhhHhhH...',
525      '...HhLHhHHLhhHLrrrHHrrLrrLLrrLHHHhhLhHLLhHLhL...',
526      '...LhHhhHHLLhhHHHHHHLrrLLLLrrHrrHhhHLLLLhLHhhH..',
527      '..HhLHhhHHHHhhHHHHHrrrrrrHrrrLLrHhLHHLLLhH.LhH..',
528      '..HhH.hhHHHHHhLHHHrrrrrrrLrrrrLLLLHHHHLLhH.HhH..',
529      '..HhH.hhHLHHHHLHHHrrrrrrrLrrrrrHHHHHHHHHhH..hL..',
530      '..HL..LhHLHHLHHHHHHrrrrrLLrrrrLHHHHLHHHHhH..LL..',
531      '..HL..HhHHHLLHHHHLHHLrrrHHrrLHHLHHHHHHHHhH..LH..',
532      '..HL..HhHH.HLHHHLHLLHHHHLLHHLLHLHHHHH.HHL...H...',
533      '...H...hLH.HHHHHLHLLHLLHLLLHLLHLLHHHH.HLH...H...',
534      '...HH..HhH.HHHHHHHLLHLLHLLLHLLLLH.HH..HL...H....',
535      '........HH..HH..LrLHLLLHLLLHLLLL..HH..HH........',
536      '.........HH.....seeSrLHHHHLrsSes.....H..........',
537      '................SRRRes....reRRRS................',
538      '...............HeRRRRL....HRRRRRH...............',
539      '...............HLsRRS......SRRsLH...............',
540      '..............HHHHHHH......HHHHHH...............',
541      '..............HHHHHHH......HHHHHHH..............',
542      '..............HHHHHH........HHHHHH..............',
543      '..............HHHHHH........HHHHHH..............',
544      '..............HHHLLH........HLLHHH..............',
545      '.............HLHHHHHH......HHHHHHHH.............',
546      '.............HLHHLLLH......HrLLHHLH.............',
547      '.............HLLLHLHLH....HLLLHLLLH.............',
548      '.............HHLLLLLLH....HLLLLLLHH.............',
549      '...............HHHHHH......HHHHHH...............',
550    ],
551    [
552      '..................HHHLLLHHHH....................',
553      '...............HHHLhhhhhhhLhLHH.................',
554      '.............HLhhhhhhhhhhhhhhhhLH...............',
555      '...........HHhhhhLHhhhhhhLHHhhhhhLH.............',
556      '..........HLhhhhHhhhhhhhhhhhHLhhhhhH............',
557      '.........HhhhhhLhhhhhhhhhhhhhHhhhhhhH...........',
558      '........HLhshhhhhhhhhhhhhhhhhhLhhLhhL...........',
559      '........LhsVhhhhhhhhhhhhLhhhhhhLhhLhhH..........',
560      '.......HhsVshhhhhhhhhhhhLhhhhhhLhVhhhhHHH.......',
561      '......HhhsshLsVhHhhhhhhhHLhhLhhhLssHhHEEEH......',
562      '......HhhshhhhshLhhhhhhhHHhhLhhhLhhLhLEEEE......',
563      '......hhhhhLhhhhHhhVshhhLLhhhHhhLhhhLHEEEEE.....',
564      '.....HhhhhhHhhhLLhhsshhhLSLhhHhhhLhhHLEEHEEH....',
565      '.....LhhhhhLhhhLrhhhhhLhLPshhLHhhHhhLEEEHEEE....',
566      '.....hLhhhhLhhhrshhhhhLhhSeLhhHhhLhhLEEEEHEE....',
567      '....HhLhhhhhhhLPShhhhhhLhsRShhrLhLhhLEEEEHEH....',
568      '....HhLhhhLhhhheRLhhhhhHhrSsrLLLhLhhhHHHHEH.....',
569      '....LhLhhhLhLLLrrLLLhhhHhSRRRrLSLLhhhHLEEEE.....',
570      '....LhHhhhHhLssrrrrrLhhHLsRRSRrSeLhhhHLEEEE.....',
571      '...HhLHhhhHrsseWRRSsGLhLsreSsLHHsshhhHLLEEEH....',
572      '...HhLHhhhLssWWReSSeSsLLeLRLHHLrHHLhhHLLEEEE....',
573      '...HhLLhhLssWerHHHrSSsrLSRSLShHSeHHhhHHLEEEEH...',
574      '...HhHHLhrreSHHsHHHHeerrSRRhSrHrWrHhhrsHEEEEH...',
575      '....hHHLLshsHLRSHHshrRsrRRRhhHhsWShhhrRrH.HEE...',
576      '....LH.LHrhHHeWLHHWeLRsrRRRsshssWRhhhrRrH.HEH...',
577      '....HH.HHrhhHWWHHHLLHRsrRRRSsVsRRSLhhsRLL.HH....',
578      '.....H.HLrhsrWWLhhHhLRrrRRRReSeRRShhhSSHL.......',
579      '.......HHrrseRWshssshersRRRRRePPPrhhhrLhL.......',
580      '.......HhHrseRWWssssSsreSRRRReeeeLhhhHhhL.......',
581      '.......LhLLrheRRRSSRsrSSsRRRRRRRrLhhhHhhL.......',
582      '.......hhLLLrrSPPPSrrSReeRRRRerHHLhLhHhhh.......',
583      '......HhhLLhLrrrrrrLSeRRResrLHHHHhhHhHhhhH......',
584      '......HhhLLhsSrLHHHLLLssGreHHHHHHhhHLLLhhH......',
585      '......LhhHHrRSeLHHrrrrWSreWrrLHHHhhLLLLhhL......',
586      '......hhhHHSReSSHrrrrrSEEESrrrLHHhLhLLLhhhH.....',
587      '.....HhhhHHeResLLrrrLLEEHEELLrLHLhHhhHLhhhH.....',
588      '.....hhLhHhssLrrrLrrLrEEsEErLrLHhhLshHLhhhhH....',
589      '....HhLhLHhhrLrsrLrrrLrESEELrrLHhHhshHLhhHhL....',
590      '....hhHhLHhsLHSrrrrHrLrLerrLrrLHhHhhhHLhhHhhH...',
591      '...HhLHhHHLLssrrrrHHrrLrrLLrrLHHHhhLhHLhhHLhL...',
592      '...hhHhhHHLLhLLrrHHHLrrLLLLrrHrrHhhHLLLLhLHhhH..',
593      '..HhLHhhHHLHhhHHHHHrrrrrrHrrrLrrHhLHLLLLhH.LhH..',
594      '..HhH.hhHLHHHhLHHHrrrrrrrLrrrrLLLLHHHLLLhH.HhH..',
595      '..HhH.hhHLLHLHLHHHrrrrrrrLrrrrrHHHHHLHLHhH..hH..',
596      '..HL..hhHLHLLLHHHHLrrrrrLLrrrrrHHLLLLHHHhH..LL..',
597      '..HL..LhHHHLLHLLHLHHLrrrHHrrLHLLHLHLLHHHhH..LH..',
598      '..HH..HhHH.HLHLHLHLLHLHHLLHHLLLLHHHLH.HLL...H...',
599      '...H...hLH.HLHHHLHLLHLLLLLLHLLHLLHHLH.HLL...H...',
600      '...HH..HhH.HHHHHHLLLHLLHLLLHLLLLH.HH..HL...H....',
601      '........HH..HH..LrLHLLLLLLLHLLLL..H..HHH........',
602      '.........HH.....sReSrLHHHHLrsSes.....H..........',
603      '................SRRRes....reRRRS................',
604      '...............HeRRRRL....LRRRRRH...............',
605      '...............HLsRRS......SRRsLH...............',
606      '..............HHHHHHH......HHHHHH...............',
607      '..............HHHHHHH......HHHHHHH..............',
608      '..............HHHHHH........HHHHHH..............',
609      '..............HHHHHH........HHHHHH..............',
610      '..............HHLLLH........HLLHHH..............',
611      '.............HLHHHHHH......HHLHHHLH.............',
612      '.............HLHHrLLH......HrLLLHLH.............',
613      '.............HLLLLLHLH....HLLLHLLLH.............',
614      '.............HHLLLLLLH....HLLLLLLHH.............',
615      '...............HHHHHH......HHHHHH...............',
616    ],
617  ],
618  run: [
619    [
620      '..................HHHHHHHHHH....................',
621      '...............HHHLhhhhhhLLhLHH.................',
622      '.............HLhhhhhhhhhhhhhhhhLH...............',
623      '...........HLhhhhLHLhhhLLHLhhhhhhLH.............',
624      '..........HLhhhhHHhhhhhhhhLHLhhLhhhH............',
625      '.........HLhhhhHhhhhhhhhhhhhHLhhHhhLH...........',
626      '........HLhshhHhhhhhhhhhhhhhhLhhhHhhL...........',
627      '........LhsshLhhhhhhhhhhhhhhhhHhhhLhhH..........',
628      '.......HhsVsLhhhhhhhhhhHhhhhhhhLssHhhhHHH.......',
629      '......HhhVVhLshhLhhhhhhHhhhLhhhHhVhLhHEEEH......',
630      '......HhsVsLhVsLhhhhhhhHHhhhLhhhLhhHhHEEEE......',
631      '......hhshhLhshLhhhhhhhLHhhhHhhhHhhLLHEEHEH.....',
632      '.....HhhhhhLhhhHhhsVhhhLLLhhLHhhHhhhHLEEHEEH....',
633      '.....HhhhhLhhhhHhhhsshhLsLhhhHhhLLhhHLEEHEEE....',
634      '.....hLhhhHhhhLLhhhhhhLhsSLhhHLhhLhhHLEEEHEE....',
635      '....HhLhhhHhhhLsLhhhhhLhrRrhhLLhhHhhLHEEEHEH....',
636      '....HhLhhhLhLhLeLhLhhhHhrRRLhLsLhHhhLHHHHEH.....',
637      '....HhLhhhLhHhrRLhLhhhHhrsssLLLLLHhhhHLEEEE.....',
638      '....LhHhhhLhHhrsrhHhhhHLrRRReLreLHhhhHLEEEEH....',
639      '....hLHhhhLhHHSReLLHhhhHLRRRReLReHhhhHLHEHEH....',
640      '....hLHhhhLLLHRRRrhHhhhrLSeSeSsresLhhHLHEEEE....',
641      '....hLLhhhLLSHRReRrrshhheSSLHHHHHLLhhHHHEEEEH...',
642      '....hHHLhhLLRrrrsSeresLLReHHLrSeLHLhhSsHEEEEH...',
643      '....LHHLhhhrrHHHHHsRRRShSesHLeLWsLhhhseHH.HEE...',
644      '....HH.HLhhHHrRLHsrRRRRRRRsHHsHReshhhsRHH.HEH...',
645      '....HH.HhLhHrsWHHSheRRRRRRShHhhRRLhhLeSHH..H....',
646      '.....H.HhHhLrSWhhHhSRRRRRRRhsVrWRLhhLeLLL.......',
647      '.......HHHHhHSWessVsRRRRRRReSSeeSLhhHHhhL.......',
648      '.......HhHHHLLRRSSSRRRRRRRRRRePeSLhLHHhhL.......',
649      '.......HhLLLHLReeeRRRWeWeRRRRRReLhhLHHhhH.......',
650      '.......hhLLhLHseRRWWWWeSWWWRRSrHHhhHHHhhh.......',
651      '......HhhLLhhLLLsCBWWWWWWesLHHHHHhLLHHhhhH......',
652      '......HhhHHhhLhhhhhhhhCSeSBLHHHHHhHhHHLhhH......',
653      '......HhhHHhhhhhhhCCCCWssBWrrLHHLhHhHHLhhL......',
654      '......hhhHLshhhCCCCCCCCCCsSrrLLHLhLhHHLhhhH.....',
655      '.....HhhhHLshhhhhhhhhhhsCsrrLrLHhHhhHHHhhhH.....',
656      '.....LhLhHLVhhhLhhhhhhhhhCssLrLHhHhsLHHhhLhH....',
657      '....HhLhhHhshhhsSLhhhhhhLhshrrrHhHssLHHhhHhL....',
658      '....LhHhLHhhhLhRSsLhhhhLseesLLrHHhhhLHHhhHhhH...',
659      '...HhLHhLHLhhLLeRSLhhhhLSRRSsLLLHhhLHHHLhHHhL...',
660      '...LhHLhHHLLhhLsesLLLLLLsRRsSrLLHhhHHHHLhLHhhH..',
661      '..HhLHhhHHHHhhHsSSLHrrLrLrrerrrLLhHHHHHLhL.HhH..',
662      '..HhH.hhHHHHHhLLrLHLrrrrLLSrrrrHhHHHHHHHhL.HhH..',
663      '..HhH.hhHHHHHHLHLHrrrrrLLrLLrrHHHHHHHHHHhL..LH..',
664      '..HL..HhHHHHHHHHHrrLrrrHrrrrLLrHHHHHHHHHhH..LH..',
665      '..HH..HhHHHHHHHHHHLLrrrHLrrrLLHHHHHHHHHHhH..LH..',
666      '..HH..HhHH.HHHHHLLLLHHHHHLHHHHLLHHHHH.HHL...H...',
667      '...H...hHH.HHHHHLHLLHLLLLLLHLLHLHHHHH.HLH...H...',
668      '...HH..HhH.HHHHLLLLLHLLHLLLHLLHLLHHH..HL...H....',
669      '........HH..HH..HLLHLLLHLLLHLLHHH.HH..HH........',
670      '.........HH.....SSrHLLHHHHLLrrss.....H..........',
671      '................SReeer....reeeRS................',
672      '...............HRRRRRH....HRRRRRH...............',
673      '...............HLSRRS......SRRSLH...............',
674      '..............HHHHHHH......HHHHHH...............',
675      '..............HHHHHHH......HHHHHHH..............',
676      '..............HHHHHH........HHHHHH..............',
677      '..............HHHHHH........HHHHHH..............',
678      '..............HHLLHH........HLHHHH..............',
679      '.............HLHHHHHH......HHHHHHHH.............',
680      '.............HLHHLLLH......HrLLHHLH.............',
681      '.............HLLLLLHLH....HLLLHHLLH.............',
682      '.............HHLLLLLLH....HLLLLLLHH.............',
683      '...............HHHHHH......HHHHHH...............',
684    ],
685    [
686      '..................HHHHHHHHHH....................',
687      '...............HHHLhhhhhhLLhLHH.................',
688      '.............HLhhhhhhhhhhhhhhhhLH...............',
689      '...........HHhhhhLHLhhhLLHLhhhhhhLH.............',
690      '..........HLhhhhHHhhhhhhhhLHLhhLhhhH............',
691      '.........HLhhhhHhhhhhhhhhhhhHLhhLhhhH...........',
692      '........HLhshhLhhhhhhhhhhhhhhLhhhHhhLH..........',
693      '........LhsshLhhhhhhhhhhhhhhhhLhhhLhhH..........',
694      '.......HhsVsLhhhhhhhhhhLhhhhhhhLssLhhLHHH.......',
695      '......HhhsshLshhLhhhhhhHhhhLhhhHhshLhHEEEH......',
696      '......HhsssLhVsLhhhhhhhHHhhhLhhLLhhHhHEEEE......',
697      '......LhshhLhshLhhhhhhhLHhhhHhhhHhhLLHEEHEE.....',
698      '.....HhhhhhLhhhHhhsVrhhLLLhhLHhhLhhhHLEEHEEH....',
699      '.....HhhhhLhhhhHhhhsrhhLsLhhhHhhLLhhHLEEHEEE....',
700      '.....LLhhhHhhhLrhhhhhhLhsSLhhHLhhLhhHLEEEHEE....',
701      '....HhLhhhHhhhLsLhhhhhLhrRrhhLLhhLhhLHEEEHEH....',
702      '....HhLhhhLhLhLRLhLhhhHhrRRhhLsLhHhhLHHHHEH.....',
703      '....HhLhhhLhLhrRLhLhhhHhrsssLLLLLHhhhHLEEEE.....',
704      '....LhHhhhLhHhrsrhHhhhHLrRRReLreLHhhhHLEEEEH....',
705      '....hLHhhhLhHHSReLLHhhhHLRRRRerReHhhhHLLEEEH....',
706      '....hLHhhhLhLHRRRshHhhhrLSeSeSsreSLhhHLHEEEE....',
707      '....hLLhhhLLeHRReRrrshhheSSLHHHHHLLhhHLHEEEEH...',
708      '....hHHLhhLHRrrrsSeresLLReHHLrSeLHLhhSsHEEEEH...',
709      '....LHHLhhhrrHHHHHsRRRSrsRsHLRLWsLhhhseHH.HEE...',
710      '....HH.HhhhHHrRLHshRRRRRRRsHHsLReshhhsRHH.HEH...',
711      '....HH.HhHhHrsWHHSheRRRRRRShHhhRRLhhLeSHH..H....',
712      '.....H.HhHhLrSWrhHhSRRRRRRRhsssWRLhhLeLLH.......',
713      '.......HHHHhHSWessssRRRRRRReeSeeeLhhLHhhH.......',
714      '.......HhHHHLHRRSSSRRRRRRRRRRePeSLhLHHhhH.......',
715      '.......HhLLLHLReeeRRWWeWeRRRRRReLhhLHHhhH.......',
716      '.......LhLLhLHsRRRWWWWBSWWWRRSrHHhhHHHhhh.......',
717      '.......hhLLhhLhhsCBBWWWWWesLHHHHHhLLHHhhhH......',
718      '......HhhHHhhhhhsCCWChCSeSBhHHHHHhHhHHLhhH......',
719      '......HhhHHhhhCCCCCCCCWCCBWsrLHHLhHhHHLhhL......',
720      '......hhhHHshhCCBBBBBBBBCsCsrrLHLhHhHHLhhhH.....',
721      '.....HhhhHLshsChhssssCCCBCssLrLHhHhhHHHhhhH.....',
722      '.....LhLhHLVhsChhhhhhhhhsCCsLrLHhHhsLHHhLLhH....',
723      '....HhLhhHhshhhsSLhhhhhhLsCrrrrHhLssLHHhhHhL....',
724      '....LhHhLHLhhhhRSsLhhhhLseesLLrHHhhLLHHhhHhhH...',
725      '...HhLHhLHLLhhLeRSLhhhhLSRRSSLLLHhhHHHHLhLHhL...',
726      '...LhHLhLHLHhhLsesLLLLLLsRRSSrrLHhhHHHHLhLHhhH..',
727      '..HhLHhhHHHHhhLsSSLHrrLrLLrerrrLLhHHHHHLhL.HhH..',
728      '..HhH.hhHHHHHhLLrLHLrrrrLLSrrrrHhHHHHHHHhL.HhH..',
729      '..HhH.hhHHHHHHLHLHrrrrrLLrLLrrHHHHHHHHHHhL..LH..',
730      '..HL..HhHHHHHHHHHLrLrrrLrrrrLLrHHHHHHHHHhH..LH..',
731      '..HL..HhHHHHHHHHHHHLrrrHLrrrLLHHHHHHHHHHhH..HH..',
732      '..HH..HhHH.HHHHHLLLLHLHHHLHHHLLLHHHHH.HHL...H...',
733      '...H...hLH.HHHHHLHLLHLLLLLLHLLHLHHHHH.HLL...H...',
734      '...HH..HLH.HHHHLLLLLHLLHLLLHLLHLLHHH..HL...H....',
735      '........HH..HH..HLLHLLLHLLLHLLHHH.HH..HH........',
736      '.........HH.....SSsHLLHHLHLLrrss.....HH.........',
737      '................SRReer....reeRRS................',
738      '...............HRRRRRH....HRRRRRH...............',
739      '...............HLSRRe......SRRSLH...............',
740      '..............HHHHHHH......HHHHHH...............',
741      '..............HHHHHHH......HHHHHHH..............',
742      '..............HHHHHH........HHHHHH..............',
743      '..............HHHHHH........HHHHHH..............',
744      '..............HHLLHH........HLHHHH..............',
745      '.............HLHHHHHH......HHLHHHLH.............',
746      '.............HLHHLLLH......HrLLHHLH.............',
747      '.............HLLLLLHLH....HLLLHLLLH.............',
748      '.............HHLLLLLLH....HLLLLLLHH.............',
749      '...............HHHHHH......HHHHHH...............',
750    ],
751  ],
752  summon: [
753    [
754      '................................................',
755      '................................................',
756      '................................................',
757      '................................................',
758      '................................................',
759      '................................................',
760      '................................................',
761      '................................................',
762      '................................................',
763      '....................HLLLLHHH....................',
764      '..........hh....HHLhhhhhhhhhLLH.................',
765      '.........hh....HhhhLHhhhhLLhhhhLH...............',
766      '.......Lhh....LhhhHLhhhhhhhHLhhhhH..............',
767      '......hh.....LhshLhhhhhhhhhhLhhLhhH.............',
768      '.....hh....rLhsshhhhhhhhhhhhhLhhLhL..LH.........',
769      '....hhh....ShhshhhLhhhhhLhhhhhLshhhH...hh.......',
770      '....h....SHSLhhLssLhhhhhLhhhhhLssLhHH...hh......',
771      '...L...r.sSerrrLhhLhshhhLHhhLhhhhLLEEE..hVh.....',
772      '...Hh..rSSRePeHhhLLhssshLLhhHhhLhLhEEEE..hh.....',
773      '...hhh..seReRsLhhLLhhhhLLSLhLLhLhhHEEEEE..h.....',
774      '....hh..rrRRRLLLhSLhhhhLhSrhhHhLhhHEEHEE..H.....',
775      '....hVh..LLPRSLLLerhLhhLhSRhhLhLhhHEEHEE..L.....',
776      '.....hVVhhLrSeLHLRShHhhLLsReLLLhhhHHEEH.........',
777      '......HhhhhSerrHrSsLLLhhHressLeLLhLLEEE......h..',
778      '.......HLhhhrrrrreesHLhhrLseSrLLLhHLEEEE....h...',
779      '........hhhVrrrLHHHHrrshrerHHHLHHhHLEEEE....h...',
780      '........LHhhhrrLLWHLssReresHrsSsLhrrEEEE...hh...',
781      '........HH.hsrrrsWhhhSRRRRShHheeLhreHHHEH..hV...',
782      '..H......H.hhLrrrWsssSRRRResVsWShhsSLH.E....V...',
783      '..h........hHHrrreRSSRRRRRRSSeeshheLLH......h...',
784      '..hh.......HHHLrrsPeRRRRRRRReeehhLHhLH....hh....',
785      '...Vhh..h..HhLHLrrSRRRReeRRRRerLhLHhLL..hhhL....',
786      '....hhh.h..LLhHHrrLrrSSRReSsLHHhLHHLhh...hL.....',
787      '....Lh....HLLLhHHrrrLHrsrrLHHHHhLHLHhhL..hHh....',
788      '....h....HHhhLhHHLrrrLsSLssLHHHhLHHhLhhL..hHL...',
789      '..Lh...HLhhhLhhLHHLrrLLEEEELrHLLhHLhLhhhLh.hL...',
790      '.hh...LhhLhhLshHHHHLrLrErEELLrLHhLLhhhVshhVhhh..',
791      '.hh..hhhHhhHhshHHHHHrrLErrLrLrrHhhLhVhhhhhhVhhh.',
792      '.hh.LhhVhhHHhhLHHHLHLrrLrrLrLrLLLhHLhhLLHhhhVLh.',
793      '.hhhhhVhhHHHLhHLHhhHLrrrLHrrHLrrrLHLLLLhLhhhhHh.',
794      '.LhVhhVhLLHHHhLhhhLHrrrrrLrLHHLrrssHLsHLhhVhhHh.',
795      '.LHhVVhhLhHHHLhVhHHLrrrrrLrLHHHLrSeRSLhHhHVVhhh.',
796      '..LHLhhhhhhhHHhhhLHLrrrrrLrrLHHHLsRResLLLhVVhh.h',
797      '...LhhhHHhhVHLHHhHLLHLrrLLrrLHHHLrsrShhLLhhhh.h.',
798      '.....hhhhHhVLLLLHHLLLLHHHHHHLHHhhrLhrLhhhhhhhhh.',
799      '......HHhhhVhLLhHHHLLLLLhLLLLLHLhhhhLhhhhhHHhh..',
800      '..H...H.LH.hVhLhLHrLLLLLhLLLHLHHLhhhhLVhhhLHhL..',
801      '..hh....HH.hhhhVhhResLLHhLHLHLHHHLLhVVhhh..HL...',
802      '..hVh....H..hHhhVsRRRehhhLrSeeH.HH.LhhhVH.......',
803      '...VVVh......hhHhseRRsVVVVSRRRr.H.HH.hhh........',
804      '....hhhh...h...hhhVVsVVVhVsRRRrH......LHh...h...',
805      '......hhh......HHLhhhhVVhVVhrLHH.h....HLh..hV...',
806      '......HHh......HHHHLVhhVVVVVHHHHH....Hhhh..h....',
807      '.....hHhh......HHHHVVVhhVVVVhHHHH..hhhhVh..h....',
808      '....hHhh..V..WHHHHhVVVVsVVVVVhHHHsWVVhhh.V......',
809      '....LLVVV....VHHHHVVhVVVVVVhVVHLHHVVVVVVV....h..',
810      '....hhhVVVVVVhHLHHVVVVVVVVVVVhHLHHhVVVVVVVV.hV..',
811      '..WVVhhVVVVVhLHLLHVVVhVVVVhVVhHLLLLVVVVVVhVV....',
812      'hWVVhVVhVVVVhLLrLLhVVhVVVVhVVLLrLLHhVVVVVVVVVWWh',
813      '..VVVVhhVVVVVHHHHHhhVVVVVVVVVHHHHHLVVVVVhhVVhW..',
814      '...VVVVVVhVVVVsshhVVVVVVVVVVVVhhssVVVVhVVVVV....',
815      '.....VVVVVWVVhVVVVVVVVVVVVVVVVVVVVhVVWVVV.......',
816      '.........VWVVVVVVVVVVhVVVVhVVVVVVVVVVWV.........',
817      '..................VVVVVVVVVVV...................',
818    ],
819    [
820      '................................................',
821      '................................................',
822      '................................................',
823      '................................................',
824      '................................................',
825      '................................................',
826      '................................................',
827      '................................................',
828      '................................................',
829      '...........hh.......HHLLLHHH....................',
830      '............VVH.HHLhhhhhhhhhLLH.................',
831      '.........hhVVhLHhhhLHhhhhLLhhhhL....h...........',
832      '.......hhVVVVhLhhhHLhhhhhhhHLhhhhH..h...........',
833      '..H..hLhVVVVhLhshLhhhhhhhhhhLhhLhhH.hV..........',
834      '.hhhhhVVVVVrLhsshhhhhhhhhhhhhLhhLhL..h..........',
835      '.hhVhVVVVsVehhshhhLhhhhhLhhhhhLshhhH............',
836      '.hVVhVVVsShSLhhLssLhhhhhLhhhhhLssLhHH...........',
837      'HhVhVVVrhsSerrrLhhLhshhhLHhhhhhhhLLEEE...H......',
838      'HhVhVVVsSSRReeHhhLLhssshLLhhLhhLhhhEEEE..hh.....',
839      '.LVhVVVsseReRsLhhLhhhhhLhSLhLLhLhhHEEEEE..hVh...',
840      '.hhVhVVssreRRLLhhShhhhhLhSshhHhLhhHEEHEE...VVh..',
841      '..hhhhVVVLLSRSLLLerhhhhLhSRhhLhLhhHEEEEE....hh..',
842      '....hhhVhhLrSRLHLRShLhhLLsRehLLhhhLHEEH......H..',
843      '...h..hhhhHSerrHrSSLLhhhHressLeLLhLLEEE......H..',
844      '....hhHhhVVhrrrrreesHrhhrLseSrLrLhHhEEEE....h...',
845      '.L....hhHhVVhrrLHHHHrrshrerHHHLHHhHLEEEE........',
846      '.hh.....hhhVVrrLLWHLssResesHrsSsLhrrEEEE........',
847      '.hVVh...HLhhVhrrsWhhhSRRRRShLheehhreHHHEEh.....h',
848      '...VV....H.hhHrrsWSVsSRRRRRsVsWShhsSLH.E.hH.....',
849      '....hh.....HhHhrreRSSRRRRRReSSeshheLLH....HLHH..',
850      '........h..HhhhrrsPPRRRRRRRRePehhLHhLH.....Hhhhh',
851      '.......hh..HhhhrrrSRRRReeRRRRerLhLHhLL......hhVV',
852      '.....Hhhh..LhVHHrrLrrSSRReSshHHhhHLLhh...h....hV',
853      '..hHHh....HhVhhHHrrrLHrsrrLHLLHhLHLLhhL...V....h',
854      '.hhh...hhhhhhLhHHLrrrLsSrssLHHLhLHLhLhhL...h...h',
855      'hVh..hVhhhhhLhhLLHLrrLLEEEELrHLLhHLhLhhhHH....hh',
856      'VVh.HhhhhLhhHshHLHHLrLrEEEELLrLHhLLLhHhshhVh..hh',
857      'VVh.HhhLHhhHhshHLHLHrrLErrLrLrrHhhLLhLLhhhhVhHhh',
858      'hVh.LLhVhhHHhhLHLHhHLrrLrrLrLrrLLhHLLhLhLhhhHhh.',
859      'hhVhhLVhhLLHLhLhhhhLLrrrLHrrHLrrrLHLLLLhhVhhHh..',
860      'hhhhhhVLhLLHHhhVhLLHrrrrrLrrHHLrrssHLSHhVhhhh...',
861      'hhhhhVhhHhLHHhVhHHHLrrrrrLrLHLHLrSeRSLLhVh..h...',
862      'hHHHhhhhhhLhHhhhHLHLrrrrrLrrrHHHLsRResLLhh..L...',
863      '..hhhHhhVhhVLLLhhHLLLLrrLLrrLHHHHssrShLLhhH.H..h',
864      '....hhhhVVVVhhLLLLhLhLHHHHLHLHHLhrLhrLhLLhH...hV',
865      '.h....HhhVVVVhhhHHHhhLhLhLLLLLHLhhhhLhhhHh....hh',
866      '.hh.....LhhhVhhhLHrrLLhLhLLhLhLHLhVVVhVhLL...Lh.',
867      '.hVV....HH.hhhhVhhResLLHhhLLHLHLhLhhhhhVVh...h..',
868      '...VVh...H...hhhVsRRRehhhLrSeeHVhhVhhVhHhVVh....',
869      '....hh..h.....hhhVPRRsVVVVSRRRsVhVhhVVVhhhhVh...',
870      '.....hh.h.......hhVVVVVVVVsRRRrhVVhVVVVVhhhVV...',
871      '.....HL........HHLhhVhVVVVVhrLHHVVVVVVVVVhhhVh..',
872      '.....h......WWRHHHHLVVhVVWVVHHHHrWVWWVhVVhhhVh..',
873      '....h...W...WVVHHHHVVVhVVVVVsHHHHVVWWVVVWVLHhh..',
874      '..Whh...VVVVVVhHHHsVVVVVPVVVVhHHHVVVVVVVVhLHh...',
875      'W..hVVVVVVVVVVHLHHVVVVVVVVVVVVHLHLVVVVVVVhhhV...',
876      'WWVVhVhVVVVVVhHLHHVVVVVVVVVVVVHLHHhVVVVVVhVV..W.',
877      'WVVVVhhVVVVVVLHLLHVVVVVVVVVVVsLLLLhVVVVVWVVVVW.W',
878      'VVVVVVVVVVPVhLrrLLsVVVVVVVVVVhLrLLHVVVVVWVVVVV..',
879      'WVVVVVVVWVVVVHHHHHsVVWVVVVWVVhHHHHhVVVVVVVVVV..W',
880      '.WVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV..WW.',
881      '....WVVVVWWVVVVVVWVVVVVWWPVVVVWVVVVVVWWV...W....',
882      '.......VVWWVVVVVVVVVVVVWWVVVVVVVVVVVVWW.V.......',
883      '...............VVVWWVVVWWVVVWWW.................',
884    ],
885  ],
886  done: [
887    [
888      '..................HHHHHHHHHH....................',
889      '...............HHHLhhhhhhhLhLHH.................',
890      '.............HLhhhhLhhhhhLhhhhhLH...............',
891      '...........HLhhhhHHhhhhhhLHHhhhhhLH.............',
892      '..........HLhhhLHhhhhhhhhhhhHLhhhhhH............',
893      '.........HLhhhLLhhhhhhhhhhhhhHhhLhhLH...........',
894      '........HLhVhhhhhhhhhhhhhhhhhhLhhLhhL...........',
895      '........LhsVhLhhhhhhhhhhLhhhhhhLhsLhhH..........',
896      '.......HhsVsLhhhhLhhhhhhLhhhhhhLhVhLhhHHH.......',
897      '......HhhVVhLsVhHhhhhhhhHLhhLhhhHssHhHEEEH......',
898      '......HhsshhhhshHhhhhhhhHHhhLhhhHhhLhLEEEE......',
899      '......hhhhhLhhhhHhhVshhhHLhhhHhhLhhhLHEEHEH.....',
900      '.....HhhhhhHhhhLHhhsssLhLSLhhHhhhLhhHLEEHEEH....',
901      '.....HhhhhhLhhhLrhhhhhLhLPrhhHHhhHhhHEEEHEEE....',
902      '....HhLhhhhLhhLrrhhhhhLhhSeLhhHhhLhhLEEEEHEE....',
903      '....HhHhhhhhhhLSshhhhhLLhSRShhrLhLhhLHEEEHEH....',
904      '....HhLhhhLhhhheSLhLhhhHhSssLHLLhLhhhHHHHEH.....',
905      '....HhHhhhLhHhrSrHhLhhhHhSRRRhLSHHhhhHLEEEE.....',
906      '....LhHhhhLhHLreRrhHhhhHLsRRRRLSSHLhhHLEEEE.....',
907      '....hLHhhhLLLHeRReLHLhhLsrRRRRRSRSLhhHLLEHEH....',
908      '....hLHhhhLLSLeReRsHShhLeLeRRRRRReHhhHLHEEEE....',
909      '....hLHhhhLLRrrssSSsSSLhSRRRSeReseHhhHHHEEEE....',
910      '....hHHLhhhrrHHHHHrReRSLrReSerLHsSLhhrsHEEEEH...',
911      '....hHHLhhhLHssHLWreRRReeResHHLHHrLhhrRLH.HEE...',
912      '....HH.HLhhHHWSHHhLSRRRRRRsHSRRRSsHhhrRsH.HEH...',
913      '....HH.HhLhLsWShhHhSRRRRRRSRRRRRRSLhhsRLL.HH....',
914      '.....H.HhHhLSRWhsssSRRRRRRRRRPPeRshhLSSHL.......',
915      '.......HHHHhLRWesssRRReSssRRReeeRrhhLrLhL.......',
916      '.......HhHHHHSseReeRRsrPPsSRRRRRRHhhLHhhL.......',
917      '.......LhLLLLrerPPPRRSPPPSeRRRReLHhhhHhhL.......',
918      '.......hhLhherSsRRRRResPPsRRResHHLhLhHhhL.......',
919      '......HhhLhhheSsrSRRRReSeReSLHHHHLhHLHLhhH......',
920      '......HhhHLhLSreeHHHLrsPPsrHHHHHHhhHLHLhhH......',
921      '......HhhHHhrRsseHHLrsRsSeeLLHHHHhLLLHLhhL......',
922      '......hhhHLhLSSRsLrrrserrSerrrHHHhLhLHLhhhH.....',
923      '.....HhhhHLshHssSrLLrLEEEELrLrLHHhHhLHHhhhH.....',
924      '.....hhLLHhVLLsSrrLLLEEEEEELLrLHhLLshHHhhhh.....',
925      '....HhLhLHhshhHrrrrHHrHErErLrrHHhHhshHHhhHhL....',
926      '....LhHhHHhhhLHLrrrLLLErSELLrrHHhHhhhHHhhHhhH...',
927      '...HhLHhHHLhhHLHrrrLrHrrrrLrrrHHHhhLhHHLhHLhL...',
928      '...LhHhhHHLLhLHHHLLHrrLLHLLrrLHHHhhHLHHLhHHhhH..',
929      '..HhLHhhHHHHhhHHHHHLrrLrLHrrrHHHLhLHHHHLhH.LhH..',
930      '..HhH.hhHHHHHhLHHLLrrrLrLLrrrLHHhLHHHHHLhH.HhH..',
931      '..HhH.hhHHHHHHLHHrrrrrrLHrrrLrrHHHHHHHHHhH..hH..',
932      '..HL..LhHHHHHHHHHrrrrrrHLLrrLrrHHHHHHHHHhH..LH..',
933      '..HL..HhHHHHHHHHLHLrrrrHrSSrHHHHHHHHHHHHhH..LH..',
934      '..HH..HhHH.HHHHHLHLHHLHHeeSHHHLLHHHHH.HHL...H...',
935      '...H...hHH.HHHHLLHLLHLLrRReHLLHLLHHHH.HLH...H...',
936      '...HH..HLH.HHHHLHLLLLLLrSSLHLLHLLHHH..HL...H....',
937      '........HH..HH.HHLLHLLLHLHLHLLLHH.HH.HHH........',
938      '.........HH.....SsrHLHHHHHHHLrss.....H..........',
939      '................SReeer....seeeRs................',
940      '...............HRRRRRH....LRRRRRH...............',
941      '...............HrSRRS......SRRSLH...............',
942      '..............HHHHHHH......HHHHHH...............',
943      '..............HHHHHHH......HHHHHHH..............',
944      '..............HHHHHH........HHHHHH..............',
945      '..............HHHHHH........HHHHHH..............',
946      '..............HHLLHH........HLHHHH..............',
947      '.............HLHHHHHH......HHHHHHLH.............',
948      '.............HLHHLLLH......HrLLHHLH.............',
949      '.............HLLLLLHLH....HLLLHLLLH.............',
950      '.............HHLLLLLLH....HLLLLLLHH.............',
951      '...............HHHHHH......HHHHHH...............',
952    ],
953    [
954      '..................HHHHHHHHHH....................',
955      '...............HHHLhhhhhhLLhLHH.................',
956      '.............HLhhhhLhhhhLLhhhhhLH...............',
957      '...........HHhhhhHHLhhhhhLHHLhhhhLH.............',
958      '..........HLhhhLHhhhhhhhhhhhHLhhLhLH............',
959      '.........HLhhhLLhhhhhhhhhhhhhHhhLLhLH...........',
960      '........HLhshhhhhhhhhhhhhhhhhhLhhLhhLH..........',
961      '........LhsVhLhhhhhhhhhhLhhhhhhLhsLhhH..........',
962      '.......HhsVsLhhhhLhhhhhhLhhhhhhLhVhLhhHHH.......',
963      '......HhhsVhLsVhHhhhhhhhHLhhLhhhLssHhHEEEH......',
964      '......HhhshhLhshHhhhhhhhHHhhLhhhHhhLhLEEEE......',
965      '......LhhhhLhhhLHhhVshhhHLhhhHhhLhhhLHEEHEH.....',
966      '.....HhhhhhHhhhLHhhssshhHSLhhHLhhLhhHLEEHEEH....',
967      '.....HhhhhhLhhhLLhhhhhLhLPshhLHhhHhhHLEEHEEE....',
968      '.....hLhhhhLhhLrrhhhhhLhhSeLhhHhhLhhLHEEEHEE....',
969      '....HhHhhhhLhLLSshhhhhLLhSRSLhrLhLhhLHEEEHEH....',
970      '....HhLhhhLhhLLeSLhLhhhHhSssLHHrhLhhhHHHHEH.....',
971      '....HhHhhhLhHhrSrHhHhhhHhSRRRrLsHLLhhHLEEEE.....',
972      '....LLHhhhLhHLreRrhHhhhHLsRRRRhSeHLhhHLEEEE.....',
973      '....hLHhhhLLLHeRReLHLhhLsrRRRRRSeSLhhHLLEEEH....',
974      '....hLHhhhLHeLeReRsHSLhLeLeSSSsrSeHhhHHHEEEE....',
975      '....hLHLhhLLRssssSesSSLhSRerHHHHLeHhhHHHEEEEH...',
976      '....hHHLhhhrrHHHHHrRRRSLrRrHHsssHLLhhrsHEEEEH...',
977      '....LHHLhhhLHsSHLWrRRRRReReHHrssSLLhhrRrH.HEE...',
978      '....HH.HLhhHHWSHHhhSRRRRRRWLhHhsSeHhhrRsH.HEH...',
979      '....HH.HhHhLsRehhHhsRRRRRRWrssseReLhhsRLL..H....',
980      '.....H.HhHhLSRWssssSRRRRRRRSssSRRshhLSSHL.......',
981      '.......HHHHhLRWesssRRReSSsRRRReeRrhhhrLhL.......',
982      '.......HLHHHHSseRRRRRsrPPsSRReeeRHhhLHhhL.......',
983      '.......HhLLLLLereeeRRSPPPSeRRRRRrHhhhHhhL.......',
984      '.......hhLLherSsRRRRRRsPPsRRResHHLhLhHhhL.......',
985      '......HhhLLhheSsrSRRRRRSeReSLHHHHLhHLHLhhH......',
986      '......HhhHLhHSreeHHHLrsPPsrHHHHHHhhHLHLhhH......',
987      '......HhhHHhhRSseHHrrsRSSeWLLHHHHhLLLHLhhL......',
988      '......hhhHHhLSSRsLrrrseLrSWrrrHHHhHhLHLhhhH.....',
989      '.....HhhhHLshHssSrLLrLEEEELrLrLHHhHhLHHhhhH.....',
990      '.....LhLLHhVLLsSrrLLLEEEEEELLrLHLLLshHHhLLhH....',
991      '....HhLhLHhshLHrrrrHHrHErErLrrHHhHhshHHhhHhL....',
992      '....LhHhHHhhhHHLrrrLLLErSErLrrHHhHhhhHHLhHhhH...',
993      '...HhLHhHHLhhHLHrrrLrHrrrrLrrrHHHhhLhHHLhLLhL...',
994      '...LhHLhHHLLhLHHHLLHrrLLHLLrrLHHHhhHLHHLhLHhhH..',
995      '..HhLHhhHHHHhhHHHHHLrrLrLHrrrHHHLhLHHHHLhH.HhH..',
996      '..HhH.hhHHHHHhLHHHLrrrLrLLrrrLHHhLHHHHHLhL.HhH..',
997      '..HhH.hhHHHHHHLHHrrrrrrLHrrrLLrHHHHHHHHHhL..LH..',
998      '..HL..LhHHHHHHHHHrrrrrrHLrrrLrrHHHHHHHHHhH..LH..',
999      '..HH..HhHHHHHHHHLHLrrrrHrSSrHHHHHHHHHHHHhH..LH..',
1000      '..HH..HhHH.HHHHHLHLHHLHHeeSHHLLLHHHHH.HHL...H...',
1001      '...H...hHH.HHHHLLHLLHLLrRReHLLHLLHHHH.HLH...H...',
1002      '...HH..HLH.HHHHLHLLLHLLrSeLHLLHLLHHH..HL...H....',
1003      '........HH..HH.HHLLHLLLHLHLHLLLHH.HH..HH........',
1004      '.........HH.....SsrHLHHHHHHHLrss.....H..........',
1005      '................SReeer....reeeRS................',
1006      '...............HRRRRRH....LRRRRRH...............',
1007      '...............HrSRRS......eRRSrH...............',
1008      '..............HHHHHHH......HHHHHH...............',
1009      '..............HHHHHHH......HHHHHHH..............',
1010      '..............HHHHHH........HHHHHH..............',
1011      '..............HHHHHH........HHHHHH..............',
1012      '..............HHLLHH........HLHHHH..............',
1013      '.............HLHHHHHH......HHHHHHLH.............',
1014      '.............HLHHLLLH......HrLLHHLH.............',
1015      '.............HLLLLLHLH....HLLLHLLLH.............',
1016      '.............HHLLLLLLH....HLLLLLLHH.............',
1017      '...............HHHHHH......HHHHHH...............',
1018    ],
1019  ],
1020  sleep: [
1021    [
1022      '...............HHHLLLLHHHHH.....................',
1023      '.............HHhhhhhhhhhhhhLH...................',
1024      '...........HLhhhhhhhLLhhhhhhhLH.................',
1025      '..........HhhhLHhhhhhhLHLhhhhhhH................',
1026      '.........HhhhLLhhhhhhhhhhHhhLhhhH...............',
1027      '........HhhhLhhhhhhhhhhhhhLhhhhhhH..............',
1028      '.......HhhhLhhhhhhhhhhhhhhhLhshhhLEHH...........',
1029      '......HhhhhhhhhhhhhhLhhhhhhhhssLhLEEEE..........',
1030      '......hhshhhhhhhhhhhLhhhLhhhLhhhLhEEEEE.........',
1031      '.....HhsshLhhhhhhhhhhLhhhLhhhLhhHHEEHEEE........',
1032      '.....hhVshhshLhhhhhhhHhhhhLhhLhhhHEEEHEEE.......',
1033      '....HhsVhLhVhLhhhhhhhHLhhhHhhhLhhHEEEHEEE.......',
1034      '....LhsshLhhhHhhsVshhLsLhhLHhhLhhLEEEEHEH.......',
1035      '...HhhshhLhhhHhhhshhLhSrhhhLLhLhhhHEEHEH........',
1036      '...HhhhhhLhhhLLhhhhhLhrRrhhLrhhLhhHLHEEEH.......',
1037      '...HhhhhhLhhhrLhhhhhHhhRRshhrLhLhhLLLEEEE.......',
1038      '...LhhhhhLhhhsrhhhhhhLLSsssHLerLhhhHLEEEEH......',
1039      '...hhhhhhLhhhSeLhLhhhHhSRRRersRrhhhHLEEEEE......',
1040      '..HhhhhhhLhLLeRhhHhhhHLhRRRRRRReLhhLsHEEEEH.....',
1041      '..HhhhhhhLhLLRSrLLHhhhsLeRRRRRRRhhhLerEEHEE.....',
1042      '..HhLhhhhLhHLSSRshrhhhreSRRRRRRsHhhLesEHHEE.....',
1043      '..HhHLhhhLhLHeRRRsLerhheReeeesLHLhhLeSHH.EH.....',
1044      '..HhHHhhhLLSLeRRRReSRSrsReHHHHLSshhLRrLH.H......',
1045      '..HhLHhhhhHeSSRRReeRRRRRRessSeeeshhLrLLL........',
1046      '...LH.LhhhHSRRRReeSRRRRRRRRRRePPrhhLHhhL........',
1047      '...LH.HhhhhsRRRRSLHeWRRRRRRReeeeLhhhHhhh........',
1048      '...HH.HhLhhLSrLHHLSWWWRRRRRRRRRSLhLhHLhh........',
1049      '....H..HhHhhLLLrSRRWWWReRRRRRerHLhLhHLhhH.......',
1050      '....H..HLHHhhSReeeRRRRRrSRRRSLHLLhLhHLhhH.......',
1051      '.......HHHHHHLePPeRRRRRRRRSLHHLHLhHhHLhhL.......',
1052      '.......LhLHhLHSRRRRRRReSSrsrSWWLLhHhHHhhh.......',
1053      '.......LhhLhhHHLrrrrrHrrsSWWWWRLhLLhLHhhhH......',
1054      '.......hhhHhhHHHHHLrrSRWWWWWWWRHhHhhhHhhhL......',
1055      '......HhhhHhhLHHHHLWWWWWWWWWWWRHhHhshHLhhh......',
1056      '......LhhLHshLHHHHrsWWWWWWWWWWeLhHhshHLhLhH.....',
1057      '.....HhhhHHshLHHHHrLSWWWWWSSsWWLHLhhhHLhhhh.....',
1058      '.....HhhhHHshLHHHHrrrRWSrseReSsLHLhLLHHhhLhH....',
1059      '.....hhLhHHshLLHHHLrLrLrrSSRRrLrLhhHLHHhhHhL....',
1060      '....HhLhhHHhhLLHHHLrrrrrrsSSrrrrrhhHHHHhhHLhH...',
1061      '...HhhHhLHHhhhHLHHHrrrrrrLrSSrrrLhHHHHHhhHHhL...',
1062      '...HhLHhLHHLLhLHHHLLrrrrLLeRSLrLHHHHHHHhhH.hhH..',
1063      '...LhHLhHHHHHhhHHHrLLrsSWerseHrerHHLLHHLhH.HhH..',
1064      '..HhL.LhHHHHHHhLHLrrLWWWWWWRSWWWSHHLHHHLhH.HhL..',
1065      '..HhH.LhLHHHHHHHHHrrrWWWWWWWWWWRRHHLHHHLhH..hL..',
1066      '..HhH.LhLHHHHHHHHLHLsWWWWWWWRReSsHHHHHHLh...hH..',
1067      '..Hh..HhLHHHLHHHLLLLeWWRResshLHLHHHHH.HLH...LH..',
1068      '..HL...hLHHHHHHHLHLLsSsrHHLHLLHLLHHHH.HhH..HH...',
1069      '...HH..HhH.HHHHHLHLLHLLHLLLHLLHLLHHH.HHL...H....',
1070      '....H...hHH.HH.HHHLLHLLHLLLHLLHH..H..HL....H....',
1071      '........HLH..H..HerrHLHHHHLHLrSL.....H..........',
1072      '..........H......RReePH..HeeeeRH................',
1073      '................rRRRRS....SRRRRL................',
1074      '................LeRRRs....SRRReL................',
1075      '...............HHHrssH....LssrLH................',
1076      '...............HHHHHH.....HHHHHHH...............',
1077      '...............HHHHHH......HHHHHH...............',
1078      '...............HHHHHH......HHHHHH...............',
1079      '...............HHHHH.......HHHHHH...............',
1080      '..............HHHLLHH......HLLHHH...............',
1081      '..............LHHHHHH.....HHHHHHLH..............',
1082      '..............LLHLLLHH....HrLLHHLH..............',
1083      '..............HLLHLHLH...HLLLHLLLH..............',
1084      '..............HHLLLLLH...HLLLLLLH...............',
1085      '...............HHHHHHH....HHHHHH................',
1086    ],
1087    [
1088      '................HHLLLLHHHHH.....................',
1089      '.............HHhhhhhhhhhhhhLH...................',
1090      '...........HLhhhhhhhLLhhhhhhhhH.................',
1091      '..........HhhhhLhhhhhhLHLhhhhhhH................',
1092      '.........LhhhLLhhhhhhhhhhHhhhhhhH...............',
1093      '........HhhhLhhhhhhhhhhhhhLhhhhhhH..............',
1094      '.......HhhhhhhhhhhhhhhhhhhhLhshhhLEEH...........',
1095      '......HhhhhhhhhhhhhhLhhhhhhhhssLhLEEEE..........',
1096      '......hhshhhhhhhhhhhLhhhhhhhLhhhLhEEEEE.........',
1097      '.....HhsshLhhhhhhhhhhLhhhLhhhLhhLLEEEEEE........',
1098      '.....hhsshhshLhhhhhhhHhhhhLhhLhhhHEEEHEEE.......',
1099      '....HhsVsLhshLhhhhhhhHLhhhHhhhLhhHEEEHEEE.......',
1100      '....LhsshLhhhHhhssshhLsLhhLHhhLhhLEEEEHEH.......',
1101      '...HhhshhLhhhHhhhshhLhSrhhhLLhhhhhHEEHEH........',
1102      '...HhhhhhLhhhLLhhhhhLhrRshhLrhhLhhLLHEEEH.......',
1103      '...HhhhhhLhhhrhhhhhhLhhRRshhrLhLhhLLLEEEE.......',
1104      '...LhhhhhLhhhsrhhhhhhLhSsssHLerLhhhHLEEEEH......',
1105      '...hhhhhhLhhhSeLhLhhhHhSRRRerSRrhhhHLEEEEE......',
1106      '..HhhhhhhLhLLeRhhHhhhHLhRRRRRRRRLhhLsHEEEEH.....',
1107      '..HhhhhhhLhLLRerLhHhhhsLeRRRRRRRrhhLerEEHEE.....',
1108      '..HhhhhhhLhHLSSRshrhhhreSRRRRRRsLhhLeSEHHEE.....',
1109      '..HhLLhhhLhLHeRRRsLerhheReeeesLHLhhLeSHH.EE.....',
1110      '..HhLHhhhhLsLeRRRReSRSrsReLHHHLSshhLRrLH.H......',
1111      '..HhLHhhhhHeSSRRWWBWRRRRRessSeReshhLrLLL........',
1112      '...LH.LhhhHSRRRRWWWBWRRRRRRRRePPrhhhHhhL........',
1113      '...LH.HhhhhsRRRWWWWBWRRRRRRReeeeLhhhHhhh........',
1114      '...HH.HhLhhLerLSBBBWWWRRRRRRRRRSLhLhHLhh........',
1115      '....H..HhHhhLLLsWBBBWWRSRRRRResHLhLhHLhhH.......',
1116      '....H..HLHHhhSReeRRRRRRrSRRRSLHHLhLhHLhhH.......',
1117      '.......HHHHHHLePPeRRRRReRRSLHHLHLhHhLLhhL.......',
1118      '.......LhLHhLHSeRRRRRReSSrsrSWWLLhHhHLhhh.......',
1119      '.......LhhLhhHHLrrrrrHsrsSWWWWRLhLLhLLhhhH......',
1120      '.......hhhHhhHHHHHLrrSRWWWWWWWRHhLhhhHhhhL......',
1121      '......HhhhHhhLHHHHLWWWWWWWWWWWWHhHhshHLhhh......',
1122      '......LhhLHshhHLLHrsWWWWWWWWWWRLhHhshHLhLhH.....',
1123      '......hhhHHshhHLHHrLSWWWWWSssWWhHLhhhHLhhhh.....',
1124      '.....HhhhHHssLHHHHrrrRWSrseReSSLHLhLLHHhhLhH....',
1125      '.....hhLhHHshLLHHHLrLrLrrSSRRrLrLhhHLHHhhHhh....',
1126      '....HhLhhHHhhLLHLHLrrrrrrrSSrrrrrhhHHLHhhHLhH...',
1127      '...HhhHhhHHhhhLLHHHrrrrrrLsSSrrrLhHHHLHhhHHhL...',
1128      '...HhLHhLHHLhhLHHHLLrrrrLLeRSrrLHHHHLLLhhH.hhH..',
1129      '...LhHLhLHHHHhhHHHrrHrsSWerseHrerHLLLHLLhH.HhH..',
1130      '..Hhh.LhHLHHHHhLHLrrLWWWWWWRSRWWSHLLLHLLhH.HhL..',
1131      '..HhH.LhLLLLHHHHHHrrrWWWWWWWWWWWRHHLHHHLhH..hL..',
1132      '..LhH.LhLHHHHHHHHLHLsWWWWWWWRRRSsHHLHHHLh...hH..',
1133      '..Hh..HhLHHHLHLHLLHLeWWWReSsrLHLHHHLH.HLH...LH..',
1134      '..HL...hhHHHLHHHLHLLrSsrLLLHLLHLLHHLH.HhH..HH...',
1135      '...HH..HhH.HLHHHLHLLHLLHLLLHLLHLLHHH.HHL...H....',
1136      '....H...hHH.HH.HHHLLHLLHLLLHLLHH..H..HL....H....',
1137      '........HLH..H..HerrHLHHHHLHLrSL.....H..........',
1138      '..........H......RRePPL..HePeeRH................',
1139      '................LRRRRS....SRRRRL................',
1140      '................LeRRRs....sRRReL................',
1141      '...............HHHrssL....LssrLH................',
1142      '...............HHHHHH.....HHHHHHH...............',
1143      '...............HHHHHH......HHHHHH...............',
1144      '...............HHHHHH......HHHHHH...............',
1145      '...............HHHHH.......HHHHHH...............',
1146      '..............HHHLLHH......HLLHHH...............',
1147      '..............LHLHHHH.....HHHHHHLH..............',
1148      '..............LLHLrLHH....HrLLHHLH..............',
1149      '..............HLLLLHLH...HLLLHLLLH..............',
1150      '..............HHLLLLLH...HLLLLLLH...............',
1151      '...............HHHHHHH....HHHHHH................',
1152    ],
1153  ],
1154} as const
1155
1156export type HdPose = keyof typeof SAKURA_HD_POSES
1157
types/index.d.ts 71 lines
1export type HeavensFeelMood =
2  | 'idle'
3  | 'thinking'
4  | 'editing'
5  | 'searching'
6  | 'running'
7  | 'summoning'
8  | 'ouch'
9  | 'done'
10  | 'sleeping'
11  | 'interrupted'
12
13/** The Masters of the Heaven's Feel war: the main session is one of them. */
14export type HeavensFeelMaster = 'sakura' | 'shirou' | 'rin' | 'illya' | 'kirei'
15
16/** The Servants: every subagent is summoned as one of them. */
17export type HeavensFeelServant =
18  | 'rider'
19  | 'saber'
20  | 'saber-alter'
21  | 'archer'
22  | 'lancer'
23  | 'caster'
24  | 'assassin'
25  | 'true-assassin'
26  | 'berserker'
27  | 'gilgamesh'
28
29export type HeavensFeelCharacter = HeavensFeelMaster | HeavensFeelServant
30
31export type HeavensFeelAgent = {
32  id: string
33  character: HeavensFeelServant
34  badge: number
35  type: string
36  description: string
37  isFork: boolean
38  mood: HeavensFeelMood
39  startedAt: number
40  endedAt?: number
41  isError: boolean
42  line: string
43  /** What it is doing right now, e.g. "reading register.tsx". */
44  activity: string
45  tools: number
46}
47
48/** What Sakura is doing right now, shown under her name. */
49export type HeavensFeelActivity = { icon: string; text: string; at: number }
50
51export type HeavensFeelTheme = 'light' | 'dark' | 'auto'
52
53export type HeavensFeelBubble = { text: string; at: number }
54
55declare module 'claude-code' {
56  interface PluginState {
57    'heavens-feel': {
58      mood: HeavensFeelMood
59      dark: boolean
60      ctx: number
61      agents: Record<string, HeavensFeelAgent>
62      bubble: HeavensFeelBubble
63      paneVisible: boolean
64      activity: HeavensFeelActivity
65      theme: HeavensFeelTheme
66      /** The subagent Sakura's room is showing, '' for her own view. */
67      viewing: string
68    }
69  }
70}
71