From 26f4a6120b4434d1d950cfd8009a981c09dfc317 Mon Sep 17 00:00:00 2001 From: Ryan Lanny Jenkins Date: Wed, 4 Jun 2025 20:40:05 -0500 Subject: [PATCH] Move save states to a browser console interface --- src/store/saves.ts | 59 +++ src/store/useGameStore.ts | 1005 ++++++++++++++++++------------------ src/store/useSaveSystem.ts | 42 +- 3 files changed, 565 insertions(+), 541 deletions(-) create mode 100644 src/store/saves.ts diff --git a/src/store/saves.ts b/src/store/saves.ts new file mode 100644 index 0000000..8d5136c --- /dev/null +++ b/src/store/saves.ts @@ -0,0 +1,59 @@ +import { GameState } from '../types' + +const SAVE_SLOT_PREFIX = 'dionysian_idle_save_' + +const saveGame = (slot: number, state: GameState) => { + try { + const saveData = JSON.stringify(state) + localStorage.setItem(`${SAVE_SLOT_PREFIX}${slot}`, saveData) + return true + } catch (error) { + console.error('Failed to save game:', error) + return false + } +} + +const loadGame = (slot: number): GameState | null => { + try { + const saveData = localStorage.getItem(`${SAVE_SLOT_PREFIX}${slot}`) + if (!saveData) return null + return JSON.parse(saveData) + } catch (error) { + console.error('Failed to load game:', error) + return null + } +} + +export const hasSaveInSlot = (slot: number): boolean => { + return !!localStorage.getItem(`${SAVE_SLOT_PREFIX}${slot}`) +} + +// Create the global saves object +declare global { + interface Window { + saves: { + save: (slot: number) => void + load: (slot: number) => void + hasSave: (slot: number) => boolean + } + } +} + +// Initialize the global saves object +window.saves = { + save: (slot: number) => { + const state = (window as any).gameStore?.getState() + if (state) { + saveGame(slot, state) + console.log(`Game saved to slot ${slot}`) + } + }, + load: (slot: number) => { + const savedState = loadGame(slot) + if (savedState && (window as any).gameStore?.setState) { + (window as any).gameStore.setState(savedState) + console.log(`Game loaded from slot ${slot}`) + } + }, + hasSave: hasSaveInSlot +} \ No newline at end of file diff --git a/src/store/useGameStore.ts b/src/store/useGameStore.ts index 3d0fbb9..e1d7285 100644 --- a/src/store/useGameStore.ts +++ b/src/store/useGameStore.ts @@ -109,8 +109,6 @@ export const useGameStore = create< setActionCooldown: (cooldown: number) => void buyItem: (itemId: string) => void sellItem: (itemId: string) => void - saveToSlot: (slot: number) => void - loadFromSlot: (slot: number) => void purchaseUpgrade: (upgradeId: string) => void pray: () => void buyEquipment: (equipmentId: string) => void @@ -118,560 +116,555 @@ export const useGameStore = create< useEquipment: (equipmentId: string) => void addCash: (amount: number) => void } ->((set, get) => ({ - 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, - actionCooldown: 0, - tickCount: 0, - consoleMessages: [], - purchasedUpgrades: [], - piety: 50, - landPurchases: 0, - equipment: {}, - milestones: {}, +>((set, get) => { + // Expose store methods to window for the save system + ;(window as any).gameStore = { + getState: get, + setState: set + } - assignCrop: (row, col, cropId) => { - set( - produce((state) => { - state.plots[row][col].intended = cropId - }), - ) - }, + 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, + actionCooldown: 0, + tickCount: 0, + consoleMessages: [], + purchasedUpgrades: [], + piety: 50, + landPurchases: 0, + equipment: {}, + milestones: {}, - plant: (row, col) => { - const { plots, inventory, actionCooldown } = get() + assignCrop: (row, col, cropId) => { + set( + produce((state) => { + state.plots[row][col].intended = cropId + }), + ) + }, - if (actionCooldown > 0) { - return - } + plant: (row, col) => { + const { plots, inventory, actionCooldown } = get() - const plot = plots[row][col] - if (plot.intended && !plot.current) { - const crop = CROPS[plot.intended] - const seedId = crop.seedType + if (actionCooldown > 0) { + return + } + + const plot = plots[row][col] + if (plot.intended && !plot.current) { + const crop = CROPS[plot.intended] + const seedId = crop.seedType + + if (inventory[seedId] && inventory[seedId] > 0) { + set( + produce((state) => { + state.plots[row][col].current = { + cropId: plot.intended!, + progress: 0, + mature: false, + } + + state.inventory[seedId] = state.inventory[seedId] - 1 + state.actionCooldown = COOLDOWN_DURATION + }), + ) + } + } + }, + + water: (row, col) => { + const { plots, actionCooldown, purchasedUpgrades } = get() + + if (actionCooldown > 0) { + return + } + + const plot = plots[row][col] + if (plot.current && plot.moisture < 1) { + // Calculate cooldown reduction from upgrades (multiplicative) + let cooldownMultiplier = 1 + if (purchasedUpgrades.includes('aqueous_vigor_1')) { + cooldownMultiplier *= 0.75 // 25% reduction + } + if (purchasedUpgrades.includes('aqueous_vigor_2')) { + cooldownMultiplier *= 0.75 // Additional 25% reduction + } + + const finalCooldown = Math.max(0, COOLDOWN_DURATION * cooldownMultiplier) - if (inventory[seedId] && inventory[seedId] > 0) { set( produce((state) => { - state.plots[row][col].current = { - cropId: plot.intended!, - progress: 0, - mature: false, + state.plots[row][col].moisture = 1 + state.actionCooldown = finalCooldown + + // 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) { + // Check and water adjacent plots (up, right, down, left) + const adjacentPositions = [ + [row - 1, col], // up + [row, col + 1], // right + [row + 1, col], // down + [row, col - 1], // left + ] + + adjacentPositions.forEach(([adjRow, adjCol]) => { + // Check if the adjacent position is within bounds + if ( + adjRow >= 0 && + adjRow < state.plots.length && + adjCol >= 0 && + adjCol < state.plots[0].length + ) { + const adjPlot = state.plots[adjRow][adjCol] + state.plots[adjRow][adjCol].moisture = Math.min( + 1, + adjPlot.moisture + diffusionAmount + ) + } + }) + } + }), + ) + } + }, + + harvest: (row, col) => { + const { plots, actionCooldown } = get() + + if (actionCooldown > 0) { + 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 + state.plots[row][col].current = { + cropId: plot.current!.cropId, + progress: crop.regrowthProgress!, + mature: false, + } + } else { + // For regular crops, clear the plot + state.plots[row][col].current = undefined } - state.inventory[seedId] = state.inventory[seedId] - 1 + state.inventory[yieldItem] = + (state.inventory[yieldItem] || 0) + yieldAmount + state.plots[row][col].fertility = Math.max( + 0, + state.plots[row][col].fertility - crop.fertilityDepletion, + ) + state.actionCooldown = COOLDOWN_DURATION + + // Track milestone for crop harvest + const milestoneKey = `crops_harvested_${crop.id}` + state.milestones[milestoneKey] = (state.milestones[milestoneKey] || 0) + yieldAmount + }), + ) + } + }, + + remove: (row, col) => { + const { plots, actionCooldown } = get() + + if (actionCooldown > 0) { + return + } + + const plot = plots[row][col] + if (plot.current) { + set( + produce((state) => { + state.plots[row][col].current = undefined state.actionCooldown = COOLDOWN_DURATION }), ) } - } - }, - - water: (row, col) => { - const { plots, actionCooldown, purchasedUpgrades } = get() - - if (actionCooldown > 0) { - return - } - - const plot = plots[row][col] - if (plot.current && plot.moisture < 1) { - // Calculate cooldown reduction from upgrades (multiplicative) - let cooldownMultiplier = 1 - if (purchasedUpgrades.includes('aqueous_vigor_1')) { - cooldownMultiplier *= 0.75 // 25% reduction - } - if (purchasedUpgrades.includes('aqueous_vigor_2')) { - cooldownMultiplier *= 0.75 // Additional 25% reduction - } - - const finalCooldown = Math.max(0, COOLDOWN_DURATION * cooldownMultiplier) + }, + tick: () => { set( produce((state) => { - state.plots[row][col].moisture = 1 - state.actionCooldown = finalCooldown - - // 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) { - // Check and water adjacent plots (up, right, down, left) - const adjacentPositions = [ - [row - 1, col], // up - [row, col + 1], // right - [row + 1, col], // down - [row, col - 1], // left - ] - - adjacentPositions.forEach(([adjRow, adjCol]) => { - // Check if the adjacent position is within bounds - if ( - adjRow >= 0 && - adjRow < state.plots.length && - adjCol >= 0 && - adjCol < state.plots[0].length - ) { - const adjPlot = state.plots[adjRow][adjCol] - state.plots[adjRow][adjCol].moisture = Math.min( + // Update plots + state.plots.forEach((row: PlotState[], rowIndex: number) => { + row.forEach((plot: PlotState, colIndex: number) => { + // Regenerate fertility every 100 ticks + if (state.tickCount % 100 === 0 && plot.fertility < 1) { + state.plots[rowIndex][colIndex].fertility = Math.min( 1, - adjPlot.moisture + diffusionAmount + plot.fertility + 0.1, ) } - }) - } - }), - ) - } - }, - harvest: (row, col) => { - const { plots, actionCooldown } = get() - - if (actionCooldown > 0) { - 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 - state.plots[row][col].current = { - cropId: plot.current!.cropId, - progress: crop.regrowthProgress!, - 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.actionCooldown = COOLDOWN_DURATION - - // Track milestone for crop harvest - const milestoneKey = `crops_harvested_${crop.id}` - state.milestones[milestoneKey] = (state.milestones[milestoneKey] || 0) + yieldAmount - }), - ) - } - }, - - remove: (row, col) => { - const { plots, actionCooldown } = get() - - if (actionCooldown > 0) { - return - } - - const plot = plots[row][col] - if (plot.current) { - set( - produce((state) => { - state.plots[row][col].current = undefined - state.actionCooldown = COOLDOWN_DURATION - }), - ) - } - }, - - tick: () => { - set( - produce((state) => { - // Update plots - state.plots.forEach((row: PlotState[], rowIndex: number) => { - row.forEach((plot: PlotState, colIndex: number) => { - // Regenerate fertility every 100 ticks - if (state.tickCount % 100 === 0 && plot.fertility < 1) { - state.plots[rowIndex][colIndex].fertility = Math.min( - 1, - plot.fertility + 0.1, - ) - } - - if (!plot.current || plot.current.mature) { - return - } - - const crop = CROPS[plot.current.cropId] - const waterNeeded = crop.waterPerTick - - // Check if water is running low (less than 25% of what's needed) - if (plot.moisture < waterNeeded * 0.25 && plot.moisture > 0) { - addConsoleMessage( - state, - `Plot (${rowIndex + 1},${colIndex + 1}) ${crop.name} is running low on water!`, - ) - } - - // 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 + if (!plot.current || plot.current.mature) { + return } - const newProgress = plot.current.progress + growthRate - const mature = newProgress >= crop.growthTicks + const crop = CROPS[plot.current.cropId] + const waterNeeded = crop.waterPerTick - 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) { + // Check if water is running low (less than 25% of what's needed) + if (plot.moisture < waterNeeded * 0.25 && plot.moisture > 0) { addConsoleMessage( state, - `Plot (${rowIndex + 1},${colIndex + 1}) ${crop.name} is ready to harvest!`, + `Plot (${rowIndex + 1},${colIndex + 1}) ${crop.name} is running low on water!`, ) } - state.plots[rowIndex][colIndex].current.mature = mature + // 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 >= crop.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}` + ) + } } }) - }) - // 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 + 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 + }), + ) + }, + + setActionCooldown: (cooldown) => { + set( + produce((state) => { + state.actionCooldown = cooldown + }), + ) + }, + + 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}`) + }), + ) + }, + + pray: () => { + const { actionCooldown, piety } = get() + + if (actionCooldown > 0) { + 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', ) - 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 - + } else if (result > 100) { + if (progressRandomImmaturePlot(state, 0.3)) { addConsoleMessage( state, - `${EQUIPMENT[equipmentInstance.type].name} produced ${recipe.outputAmount} ${ITEMS[recipe.outputItem].name}` + '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.tickCount = state.tickCount + 1 - }), - ) - }, + state.actionCooldown = 3000 + }), + ) + }, - upgradeField: () => { - set( - produce((state) => { - if (state.fieldSize >= state.maxFieldSize) { - return - } + buyEquipment: (equipmentId) => { + const { cash, equipment } = get() + const equipmentDef = EQUIPMENT[equipmentId] - const upgradeIndex = state.fieldSize - INITIAL_FIELD_SIZE - const cost = state.fieldUpgradeCosts[upgradeIndex] + if (!equipmentDef || cash < equipmentDef.cost) { + return + } - if (state.cash < cost) { - return - } + // Generate a unique ID for this instance of equipment + const instanceId = `${equipmentId}_${Object.keys(equipment).length + 1}` - 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 - }), - ) - }, - - setActionCooldown: (cooldown) => { - set( - produce((state) => { - state.actionCooldown = cooldown - }), - ) - }, - - 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!') + set( + produce((state) => { + state.cash -= equipmentDef.cost + state.equipment[instanceId] = { + id: instanceId, + type: equipmentId, + progress: 0, + isProcessing: false, } - } else { - state.inventory[itemId] = (state.inventory[itemId] || 0) + 1 - } - }), - ) - }, + addConsoleMessage(state, `Purchased ${equipmentDef.name}`) + }), + ) + }, - sellItem: (itemId) => { - const { inventory } = get() - const item = ITEMS[itemId] + configureEquipment: (equipmentId, recipeId) => { + const { equipment } = get() + const equipmentInstance = equipment[equipmentId] - if ( - !item || - item.sellPrice === null || - !inventory[itemId] || - inventory[itemId] <= 0 - ) { - return - } + if (!equipmentInstance || equipmentInstance.isProcessing) { + return + } - set( - produce((state) => { - state.cash += item.sellPrice - state.inventory[itemId] -= 1 - }), - ) - }, + // Verify the recipe exists for this equipment + const recipe = EQUIPMENT[equipmentInstance.type].recipes.find(r => r.id === recipeId) + if (!recipe) { + return + } - saveToSlot: (slot: number) => { - const state = get() - const { ...gameState } = state - saveGame(slot, gameState) - }, + set( + produce((state) => { + state.equipment[equipmentId].selectedRecipeId = recipeId + }), + ) + }, - loadFromSlot: (slot: number) => { - const savedState = loadGame(slot) - if (savedState) { - set(savedState) - } - }, + useEquipment: (equipmentId) => { + const { equipment, inventory, actionCooldown } = get() + const equipmentInstance = equipment[equipmentId] - purchaseUpgrade: (upgradeId) => { - const { inventory, purchasedUpgrades } = get() - const upgrade = UPGRADES[upgradeId] + if ( + !equipmentInstance || + equipmentInstance.isProcessing || + actionCooldown > 0 || + !equipmentInstance.selectedRecipeId + ) { + return + } - if (!upgrade || purchasedUpgrades.includes(upgradeId)) { - return - } + const recipe = EQUIPMENT[equipmentInstance.type].recipes.find(r => r.id === equipmentInstance.selectedRecipeId) + if (!recipe) { + return + } - // Check if player can afford all costs - const canAfford = upgrade.cost.every( - (cost) => - inventory[cost.itemId] !== undefined && - inventory[cost.itemId] >= cost.amount, - ) + // Check if we have enough input items + const inputCount = inventory[recipe.inputItem] || 0 + if (inputCount < recipe.inputAmount) { + return + } - if (!canAfford) { - 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.actionCooldown = recipe.cooldownDuration + }), + ) + }, - 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}`) - }), - ) - }, - - pray: () => { - const { actionCooldown, piety } = get() - - if (actionCooldown > 0) { - 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.actionCooldown = 3000 - }), - ) - }, - - 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, actionCooldown } = get() - const equipmentInstance = equipment[equipmentId] - - if ( - !equipmentInstance || - equipmentInstance.isProcessing || - actionCooldown > 0 || - !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.actionCooldown = recipe.cooldownDuration - }), - ) - }, - - addCash: (amount) => { - set( - produce((state) => { state.cash += amount }), - ) - }, -})) + addCash: (amount) => { + set( + produce((state) => { state.cash += amount }), + ) + }, + } +}) diff --git a/src/store/useSaveSystem.ts b/src/store/useSaveSystem.ts index fd86ef3..56a5858 100644 --- a/src/store/useSaveSystem.ts +++ b/src/store/useSaveSystem.ts @@ -1,51 +1,23 @@ import { useEffect } from 'react' -import { hasSaveInSlot, useGameStore } from './useGameStore' +import { useGameStore } from './useGameStore' +import './saves' // Import the saves module to initialize the global saves object export const useSaveSystem = () => { - const { saveToSlot, loadFromSlot } = useGameStore() - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'r' && e.ctrlKey) { - localStorage.clear() - } - - const slot = - { - '1': 1, - '2': 2, - '3': 3, - '!': 1, - '@': 2, - '#': 3, - }[e.key] ?? null - - if (slot !== null) { - if (e.shiftKey) { - saveToSlot(slot) - console.log(`Game saved to slot ${slot}`) - } else if (hasSaveInSlot(slot)) { - loadFromSlot(slot) - console.log(`Game loaded from slot ${slot}`) - } - } - } - - window.addEventListener('keydown', handleKeyDown) + // Auto-save every 30 seconds const interval = setInterval(() => { - saveToSlot(0) + window.saves.save(0) // Save to autosave slot }, 30000) - console.log('Initalized save system') + console.log('Initialized save system') return () => { - window.removeEventListener('keydown', handleKeyDown) clearInterval(interval) } - }, [saveToSlot, loadFromSlot]) + }, []) // When starting the game, load from the autosave slot useEffect(() => { - loadFromSlot(0) + window.saves.load(0) }, []) }