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

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

It draws pixel art on the terminal and animated SVG in the Claude desktop app, matching the app's light or dark theme.
claude plugin marketplace add yedidyakfir/heavens-feel
claude plugin install heavens-feel@heavens-feel
You need a Claude Code version with mods (function hooks).
/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 |
| 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.
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.
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.
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.
hooks/register.tsx 526 lines1// 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}
526hooks/cast.ts 109 lines1// 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}`
109hooks/render.ts 224 lines1// 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}
224hooks/sprites.ts 1768 lines1// 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 lines1// 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}
57hooks/views.tsx 611 lines1// 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}
611hooks/voice.ts 392 lines1// 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}
392hooks/anim.ts 297 lines1// 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}
297hooks/sprites.generated.ts 1157 lines1// 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
1157types/index.d.ts 71 lines1export 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