346 lines
8.3 KiB
TypeScript
346 lines
8.3 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,
|
|
} 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)
|
|
)
|
|
|
|
|
|
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
|
|
}
|
|
>((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: [],
|
|
|
|
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 } = get()
|
|
|
|
if (actionCooldown > 0) {
|
|
return
|
|
}
|
|
|
|
const plot = plots[row][col]
|
|
if (plot.current && plot.moisture < 1) {
|
|
set(
|
|
produce((state) => {
|
|
state.plots[row][col].moisture = 1
|
|
state.actionCooldown = COOLDOWN_DURATION
|
|
}),
|
|
)
|
|
}
|
|
},
|
|
|
|
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,
|
|
...gameState
|
|
} = state
|
|
saveGame(slot, gameState)
|
|
},
|
|
|
|
loadFromSlot: (slot: number) => {
|
|
const savedState = loadGame(slot)
|
|
if (savedState) {
|
|
set(savedState)
|
|
}
|
|
},
|
|
}))
|