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