| 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'; |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| 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() }; |
| } |
|
|
| |
| 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() }); |
| } |