codev / src /commands /buddy /buddy.ts
chenbhao's picture
init: verspercode v0.1.0
064bfd6
Raw
History Blame
8.97 kB
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<Species, string[]> = {
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<T>(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<LocalCommandCall>[1],
reaction: string | undefined,
): void {
context.setAppState(prev =>
prev.companionReaction === reaction
? prev
: { ...prev, companionReaction: reaction },
)
}
function hatch(context: Parameters<LocalCommandCall>[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 <name>' }
}
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 <short personality line>',
}
}
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 <message>' }
}
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 <name>, /buddy bio <text>, /buddy mute, /buddy unmute, /buddy dismiss, /buddy react <text>, /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.',
}
}