import type { LocalCommandCall } from '../../types/command.js' import { companionUserId, getCompanion, roll } from '../../buddy/companion.js' import type { Species, StoredCompanion } from '../../buddy/types.js' import { RARITY_STARS } from '../../buddy/types.js' import { saveGlobalConfig } from '../../utils/config.js' const NAME_BANK: Record = { duck: ['Pebble', 'Puddle', 'Nib', 'Mochi'], goose: ['Brass', 'Comet', 'Honk', 'Marble'], blob: ['Gloop', 'Boba', 'Murmur', 'Pixel'], cat: ['Juniper', 'Miso', 'Static', 'Velvet'], dragon: ['Ember', 'Cinder', 'Rune', 'Flare'], octopus: ['Ink', 'Tangle', 'Coral', 'Nori'], owl: ['Sumi', 'Orbit', 'Mote', 'Aster'], penguin: ['Tux', 'Floe', 'Skipper', 'Chill'], turtle: ['Moss', 'Harbor', 'Slate', 'Ripple'], snail: ['Spiral', 'Dew', 'Button', 'Lilt'], ghost: ['Wisp', 'Echo', 'Pale', 'Veil'], axolotl: ['Bubble', 'Glim', 'Lotus', 'Sprig'], capybara: ['Loaf', 'Basil', 'Drift', 'Sunny'], cactus: ['Prickle', 'Agave', 'Dot', 'Needle'], robot: ['Servo', 'Hex', 'Patch', 'Relay'], rabbit: ['Thistle', 'Biscuit', 'Hopper', 'Fern'], mushroom: ['Spore', 'Truffle', 'Pip', 'Toad'], chonk: ['Boulder', 'Nugget', 'Mallow', 'Crumb'], } const PERSONALITY_BANK = [ 'calm, observant, and quietly approving', 'chaotic, affectionate, and slightly smug', 'earnest, curious, and proud of tiny victories', 'sleepy, loyal, and unexpectedly insightful', 'dramatic, playful, and obsessed with good refactors', ] const PET_LINES = [ 'leans into the petting and radiates approval.', 'does a tiny bounce and looks extremely pleased.', 'blinks slowly like this is now an official ritual.', 'makes a happy little noise only you can hear.', ] function pickFrom(items: readonly T[], seed: number): T { return items[seed % items.length]! } function normalizeName(name: string): string { const trimmed = name.trim().replace(/\s+/g, ' ') return trimmed.slice(0, 32) } function createSoul(customName?: string): StoredCompanion { const { bones, inspirationSeed } = roll(companionUserId()) return { name: customName ? normalizeName(customName) : pickFrom(NAME_BANK[bones.species], inspirationSeed), personality: pickFrom(PERSONALITY_BANK, Math.floor(inspirationSeed / 7)), hatchedAt: Date.now(), } } function persistCompanion(soul: StoredCompanion): void { saveGlobalConfig(current => ({ ...current, companion: soul, companionMuted: false, })) } function renderCompanionStatus(): string { const companion = getCompanion() if (!companion) { return 'No buddy yet. Run /buddy or /buddy hatch to summon one.' } const stats = Object.entries(companion.stats) .map(([name, value]) => `${name}:${value}`) .join(' ') return [ `${RARITY_STARS[companion.rarity]} ${companion.name} the ${companion.species}`, `eyes ${companion.eye} · hat ${companion.hat} · shiny ${companion.shiny ? 'yes' : 'no'}`, `personality: ${companion.personality}`, `stats: ${stats}`, ].join('\n') } function setReaction( context: Parameters[1], reaction: string | undefined, ): void { context.setAppState(prev => prev.companionReaction === reaction ? prev : { ...prev, companionReaction: reaction }, ) } function hatch(context: Parameters[1], customName?: string) { const soul = createSoul(customName) persistCompanion(soul) const companion = getCompanion() const reaction = companion ? `${companion.name} appears with a tiny ${companion.hat === 'none' ? 'flourish' : companion.hat}.` : undefined setReaction(context, reaction) return companion } function ensureCompanion(): string | undefined { if (!getCompanion()) { return 'No buddy yet. Run /buddy or /buddy hatch first.' } } export const call: LocalCommandCall = async (args, context) => { const raw = args.trim() const [action = '', ...rest] = raw.split(/\s+/) const normalizedAction = action.toLowerCase() const remainder = raw.slice(action.length).trim() if (normalizedAction === '' || normalizedAction === 'status') { if (!getCompanion()) { const companion = hatch(context) return { type: 'text', value: companion ? `Hatched ${companion.name}.\n${renderCompanionStatus()}` : 'Buddy hatched.', } } return { type: 'text', value: renderCompanionStatus() } } if (normalizedAction === 'hatch') { const customName = remainder const existing = getCompanion() if (existing) { return { type: 'text', value: `Buddy already hatched.\n${renderCompanionStatus()}`, } } const companion = hatch(context, customName || undefined) return { type: 'text', value: companion ? `Hatched ${companion.name}.\n${renderCompanionStatus()}` : 'Buddy hatched.', } } if (normalizedAction === 'pet') { const missing = ensureCompanion() if (missing) return { type: 'text', value: missing } const companion = getCompanion()! const petSeed = Date.now() const line = pickFrom(PET_LINES, petSeed) context.setAppState(prev => ({ ...prev, companionPetAt: petSeed, companionReaction: `${companion.name} ${line}`, })) return { type: 'text', value: `${companion.name} ${line}`, } } if (normalizedAction === 'mute') { const missing = ensureCompanion() if (missing) return { type: 'text', value: missing } saveGlobalConfig(current => ({ ...current, companionMuted: true })) setReaction(context, undefined) return { type: 'text', value: 'Buddy muted.' } } if (normalizedAction === 'unmute') { const missing = ensureCompanion() if (missing) return { type: 'text', value: missing } saveGlobalConfig(current => ({ ...current, companionMuted: false })) return { type: 'text', value: 'Buddy unmuted.' } } if (normalizedAction === 'rename' || normalizedAction === 'name') { const missing = ensureCompanion() if (missing) return { type: 'text', value: missing } const nextName = normalizeName(remainder) if (!nextName) { return { type: 'text', value: 'Usage: /buddy rename ' } } saveGlobalConfig(current => ({ ...current, companion: current.companion ? { ...current.companion, name: nextName } : current.companion, })) setReaction(context, `${nextName} accepts the rename with theatrical dignity.`) return { type: 'text', value: `Buddy renamed to ${nextName}.` } } if ( normalizedAction === 'bio' || normalizedAction === 'persona' || normalizedAction === 'personality' ) { const missing = ensureCompanion() if (missing) return { type: 'text', value: missing } if (!remainder) { return { type: 'text', value: 'Usage: /buddy bio ', } } const personality = remainder.slice(0, 160) saveGlobalConfig(current => ({ ...current, companion: current.companion ? { ...current.companion, personality } : current.companion, })) const companion = getCompanion()! setReaction(context, `${companion.name} seems pleased with the new reputation.`) return { type: 'text', value: `Buddy personality updated to: ${personality}` } } if (normalizedAction === 'react' || normalizedAction === 'say') { const missing = ensureCompanion() if (missing) return { type: 'text', value: missing } if (!remainder) { return { type: 'text', value: 'Usage: /buddy react ' } } setReaction(context, remainder.slice(0, 120)) return { type: 'text', value: 'Buddy reaction queued.' } } if (normalizedAction === 'dismiss' || normalizedAction === 'hide') { setReaction(context, undefined) return { type: 'text', value: 'Buddy bubble dismissed.' } } if (normalizedAction === 'reset') { saveGlobalConfig(current => ({ ...current, companion: undefined, companionMuted: false, })) context.setAppState(prev => ({ ...prev, companionReaction: undefined, companionPetAt: undefined, })) return { type: 'text', value: 'Buddy reset. Run /buddy to hatch a fresh companion.', } } if (normalizedAction === 'help') { return { type: 'text', value: 'Usage: /buddy, /buddy status, /buddy hatch [name], /buddy pet, /buddy rename , /buddy bio , /buddy mute, /buddy unmute, /buddy dismiss, /buddy react , /buddy reset', } } if (rest.length === 0 && !getCompanion()) { const companion = hatch(context, raw) return { type: 'text', value: companion ? `Hatched ${companion.name}.\n${renderCompanionStatus()}` : 'Buddy hatched.', } } return { type: 'text', value: 'Unknown /buddy action. Use /buddy help for available actions.', } }