import { create } from 'zustand' import { produce } from 'immer' import { INITIAL_CASH, INITIAL_FIELD_SIZE, FIELD_UPGRADE_COSTS, INITIAL_INVENTORY, CROPS, INITIAL_GAME_SPEED, ITEMS, UPGRADES, EQUIPMENT, TICK_INTERVAL, secondsToTicks, } from '../constants' import { GameState, PlotState, Equipment } from '../types' const initializeField = (size: number): PlotState[][] => { return Array(size) .fill(0) .map(() => Array(size) .fill(0) .map(() => ({ moisture: 0, fertility: 1, })), ) } const SAVE_SLOT_PREFIX = 'dionysian_idle_save_' export const hasSaveInSlot = (slot: number): boolean => { return !!localStorage.getItem(`${SAVE_SLOT_PREFIX}${slot}`) } const addConsoleMessage = (state: GameState, text: string) => (state.consoleMessages = [ { id: Math.random().toString(36).substring(7), text, timestamp: Date.now(), }, ...state.consoleMessages, ].slice(0, 50)) const progressRandomImmaturePlot = ( state: GameState, growthAmount: number, ): boolean => { // Find all immature plants const immaturePlots: { row: number; col: number }[] = [] state.plots.forEach((row, rowIndex) => { row.forEach((plot, colIndex) => { if (plot.current && !plot.current.mature) { immaturePlots.push({ row: rowIndex, col: colIndex }) } }) }) if (immaturePlots.length > 0) { // Select a random immature plot and progress it const randomPlot = immaturePlots[Math.floor(Math.random() * immaturePlots.length)] const plot = state.plots[randomPlot.row][randomPlot.col] const crop = CROPS[plot.current!.cropId] const growthTicks = secondsToTicks(crop.growthTimeSeconds) const actualGrowthAmount = growthTicks * growthAmount plot.current!.progress = Math.min( growthTicks, plot.current!.progress + actualGrowthAmount, ) return true } return false } export const useGameStore = create< GameState & { plant: (row: number, col: number, cropId: string) => void water: (row: number, col: number) => void harvest: (row: number, col: number) => void remove: (row: number, col: number) => void tick: () => void upgradeField: () => void setGameSpeed: (speed: number) => void buyItem: (itemId: string) => void sellItem: (itemId: string) => void purchaseUpgrade: (upgradeId: string) => void pray: () => void buyEquipment: (equipmentId: string) => void configureEquipment: (equipmentId: string, recipeId: string) => void useEquipment: (equipmentId: string) => void addCash: (amount: number) => void addConsoleMessage: (text: string) => void } >((set, get) => { // Expose store methods to window for the save system ;(window as any).gameStore = { getState: get, setState: set } return { cash: INITIAL_CASH, inventory: INITIAL_INVENTORY, fieldSize: INITIAL_FIELD_SIZE, maxFieldSize: INITIAL_FIELD_SIZE + FIELD_UPGRADE_COSTS.length, fieldUpgradeCosts: FIELD_UPGRADE_COSTS, plots: initializeField(INITIAL_FIELD_SIZE), gameSpeed: INITIAL_GAME_SPEED, stamina: 10, maxStamina: 10, tickCount: 0, consoleMessages: [], purchasedUpgrades: [], piety: 50, landPurchases: 0, equipment: {}, milestones: {}, plant: (row, col, cropId) => { const { plots, inventory, stamina } = get() if (stamina < 1) { return } const plot = plots[row][col] const crop = CROPS[cropId] const seedId = crop.seedType if (!plot.current && inventory[seedId] && inventory[seedId] > 0) { set( produce((state) => { state.plots[row][col].current = { cropId: cropId, progress: 0, mature: false, } state.inventory[seedId] = state.inventory[seedId] - 1 state.stamina = Math.max(0, state.stamina - 1) }), ) } }, water: (row, col) => { const { plots, stamina, purchasedUpgrades } = get() const plot = plots[row][col] if (plot.current && plot.moisture < 1) { // Calculate stamina cost reduction from upgrades (multiplicative) let staminaMultiplier = 1 if (purchasedUpgrades.includes('aqueous_vigor_1')) { staminaMultiplier *= 0.75 // 25% reduction } if (purchasedUpgrades.includes('aqueous_vigor_2')) { staminaMultiplier *= 0.75 // Additional 25% reduction } const staminaCost = staminaMultiplier if (stamina < staminaCost) { return } set( produce((state) => { state.plots[row][col].moisture = 1 state.stamina = Math.max(0, state.stamina - staminaCost) // Handle water diffusion to adjacent plots const diffusionAmount = purchasedUpgrades.includes('aqua_diffundere_2') ? 0.3 : purchasedUpgrades.includes('aqua_diffundere_1') ? 0.1 : 0 if (diffusionAmount > 0) { // Apply diffusion to adjacent plots const adjacentCoords = [ [row - 1, col], [row + 1, col], [row, col - 1], [row, col + 1], ] adjacentCoords.forEach(([r, c]) => { if ( r >= 0 && r < state.plots.length && c >= 0 && c < state.plots[0].length ) { state.plots[r][c].moisture = Math.min( 1, state.plots[r][c].moisture + diffusionAmount, ) } }) } }), ) } }, harvest: (row, col) => { const { plots, stamina } = get() if (stamina < 1) { return } const plot = plots[row][col] if (plot.current && plot.current.mature) { const crop = CROPS[plot.current.cropId] const yieldItem = crop.yieldType const yieldAmount = crop.yield set( produce((state) => { if (crop.isPerennial) { // For perennial crops, reset progress instead of clearing const regrowthTicks = secondsToTicks(crop.regrowthTimeSeconds!) state.plots[row][col].current = { cropId: plot.current!.cropId, progress: regrowthTicks, mature: false, } } else { // For regular crops, clear the plot state.plots[row][col].current = undefined } state.inventory[yieldItem] = (state.inventory[yieldItem] || 0) + yieldAmount state.plots[row][col].fertility = Math.max( 0, state.plots[row][col].fertility - crop.fertilityDepletion, ) state.stamina = Math.max(0, state.stamina - 1) // Track milestone for crop harvest const milestoneKey = `crops_harvested_${crop.id}` state.milestones[milestoneKey] = (state.milestones[milestoneKey] || 0) + yieldAmount }), ) } }, remove: (row, col) => { const { plots, stamina } = get() if (stamina < 1) { return } const plot = plots[row][col] if (plot.current) { set( produce((state) => { state.plots[row][col].current = undefined state.stamina = Math.max(0, state.stamina - 1) }), ) } }, tick: () => { set( produce((state) => { // Regenerate stamina state.stamina = Math.min( state.maxStamina, state.stamina + 0.5, ) // Process each plot state.plots.forEach((row: PlotState[], rowIndex: number) => { row.forEach((plot: PlotState, colIndex: number) => { // Regenerate fertility every 100 ticks if (state.tickCount % 100 === 0) { state.plots[rowIndex][colIndex].fertility = Math.min( 1, plot.fertility + 0.01, ) } if (plot.current && !plot.current.mature) { const crop = CROPS[plot.current.cropId] const growthTicks = secondsToTicks(crop.growthTimeSeconds) const waterNeeded = crop.waterPerSecond * (TICK_INTERVAL / 1000) // Only grow if fertility is above 0.2 if (plot.moisture >= waterNeeded && plot.fertility >= 0.2) { let growthRate = 1 // Half growth rate if fertility is between 0.2 and 0.5 if (plot.fertility < 0.5) { growthRate = 0.5 } const newProgress = plot.current.progress + growthRate const mature = newProgress >= growthTicks state.plots[rowIndex][colIndex].moisture = plot.moisture - waterNeeded state.plots[rowIndex][colIndex].current.progress = newProgress // If the plot just became mature, add a message if (mature && !plot.current.mature) { addConsoleMessage( state, `Plot (${rowIndex + 1},${colIndex + 1}) ${crop.name} is ready to harvest!`, ) } state.plots[rowIndex][colIndex].current.mature = mature } } }) }) // Update equipment progress Object.entries(state.equipment).forEach(([, equipment]) => { // TODO: this type cast shouldn't be necessary const equipmentInstance = equipment as Equipment if (equipmentInstance.isProcessing) { const recipe = EQUIPMENT[equipmentInstance.type].recipes.find( r => r.id === equipmentInstance.selectedRecipeId ) if (!recipe) return equipmentInstance.progress += 1 // Check if processing is complete if (equipmentInstance.progress >= recipe.processTicks) { equipmentInstance.isProcessing = false equipmentInstance.progress = 0 // Add output items state.inventory[recipe.outputItem] = (state.inventory[recipe.outputItem] || 0) + recipe.outputAmount addConsoleMessage( state, `${EQUIPMENT[equipmentInstance.type].name} produced ${recipe.outputAmount} ${ITEMS[recipe.outputItem].name}` ) } } }) state.tickCount = state.tickCount + 1 }), ) }, upgradeField: () => { set( produce((state) => { if (state.fieldSize >= state.maxFieldSize) { return } const upgradeIndex = state.fieldSize - INITIAL_FIELD_SIZE const cost = state.fieldUpgradeCosts[upgradeIndex] if (state.cash < cost) { return } const newSize = state.fieldSize + 1 state.cash = state.cash - cost state.fieldSize = newSize state.plots = initializeField(newSize) }), ) }, setGameSpeed: (speed) => { set( produce((state) => { state.gameSpeed = speed }), ) }, buyItem: (itemId) => { const { cash, landPurchases } = get() const item = ITEMS[itemId] if (!item || item.buyPrice === null || item.buyPrice === undefined) { return } // Calculate price for Additional Land based on number of purchases let price = item.buyPrice if (itemId === 'additional_land') { price = Math.floor(100 * Math.pow(1.5, landPurchases)) } if (cash < price) { return } set( produce((state) => { state.cash -= price // Special handling for Additional Land if (itemId === 'additional_land') { state.landPurchases += 1 if (state.fieldSize < state.maxFieldSize) { const newSize = state.fieldSize + 1 // Add a new row state.plots.push( Array(newSize) .fill(0) .map(() => ({ moisture: 0, fertility: 1, })), ) // Add a new column to each existing row state.plots.forEach((row: PlotState[]) => { row.push({ moisture: 0, fertility: 1, }) }) state.fieldSize = newSize addConsoleMessage(state, 'Field size increased!') } } else { state.inventory[itemId] = (state.inventory[itemId] || 0) + 1 } }), ) }, sellItem: (itemId) => { const { inventory } = get() const item = ITEMS[itemId] if ( !item || item.sellPrice === null || !inventory[itemId] || inventory[itemId] <= 0 ) { return } set( produce((state) => { state.cash += item.sellPrice state.inventory[itemId] -= 1 }), ) }, purchaseUpgrade: (upgradeId) => { const { inventory, purchasedUpgrades } = get() const upgrade = UPGRADES[upgradeId] if (!upgrade || purchasedUpgrades.includes(upgradeId)) { return } // Check if player can afford all costs const canAfford = upgrade.cost.every( (cost) => inventory[cost.itemId] !== undefined && inventory[cost.itemId] >= cost.amount, ) if (!canAfford) { return } set( produce((state) => { // Deduct all costs upgrade.cost.forEach((cost) => { state.inventory[cost.itemId] -= cost.amount }) state.purchasedUpgrades.push(upgradeId) addConsoleMessage(state, `Purchased upgrade: ${upgrade.name}`) // Handle stamina upgrades if (upgradeId === 'stamina_1') { state.maxStamina += 5 state.stamina = Math.min(state.maxStamina, state.stamina + 5) } else if (upgradeId === 'stamina_2') { state.maxStamina += 8 state.stamina = Math.min(state.maxStamina, state.stamina + 8) } else if (upgradeId === 'stamina_3') { state.maxStamina += 12 state.stamina = Math.min(state.maxStamina, state.stamina + 12) } else if (upgradeId === 'stamina_4') { state.maxStamina += 15 state.stamina = Math.min(state.maxStamina, state.stamina + 15) } }), ) }, pray: () => { const { stamina, piety } = get() if (stamina < 1) { return } set( produce((state) => { const roll = Math.random() const result = roll * piety if (result < 50 || roll < 0.3) { state.piety += 1 addConsoleMessage( state, 'Nothing happens, but you feel the approval of the gods', ) } else if (result > 100) { if (progressRandomImmaturePlot(state, 0.3)) { addConsoleMessage( state, 'The gods have bestowed a significant blessing on one of your crops', ) } } else if (result > 50) { if (progressRandomImmaturePlot(state, 0.1)) { addConsoleMessage( state, 'The gods have bestowed a minor blessing on one of your crops', ) } } state.stamina = Math.max(0, state.stamina - 1) }), ) }, buyEquipment: (equipmentId) => { const { cash, equipment } = get() const equipmentDef = EQUIPMENT[equipmentId] if (!equipmentDef || cash < equipmentDef.cost) { return } // Generate a unique ID for this instance of equipment const instanceId = `${equipmentId}_${Object.keys(equipment).length + 1}` set( produce((state) => { state.cash -= equipmentDef.cost state.equipment[instanceId] = { id: instanceId, type: equipmentId, progress: 0, isProcessing: false, } addConsoleMessage(state, `Purchased ${equipmentDef.name}`) }), ) }, configureEquipment: (equipmentId, recipeId) => { const { equipment } = get() const equipmentInstance = equipment[equipmentId] if (!equipmentInstance || equipmentInstance.isProcessing) { return } // Verify the recipe exists for this equipment const recipe = EQUIPMENT[equipmentInstance.type].recipes.find(r => r.id === recipeId) if (!recipe) { return } set( produce((state) => { state.equipment[equipmentId].selectedRecipeId = recipeId }), ) }, useEquipment: (equipmentId) => { const { equipment, inventory, stamina } = get() const equipmentInstance = equipment[equipmentId] if ( !equipmentInstance || equipmentInstance.isProcessing || stamina < 1 || !equipmentInstance.selectedRecipeId ) { return } const recipe = EQUIPMENT[equipmentInstance.type].recipes.find(r => r.id === equipmentInstance.selectedRecipeId) if (!recipe) { return } // Check if we have enough input items const inputCount = inventory[recipe.inputItem] || 0 if (inputCount < recipe.inputAmount) { return } set( produce((state) => { // Deduct input items state.inventory[recipe.inputItem] -= recipe.inputAmount // Start processing state.equipment[equipmentId].isProcessing = true state.equipment[equipmentId].progress = 0 state.stamina = Math.max(0, state.stamina - 1) }), ) }, addCash: (amount) => { set( produce((state) => { state.cash += amount }), ) }, addConsoleMessage: (text: string) => { addConsoleMessage(get(), text) }, } })