Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
3.7 kB · 133 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134import { getGlobalConfig } from '../utils/config.js'import { type Companion, type CompanionBones, EYES, HATS, RARITIES, RARITY_WEIGHTS, type Rarity, SPECIES, STAT_NAMES, type StatName,} from './types.js'
// Mulberry32 — tiny seeded PRNG, good enough for picking ducksfunction mulberry32(seed: number): () => number { let a = seed >>> 0 return function () { a |= 0 a = (a + 0x6d2b79f5) | 0 let t = Math.imul(a ^ (a >>> 15), 1 | a) t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t return ((t ^ (t >>> 14)) >>> 0) / 4294967296 }}
function hashString(s: string): number { if (typeof Bun !== 'undefined') { return Number(BigInt(Bun.hash(s)) & 0xffffffffn) } let h = 2166136261 for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i) h = Math.imul(h, 16777619) } return h >>> 0}
function pick<T>(rng: () => number, arr: readonly T[]): T { return arr[Math.floor(rng() * arr.length)]!}
function rollRarity(rng: () => number): Rarity { const total = Object.values(RARITY_WEIGHTS).reduce((a, b) => a + b, 0) let roll = rng() * total for (const rarity of RARITIES) { roll -= RARITY_WEIGHTS[rarity] if (roll < 0) return rarity } return 'common'}
const RARITY_FLOOR: Record<Rarity, number> = { common: 5, uncommon: 15, rare: 25, epic: 35, legendary: 50,}
// One peak stat, one dump stat, rest scattered. Rarity bumps the floor.function rollStats( rng: () => number, rarity: Rarity,): Record<StatName, number> { const floor = RARITY_FLOOR[rarity] const peak = pick(rng, STAT_NAMES) let dump = pick(rng, STAT_NAMES) while (dump === peak) dump = pick(rng, STAT_NAMES)
const stats = {} as Record<StatName, number> for (const name of STAT_NAMES) { if (name === peak) { stats[name] = Math.min(100, floor + 50 + Math.floor(rng() * 30)) } else if (name === dump) { stats[name] = Math.max(1, floor - 10 + Math.floor(rng() * 15)) } else { stats[name] = floor + Math.floor(rng() * 40) } } return stats}
const SALT = 'friend-2026-401'
export type Roll = { bones: CompanionBones inspirationSeed: number}
function rollFrom(rng: () => number): Roll { const rarity = rollRarity(rng) const bones: CompanionBones = { rarity, species: pick(rng, SPECIES), eye: pick(rng, EYES), hat: rarity === 'common' ? 'none' : pick(rng, HATS), shiny: rng() < 0.01, stats: rollStats(rng, rarity), } return { bones, inspirationSeed: Math.floor(rng() * 1e9) }}
// Called from three hot paths (500ms sprite tick, per-keystroke PromptInput,// per-turn observer) with the same userId → cache the deterministic result.let rollCache: { key: string; value: Roll } | undefinedexport function roll(userId: string): Roll { const key = userId + SALT if (rollCache?.key === key) return rollCache.value const value = rollFrom(mulberry32(hashString(key))) rollCache = { key, value } return value}
export function rollWithSeed(seed: string): Roll { return rollFrom(mulberry32(hashString(seed)))}
export function companionUserId(): string { const config = getGlobalConfig() 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.export function getCompanion(): Companion | undefined { const stored = getGlobalConfig().companion if (!stored) return undefined const { bones } = roll(companionUserId()) // bones last so stale bones fields in old-format configs get overridden return { ...stored, ...bones }}