Compare commits

..

2 commits

Author SHA1 Message Date
Ryan Lanny Jenkins d75e9f3cbb Set up prettier. 2025-05-18 10:17:42 -05:00
Ryan Lanny Jenkins 39309aa9e7 Add a simple save system 2025-05-18 10:08:52 -05:00
16 changed files with 759 additions and 545 deletions

5
.prettierrc Normal file
View file

@ -0,0 +1,5 @@
{
"semi": false,
"singleQuote": true
}

17
package-lock.json generated
View file

@ -24,6 +24,7 @@
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"@wyw-in-js/vite": "^0.6.0", "@wyw-in-js/vite": "^0.6.0",
"prettier": "3.5.3",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "^6.0.1" "vite": "^6.0.1"
} }
@ -1628,6 +1629,22 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/prettier": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/react": { "node_modules/react": {
"version": "18.3.1", "version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",

View file

@ -7,7 +7,8 @@
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"type-check": "tsc --noEmit", "type-check": "tsc --noEmit",
"preview": "vite preview" "preview": "vite preview",
"lint": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
"@linaria/core": "^6.3.0", "@linaria/core": "^6.3.0",
@ -26,6 +27,7 @@
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"@wyw-in-js/vite": "^0.6.0", "@wyw-in-js/vite": "^0.6.0",
"prettier": "3.5.3",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "^6.0.1" "vite": "^6.0.1"
} }

View file

@ -1,25 +1,60 @@
import React from 'react'; import React, { useEffect } from 'react'
import { Tabs, TabsContent, TabsList, TabsTrigger } from './components/CustomTabs'; import {
import { useGameTick } from './hooks/useGameTick'; Tabs,
import Field from './components/Field'; TabsContent,
import Warehouse from './components/Warehouse'; TabsList,
import Market from './components/Market'; TabsTrigger,
import { ActionCooldown } from './components/ActionCooldown'; } from './components/CustomTabs'
import { useGameTick } from './hooks/useGameTick'
import Field from './components/Field'
import Warehouse from './components/Warehouse'
import Market from './components/Market'
import { ActionCooldown } from './components/ActionCooldown'
import { useGameStore } from './store/useGameStore'
import { hasSaveInSlot } from './utils/saveSystem'
const appContainerStyle: React.CSSProperties = { const appContainerStyle: React.CSSProperties = {
maxWidth: '1200px', maxWidth: '1200px',
margin: '0 auto', margin: '0 auto',
padding: '2rem' padding: '2rem',
}; }
const tabsListStyles: React.CSSProperties = { const tabsListStyles: React.CSSProperties = {
display: 'grid', display: 'grid',
width: '100%', width: '100%',
gridTemplateColumns: 'repeat(4, 1fr)' gridTemplateColumns: 'repeat(4, 1fr)',
}; }
function App() { function App() {
useGameTick(); useGameTick()
const { saveToSlot, loadFromSlot } = useGameStore()
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
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)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [saveToSlot, loadFromSlot])
return ( return (
<div> <div>
@ -30,7 +65,9 @@ function App() {
<TabsTrigger value="fields">Fields</TabsTrigger> <TabsTrigger value="fields">Fields</TabsTrigger>
<TabsTrigger value="warehouse">Warehouse</TabsTrigger> <TabsTrigger value="warehouse">Warehouse</TabsTrigger>
<TabsTrigger value="market">Market</TabsTrigger> <TabsTrigger value="market">Market</TabsTrigger>
<TabsTrigger value="temple" disabled>Temple</TabsTrigger> <TabsTrigger value="temple" disabled>
Temple
</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="fields"> <TabsContent value="fields">
<Field /> <Field />
@ -44,7 +81,7 @@ function App() {
</Tabs> </Tabs>
</div> </div>
</div> </div>
); )
} }
export default App export default App

