DionysianIdle/src/store/useGameStore.ts
2025-05-24 15:50:55 -05:00

464 lines
12 KiB
TypeScript

import { create } from 'zustand'
import { produce } from 'immer'
import {
INITIAL_CASH,
INITIAL_FIELD_SIZE,
FIELD_UPGRADE_COSTS,
INITIAL_INVENTORY,
CROPS,
INITIAL_GAME_SPEED,
COOLDOWN_DURATION,
MARKET_ITEMS,
UPGRADES,
} from '../constants'
import { GameState, PlotState } 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_'
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}`)
}
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 actualGrowthAmount = crop.growthTicks * growthAmount
plot.current!.progress = Math.min(
crop.growthTicks,
plot.current!.progress + actualGrowthAmount
)
return true
}
return false
}
export const useGameStore = create<
GameState & {
plant: (row: number, col: number) => void
water: (row: number, col: number) => void
harvest: (row: number, col: number) => void
tick: () => void
assignCrop: (row: number, col: number, cropId: string) => void
upgradeField: () => void
setGameSpeed: (speed: number) => void
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
}
>((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,
assignCrop: (row, col, cropId) => {
set(
produce((state) => {
state.plots[row][col].intended = cropId
}),
)
},
plant: (row, col) => {
const { plots, inventory, actionCooldown } = get()
if (actionCooldown > 0) {
return
}
const plot = plots[row][col]
if (plot.intended && !plot.current) {
const seedId = `${plot.intended}_seed`
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
)
set(
produce((state) => {
state.plots[row][col].moisture = 1
state.actionCooldown = finalCooldown
}),
)
}
},
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) => {
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
}),
)
}
},
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
}
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
}
})
})
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 } = get()
const item = MARKET_ITEMS[itemId]
if (!item || item.buyPrice === null || item.buyPrice === undefined) {
return
}
if (cash < item.buyPrice) {
return
}
set(
produce((state) => {
state.cash -= item.buyPrice!
state.inventory[itemId] = (state.inventory[itemId] || 0) + 1
}),
)
},
sellItem: (itemId) => {
const { inventory } = get()
const item = MARKET_ITEMS[itemId]
if (
!item ||
item.sellPrice === null ||
!inventory[itemId] ||
inventory[itemId] <= 0
) {
return
}
set(
produce((state) => {
state.cash += item.sellPrice
state.inventory[itemId] -= 1
}),
)
},
saveToSlot: (slot: number) => {
const state = get()
const {
plant,
water,
harvest,
tick,
assignCrop,
upgradeField,
setGameSpeed,
setActionCooldown,
buyItem,
sellItem,
saveToSlot,
loadFromSlot,
purchaseUpgrade,
pray,
...gameState
} = state
saveGame(slot, gameState)
},
loadFromSlot: (slot: number) => {
const savedState = loadGame(slot)
if (savedState) {
set(savedState)
}
},
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, plots } = 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
})
)
},
}))