Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions core/frame-utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
export const HL = 8
// Sprite canvas geometry. Every frame is padded to HL rows and SW cols so
// layers compose cleanly and the runtime can rely on fixed bounds.
export const HL = 12
export const SW = 22

export const pad = (lines: string[], width: number) => {
export const pad = (lines: string[], width: number = SW) => {
const padded = lines.map(l => l.padEnd(width))
while (padded.length < HL) padded.push(" ".repeat(width))
return padded
}

// Pad with the body block sitting in a specific row band. Useful when a layer
// only paints a few rows (e.g. an aura band at the top, or just the eyes).
export const padAt = (lines: string[], topRow: number, width: number = SW) => {
const out: string[] = []
for (let r = 0; r < HL; r++) {
if (r >= topRow && r - topRow < lines.length) out.push(lines[r - topRow].padEnd(width))
else out.push(" ".repeat(width))
}
return out
}

export const mergeLayers = (layers: string[][]): string[] => {
if (layers.length === 0) return []
if (layers.length === 1) return layers[0]
Expand Down
253 changes: 189 additions & 64 deletions core/pets/cat.ts
Original file line number Diff line number Diff line change
@@ -1,132 +1,257 @@
import type { PetState, PetAnimations } from "../types"
import { pad } from "../frame-utils"
import type { PetAnimations, AnimLayer, PetState } from "../types"
import { pad, padAt, SW } from "../frame-utils"
import { auraFor, groundShadow } from "./effects"

export const catFrames: Record<PetState, string[][]> = {
idle: [
pad([" /\\_____/\\ "," / o o \\ ","( == ^ == )"," \\ '-' / "," (__) (__) "], 14),
pad([" /\\_____/\\ "," / - - \\ ","( == ^ == )"," \\ '-' / "," (__) (__) "], 14),
],
happy: [
pad([" /\\_____/\\ "," / ^ ^ \\ ","( == ω == )"," \\ '-' / "," (__) (__) "], 14),
pad([" /\\_____/\\ "," / ^ ^ \\ ","( == ω == )"," \\ '-' / "," | ♥ | "," (__) (__) "], 14),
],
sleeping: [
pad([" /\\_____/\\ "," / - - \\ ","( == z z )"," \\ '-' / "," (__) (__) "], 14),
pad([" /\\_____/\\ "," / - - \\ ","( == Z z )"," \\ '-' / "," (__) (__) "], 14),
],
eating: [
pad([" /\\_____/\\ "," / o o \\ ","( == ω == )"," \\ nom / "," (__) (__) "], 14),
pad([" /\\_____/\\ "," / ^ ^ \\ ","( == ω == )"," \\ nom / "," (__) (__) "], 14),
],
playing: [
pad([" /\\_____/\\ "," / ^ ^ \\"," ( == ω == ) "," \\ '-' / "," (__) (__) "], 14),
pad([" /\\_____/\\ "," / ^ ^ \\ ","( == ω == )"," \\ '-' / "," (__) (__) "], 14),
],
excited: [
pad([" /\\_____/\\ "," / ^ ^ \\ ","( == ω == )"," \\ '-' / "," | ♥ | "," (__) (__) "], 14),
pad([" /\\_____/\\ "," / ^ ^ \\ ","( == ω== )"," \\ '-' / "," (__) (__) "], 14),
],
sad: [
pad([" /\\_____/\\ "," / - - \\ ","( == T T )"," \\ '-' / "," (__) (__) "], 14),
pad([" /\\_____/\\ "," / - - \\ ","( == T T )"," \\ '-' / "," (__) (__) "," ;_; "], 14),
// Body rows 2-9 (aura sits in rows 0-1, ground shadow on row 11).
const body = (rows: string[]): string[] => padAt(rows, 2, SW)

// Cat body — `/\___/\` ears, a fuzzy round body, and two paw pairs at the
// bottom. The eyes and nose are kept blank so the eyes layer can overlay them.
const catBody = body([
" /\\_____/\\ ",
" ( ) ",
" | | ",
" | ω | ",
" | '---' | ",
" \\ / ",
" |_____| ",
" (__) (__) ",
])

const catEyes = (left: string, right: string): string[] =>
padAt([` | ${left} ${right} | `], 4, SW)

// A 2-cell tail that flicks left/right at the right edge of the body.
const tail = (frame: string[]): AnimLayer => ({
id: "tail",
steps: frame.map(f => ({ frame: padAt([f], 8, SW), duration: 700 })),
loop: true,
})

const idleEyesLayer: AnimLayer = {
id: "eyes",
steps: [
{ frame: catEyes("●", "●"), durationRange: [2500, 4500] },
{ frame: catEyes("-", "-"), duration: 120 },
{ frame: catEyes("·", "·"), duration: 80 },
{ frame: catEyes("-", "-"), duration: 120 },
],
loop: true,
}

const idleBodyLayer: AnimLayer = {
id: "body",
steps: [{ frame: catBody, duration: 4800 }],
loop: true,
}

const idleTailLayer: AnimLayer = tail([
" ╱╲ ",
" ╱ ",
])

// catFrames is the public still-image dictionary used by static renderers
// (statusline, OpenCode seed sprite). It mirrors the body layer at frame 0.
export const catFrames: Record<PetState, string[][]> = {
idle: [catBody],
happy: [body([
" /\\_____/\\ ",
" ( ^ ^ ) ",
" | ● ● | ",
" | ω | ",
" | \\___/ | ",
" \\ / ",
" |_____| ",
" (__) (__) ",
])],
sleeping: [body([
" /\\_____/\\ ",
" ( ─ ─ ) ",
" | - - | ",
" | ω | ",
" | ... | ",
" \\ / ",
" |_____| ",
" (__) (__) ",
])],
eating: [body([
" /\\_____/\\ ",
" ( o o ) ",
" | ● ● | ",
" | nom | ",
" | \\___/ | ",
" \\ / ",
" |_____| ",
" (__) (__) ",
])],
playing: [body([
" /\\___/\\ ",
" ( ^ ^ ) ",
" | ● ● | ",
" | ω | ",
" | \\__/ | ",
" \\ / ",
" |____| ",
" (_)( )(_) ",
])],
excited: [body([
" /\\_____/\\ ",
" ( ★ ★ ) ",
" | ● ● | ",
" | ω | ",
" | \\___/ | ",
" \\ / ",
" |_____| ",
" (__) (__) ",
])],
sad: [body([
" /\\_____/\\ ",
" ( ╥ ╥ ) ",
" | ● ● | ",
" | ω | ",
" | '---' | ",
" \\ / ",
" |_____| ",
" (__) (__) ",
])],
}

const happyBody = catFrames.happy[0]
const eatingBody = catFrames.eating[0]
const sadBody = catFrames.sad[0]
const sleepingBody = catFrames.sleeping[0]
const excitedBody = catFrames.excited[0]
const playingBody = catFrames.playing[0]

export const catAnim: PetAnimations = {
states: {
idle: [
{
id: "body",
steps: [{ frame: pad([" /\\_____/\\ ", " / \\ ", "( ^ == )", " \\ '-' / ", " (__) (__) "], 14), duration: 5000 }],
loop: true,
},
{
id: "eyes",
steps: [
{ frame: pad([" ", " / o o \\ ", " ", " ", " "], 14), durationRange: [2000, 4000] },
{ frame: pad([" ", " / - - \\ ", " ", " ", " "], 14), duration: 150 },
{ frame: pad([" ", " / · · \\ ", " ", " ", " "], 14), duration: 80 },
{ frame: pad([" ", " / - - \\ ", " ", " ", " "], 14), duration: 150 },
],
loop: true,
},
],
idle: [idleBodyLayer, idleEyesLayer, idleTailLayer, groundShadow(), auraFor("idle")!],
happy: [
{
id: "base",
steps: [
{ frame: pad([" /\\_____/\\ ", " / ^ ^ \\ ", "( == ω == )", " \\ '-' / ", " (__) (__) "], 14), durationRange: [1500, 3000] },
{ frame: pad([" /\\_____/\\ ", " / ^ ^ \\ ", "( == ω == )", " \\ '-' / ", " | ♥ | ", " (__) (__) "], 14), duration: 800 },
{ frame: happyBody, durationRange: [1200, 2400] },
{ frame: catFrames.excited[0], duration: 600 },
],
loop: true,
},
groundShadow(),
auraFor("happy")!,
],
sleeping: [
{
id: "base",
steps: [
{ frame: pad([" /\\_____/\\ ", " / - - \\ ", "( == z z )", " \\ '-' / ", " (__) (__) "], 14), durationRange: [2000, 3000] },
{ frame: pad([" /\\_____/\\ ", " / - - \\ ", "( == Z z )", " \\ '-' / ", " (__) (__) "], 14), duration: 1500 },
{ frame: sleepingBody, durationRange: [2400, 3600] },
{ frame: pad([
" ",
" ",
" /\\_____/\\ ",
" ( ─ ─ ) ",
" | - - | ",
" | ω | ",
" | ZZZ | ",
" \\ / ",
" |_____| ",
" (__) (__) ",
], SW), duration: 1800 },
],
loop: true,
},
groundShadow(),
auraFor("sleeping")!,
],
eating: [
{
id: "base",
steps: [
{ frame: pad([" /\\_____/\\ ", " / o o \\ ", "( == ω == )", " \\ nom / ", " (__) (__) "], 14), duration: 400 },
{ frame: pad([" /\\_____/\\ ", " / ^ ^ \\ ", "( == ω == )", " \\ nom / ", " (__) (__) "], 14), duration: 300 },
{ frame: eatingBody, duration: 360 },
{ frame: pad([
" ",
" ",
" /\\_____/\\ ",
" ( ^ ^ ) ",
" | ● ● | ",
" | NOM | ",
" | \\___/ | ",
" \\ / ",
" |_____| ",
" (__) (__) ",
], SW), duration: 280 },
],
loop: true,
},
groundShadow(),
auraFor("eating")!,
],
playing: [
{
id: "base",
steps: [
{ frame: pad([" /\\_____/\\ ", " / ^ ^ \\", " ( == ω == ) ", " \\ '-' / ", " (__) (__) "], 14), duration: 500 },
{ frame: pad([" /\\_____/\\ ", " / ^ ^ \\ ", "( == ω == )", " \\ '-' / ", " (__) (__) "], 14), duration: 500 },
{ frame: playingBody, duration: 420 },
{ frame: happyBody, duration: 420 },
],
loop: true,
},
groundShadow(),
auraFor("playing")!,
],
excited: [
{
id: "base",
steps: [
{ frame: pad([" /\\_____/\\ ", " / ^ ^ \\ ", "( == ω == )", " \\ '-' / ", " | ♥ | ", " (__) (__) "], 14), duration: 300 },
{ frame: pad([" /\\_____/\\ ", " / ^ ^ \\ ", "( == ω== )", " \\ '-' / ", " (__) (__) "], 14), duration: 300 },
{ frame: excitedBody, duration: 260 },
{ frame: happyBody, duration: 260 },
],
loop: true,
},
groundShadow(),
auraFor("excited")!,
],
sad: [
{
id: "base",
steps: [
{ frame: pad([" /\\_____/\\ ", " / - - \\ ", "( == T T )", " \\ '-' / ", " (__) (__) "], 14), durationRange: [4000, 6000] },
{ frame: pad([" /\\_____/\\ ", " / - - \\ ", "( == T T )", " \\ '-' / ", " (__) (__) ", " ;_; "], 14), duration: 2000 },
],
steps: [{ frame: sadBody, durationRange: [3000, 5000] }],
loop: true,
},
groundShadow(),
auraFor("sad")!,
],
},
transitions: [
{
from: "idle",
to: "happy",
steps: [
{ frame: pad([" /\\_____/\\ ", " / O O \\ ", "( == ! == )", " \\ '-' / ", " (__) (__) "], 14), duration: 200 },
{ frame: pad([" /\\_____/\\ ", " / ^ ^ \\ ", "( == ω == )", " \\ '-' / ", " (__) (__) "], 14), duration: 300 },
{ frame: body([
" /\\_____/\\ ",
" ( O O ) ",
" | ● ● | ",
" | ! | ",
" | '---' | ",
" \\ / ",
" |_____| ",
" (__) (__) ",
]), duration: 220 },
{ frame: happyBody, duration: 280 },
],
},
{
from: "happy",
to: "sad",
steps: [
{ frame: pad([" /\\_____/\\ ", " / - - \\ ", "( == ... == )", " \\ '-' / ", " (__) (__) "], 14), duration: 400 },
{ frame: body([
" /\\_____/\\ ",
" ( - - ) ",
" | ● ● | ",
" | ... | ",
" | '---' | ",
" \\ / ",
" |_____| ",
" (__) (__) ",
]), duration: 380 },
],
},
],
}

Loading
Loading