import starterProfiles from './starterProfiles.json'; import { generatePhenotype } from './phenotype'; import { newId } from './rng'; import type { Alleles, GameState, GrowthStage, SeedProfile, StarterProfile, Stabilities } from './types'; export function nowIso(): string { return new Date().toISOString(); } export function emptyGameState(): GameState { return { schema: 'weedsim.game_state/v1', seeds: [], selection: [], updatedAt: nowIso(), }; } export function canAttempt(seed: SeedProfile): boolean { return seed.attemptsUsed < seed.maxAttempts; } export function advanceGrowthStage(stage: GrowthStage): GrowthStage { if (stage === 'SEED') return 'SEEDLING'; if (stage === 'SEEDLING') return 'MATURE'; return 'MATURE'; } // Real-time growth: one in-game "day" of growTime = one wall-clock second. const GROW_MIN_SECONDS = 10; const GROW_MAX_SECONDS = 300; function growDurationMs(seed: SeedProfile): number { const seconds = Math.min(GROW_MAX_SECONDS, Math.max(GROW_MIN_SECONDS, Math.round(seed.growTime))); return seconds * 1000; } /** SEED -> SEEDLING with a wall-clock maturation deadline. No-op for other stages. */ export function beginGrowing(seed: SeedProfile, now = Date.now()): SeedProfile { if (seed.growthStage !== 'SEED') return seed; return { ...seed, growthStage: 'SEEDLING', maturesAt: new Date(now + growDurationMs(seed)).toISOString() }; } /** Flip seedlings whose timers have elapsed. Legacy seedlings without a deadline mature immediately. */ export function matureReadySeeds(state: GameState, now = Date.now()): GameState { let changed = false; const seeds = state.seeds.map((seed) => { if (seed.growthStage !== 'SEEDLING') return seed; const deadline = seed.maturesAt ? Date.parse(seed.maturesAt) : 0; if (!deadline || deadline <= now) { changed = true; return { ...seed, growthStage: 'MATURE' as GrowthStage, maturesAt: null }; } return seed; }); return changed ? { ...state, seeds, updatedAt: nowIso() } : state; } export function growSecondsLeft(seed: SeedProfile, now = Date.now()): number { if (seed.growthStage !== 'SEEDLING' || !seed.maturesAt) return 0; return Math.max(0, Math.ceil((Date.parse(seed.maturesAt) - now) / 1000)); } export function cloneAlleles(alleles: Alleles): Alleles { return { THC: [...alleles.THC], CBD: [...alleles.CBD], Yield: [...alleles.Yield], GrowTime: [...alleles.GrowTime], LeafColor: { Hue: [...alleles.LeafColor.Hue], Saturation: [...alleles.LeafColor.Saturation], Value: [...alleles.LeafColor.Value], }, BudColor: { Hue: [...alleles.BudColor.Hue], Saturation: [...alleles.BudColor.Saturation], Value: [...alleles.BudColor.Value], }, }; } export function cloneStabilities(stabilities: Stabilities): Stabilities { return { ...stabilities }; } function roundTrait(value: number): number { return Math.round(value * 10) / 10; } export function reexpressSeed(seed: SeedProfile): SeedProfile { const phenotype = generatePhenotype(seed.alleles, seed.stabilities); return { ...seed, thc: roundTrait(phenotype.thc), cbd: roundTrait(phenotype.cbd), yield: roundTrait(phenotype.yield), growTime: roundTrait(phenotype.growTime), budColor: phenotype.budColorRgb, leafColor: phenotype.leafColorRgb, budPalette: phenotype.budPaletteRgb, leafPalette: phenotype.leafPaletteRgb, budPattern: phenotype.budPattern, leafPattern: phenotype.leafPattern, }; } export function normalizeGameState(state: GameState): GameState { const validSelection = new Set(state.seeds.map((seed) => seed.seedId)); return { ...emptyGameState(), ...state, seeds: state.seeds.map(reexpressSeed), selection: state.selection.filter((seedId) => validSelection.has(seedId)), }; } function seedFromStarter(profile: StarterProfile): SeedProfile { return reexpressSeed({ seedId: newId(), strainName: profile.name, type: profile.type, alleles: cloneAlleles(profile.alleles), stabilities: cloneStabilities(profile.stabilities), thc: profile.traits_base.THC, cbd: profile.traits_base.CBD, yield: profile.traits_base.Yield, growTime: profile.traits_base.GrowTime, budColor: [0, 0, 0], leafColor: [0, 0, 0], budPalette: [], leafPalette: [], budPattern: 'solid', leafPattern: 'solid', baseImageName: profile.base_image_name, growthStage: 'SEED', lineage: [null, null], description: profile.description ?? '', quantity: 1, attemptsUsed: 0, maxAttempts: 2, isStarter: true, }); } export function createStarterSeeds(): SeedProfile[] { return (starterProfiles as StarterProfile[]).map(seedFromStarter); } export function withStarters(state: GameState): GameState { return normalizeGameState({ ...state, seeds: [...state.seeds, ...createStarterSeeds()], updatedAt: nowIso(), }); } export function findSeed(state: GameState, seedId: string): SeedProfile | undefined { return state.seeds.find((seed) => seed.seedId === seedId); } export function replaceSeeds(state: GameState, seeds: SeedProfile[]): GameState { return normalizeGameState({ ...state, seeds, updatedAt: nowIso() }); }