weed-sim / src /shared /state.ts
Yufok1
Real-time growth: growTime days become wall-clock seconds
6212a21
Raw
History Blame Contribute Delete
5.25 kB
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() });
}