View file

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react'
import { styled } from '@linaria/react'; import { styled } from '@linaria/react'
import { useGameStore } from '../store/useGameStore'; import { useGameStore } from '../store/useGameStore'
const CooldownContainer = styled.div` const CooldownContainer = styled.div`
position: relative; position: relative;
@ -9,51 +9,51 @@ const CooldownContainer = styled.div`
width: 100%; width: 100%;
background-color: #e5e7eb; background-color: #e5e7eb;
overflow: hidden; overflow: hidden;
`; `
const ProgressBar = styled.div<{ progress: string }>` const ProgressBar = styled.div<{ progress: string }>`
height: 100%; height: 100%;
background-color: #3b82f6; background-color: #3b82f6;
width: ${props => props.progress}; width: ${(props) => props.progress};
`; `
export const ActionCooldown = () => { export const ActionCooldown = () => {
const { actionCooldown, setActionCooldown } = useGameStore(); const { actionCooldown, setActionCooldown } = useGameStore()
const [progress, setProgress] = useState(0); const [progress, setProgress] = useState(0)
useEffect(() => { useEffect(() => {
if (actionCooldown <= 0) { if (actionCooldown <= 0) {
setProgress(0); setProgress(0)
return; return
} }
const startTime = Date.now(); const startTime = Date.now()
const duration = actionCooldown; const duration = actionCooldown
const updateProgress = () => { const updateProgress = () => {
const elapsed = Date.now() - startTime; const elapsed = Date.now() - startTime
const newProgress = Math.min(100, (elapsed / duration) * 100); const newProgress = Math.min(100, (elapsed / duration) * 100)
if (newProgress >= 100) { if (newProgress >= 100) {
setProgress(0); setProgress(0)
setActionCooldown(0); setActionCooldown(0)
return; return
} }
setProgress(newProgress); setProgress(newProgress)
requestAnimationFrame(updateProgress); requestAnimationFrame(updateProgress)
}; }
const animationFrame = requestAnimationFrame(updateProgress); const animationFrame = requestAnimationFrame(updateProgress)
return () => { return () => {
cancelAnimationFrame(animationFrame); cancelAnimationFrame(animationFrame)
}; }
}, [actionCooldown, setActionCooldown]); }, [actionCooldown, setActionCooldown])
return ( return (
<CooldownContainer> <CooldownContainer>
<ProgressBar progress={`${progress}%`} /> <ProgressBar progress={`${progress}%`} />
</CooldownContainer> </CooldownContainer>
); )
}; }

View file

@ -1,35 +1,35 @@
import React, { useState } from 'react'; import React, { useState } from 'react'
export interface TabProps { export interface TabProps {
value: string; value: string
children: React.ReactNode; children: React.ReactNode
} }
export interface TabsProps { export interface TabsProps {
defaultValue: string; defaultValue: string
children: React.ReactNode; children: React.ReactNode
} }
export interface TabsListProps { export interface TabsListProps {
children: React.ReactNode; children: React.ReactNode
style?: React.CSSProperties; style?: React.CSSProperties
} }
export interface TabsContentProps { export interface TabsContentProps {
value: string; value: string
children: React.ReactNode; children: React.ReactNode
} }
export interface TabsTriggerProps { export interface TabsTriggerProps {
value: string; value: string
children: React.ReactNode; children: React.ReactNode
disabled?: boolean; disabled?: boolean
onClick?: () => void; onClick?: () => void
} }
const tabsContainerStyle: React.CSSProperties = { const tabsContainerStyle: React.CSSProperties = {
width: '100%' width: '100%',
}; }
const tabsListContainerStyle: React.CSSProperties = { const tabsListContainerStyle: React.CSSProperties = {
display: 'inline-flex', display: 'inline-flex',
@ -39,10 +39,13 @@ const tabsListContainerStyle: React.CSSProperties = {
borderRadius: '0.5rem', borderRadius: '0.5rem',
backgroundColor: '#f4f4f5', backgroundColor: '#f4f4f5',
padding: '0.25rem', padding: '0.25rem',
color: '#71717a' color: '#71717a',
}; }
const getTabButtonStyle = (active: boolean, disabled: boolean): React.CSSProperties => ({ const getTabButtonStyle = (
active: boolean,
disabled: boolean,
): React.CSSProperties => ({
display: 'inline-flex', display: 'inline-flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
@ -57,68 +60,71 @@ const getTabButtonStyle = (active: boolean, disabled: boolean): React.CSSPropert
boxShadow: active ? '0 1px 3px rgba(0, 0, 0, 0.1)' : 'none', boxShadow: active ? '0 1px 3px rgba(0, 0, 0, 0.1)' : 'none',
opacity: disabled ? 0.5 : 1, opacity: disabled ? 0.5 : 1,
cursor: disabled ? 'not-allowed' : 'pointer', cursor: disabled ? 'not-allowed' : 'pointer',
border: 'none' border: 'none',
}); })
const tabContentStyle: React.CSSProperties = { const tabContentStyle: React.CSSProperties = {
marginTop: '0.5rem', marginTop: '0.5rem',
outline: 'none' outline: 'none',
}; }
const TabsContext = React.createContext<{ const TabsContext = React.createContext<{
value: string; value: string
setValue: (value: string) => void; setValue: (value: string) => void
}>({ }>({
value: '', value: '',
setValue: () => {}, setValue: () => {},
}); })
export const Tabs: React.FC<TabsProps> = ({ defaultValue, children }) => { export const Tabs: React.FC<TabsProps> = ({ defaultValue, children }) => {
const [value, setValue] = useState(defaultValue); const [value, setValue] = useState(defaultValue)
return ( return (
<TabsContext.Provider value={{ value, setValue }}> <TabsContext.Provider value={{ value, setValue }}>
<div style={tabsContainerStyle}>{children}</div> <div style={tabsContainerStyle}>{children}</div>
</TabsContext.Provider> </TabsContext.Provider>
); )
}; }
export const TabsList: React.FC<TabsListProps> = ({ children, style }) => { export const TabsList: React.FC<TabsListProps> = ({ children, style }) => {
return <div style={{...tabsListContainerStyle, ...style}}>{children}</div>; return <div style={{ ...tabsListContainerStyle, ...style }}>{children}</div>
}; }
export const TabsTrigger: React.FC<TabsTriggerProps> = ({ export const TabsTrigger: React.FC<TabsTriggerProps> = ({
value, value,
children, children,
disabled = false, disabled = false,
onClick onClick,
}) => { }) => {
const { value: selectedValue, setValue } = React.useContext(TabsContext); const { value: selectedValue, setValue } = React.useContext(TabsContext)
const active = selectedValue === value; const active = selectedValue === value
const handleClick = () => { const handleClick = () => {
if (!disabled) { if (!disabled) {
setValue(value); setValue(value)
if (onClick) onClick(); if (onClick) onClick()
} }
}; }
return ( return (
<button <button
style={getTabButtonStyle(active, disabled)} style={getTabButtonStyle(active, disabled)}
disabled={disabled} disabled={disabled}
onClick={handleClick} onClick={handleClick}
type="button" type="button"
> >
{children} {children}
</button> </button>
); )
}; }
export const TabsContent: React.FC<TabsContentProps> = ({ value, children }) => { export const TabsContent: React.FC<TabsContentProps> = ({
const { value: selectedValue } = React.useContext(TabsContext); value,
children,
if (selectedValue !== value) return null; }) => {
const { value: selectedValue } = React.useContext(TabsContext)
return <div style={tabContentStyle}>{children}</div>;
}; if (selectedValue !== value) return null
return <div style={tabContentStyle}>{children}</div>
}

View file

@ -1,50 +1,50 @@
import React, { useState } from 'react'; import React, { useState } from 'react'
import { useGameStore } from '../store/useGameStore'; import { useGameStore } from '../store/useGameStore'
import { CROPS } from '../constants'; import { CROPS } from '../constants'
const fieldContainerStyle: React.CSSProperties = { const fieldContainerStyle: React.CSSProperties = {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
gap: '1rem', gap: '1rem',
padding: '1rem' padding: '1rem',
}; }
const titleStyle: React.CSSProperties = { const titleStyle: React.CSSProperties = {
fontSize: '1.5rem', fontSize: '1.5rem',
fontWeight: 'bold', fontWeight: 'bold',
marginBottom: '1rem', marginBottom: '1rem',
textAlign: 'center' textAlign: 'center',
}; }
const cropSelectionContainerStyle: React.CSSProperties = { const cropSelectionContainerStyle: React.CSSProperties = {
display: 'flex', display: 'flex',
flexWrap: 'wrap', flexWrap: 'wrap',
gap: '0.5rem', gap: '0.5rem',
marginBottom: '1rem', marginBottom: '1rem',
justifyContent: 'center' justifyContent: 'center',
}; }
const cropSelectionLabelStyle: React.CSSProperties = { const cropSelectionLabelStyle: React.CSSProperties = {
width: '100%', width: '100%',
textAlign: 'center', textAlign: 'center',
fontWeight: 500, fontWeight: 500,
marginBottom: '0.5rem' marginBottom: '0.5rem',
}; }
const getCropButtonStyle = (active: boolean): React.CSSProperties => ({ const getCropButtonStyle = (active: boolean): React.CSSProperties => ({
padding: '0.5rem 1rem', padding: '0.5rem 1rem',
borderRadius: '0.25rem', borderRadius: '0.25rem',
border: '1px solid #ccc', border: '1px solid #ccc',
backgroundColor: active ? '#4ade80' : '#f3f4f6', backgroundColor: active ? '#4ade80' : '#f3f4f6',
cursor: 'pointer' cursor: 'pointer',
}); })
const actionsContainerStyle: React.CSSProperties = { const actionsContainerStyle: React.CSSProperties = {
display: 'flex', display: 'flex',
gap: '0.5rem', gap: '0.5rem',
marginBottom: '1rem', marginBottom: '1rem',
justifyContent: 'center' justifyContent: 'center',
}; }
const speedSelectorContainerStyle: React.CSSProperties = { const speedSelectorContainerStyle: React.CSSProperties = {
display: 'flex', display: 'flex',
@ -54,26 +54,26 @@ const speedSelectorContainerStyle: React.CSSProperties = {
padding: '0.5rem', padding: '0.5rem',
backgroundColor: '#f9fafb', backgroundColor: '#f9fafb',
borderRadius: '0.5rem', borderRadius: '0.5rem',
border: '1px solid #e5e7eb' border: '1px solid #e5e7eb',
}; }
const speedSelectorLabelStyle: React.CSSProperties = { const speedSelectorLabelStyle: React.CSSProperties = {
fontWeight: 500, fontWeight: 500,
marginBottom: '0.5rem' marginBottom: '0.5rem',
}; }
const speedButtonsContainerStyle: React.CSSProperties = { const speedButtonsContainerStyle: React.CSSProperties = {
display: 'flex', display: 'flex',
gap: '0.5rem' gap: '0.5rem',
}; }
const getSpeedButtonStyle = (active: boolean): React.CSSProperties => ({ const getSpeedButtonStyle = (active: boolean): React.CSSProperties => ({
padding: '0.5rem 1rem', padding: '0.5rem 1rem',
borderRadius: '0.25rem', borderRadius: '0.25rem',
border: '1px solid #ccc', border: '1px solid #ccc',
backgroundColor: active ? '#4ade80' : '#f3f4f6', backgroundColor: active ? '#4ade80' : '#f3f4f6',
cursor: 'pointer' cursor: 'pointer',
}); })
const getActionButtonStyle = (disabled: boolean): React.CSSProperties => ({ const getActionButtonStyle = (disabled: boolean): React.CSSProperties => ({
padding: '0.5rem 1rem', padding: '0.5rem 1rem',
@ -81,8 +81,8 @@ const getActionButtonStyle = (disabled: boolean): React.CSSProperties => ({
fontWeight: 'bold', fontWeight: 'bold',
color: 'white', color: 'white',
backgroundColor: disabled ? '#9ca3af' : '#22c55e', backgroundColor: disabled ? '#9ca3af' : '#22c55e',
cursor: disabled ? 'not-allowed' : 'pointer' cursor: disabled ? 'not-allowed' : 'pointer',
}); })
const getFieldGridStyle = (size: number): React.CSSProperties => ({ const getFieldGridStyle = (size: number): React.CSSProperties => ({
display: 'grid', display: 'grid',
@ -91,8 +91,8 @@ const getFieldGridStyle = (size: number): React.CSSProperties => ({
gap: '0.5rem', gap: '0.5rem',
width: '100%', width: '100%',
maxWidth: '600px', maxWidth: '600px',
margin: '0 auto' margin: '0 auto',
}); })
const getPlotStyle = (bgColor: string): React.CSSProperties => ({ const getPlotStyle = (bgColor: string): React.CSSProperties => ({
border: '1px solid #78350f', border: '1px solid #78350f',
@ -103,8 +103,8 @@ const getPlotStyle = (bgColor: string): React.CSSProperties => ({
justifyContent: 'center', justifyContent: 'center',
position: 'relative', position: 'relative',
cursor: 'pointer', cursor: 'pointer',
backgroundColor: bgColor backgroundColor: bgColor,
}); })
const getMoistureIndicatorStyle = (level: number): React.CSSProperties => ({ const getMoistureIndicatorStyle = (level: number): React.CSSProperties => ({
position: 'absolute', position: 'absolute',
@ -114,69 +114,72 @@ const getMoistureIndicatorStyle = (level: number): React.CSSProperties => ({
height: `${Math.min(level * 100, 100)}%`, height: `${Math.min(level * 100, 100)}%`,
backgroundColor: 'rgba(147, 197, 253, 0.3)', backgroundColor: 'rgba(147, 197, 253, 0.3)',
transition: 'height 0.3s', transition: 'height 0.3s',
zIndex: 1 zIndex: 1,
}); })
const getMaturityProgressContainerStyle = (): React.CSSProperties => ({ const getMaturityProgressContainerStyle = (): React.CSSProperties => ({
position: 'absolute', position: 'absolute',
bottom: '10px', // Moved up from the bottom to separate from moisture indicator bottom: '10px', // Moved up from the bottom to separate from moisture indicator
left: 0, left: 0,
width: '100%', width: '100%',
height: '10px', // Increased height for better visibility height: '10px', // Increased height for better visibility
backgroundColor: 'rgba(0, 0, 0, 0.3)', // Darker background for better contrast backgroundColor: 'rgba(0, 0, 0, 0.3)', // Darker background for better contrast
zIndex: 5, // Increased z-index to ensure it's above other elements zIndex: 5, // Increased z-index to ensure it's above other elements
border: '1px solid #000' // Added border for better visibility border: '1px solid #000', // Added border for better visibility
}); })
const getMaturityProgressBarStyle = (progress: number, total: number): React.CSSProperties => ({ const getMaturityProgressBarStyle = (
progress: number,
total: number,
): React.CSSProperties => ({
position: 'absolute', position: 'absolute',
bottom: 0, bottom: 0,
left: 0, left: 0,
width: `${Math.min((progress / total) * 100, 100)}%`, width: `${Math.min((progress / total) * 100, 100)}%`,
height: '10px', // Increased height to match container height: '10px', // Increased height to match container
backgroundColor: '#ff5722', // Brighter orange color for better visibility backgroundColor: '#ff5722', // Brighter orange color for better visibility
transition: 'width 0.3s', transition: 'width 0.3s',
zIndex: 6, // Increased z-index to ensure it's above the container zIndex: 6, // Increased z-index to ensure it's above the container
boxShadow: '0 0 3px rgba(0, 0, 0, 0.5)' // Added shadow for better visibility boxShadow: '0 0 3px rgba(0, 0, 0, 0.5)', // Added shadow for better visibility
}); })
const FieldComponent: React.FC = () => { const FieldComponent: React.FC = () => {
const { const {
plots, plots,
fieldSize, fieldSize,
plant, plant,
water, water,
harvest, harvest,
assignCrop, assignCrop,
gameSpeed, gameSpeed,
setGameSpeed, setGameSpeed,
actionCooldown actionCooldown,
} = useGameStore(); } = useGameStore()
const [selectedCrop, setSelectedCrop] = useState<string | null>(null); const [selectedCrop, setSelectedCrop] = useState<string | null>(null)
const handlePlotClick = (row: number, col: number) => { const handlePlotClick = (row: number, col: number) => {
if (selectedCrop) { if (selectedCrop) {
assignCrop(row, col, selectedCrop); assignCrop(row, col, selectedCrop)
} }
}; }
const getBgColor = (hasCrop: boolean, isMature: boolean) => { const getBgColor = (hasCrop: boolean, isMature: boolean) => {
if (isMature) return "#22c55e"; if (isMature) return '#22c55e'
if (hasCrop) return "#86efac"; if (hasCrop) return '#86efac'
return "#92400e"; return '#92400e'
}; }
const availableSpeeds = [1, 2, 4, 8, 16, 32, 64] const availableSpeeds = [1, 2, 4, 8, 16, 32, 64]
return ( return (
<div style={fieldContainerStyle}> <div style={fieldContainerStyle}>
<h2 style={titleStyle}>Fields</h2> <h2 style={titleStyle}>Fields</h2>
<div style={speedSelectorContainerStyle}> <div style={speedSelectorContainerStyle}>
<p style={speedSelectorLabelStyle}>Game Speed:</p> <p style={speedSelectorLabelStyle}>Game Speed:</p>
<div style={speedButtonsContainerStyle}> <div style={speedButtonsContainerStyle}>
{availableSpeeds.map(speed => ( {availableSpeeds.map((speed) => (
<button <button
style={getSpeedButtonStyle(gameSpeed === speed)} style={getSpeedButtonStyle(gameSpeed === speed)}
onClick={() => setGameSpeed(speed)} onClick={() => setGameSpeed(speed)}
@ -187,10 +190,10 @@ const FieldComponent: React.FC = () => {
))} ))}
</div> </div>
</div> </div>
<div style={cropSelectionContainerStyle}> <div style={cropSelectionContainerStyle}>
<p style={cropSelectionLabelStyle}>Select a crop to plant:</p> <p style={cropSelectionLabelStyle}>Select a crop to plant:</p>
{Object.values(CROPS).map(crop => ( {Object.values(CROPS).map((crop) => (
<button <button
key={crop.id} key={crop.id}
style={getCropButtonStyle(selectedCrop === crop.id)} style={getCropButtonStyle(selectedCrop === crop.id)}
@ -200,59 +203,71 @@ const FieldComponent: React.FC = () => {
</button> </button>
))} ))}
</div> </div>
<div style={actionsContainerStyle}> <div style={actionsContainerStyle}>
<button <button
style={getActionButtonStyle(actionCooldown > 0)} style={getActionButtonStyle(actionCooldown > 0)}
onClick={plant} onClick={plant}
disabled={actionCooldown > 0} disabled={actionCooldown > 0}
> >
Plant Crop Plant Crop
</button> </button>
<button <button
style={getActionButtonStyle(actionCooldown > 0)} style={getActionButtonStyle(actionCooldown > 0)}
onClick={water} onClick={water}
disabled={actionCooldown > 0} disabled={actionCooldown > 0}
> >
Water Crop Water Crop
</button> </button>
<button <button
style={getActionButtonStyle(actionCooldown > 0)} style={getActionButtonStyle(actionCooldown > 0)}
onClick={harvest} onClick={harvest}
disabled={actionCooldown > 0} disabled={actionCooldown > 0}
> >
Harvest Crop Harvest Crop
</button> </button>
</div> </div>
<div style={getFieldGridStyle(fieldSize)}> <div style={getFieldGridStyle(fieldSize)}>
{plots.slice(0, fieldSize).map((row, rowIndex) => {plots.slice(0, fieldSize).map((row, rowIndex) =>
row.slice(0, fieldSize).map((plot, colIndex) => { row.slice(0, fieldSize).map((plot, colIndex) => {
const hasCrop = !!plot.current; const hasCrop = !!plot.current
const isMature = !!(plot.current?.mature); const isMature = !!plot.current?.mature
const bgColor = getBgColor(hasCrop, isMature); const bgColor = getBgColor(hasCrop, isMature)
return ( return (
<div <div
key={`${rowIndex}-${colIndex}`} key={`${rowIndex}-${colIndex}`}
style={getPlotStyle(bgColor)} style={getPlotStyle(bgColor)}
onClick={() => handlePlotClick(rowIndex, colIndex)} onClick={() => handlePlotClick(rowIndex, colIndex)}
> >
{plot.intended && !plot.current && <div>🌱 {CROPS[plot.intended]?.name}</div>} {plot.intended && !plot.current && (
{plot.current && <div>{plot.current.mature ? '🌿' : '🌱'} {CROPS[plot.current.cropId]?.name}</div>} <div>🌱 {CROPS[plot.intended]?.name}</div>
)}
{plot.current && (
<div>
{plot.current.mature ? '🌿' : '🌱'}{' '}
{CROPS[plot.current.cropId]?.name}
</div>
)}
<div style={getMoistureIndicatorStyle(plot.moisture)} /> <div style={getMoistureIndicatorStyle(plot.moisture)} />
{plot.current && !plot.current.mature && ( {plot.current && !plot.current.mature && (
<div style={getMaturityProgressContainerStyle()}> <div style={getMaturityProgressContainerStyle()}>
<div style={getMaturityProgressBarStyle(plot.current.progress, CROPS[plot.current.cropId].growthTicks)} /> <div
style={getMaturityProgressBarStyle(
plot.current.progress,
CROPS[plot.current.cropId].growthTicks,
)}
/>
</div> </div>
)} )}
</div> </div>
); )
}) }),
)} )}
</div> </div>
</div> </div>
); )
}; }
export default FieldComponent; export default FieldComponent

View file

@ -1,53 +1,59 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react'
interface FloatingMessageProps { interface FloatingMessageProps {
message: string; message: string
startPosition: { x: number, y: number }; startPosition: { x: number; y: number }
onComplete: () => void; onComplete: () => void
} }
const FloatingMessage: React.FC<FloatingMessageProps> = ({ message, startPosition, onComplete }) => { const FloatingMessage: React.FC<FloatingMessageProps> = ({
const [position, setPosition] = useState({ y: startPosition.y }); message,
const [opacity, setOpacity] = useState(1); startPosition,
onComplete,
}) => {
const [position, setPosition] = useState({ y: startPosition.y })
const [opacity, setOpacity] = useState(1)
useEffect(() => { useEffect(() => {
const startTime = Date.now(); const startTime = Date.now()
const duration = 1000; // 1 second animation const duration = 1000 // 1 second animation
const animate = () => { const animate = () => {
const elapsed = Date.now() - startTime; const elapsed = Date.now() - startTime
const progress = Math.min(elapsed / duration, 1); const progress = Math.min(elapsed / duration, 1)
setPosition({ y: startPosition.y - progress * 50 }); setPosition({ y: startPosition.y - progress * 50 })
setOpacity(1 - progress); setOpacity(1 - progress)
if (progress < 1) { if (progress < 1) {
requestAnimationFrame(animate); requestAnimationFrame(animate)
} else { } else {
onComplete(); onComplete()
} }
}; }
const animationFrame = requestAnimationFrame(animate); const animationFrame = requestAnimationFrame(animate)
return () => { return () => {
cancelAnimationFrame(animationFrame); cancelAnimationFrame(animationFrame)
}; }
}, [startPosition, onComplete]); }, [startPosition, onComplete])
return ( return (
<div style={{ <div
position: 'absolute', style={{
left: startPosition.x, position: 'absolute',
top: position.y, left: startPosition.x,
opacity, top: position.y,
fontWeight: 'bold', opacity,
pointerEvents: 'none', fontWeight: 'bold',
zIndex: 100 pointerEvents: 'none',
}}> zIndex: 100,
}}
>
{message} {message}
</div> </div>
); )
}; }
export default FloatingMessage; export default FloatingMessage

View file

@ -1,19 +1,19 @@
import React, { useState } from 'react'; import React, { useState } from 'react'
import { useGameStore } from '../store/useGameStore'; import { useGameStore } from '../store/useGameStore'
import { MARKET_ITEMS } from '../constants'; import { MARKET_ITEMS } from '../constants'
import FloatingMessage from './FloatingMessage'; import FloatingMessage from './FloatingMessage'
import { styled } from '@linaria/react'; import { styled } from '@linaria/react'
const MarketContainer = styled.div` const MarketContainer = styled.div`
padding: 1rem; padding: 1rem;
`; `
const Title = styled.h2` const Title = styled.h2`
font-size: 1.5rem; font-size: 1.5rem;
font-weight: bold; font-weight: bold;
margin-bottom: 1rem; margin-bottom: 1rem;
text-align: center; text-align: center;
`; `
const CashDisplay = styled.div` const CashDisplay = styled.div`
text-align: center; text-align: center;
@ -21,124 +21,135 @@ const CashDisplay = styled.div`
padding: 1rem; padding: 1rem;
background-color: #dcfce7; background-color: #dcfce7;
border-radius: 0.375rem; border-radius: 0.375rem;
`; `
const CashAmount = styled.h3` const CashAmount = styled.h3`
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 500; font-weight: 500;
`; `
const SectionTitle = styled.h3` const SectionTitle = styled.h3`
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 500; font-weight: 500;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
margin-top: 1.5rem; margin-top: 1.5rem;
`; `
const MarketTable = styled.table` const MarketTable = styled.table`
width: 100%; width: 100%;
border-collapse: separate; border-collapse: separate;
border-spacing: 0 0.5rem; border-spacing: 0 0.5rem;
`; `
const MarketItemRow = styled.tr` const MarketItemRow = styled.tr`
background-color: #f3f4f6; background-color: #f3f4f6;
border-radius: 0.5rem; border-radius: 0.5rem;
`; `
const MarketItemCell = styled.td` const MarketItemCell = styled.td`
padding: 0.75rem; padding: 0.75rem;
text-align: center; text-align: center;
`; `
const TableHeader = styled.th` const TableHeader = styled.th`
padding: 0.75rem; padding: 0.75rem;
text-align: center; text-align: center;
`; `
const ItemIcon = styled.span` const ItemIcon = styled.span`
font-size: 1.5rem; font-size: 1.5rem;
margin-right: 0.5rem; margin-right: 0.5rem;
`; `
const ActionButton = styled.button<{ disabled?: boolean }>` const ActionButton = styled.button<{ disabled?: boolean }>`
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
border-radius: 0.25rem; border-radius: 0.25rem;
background-color: ${props => props.disabled ? '#9ca3af' : '#3b82f6'}; background-color: ${(props) => (props.disabled ? '#9ca3af' : '#3b82f6')};
color: white; color: white;
font-weight: bold; font-weight: bold;
cursor: ${props => props.disabled ? 'not-allowed' : 'pointer'}; cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')};
border: none; border: none;
`; `
interface FloatingMessageData { interface FloatingMessageData {
id: string; id: string
message: string; message: string
position: { x: number, y: number }; position: { x: number; y: number }
} }
const MarketComponent: React.FC = () => { const MarketComponent: React.FC = () => {
const { inventory, cash, buyItem, sellItem } = useGameStore(); const { inventory, cash, buyItem, sellItem } = useGameStore()
const [floatingMessages, setFloatingMessages] = useState<FloatingMessageData[]>([]); const [floatingMessages, setFloatingMessages] = useState<
FloatingMessageData[]
>([])
const handleBuy = (itemId: string, e: React.MouseEvent) => { const handleBuy = (itemId: string, e: React.MouseEvent) => {
const item = MARKET_ITEMS[itemId]; const item = MARKET_ITEMS[itemId]
if (!item || item.buyPrice === null || cash < item.buyPrice) return; if (!item || item.buyPrice === null || cash < item.buyPrice) return
buyItem(itemId); buyItem(itemId)
const rect = (e.target as HTMLElement).getBoundingClientRect(); const rect = (e.target as HTMLElement).getBoundingClientRect()
const position = { x: rect.left + rect.width / 2, y: rect.top }; const position = { x: rect.left + rect.width / 2, y: rect.top }
setFloatingMessages([ setFloatingMessages([
...floatingMessages, ...floatingMessages,
{ {
id: `buy-${itemId}-${Date.now()}`, id: `buy-${itemId}-${Date.now()}`,
message: `+1 ${item.emoji} ${item.name}`, message: `+1 ${item.emoji} ${item.name}`,
position position,
} },
]); ])
}; }
const handleSell = (itemId: string, e: React.MouseEvent) => { const handleSell = (itemId: string, e: React.MouseEvent) => {
const item = MARKET_ITEMS[itemId]; const item = MARKET_ITEMS[itemId]
if (!item || item.sellPrice === null || !inventory[itemId] || inventory[itemId] <= 0) return; if (
!item ||
sellItem(itemId); item.sellPrice === null ||
!inventory[itemId] ||
const rect = (e.target as HTMLElement).getBoundingClientRect(); inventory[itemId] <= 0
const position = { x: rect.left + rect.width / 2, y: rect.top }; )
return
sellItem(itemId)
const rect = (e.target as HTMLElement).getBoundingClientRect()
const position = { x: rect.left + rect.width / 2, y: rect.top }
setFloatingMessages([ setFloatingMessages([
...floatingMessages, ...floatingMessages,
{ {
id: `sell-${itemId}-${Date.now()}`, id: `sell-${itemId}-${Date.now()}`,
message: `-1 ${item.emoji} ${item.name}`, message: `-1 ${item.emoji} ${item.name}`,
position position,
} },
]); ])
}; }
const removeFloatingMessage = (messageId: string) => { const removeFloatingMessage = (messageId: string) => {
setFloatingMessages(floatingMessages.filter(msg => msg.id !== messageId)); setFloatingMessages(floatingMessages.filter((msg) => msg.id !== messageId))
}; }
const sellableItems = Object.entries(MARKET_ITEMS) const sellableItems = Object.entries(MARKET_ITEMS)
.filter(([_, item]) => item.sellPrice !== null && inventory[item.id] && inventory[item.id] > 0) .filter(
.map(([_, item]) => item); ([_, item]) =>
item.sellPrice !== null && inventory[item.id] && inventory[item.id] > 0,
)
.map(([_, item]) => item)
const buyableItems = Object.entries(MARKET_ITEMS) const buyableItems = Object.entries(MARKET_ITEMS)
.filter(([_, item]) => item.buyPrice !== null) .filter(([_, item]) => item.buyPrice !== null)
.map(([_, item]) => item); .map(([_, item]) => item)
return ( return (
<MarketContainer> <MarketContainer>
<Title>Market</Title> <Title>Market</Title>
<CashDisplay> <CashDisplay>
<CashAmount>Cash: ${cash.toFixed(2)}</CashAmount> <CashAmount>Cash: ${cash.toFixed(2)}</CashAmount>
</CashDisplay> </CashDisplay>
{sellableItems.length > 0 && ( {sellableItems.length > 0 && (
<> <>
<SectionTitle>Sell Items</SectionTitle> <SectionTitle>Sell Items</SectionTitle>
@ -152,7 +163,7 @@ const MarketComponent: React.FC = () => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{sellableItems.map(item => ( {sellableItems.map((item) => (
<MarketItemRow key={`sell-${item.id}`}> <MarketItemRow key={`sell-${item.id}`}>
<MarketItemCell> <MarketItemCell>
<ItemIcon>{item.emoji}</ItemIcon> <ItemIcon>{item.emoji}</ItemIcon>
@ -161,7 +172,7 @@ const MarketComponent: React.FC = () => {
<MarketItemCell>${item.sellPrice}</MarketItemCell> <MarketItemCell>${item.sellPrice}</MarketItemCell>
<MarketItemCell>{inventory[item.id] || 0}</MarketItemCell> <MarketItemCell>{inventory[item.id] || 0}</MarketItemCell>
<MarketItemCell> <MarketItemCell>
<ActionButton <ActionButton
onClick={(e) => handleSell(item.id, e)} onClick={(e) => handleSell(item.id, e)}
disabled={!inventory[item.id] || inventory[item.id] <= 0} disabled={!inventory[item.id] || inventory[item.id] <= 0}
> >
@ -174,7 +185,7 @@ const MarketComponent: React.FC = () => {
</MarketTable> </MarketTable>
</> </>
)} )}
<SectionTitle>Buy Items</SectionTitle> <SectionTitle>Buy Items</SectionTitle>
<MarketTable> <MarketTable>
<thead> <thead>
@ -185,7 +196,7 @@ const MarketComponent: React.FC = () => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{buyableItems.map(item => ( {buyableItems.map((item) => (
<MarketItemRow key={`buy-${item.id}`}> <MarketItemRow key={`buy-${item.id}`}>
<MarketItemCell> <MarketItemCell>
<ItemIcon>{item.emoji}</ItemIcon> <ItemIcon>{item.emoji}</ItemIcon>
@ -193,7 +204,7 @@ const MarketComponent: React.FC = () => {
</MarketItemCell> </MarketItemCell>
<MarketItemCell>${item.buyPrice}</MarketItemCell> <MarketItemCell>${item.buyPrice}</MarketItemCell>
<MarketItemCell> <MarketItemCell>
<ActionButton <ActionButton
onClick={(e) => handleBuy(item.id, e)} onClick={(e) => handleBuy(item.id, e)}
disabled={item.buyPrice ? cash < item.buyPrice : true} disabled={item.buyPrice ? cash < item.buyPrice : true}
> >
@ -204,8 +215,8 @@ const MarketComponent: React.FC = () => {
))} ))}
</tbody> </tbody>
</MarketTable> </MarketTable>
{floatingMessages.map(msg => ( {floatingMessages.map((msg) => (
<FloatingMessage <FloatingMessage
key={msg.id} key={msg.id}
message={msg.message} message={msg.message}
@ -214,7 +225,7 @@ const MarketComponent: React.FC = () => {
/> />
))} ))}
</MarketContainer> </MarketContainer>
); )
}; }
export default MarketComponent; export default MarketComponent

View file

@ -1,47 +1,47 @@
import React from 'react'; import React from 'react'
import { useGameStore } from '../store/useGameStore'; import { useGameStore } from '../store/useGameStore'
import { CROPS } from '../constants'; import { CROPS } from '../constants'
const warehouseContainerStyle: React.CSSProperties = { const warehouseContainerStyle: React.CSSProperties = {
padding: '1rem' padding: '1rem',
}; }
const titleStyle: React.CSSProperties = { const titleStyle: React.CSSProperties = {
fontSize: '1.5rem', fontSize: '1.5rem',
fontWeight: 'bold', fontWeight: 'bold',
marginBottom: '1rem', marginBottom: '1rem',
textAlign: 'center' textAlign: 'center',
}; }
const cashDisplayStyle: React.CSSProperties = { const cashDisplayStyle: React.CSSProperties = {
textAlign: 'center', textAlign: 'center',
marginBottom: '1.5rem', marginBottom: '1.5rem',
padding: '1rem', padding: '1rem',
backgroundColor: '#dcfce7', backgroundColor: '#dcfce7',
borderRadius: '0.375rem' borderRadius: '0.375rem',
}; }
const cashAmountStyle: React.CSSProperties = { const cashAmountStyle: React.CSSProperties = {
fontSize: '1.25rem', fontSize: '1.25rem',
fontWeight: 500 fontWeight: 500,
}; }
const inventoryTitleStyle: React.CSSProperties = { const inventoryTitleStyle: React.CSSProperties = {
fontSize: '1.25rem', fontSize: '1.25rem',
fontWeight: 500, fontWeight: 500,
marginBottom: '0.5rem' marginBottom: '0.5rem',
}; }
const getInventoryGridStyle = (): React.CSSProperties => { const getInventoryGridStyle = (): React.CSSProperties => {
const style: React.CSSProperties = { const style: React.CSSProperties = {
display: 'grid', display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)', gridTemplateColumns: 'repeat(2, 1fr)',
gap: '1rem', gap: '1rem',
marginTop: '1rem' marginTop: '1rem',
}; }
return style; return style
}; }
const inventoryItemStyle: React.CSSProperties = { const inventoryItemStyle: React.CSSProperties = {
backgroundColor: '#f3f4f6', backgroundColor: '#f3f4f6',
@ -51,67 +51,68 @@ const inventoryItemStyle: React.CSSProperties = {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
textAlign: 'center' textAlign: 'center',
}; }
const itemIconStyle: React.CSSProperties = { const itemIconStyle: React.CSSProperties = {
fontSize: '1.5rem', fontSize: '1.5rem',
marginBottom: '0.5rem' marginBottom: '0.5rem',
}; }
const itemNameStyle: React.CSSProperties = { const itemNameStyle: React.CSSProperties = {
fontWeight: 500, fontWeight: 500,
marginBottom: '0.25rem' marginBottom: '0.25rem',
}; }
const formatItemName = (id: string) => { const formatItemName = (id: string) => {
if (id.endsWith('_seed')) { if (id.endsWith('_seed')) {
const cropId = id.replace('_seed', ''); const cropId = id.replace('_seed', '')
return `${CROPS[cropId]?.name || cropId} Seeds`; return `${CROPS[cropId]?.name || cropId} Seeds`
} }
return CROPS[id]?.name || id; return CROPS[id]?.name || id
}; }
const getItemIcon = (id: string) => { const getItemIcon = (id: string) => {
if (id.endsWith('_seed')) { if (id.endsWith('_seed')) {
return '🌰'; return '🌰'
} }
const cropIcons: Record<string, string> = { const cropIcons: Record<string, string> = {
celery: '🥬', celery: '🥬',
corn: '🌽', corn: '🌽',
}; }
return cropIcons[id] || '📦'; return cropIcons[id] || '📦'
}; }
const WarehouseComponent: React.FC = () => { const WarehouseComponent: React.FC = () => {
const { inventory, cash } = useGameStore(); const { inventory, cash } = useGameStore()
return ( return (
<div style={warehouseContainerStyle}> <div style={warehouseContainerStyle}>
<h2 style={titleStyle}>Warehouse</h2> <h2 style={titleStyle}>Warehouse</h2>
<div style={cashDisplayStyle}> <div style={cashDisplayStyle}>
<h3 style={cashAmountStyle}>Cash: ${cash.toFixed(2)}</h3> <h3 style={cashAmountStyle}>Cash: ${cash.toFixed(2)}</h3>
</div> </div>
<h3 style={inventoryTitleStyle}>Inventory:</h3> <h3 style={inventoryTitleStyle}>Inventory:</h3>
<div style={getInventoryGridStyle()}> <div style={getInventoryGridStyle()}>
{Object.entries(inventory).map(([id, count]) => ( {Object.entries(inventory).map(
count > 0 && ( ([id, count]) =>
<div key={id} style={inventoryItemStyle}> count > 0 && (
<div style={itemIconStyle}>{getItemIcon(id)}</div> <div key={id} style={inventoryItemStyle}>
<h4 style={itemNameStyle}>{formatItemName(id)}</h4> <div style={itemIconStyle}>{getItemIcon(id)}</div>
<p>Quantity: {count}</p> <h4 style={itemNameStyle}>{formatItemName(id)}</h4>
</div> <p>Quantity: {count}</p>
) </div>
))} ),
)}
</div> </div>
</div> </div>
); )
}; }
export default WarehouseComponent; export default WarehouseComponent

View file

@ -1,50 +1,70 @@
import { CropDefinitions } from '../types'; import { CropDefinitions } from '../types'
export const INITIAL_CASH = 50; export const INITIAL_CASH = 50
export const INITIAL_FIELD_SIZE = 4; export const INITIAL_FIELD_SIZE = 4
export const COOLDOWN_DURATION = 2000; // 2 seconds in milliseconds export const COOLDOWN_DURATION = 2000 // 2 seconds in milliseconds
export const TICK_INTERVAL = 12000; // 12 seconds in milliseconds export const TICK_INTERVAL = 12000 // 12 seconds in milliseconds
export const GAME_SPEEDS = { export const GAME_SPEEDS = {
NORMAL: 1, FAST: 5, SUPER_FAST: 10 NORMAL: 1,
}; FAST: 5,
export const INITIAL_GAME_SPEED = GAME_SPEEDS.NORMAL; SUPER_FAST: 10,
}
export const INITIAL_GAME_SPEED = GAME_SPEEDS.NORMAL
export const FIELD_UPGRADE_COSTS = [100, 1000, 10000, 100000]; export const FIELD_UPGRADE_COSTS = [100, 1000, 10000, 100000]
export const CROPS: CropDefinitions = { export const CROPS: CropDefinitions = {
celery: { celery: {
id: 'celery', id: 'celery',
name: 'Celery', name: 'Celery',
growthTicks: 36, growthTicks: 36,
waterPerTick: 1/20, waterPerTick: 1 / 20,
yield: 1, yield: 1,
yieldType: 'celery' yieldType: 'celery',
}, },
corn: { corn: {
id: 'corn', id: 'corn',
name: 'Corn', name: 'Corn',
growthTicks: 120, growthTicks: 120,
waterPerTick: 1/40, waterPerTick: 1 / 40,
yield: 5, yield: 5,
yieldType: 'corn' yieldType: 'corn',
} },
}; }
export const INITIAL_INVENTORY = { export const INITIAL_INVENTORY = {
'celery_seed': 8 celery_seed: 8,
}; }
export interface MarketItem { export interface MarketItem {
id: string; id: string
name: string; name: string
emoji: string; emoji: string
buyPrice: number | null; // null means not buyable buyPrice: number | null // null means not buyable
sellPrice: number | null; // null means not sellable sellPrice: number | null // null means not sellable
} }
export const MARKET_ITEMS: Record<string, MarketItem> = { export const MARKET_ITEMS: Record<string, MarketItem> = {
'celery_seed': { id: 'celery_seed', name: 'Celery Seeds', emoji: '🌰', buyPrice: 1, sellPrice: null }, celery_seed: {
'corn_seed': { id: 'corn_seed', name: 'Corn Seeds', emoji: '🌰', buyPrice: 5, sellPrice: null }, id: 'celery_seed',
'celery': { id: 'celery', name: 'Celery', emoji: '🥬', buyPrice: null, sellPrice: 5 }, name: 'Celery Seeds',
'corn': { id: 'corn', name: 'Corn', emoji: '🌽', buyPrice: null, sellPrice: 8 }, emoji: '🌰',
}; buyPrice: 1,
sellPrice: null,
},
corn_seed: {
id: 'corn_seed',
name: 'Corn Seeds',
emoji: '🌰',
buyPrice: 5,
sellPrice: null,
},
celery: {
id: 'celery',
name: 'Celery',
emoji: '🥬',
buyPrice: null,
sellPrice: 5,
},
corn: { id: 'corn', name: 'Corn', emoji: '🌽', buyPrice: null, sellPrice: 8 },
}

View file

@ -3,16 +3,16 @@ import { useGameStore } from '../store/useGameStore'
import { TICK_INTERVAL } from '../constants' import { TICK_INTERVAL } from '../constants'
export const useGameTick = () => { export const useGameTick = () => {
const tick = useGameStore(state => state.tick) const tick = useGameStore((state) => state.tick)
const gameSpeed = useGameStore(state => state.gameSpeed) const gameSpeed = useGameStore((state) => state.gameSpeed)
const intervalRef = useRef<number | null>(null) const intervalRef = useRef<number | null>(null)
useEffect(() => { useEffect(() => {
if (intervalRef.current !== null) { if (intervalRef.current !== null) {
clearInterval(intervalRef.current) clearInterval(intervalRef.current)
} }
const adjustedInterval = TICK_INTERVAL / gameSpeed; const adjustedInterval = TICK_INTERVAL / gameSpeed
intervalRef.current = window.setInterval(() => { intervalRef.current = window.setInterval(() => {
tick() tick()
}, adjustedInterval) }, adjustedInterval)
@ -23,4 +23,4 @@ export const useGameTick = () => {
} }
} }
}, [tick, gameSpeed]) }, [tick, gameSpeed])
}; }

View file

@ -24,7 +24,9 @@ git /* Base styles */
/* Global styles */ /* Global styles */
body { body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
@ -33,4 +35,3 @@ body {
* { * {
box-sizing: inherit; box-sizing: inherit;
} }

View file

@ -1,37 +1,46 @@
import { create } from 'zustand'; import { create } from 'zustand'
import { produce } from 'immer'; import { produce } from 'immer'
import { import {
INITIAL_CASH, INITIAL_CASH,
INITIAL_FIELD_SIZE, INITIAL_FIELD_SIZE,
FIELD_UPGRADE_COSTS, FIELD_UPGRADE_COSTS,
INITIAL_INVENTORY, INITIAL_INVENTORY,
CROPS, CROPS,
INITIAL_GAME_SPEED, INITIAL_GAME_SPEED,
COOLDOWN_DURATION, COOLDOWN_DURATION,
MARKET_ITEMS MARKET_ITEMS,
} from '../constants'; } from '../constants'
import { GameState, PlotState } from '../types'; import { GameState, PlotState } from '../types'
import { saveGame, loadGame } from '../utils/saveSystem'
const initializeField = (size: number): PlotState[][] => { const initializeField = (size: number): PlotState[][] => {
return Array(size).fill(0).map(() => return Array(size)
Array(size).fill(0).map(() => ({ .fill(0)
moisture: 0 .map(() =>
})) Array(size)
); .fill(0)
}; .map(() => ({
moisture: 0,
})),
)
}
export const useGameStore = create<GameState & { export const useGameStore = create<
plant: () => void; GameState & {
water: () => void; plant: () => void
harvest: () => void; water: () => void
tick: () => void; harvest: () => void
assignCrop: (row: number, col: number, cropId: string) => void; tick: () => void
upgradeField: () => void; assignCrop: (row: number, col: number, cropId: string) => void
setGameSpeed: (speed: number) => void; upgradeField: () => void
setActionCooldown: (cooldown: number) => void; setGameSpeed: (speed: number) => void
buyItem: (itemId: string) => void; setActionCooldown: (cooldown: number) => void
sellItem: (itemId: string) => void; buyItem: (itemId: string) => void
}>((set, get) => ({ sellItem: (itemId: string) => void
saveToSlot: (slot: number) => void
loadFromSlot: (slot: number) => void
}
>((set, get) => ({
cash: INITIAL_CASH, cash: INITIAL_CASH,
inventory: INITIAL_INVENTORY, inventory: INITIAL_INVENTORY,
fieldSize: INITIAL_FIELD_SIZE, fieldSize: INITIAL_FIELD_SIZE,
@ -40,39 +49,44 @@ export const useGameStore = create<GameState & {
plots: initializeField(INITIAL_FIELD_SIZE), plots: initializeField(INITIAL_FIELD_SIZE),
gameSpeed: INITIAL_GAME_SPEED, gameSpeed: INITIAL_GAME_SPEED,
actionCooldown: 0, actionCooldown: 0,
tickCount: 0,
assignCrop: (row, col, cropId) => { assignCrop: (row, col, cropId) => {
set(produce(state => { set(
state.plots[row][col].intended = cropId; produce((state) => {
})); state.plots[row][col].intended = cropId
}),
)
}, },
plant: () => { plant: () => {
const { plots, inventory, actionCooldown } = get(); const { plots, inventory, actionCooldown } = get()
if (actionCooldown > 0) { if (actionCooldown > 0) {
return; return
} }
for (let row = 0; row < plots.length; row++) { for (let row = 0; row < plots.length; row++) {
for (let col = 0; col < plots[row].length; col++) { for (let col = 0; col < plots[row].length; col++) {
const plot = plots[row][col]; const plot = plots[row][col]
if (plot.intended && !plot.current) { if (plot.intended && !plot.current) {
const seedId = `${plot.intended}_seed`; const seedId = `${plot.intended}_seed`
if (inventory[seedId] && inventory[seedId] > 0) { if (inventory[seedId] && inventory[seedId] > 0) {
set(produce(state => { set(
state.plots[row][col].current = { produce((state) => {
cropId: plot.intended!, state.plots[row][col].current = {
progress: 0, cropId: plot.intended!,
mature: false progress: 0,
}; mature: false,
}
state.inventory[seedId] = state.inventory[seedId] - 1;
state.actionCooldown = COOLDOWN_DURATION; state.inventory[seedId] = state.inventory[seedId] - 1
})); state.actionCooldown = COOLDOWN_DURATION
return; }),
)
return
} }
} }
} }
@ -80,153 +94,203 @@ export const useGameStore = create<GameState & {
}, },
water: () => { water: () => {
const { plots, actionCooldown } = get(); const { plots, actionCooldown } = get()
if (actionCooldown > 0) { if (actionCooldown > 0) {
return; return
} }
let driestRow = -1; let driestRow = -1
let driestCol = -1; let driestCol = -1
let lowestMoisture = 1; let lowestMoisture = 1
for (let row = 0; row < plots.length; row++) { for (let row = 0; row < plots.length; row++) {
for (let col = 0; col < plots[row].length; col++) { for (let col = 0; col < plots[row].length; col++) {
if (plots[row][col].current && plots[row][col].moisture < lowestMoisture) { if (
lowestMoisture = plots[row][col].moisture; plots[row][col].current &&
driestRow = row; plots[row][col].moisture < lowestMoisture
driestCol = col; ) {
lowestMoisture = plots[row][col].moisture
driestRow = row
driestCol = col
} }
} }
} }
if (driestRow >= 0 && driestCol >= 0) { if (driestRow >= 0 && driestCol >= 0) {
set(produce(state => { set(
state.plots[driestRow][driestCol].moisture = 1; produce((state) => {
state.actionCooldown = COOLDOWN_DURATION; state.plots[driestRow][driestCol].moisture = 1
})); state.actionCooldown = COOLDOWN_DURATION
}),
)
} }
}, },
harvest: () => { harvest: () => {
const { plots, actionCooldown } = get(); const { plots, actionCooldown } = get()
if (actionCooldown > 0) { if (actionCooldown > 0) {
return; return
} }
for (let row = 0; row < plots.length; row++) { for (let row = 0; row < plots.length; row++) {
for (let col = 0; col < plots[row].length; col++) { for (let col = 0; col < plots[row].length; col++) {
const plot = plots[row][col]; const plot = plots[row][col]
if (plot.current && plot.current.mature) { if (plot.current && plot.current.mature) {
const crop = CROPS[plot.current.cropId]; const crop = CROPS[plot.current.cropId]
const yieldItem = crop.yieldType; const yieldItem = crop.yieldType
const yieldAmount = crop.yield; const yieldAmount = crop.yield
set(produce(state => { set(
state.plots[row][col].current = undefined; produce((state) => {
state.inventory[yieldItem] = (state.inventory[yieldItem] || 0) + yieldAmount; state.plots[row][col].current = undefined
state.actionCooldown = COOLDOWN_DURATION; state.inventory[yieldItem] =
})); (state.inventory[yieldItem] || 0) + yieldAmount
return; state.actionCooldown = COOLDOWN_DURATION
}),
)
return
} }
} }
} }
}, },
tick: () => { tick: () => {
set(produce(state => { set(
// Update cooldown produce((state) => {
if (state.actionCooldown > 0) { // Update plots
state.actionCooldown = Math.max(0, state.actionCooldown - (COOLDOWN_DURATION / 20)); state.plots.forEach((row: PlotState[], rowIndex: number) => {
} row.forEach((plot: PlotState, colIndex: number) => {
if (!plot.current || plot.current.mature) {
// Update plots return
state.plots.forEach((row: PlotState[], rowIndex: number) => { }
row.forEach((plot: PlotState, colIndex: number) => {
if (!plot.current || plot.current.mature) { const crop = CROPS[plot.current.cropId]
return; const waterNeeded = crop.waterPerTick
}
if (plot.moisture >= waterNeeded) {
const crop = CROPS[plot.current.cropId]; const newProgress = plot.current.progress + 1
const waterNeeded = crop.waterPerTick; const mature = newProgress >= crop.growthTicks
if (plot.moisture >= waterNeeded) { state.plots[rowIndex][colIndex].moisture =
const newProgress = plot.current.progress + 1; plot.moisture - waterNeeded
const mature = newProgress >= crop.growthTicks; state.plots[rowIndex][colIndex].current.progress = newProgress
state.plots[rowIndex][colIndex].current.mature = mature
state.plots[rowIndex][colIndex].moisture = plot.moisture - waterNeeded; }
state.plots[rowIndex][colIndex].current.progress = newProgress; })
state.plots[rowIndex][colIndex].current.mature = mature; })
}
}); state.tickCount = state.tickCount + 1
}); }),
})); )
}, },
upgradeField: () => { upgradeField: () => {
set(produce(state => { set(
if (state.fieldSize >= state.maxFieldSize) { produce((state) => {
return; if (state.fieldSize >= state.maxFieldSize) {
} return
}
const upgradeIndex = state.fieldSize - INITIAL_FIELD_SIZE;
const cost = state.fieldUpgradeCosts[upgradeIndex]; const upgradeIndex = state.fieldSize - INITIAL_FIELD_SIZE
const cost = state.fieldUpgradeCosts[upgradeIndex]
if (state.cash < cost) {
return; if (state.cash < cost) {
} return
}
const newSize = state.fieldSize + 1;
const newSize = state.fieldSize + 1
state.cash = state.cash - cost;
state.fieldSize = newSize; state.cash = state.cash - cost
state.plots = initializeField(newSize); state.fieldSize = newSize
})); state.plots = initializeField(newSize)
}),
)
}, },
setGameSpeed: (speed) => { setGameSpeed: (speed) => {
set(produce(state => { set(
state.gameSpeed = speed; produce((state) => {
})); state.gameSpeed = speed
}),
)
}, },
setActionCooldown: (cooldown) => { setActionCooldown: (cooldown) => {
set(produce(state => { set(
state.actionCooldown = cooldown; produce((state) => {
})); state.actionCooldown = cooldown
}),
)
}, },
buyItem: (itemId) => { buyItem: (itemId) => {
const { cash } = get(); const { cash } = get()
const item = MARKET_ITEMS[itemId]; const item = MARKET_ITEMS[itemId]
if (!item || item.buyPrice === null) { if (!item || item.buyPrice === null) {
return; return
} }
if (cash < item.buyPrice) { if (cash < item.buyPrice) {
return; return
} }
set(produce(state => { set(
state.cash -= item.buyPrice; produce((state) => {
state.inventory[itemId] = (state.inventory[itemId] || 0) + 1; state.cash -= item.buyPrice
})); state.inventory[itemId] = (state.inventory[itemId] || 0) + 1
}),
)
}, },
sellItem: (itemId) => { sellItem: (itemId) => {
const { inventory } = get(); const { inventory } = get()
const item = MARKET_ITEMS[itemId]; const item = MARKET_ITEMS[itemId]
if (!item || item.sellPrice === null || !inventory[itemId] || inventory[itemId] <= 0) { if (
return; !item ||
item.sellPrice === null ||
!inventory[itemId] ||
inventory[itemId] <= 0
) {
return
} }
set(produce(state => { set(
state.cash += item.sellPrice; produce((state) => {
state.inventory[itemId] -= 1; 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)
}
},
}))

View file

@ -1,48 +1,48 @@
export interface Crop { export interface Crop {
id: string; id: string
name: string; name: string
growthTicks: number; growthTicks: number
waterPerTick: number; waterPerTick: number
yield: number; yield: number
yieldType: string; yieldType: string
} }
export interface CropDefinitions { export interface CropDefinitions {
[key: string]: Crop; [key: string]: Crop
} }
export interface PlotState { export interface PlotState {
intended?: string; intended?: string
current?: { current?: {
cropId: string; cropId: string
progress: number; progress: number
mature: boolean; mature: boolean
}; }
moisture: number; moisture: number
} }
export interface InventoryItem { export interface InventoryItem {
id: string; id: string
count: number; count: number
} }
export interface GameState { export interface GameState {
cash: number; cash: number
inventory: Record<string, number>; inventory: Record<string, number>
fieldSize: number; fieldSize: number
maxFieldSize: number; maxFieldSize: number
fieldUpgradeCosts: number[]; fieldUpgradeCosts: number[]
plots: PlotState[][]; plots: PlotState[][]
gameSpeed: number; gameSpeed: number
actionCooldown: number; actionCooldown: number
} }
export interface MarketTransaction { export interface MarketTransaction {
itemId: string; itemId: string
amount: number; amount: number
type: 'buy' | 'sell'; type: 'buy' | 'sell'
position: { position: {
x: number; x: number
y: number; y: number
}; }
} }

29
src/utils/saveSystem.ts Normal file
View file

@ -0,0 +1,29 @@
import { GameState } from '../types'
const SAVE_SLOT_PREFIX = 'dionysian_idle_save_'
export 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
}
}
export 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}`)
}