From 1a07c2505997475442aae8ed8927b9e9e8fb81d4 Mon Sep 17 00:00:00 2001 From: xiaomingbusi Date: Thu, 16 Apr 2026 08:35:52 +0800 Subject: [PATCH] feat: fix companion randomization - support 18 species with rarity system --- src/buddy/companion.ts | 31 +++++++++++++++++++++++++++---- src/buddy/types.ts | 11 +++++++---- src/commands/buddy/buddy.tsx | 7 +++++-- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/buddy/companion.ts b/src/buddy/companion.ts index 09c38386..52f3359f 100644 --- a/src/buddy/companion.ts +++ b/src/buddy/companion.ts @@ -7,6 +7,7 @@ import { RARITIES, RARITY_WEIGHTS, type Rarity, + type Species, SPECIES, STAT_NAMES, type StatName, @@ -121,13 +122,35 @@ export function companionUserId(): string { return config.oauthAccount?.accountUuid ?? config.userID ?? 'anon' } -// Regenerate bones from userId, merge with stored soul. Bones never persist -// so species renames and SPECIES-array edits can't break stored companions, -// and editing config.companion can't fake a rarity. +function storedAppearanceSeed(stored: { + appearanceSeed?: string + hatchedAt: number + name: string + personality: string +}): string { + return ( + stored.appearanceSeed ?? + `legacy:${stored.hatchedAt}:${stored.name}:${stored.personality}` + ) +} + +function applyStoredOverrides( + bones: CompanionBones, + stored: { speciesOverride?: Species }, +): CompanionBones { + return stored.speciesOverride + ? { ...bones, species: stored.speciesOverride } + : bones +} + +// Regenerate bones from the stored hatch seed, merge with the saved soul. +// Old configs without a seed get a deterministic legacy fallback so they stop +// changing every render without requiring users to re-hatch. export function getCompanion(): Companion | undefined { const stored = getGlobalConfig().companion if (!stored) return undefined - const { bones } = roll(companionUserId()) + const { bones: rolledBones } = rollWithSeed(storedAppearanceSeed(stored)) + const bones = applyStoredOverrides(rolledBones, stored) // bones last so stale bones fields in old-format configs get overridden return { ...stored, ...bones } } diff --git a/src/buddy/types.ts b/src/buddy/types.ts index 8f1c82aa..8f17b96f 100644 --- a/src/buddy/types.ts +++ b/src/buddy/types.ts @@ -118,10 +118,13 @@ export type Companion = CompanionBones & hatchedAt: number } -// What actually persists in config. Bones are regenerated from hash(userId) -// on every read so species renames don't break stored companions and users -// can't edit their way to a legendary. -export type StoredCompanion = CompanionSoul & { hatchedAt: number } +// What actually persists in config. The hatch seed locks in a companion's +// bones so the sprite/species stop changing between renders and restarts. +export type StoredCompanion = CompanionSoul & { + hatchedAt: number + appearanceSeed?: string + speciesOverride?: Species +} export const RARITY_WEIGHTS = { common: 60, diff --git a/src/commands/buddy/buddy.tsx b/src/commands/buddy/buddy.tsx index 56d7e9c9..235ce6c8 100644 --- a/src/commands/buddy/buddy.tsx +++ b/src/commands/buddy/buddy.tsx @@ -4,6 +4,7 @@ import type { LocalJSXCommandCall } from '../../types/command.js' import { getCompanion, roll, + rollWithSeed, companionUserId, } from '../../buddy/companion.js' import { renderSprite } from '../../buddy/sprites.js' @@ -73,8 +74,9 @@ function CompanionCard({ ) return } - // Hatch a new companion with a generated name - const { bones } = roll(companionUserId()) + // Hatch a new companion with a generated name and random seed + const appearanceSeed = `hatch:${Date.now()}:${Math.random().toString(36).slice(2)}` + const { bones } = rollWithSeed(appearanceSeed) const adjectives = [ 'Bright', 'Cozy', 'Swift', 'Calm', 'Wise', 'Bold', 'Fuzzy', 'Lucky', 'Snappy', 'Quirky', @@ -90,6 +92,7 @@ function CompanionCard({ name, personality: `A ${bones.rarity} ${bones.species} who loves debugging and hanging out.`, hatchedAt: Date.now(), + appearanceSeed, } saveGlobalConfig(c => ({ ...c, companion: soul })) onDone(