Merge pull request #59 from xiaomingbusi/pr/companion-randomization

feat: fix companion randomization - support 18 species with rarity sy…
This commit is contained in:
程序员阿江-Relakkes 2026-04-16 11:02:14 +08:00 committed by GitHub
commit 8f0e46ec5c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 39 additions and 10 deletions

View File

@ -7,6 +7,7 @@ import {
RARITIES, RARITIES,
RARITY_WEIGHTS, RARITY_WEIGHTS,
type Rarity, type Rarity,
type Species,
SPECIES, SPECIES,
STAT_NAMES, STAT_NAMES,
type StatName, type StatName,
@ -121,13 +122,35 @@ export function companionUserId(): string {
return config.oauthAccount?.accountUuid ?? config.userID ?? 'anon' return config.oauthAccount?.accountUuid ?? config.userID ?? 'anon'
} }
// Regenerate bones from userId, merge with stored soul. Bones never persist function storedAppearanceSeed(stored: {
// so species renames and SPECIES-array edits can't break stored companions, appearanceSeed?: string
// and editing config.companion can't fake a rarity. 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 { export function getCompanion(): Companion | undefined {
const stored = getGlobalConfig().companion const stored = getGlobalConfig().companion
if (!stored) return undefined 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 // bones last so stale bones fields in old-format configs get overridden
return { ...stored, ...bones } return { ...stored, ...bones }
} }

View File

@ -118,10 +118,13 @@ export type Companion = CompanionBones &
hatchedAt: number hatchedAt: number
} }
// What actually persists in config. Bones are regenerated from hash(userId) // What actually persists in config. The hatch seed locks in a companion's
// on every read so species renames don't break stored companions and users // bones so the sprite/species stop changing between renders and restarts.
// can't edit their way to a legendary. export type StoredCompanion = CompanionSoul & {
export type StoredCompanion = CompanionSoul & { hatchedAt: number } hatchedAt: number
appearanceSeed?: string
speciesOverride?: Species
}
export const RARITY_WEIGHTS = { export const RARITY_WEIGHTS = {
common: 60, common: 60,

View File

@ -4,6 +4,7 @@ import type { LocalJSXCommandCall } from '../../types/command.js'
import { import {
getCompanion, getCompanion,
roll, roll,
rollWithSeed,
companionUserId, companionUserId,
} from '../../buddy/companion.js' } from '../../buddy/companion.js'
import { renderSprite } from '../../buddy/sprites.js' import { renderSprite } from '../../buddy/sprites.js'
@ -73,8 +74,9 @@ function CompanionCard({
) )
return return
} }
// Hatch a new companion with a generated name // Hatch a new companion with a generated name and random seed
const { bones } = roll(companionUserId()) const appearanceSeed = `hatch:${Date.now()}:${Math.random().toString(36).slice(2)}`
const { bones } = rollWithSeed(appearanceSeed)
const adjectives = [ const adjectives = [
'Bright', 'Cozy', 'Swift', 'Calm', 'Wise', 'Bold', 'Bright', 'Cozy', 'Swift', 'Calm', 'Wise', 'Bold',
'Fuzzy', 'Lucky', 'Snappy', 'Quirky', 'Fuzzy', 'Lucky', 'Snappy', 'Quirky',
@ -90,6 +92,7 @@ function CompanionCard({
name, name,
personality: `A ${bones.rarity} ${bones.species} who loves debugging and hanging out.`, personality: `A ${bones.rarity} ${bones.species} who loves debugging and hanging out.`,
hatchedAt: Date.now(), hatchedAt: Date.now(),
appearanceSeed,
} }
saveGlobalConfig(c => ({ ...c, companion: soul })) saveGlobalConfig(c => ({ ...c, companion: soul }))
onDone( onDone(