parent
83fb08926d
commit
0d3a6e2202
@ -0,0 +1,222 @@ |
||||
import type { ToolUseContext } from '../../Tool.js' |
||||
import type { |
||||
LocalJSXCommandContext, |
||||
LocalJSXCommandOnDone, |
||||
} from '../../types/command.js' |
||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js' |
||||
import { getCompanion, roll, rollWithSeed } from '../../buddy/companion.js' |
||||
import { renderSprite, renderFace } from '../../buddy/sprites.js' |
||||
import { RARITY_STARS, SPECIES } from '../../buddy/types.js' |
||||
|
||||
// Fair reroll: pick a random seed whose species has the fewest rolls.
|
||||
// Once all species reach the same count, the pool resets effectively.
|
||||
function fairRoll(): { seed: string; species: string } { |
||||
const config = getGlobalConfig() |
||||
const counts: Record<string, number> = { ...config.companionRollCounts } |
||||
|
||||
// Find the minimum roll count
|
||||
const minCount = Math.min(...SPECIES.map(s => counts[s] ?? 0)) |
||||
|
||||
// Candidates: species at the minimum count
|
||||
const candidates = SPECIES.filter(s => (counts[s] ?? 0) === minCount) |
||||
|
||||
// Pick a random candidate
|
||||
const target = candidates[Math.floor(Math.random() * candidates.length)]! |
||||
|
||||
// Generate random seeds until we hit the target species
|
||||
let seed: string |
||||
let result: ReturnType<typeof rollWithSeed> |
||||
let attempts = 0 |
||||
do { |
||||
seed = `reroll-${Date.now()}-${Math.random().toString(36).slice(2)}-${attempts}` |
||||
result = rollWithSeed(seed) |
||||
attempts++ |
||||
} while (result.bones.species !== target && attempts < 500) |
||||
|
||||
return { seed, species: result.bones.species } |
||||
} |
||||
|
||||
function renderCard(): string { |
||||
const companion = getCompanion() |
||||
if (!companion) return 'No companion hatched yet. Run /buddy to hatch one!' |
||||
|
||||
const sprite = renderSprite(companion) |
||||
const face = renderFace(companion) |
||||
const stars = RARITY_STARS[companion.rarity] |
||||
const shinyTag = companion.shiny ? ' ✨ SHINY' : '' |
||||
const statsLines = Object.entries(companion.stats) |
||||
.map(([k, v]) => { |
||||
const bar = '█'.repeat(Math.round(v / 5)) + '░'.repeat(20 - Math.round(v / 5)) |
||||
return ` ${k.padEnd(10)} ${bar} ${v}` |
||||
}) |
||||
.join('\n') |
||||
|
||||
// Show collection progress
|
||||
const config = getGlobalConfig() |
||||
const counts = config.companionRollCounts ?? {} |
||||
const seen = SPECIES.filter(s => (counts[s] ?? 0) > 0).length |
||||
const total = SPECIES.length |
||||
|
||||
return [ |
||||
`┌─── ${companion.name} ───┐`, |
||||
` Species: ${companion.species} ${stars}${shinyTag}`, |
||||
` Rarity: ${companion.rarity}`, |
||||
` Hat: ${companion.hat}`, |
||||
` Eyes: ${companion.eye}`, |
||||
` Face: ${face}`, |
||||
'', |
||||
...sprite.map(l => ` ${l}`), |
||||
'', |
||||
' Stats:', |
||||
statsLines, |
||||
'', |
||||
` Personality: ${companion.personality}`, |
||||
` Hatched: ${new Date(companion.hatchedAt).toLocaleDateString()}`, |
||||
` Collection: ${seen}/${total} species discovered`, |
||||
`└${'─'.repeat(companion.name.length + 6)}┘`, |
||||
].join('\n') |
||||
} |
||||
|
||||
function hatchWithSeed(seed: string) { |
||||
const { bones } = rollWithSeed(seed) |
||||
const name = `Buddy_${bones.species}` |
||||
const soul = { |
||||
name, |
||||
personality: `A cheerful ${bones.species} companion with a knack for ${bones.rarity === 'legendary' ? 'legendary wisdom' : 'coding adventures'}.`, |
||||
hatchedAt: Date.now(), |
||||
} |
||||
|
||||
// Update config: save companion, seed, and bump roll count
|
||||
saveGlobalConfig(cfg => { |
||||
const counts = { ...cfg.companionRollCounts } |
||||
counts[bones.species] = (counts[bones.species] ?? 0) + 1 |
||||
return { ...cfg, companion: soul, companionSeed: seed, companionRollCounts: counts } |
||||
}) |
||||
|
||||
return { ...soul, ...bones } |
||||
} |
||||
|
||||
export async function call( |
||||
onDone: LocalJSXCommandOnDone, |
||||
context: ToolUseContext & LocalJSXCommandContext, |
||||
args: string, |
||||
): Promise<null> { |
||||
const sub = args.trim().toLowerCase() |
||||
|
||||
// No companion yet — hatch one
|
||||
if (!sub || sub === 'hatch') { |
||||
const config = getGlobalConfig() |
||||
if (config.companion) { |
||||
onDone(renderCard(), { display: 'system' }) |
||||
return null |
||||
} |
||||
const { seed } = fairRoll() |
||||
const companion = hatchWithSeed(seed) |
||||
const sprite = renderSprite(companion) |
||||
const stars = RARITY_STARS[companion.rarity] |
||||
const shinyTag = companion.shiny ? ' ✨ SHINY!' : '' |
||||
onDone( |
||||
[ |
||||
'🥚 *crack* ...', |
||||
'', |
||||
...sprite.map(l => ` ${l}`), |
||||
'', |
||||
` A ${companion.rarity} ${companion.species} appeared! ${stars}${shinyTag}`, |
||||
` Name: ${companion.name}`, |
||||
'', |
||||
' Use /buddy card to see full stats.', |
||||
' Use /buddy rename <name> to rename.', |
||||
' Use /buddy pet to show some love!', |
||||
].join('\n'), |
||||
{ display: 'system' }, |
||||
) |
||||
return null |
||||
} |
||||
|
||||
if (sub === 'card' || sub === 'stats') { |
||||
onDone(renderCard(), { display: 'system' }) |
||||
return null |
||||
} |
||||
|
||||
if (sub === 'pet') { |
||||
const companion = getCompanion() |
||||
if (!companion) { |
||||
onDone('No companion to pet! Use /buddy to hatch one.', { display: 'system' }) |
||||
return null |
||||
} |
||||
context.setAppState(prev => ({ ...prev, companionPetAt: Date.now() })) |
||||
onDone(`You pet ${companion.name}! 💕`, { display: 'system' }) |
||||
return null |
||||
} |
||||
|
||||
if (sub === 'mute') { |
||||
const config = getGlobalConfig() |
||||
const newMuted = !config.companionMuted |
||||
saveGlobalConfig(cfg => ({ ...cfg, companionMuted: newMuted })) |
||||
onDone( |
||||
newMuted |
||||
? 'Companion muted. Use /buddy mute to unmute.' |
||||
: 'Companion unmuted!', |
||||
{ display: 'system' }, |
||||
) |
||||
return null |
||||
} |
||||
|
||||
if (sub.startsWith('rename ')) { |
||||
const newName = args.trim().slice('rename '.length).trim() |
||||
if (!newName) { |
||||
onDone('Usage: /buddy rename <new name>', { display: 'system' }) |
||||
return null |
||||
} |
||||
const config = getGlobalConfig() |
||||
if (!config.companion) { |
||||
onDone('No companion to rename! Use /buddy to hatch one.', { display: 'system' }) |
||||
return null |
||||
} |
||||
saveGlobalConfig(cfg => ({ |
||||
...cfg, |
||||
companion: { ...cfg.companion!, name: newName }, |
||||
})) |
||||
onDone(`Companion renamed to ${newName}!`, { display: 'system' }) |
||||
return null |
||||
} |
||||
|
||||
if (sub === 'reroll') { |
||||
const { seed, species } = fairRoll() |
||||
const companion = hatchWithSeed(seed) |
||||
const sprite = renderSprite(companion) |
||||
const stars = RARITY_STARS[companion.rarity] |
||||
const shinyTag = companion.shiny ? ' ✨ SHINY!' : '' |
||||
const config = getGlobalConfig() |
||||
const counts = config.companionRollCounts ?? {} |
||||
const seen = SPECIES.filter(s => (counts[s] ?? 0) > 0).length |
||||
onDone( |
||||
[ |
||||
'🔄 Rerolling...', |
||||
'', |
||||
...sprite.map(l => ` ${l}`), |
||||
'', |
||||
` A ${companion.rarity} ${companion.species} appeared! ${stars}${shinyTag}`, |
||||
` Name: ${companion.name}`, |
||||
` Collection: ${seen}/${SPECIES.length} species discovered`, |
||||
].join('\n'), |
||||
{ display: 'system' }, |
||||
) |
||||
return null |
||||
} |
||||
|
||||
onDone( |
||||
[ |
||||
'Usage: /buddy [subcommand]', |
||||
'', |
||||
' /buddy — Hatch or view your companion', |
||||
' /buddy card — Show companion card with stats', |
||||
' /buddy pet — Pet your companion 💕', |
||||
' /buddy mute — Toggle companion speech bubbles', |
||||
' /buddy rename — Rename your companion', |
||||
' /buddy reroll — Release and re-hatch (fair rotation)', |
||||
].join('\n'), |
||||
{ display: 'system' }, |
||||
) |
||||
return null |
||||
} |
||||
@ -0,0 +1,12 @@ |
||||
import type { Command } from '../../commands.js' |
||||
|
||||
const buddy = { |
||||
type: 'local-jsx', |
||||
name: 'buddy', |
||||
description: 'Interact with your companion', |
||||
immediate: true, |
||||
argumentHint: '<pet|mute|card|rename|reroll>', |
||||
load: () => import('./buddy.js'), |
||||
} satisfies Command |
||||
|
||||
export default buddy |
||||
Loading…
Reference in new issue