Set up prettier.

This commit is contained in:
Ryan Lanny Jenkins 2025-05-18 10:17:42 -05:00
parent 39309aa9e7
commit d75e9f3cbb
16 changed files with 723 additions and 581 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",
"@vitejs/plugin-react": "^4.3.4",
"@wyw-in-js/vite": "^0.6.0",
"prettier": "3.5.3",
"typescript": "^5.8.3",
"vite": "^6.0.1"
}
@ -1628,6 +1629,22 @@
"dev": true,
"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": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",

View file

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

View file

@ -1,47 +1,53 @@
import React, { useEffect } from 'react';
import { Tabs, TabsContent, TabsList, TabsTrigger } 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';
import React, { useEffect } from 'react'
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} 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 = {
maxWidth: '1200px',
margin: '0 auto',
padding: '2rem'
};
padding: '2rem',
}
const tabsListStyles: React.CSSProperties = {
display: 'grid',
width: '100%',
gridTemplateColumns: 'repeat(4, 1fr)'
};
gridTemplateColumns: 'repeat(4, 1fr)',
}
function App() {
useGameTick();
const { saveToSlot, loadFromSlot } = useGameStore();
useGameTick()
const { saveToSlot, loadFromSlot } = useGameStore()
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const slot = {
'1': 1,
'2': 2,
'3': 3,
'!': 1,
'@': 2,
'#': 3
}[e.key] ?? null
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}`);
saveToSlot(slot)
console.log(`Game saved to slot ${slot}`)
} else if (hasSaveInSlot(slot)) {
loadFromSlot(slot);
console.log(`Game loaded from slot ${slot}`);
loadFromSlot(slot)
console.log(`Game loaded from slot ${slot}`)
}
}
}
@ -59,7 +65,9 @@ function App() {
<TabsTrigger value="fields">Fields</TabsTrigger>
<TabsTrigger value="warehouse">Warehouse</TabsTrigger>
<TabsTrigger value="market">Market</TabsTrigger>
<TabsTrigger value="temple" disabled>Temple</TabsTrigger>
<TabsTrigger value="temple" disabled>
Temple
</TabsTrigger>
</TabsList>
<TabsContent value="fields">
<Field />
@ -73,7 +81,7 @@ function App() {
</Tabs>
</div>
</div>
);
)
}
export default App

View file

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

View file

@ -1,35 +1,35 @@
import React, { useState } from 'react';
import React, { useState } from 'react'
export interface TabProps {
value: string;
children: React.ReactNode;
value: string
children: React.ReactNode
}
export interface TabsProps {
defaultValue: string;
children: React.ReactNode;
defaultValue: string
children: React.ReactNode
}
export interface TabsListProps {
children: React.ReactNode;
style?: React.CSSProperties;
children: React.ReactNode
style?: React.CSSProperties
}
export interface TabsContentProps {
value: string;
children: React.ReactNode;
value: string
children: React.ReactNode
}
export interface TabsTriggerProps {
value: string;
children: React.ReactNode;
disabled?: boolean;
onClick?: () => void;
value: string
children: React.ReactNode
disabled?: boolean
onClick?: () => void
}
const tabsContainerStyle: React.CSSProperties = {
width: '100%'
};
width: '100%',
}
const tabsListContainerStyle: React.CSSProperties = {
display: 'inline-flex',
@ -39,10 +39,13 @@ const tabsListContainerStyle: React.CSSProperties = {
borderRadius: '0.5rem',
backgroundColor: '#f4f4f5',
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',
alignItems: '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',
opacity: disabled ? 0.5 : 1,
cursor: disabled ? 'not-allowed' : 'pointer',
border: 'none'
});
border: 'none',
})
const tabContentStyle: React.CSSProperties = {
marginTop: '0.5rem',
outline: 'none'
};
outline: 'none',
}
const TabsContext = React.createContext<{
value: string;
setValue: (value: string) => void;
value: string
setValue: (value: string) => void
}>({
value: '',
setValue: () => {},
});
})
export const Tabs: React.FC<TabsProps> = ({ defaultValue, children }) => {
const [value, setValue] = useState(defaultValue);
const [value, setValue] = useState(defaultValue)
return (
<TabsContext.Provider value={{ value, setValue }}>
<div style={tabsContainerStyle}>{children}</div>
</TabsContext.Provider>
);
};
)
}
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> = ({
value,
children,
export const TabsTrigger: React.FC<TabsTriggerProps> = ({
value,
children,
disabled = false,
onClick
onClick,
}) => {
const { value: selectedValue, setValue } = React.useContext(TabsContext);
const active = selectedValue === value;
const { value: selectedValue, setValue } = React.useContext(TabsContext)
const active = selectedValue === value
const handleClick = () => {
if (!disabled) {
setValue(value);
if (onClick) onClick();
setValue(value)
if (onClick) onClick()
}
};
}
return (
<button
<button
style={getTabButtonStyle(active, disabled)}
disabled={disabled}
disabled={disabled}
onClick={handleClick}
type="button"
>
{children}
</button>
);
};
)
}
export const TabsContent: React.FC<TabsContentProps> = ({ value, children }) => {
const { value: selectedValue } = React.useContext(TabsContext);
if (selectedValue !== value) return null;
return <div style={tabContentStyle}>{children}</div>;
};
export const TabsContent: React.FC<TabsContentProps> = ({
value,
children,
}) => {
const { value: selectedValue } = React.useContext(TabsContext)
if (selectedValue !== value) return null
return <div style={tabContentStyle}>{children}</div>
}

View file

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

View file

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

View file

@ -1,47 +1,47 @@
import React from 'react';
import { useGameStore } from '../store/useGameStore';
import { CROPS } from '../constants';
import React from 'react'
import { useGameStore } from '../store/useGameStore'
import { CROPS } from '../constants'
const warehouseContainerStyle: React.CSSProperties = {
padding: '1rem'
};
padding: '1rem',
}
const titleStyle: React.CSSProperties = {
fontSize: '1.5rem',
fontWeight: 'bold',
marginBottom: '1rem',
textAlign: 'center'
};
textAlign: 'center',
}
const cashDisplayStyle: React.CSSProperties = {
textAlign: 'center',
marginBottom: '1.5rem',
padding: '1rem',
backgroundColor: '#dcfce7',
borderRadius: '0.375rem'
};
borderRadius: '0.375rem',
}
const cashAmountStyle: React.CSSProperties = {
fontSize: '1.25rem',
fontWeight: 500
};
fontWeight: 500,
}
const inventoryTitleStyle: React.CSSProperties = {
fontSize: '1.25rem',
fontWeight: 500,
marginBottom: '0.5rem'
};
marginBottom: '0.5rem',
}
const getInventoryGridStyle = (): React.CSSProperties => {
const style: React.CSSProperties = {
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
gap: '1rem',
marginTop: '1rem'
};
marginTop: '1rem',
}
return style;
};
return style
}
const inventoryItemStyle: React.CSSProperties = {
backgroundColor: '#f3f4f6',
@ -51,67 +51,68 @@ const inventoryItemStyle: React.CSSProperties = {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
textAlign: 'center'
};
textAlign: 'center',
}
const itemIconStyle: React.CSSProperties = {
fontSize: '1.5rem',
marginBottom: '0.5rem'
};
marginBottom: '0.5rem',
}
const itemNameStyle: React.CSSProperties = {
fontWeight: 500,
marginBottom: '0.25rem'
};
marginBottom: '0.25rem',
}
const formatItemName = (id: string) => {
if (id.endsWith('_seed')) {
const cropId = id.replace('_seed', '');
return `${CROPS[cropId]?.name || cropId} Seeds`;
const cropId = id.replace('_seed', '')
return `${CROPS[cropId]?.name || cropId} Seeds`
}
return CROPS[id]?.name || id;
};
return CROPS[id]?.name || id
}
const getItemIcon = (id: string) => {
if (id.endsWith('_seed')) {
return '🌰';
return '🌰'
}
const cropIcons: Record<string, string> = {
celery: '🥬',
corn: '🌽',
};
return cropIcons[id] || '📦';
};
}
return cropIcons[id] || '📦'
}
const WarehouseComponent: React.FC = () => {
const { inventory, cash } = useGameStore();
const { inventory, cash } = useGameStore()
return (
<div style={warehouseContainerStyle}>
<h2 style={titleStyle}>Warehouse</h2>
<div style={cashDisplayStyle}>
<h3 style={cashAmountStyle}>Cash: ${cash.toFixed(2)}</h3>
</div>
<h3 style={inventoryTitleStyle}>Inventory:</h3>
<div style={getInventoryGridStyle()}>
{Object.entries(inventory).map(([id, count]) => (
count > 0 && (
<div key={id} style={inventoryItemStyle}>
<div style={itemIconStyle}>{getItemIcon(id)}</div>
<h4 style={itemNameStyle}>{formatItemName(id)}</h4>
<p>Quantity: {count}</p>
</div>
)
))}
{Object.entries(inventory).map(
([id, count]) =>
count > 0 && (
<div key={id} style={inventoryItemStyle}>
<div style={itemIconStyle}>{getItemIcon(id)}</div>
<h4 style={itemNameStyle}>{formatItemName(id)}</h4>
<p>Quantity: {count}</p>
</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_FIELD_SIZE = 4;
export const COOLDOWN_DURATION = 2000; // 2 seconds in milliseconds
export const TICK_INTERVAL = 12000; // 12 seconds in milliseconds
export const INITIAL_CASH = 50
export const INITIAL_FIELD_SIZE = 4
export const COOLDOWN_DURATION = 2000 // 2 seconds in milliseconds
export const TICK_INTERVAL = 12000 // 12 seconds in milliseconds
export const GAME_SPEEDS = {
NORMAL: 1, FAST: 5, SUPER_FAST: 10
};
export const INITIAL_GAME_SPEED = GAME_SPEEDS.NORMAL;
NORMAL: 1,
FAST: 5,
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 = {
celery: {
id: 'celery',
name: 'Celery',
growthTicks: 36,
waterPerTick: 1/20,
waterPerTick: 1 / 20,
yield: 1,
yieldType: 'celery'
yieldType: 'celery',
},
corn: {
id: 'corn',
name: 'Corn',
growthTicks: 120,
waterPerTick: 1/40,
waterPerTick: 1 / 40,
yield: 5,
yieldType: 'corn'
}
};
yieldType: 'corn',
},
}
export const INITIAL_INVENTORY = {
'celery_seed': 8
};
celery_seed: 8,
}
export interface MarketItem {
id: string;
name: string;
emoji: string;
buyPrice: number | null; // null means not buyable
sellPrice: number | null; // null means not sellable
id: string
name: string
emoji: string
buyPrice: number | null // null means not buyable
sellPrice: number | null // null means not sellable
}
export const MARKET_ITEMS: Record<string, MarketItem> = {
'celery_seed': { id: 'celery_seed', name: 'Celery Seeds', 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 },
};
celery_seed: {
id: 'celery_seed',
name: 'Celery Seeds',
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'
export const useGameTick = () => {
const tick = useGameStore(state => state.tick)
const gameSpeed = useGameStore(state => state.gameSpeed)
const tick = useGameStore((state) => state.tick)
const gameSpeed = useGameStore((state) => state.gameSpeed)
const intervalRef = useRef<number | null>(null)
useEffect(() => {
if (intervalRef.current !== null) {
clearInterval(intervalRef.current)
}
const adjustedInterval = TICK_INTERVAL / gameSpeed;
const adjustedInterval = TICK_INTERVAL / gameSpeed
intervalRef.current = window.setInterval(() => {
tick()
}, adjustedInterval)
@ -23,4 +23,4 @@ export const useGameTick = () => {
}
}
}, [tick, gameSpeed])
};
}

View file

@ -24,7 +24,9 @@ git /* Base styles */
/* Global styles */
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;
padding: 0;
box-sizing: border-box;
@ -33,4 +35,3 @@ body {
* {
box-sizing: inherit;
}

View file

@ -1,40 +1,46 @@
import { create } from 'zustand';
import { produce } from 'immer';
import {
INITIAL_CASH,
INITIAL_FIELD_SIZE,
FIELD_UPGRADE_COSTS,
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';
import { saveGame, loadGame } from '../utils/saveSystem';
MARKET_ITEMS,
} from '../constants'
import { GameState, PlotState } from '../types'
import { saveGame, loadGame } from '../utils/saveSystem'
const initializeField = (size: number): PlotState[][] => {
return Array(size).fill(0).map(() =>
Array(size).fill(0).map(() => ({
moisture: 0
}))
);
};
return Array(size)
.fill(0)
.map(() =>
Array(size)
.fill(0)
.map(() => ({
moisture: 0,
})),
)
}
export const useGameStore = create<GameState & {
plant: () => void;
water: () => void;
harvest: () => 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) => ({
export const useGameStore = create<
GameState & {
plant: () => void
water: () => void
harvest: () => 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,
@ -46,37 +52,41 @@ export const useGameStore = create<GameState & {
tickCount: 0,
assignCrop: (row, col, cropId) => {
set(produce(state => {
state.plots[row][col].intended = cropId;
}));
set(
produce((state) => {
state.plots[row][col].intended = cropId
}),
)
},
plant: () => {
const { plots, inventory, actionCooldown } = get();
const { plots, inventory, actionCooldown } = get()
if (actionCooldown > 0) {
return;
return
}
for (let row = 0; row < plots.length; row++) {
for (let col = 0; col < plots[row].length; col++) {
const plot = plots[row][col];
const plot = plots[row][col]
if (plot.intended && !plot.current) {
const seedId = `${plot.intended}_seed`;
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;
}));
return;
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
}),
)
return
}
}
}
@ -84,163 +94,203 @@ export const useGameStore = create<GameState & {
},
water: () => {
const { plots, actionCooldown } = get();
const { plots, actionCooldown } = get()
if (actionCooldown > 0) {
return;
return
}
let driestRow = -1;
let driestCol = -1;
let lowestMoisture = 1;
let driestRow = -1
let driestCol = -1
let lowestMoisture = 1
for (let row = 0; row < plots.length; row++) {
for (let col = 0; col < plots[row].length; col++) {
if (plots[row][col].current && plots[row][col].moisture < lowestMoisture) {
lowestMoisture = plots[row][col].moisture;
driestRow = row;
driestCol = col;
if (
plots[row][col].current &&
plots[row][col].moisture < lowestMoisture
) {
lowestMoisture = plots[row][col].moisture
driestRow = row
driestCol = col
}
}
}
if (driestRow >= 0 && driestCol >= 0) {
set(produce(state => {
state.plots[driestRow][driestCol].moisture = 1;
state.actionCooldown = COOLDOWN_DURATION;
}));
set(
produce((state) => {
state.plots[driestRow][driestCol].moisture = 1
state.actionCooldown = COOLDOWN_DURATION
}),
)
}
},
harvest: () => {
const { plots, actionCooldown } = get();
const { plots, actionCooldown } = get()
if (actionCooldown > 0) {
return;
return
}
for (let row = 0; row < plots.length; row++) {
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) {
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.actionCooldown = COOLDOWN_DURATION;
}));
return;
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.actionCooldown = COOLDOWN_DURATION
}),
)
return
}
}
}
},
tick: () => {
set(produce(state => {
// Update plots
state.plots.forEach((row: PlotState[], rowIndex: number) => {
row.forEach((plot: PlotState, colIndex: number) => {
if (!plot.current || plot.current.mature) {
return;
}
const crop = CROPS[plot.current.cropId];
const waterNeeded = crop.waterPerTick;
if (plot.moisture >= waterNeeded) {
const newProgress = plot.current.progress + 1;
const mature = newProgress >= crop.growthTicks;
state.plots[rowIndex][colIndex].moisture = plot.moisture - waterNeeded;
state.plots[rowIndex][colIndex].current.progress = newProgress;
state.plots[rowIndex][colIndex].current.mature = mature;
}
});
});
set(
produce((state) => {
// Update plots
state.plots.forEach((row: PlotState[], rowIndex: number) => {
row.forEach((plot: PlotState, colIndex: number) => {
if (!plot.current || plot.current.mature) {
return
}
state.tickCount = state.tickCount + 1;
}));
const crop = CROPS[plot.current.cropId]
const waterNeeded = crop.waterPerTick
if (plot.moisture >= waterNeeded) {
const newProgress = plot.current.progress + 1
const mature = newProgress >= crop.growthTicks
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: () => {
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);
}));
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;
}));
set(
produce((state) => {
state.gameSpeed = speed
}),
)
},
setActionCooldown: (cooldown) => {
set(produce(state => {
state.actionCooldown = cooldown;
}));
set(
produce((state) => {
state.actionCooldown = cooldown
}),
)
},
buyItem: (itemId) => {
const { cash } = get();
const item = MARKET_ITEMS[itemId];
const { cash } = get()
const item = MARKET_ITEMS[itemId]
if (!item || item.buyPrice === null) {
return;
return
}
if (cash < item.buyPrice) {
return;
return
}
set(produce(state => {
state.cash -= item.buyPrice;
state.inventory[itemId] = (state.inventory[itemId] || 0) + 1;
}));
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];
const { inventory } = get()
const item = MARKET_ITEMS[itemId]
if (!item || item.sellPrice === null || !inventory[itemId] || inventory[itemId] <= 0) {
return;
if (
!item ||
item.sellPrice === null ||
!inventory[itemId] ||
inventory[itemId] <= 0
) {
return
}
set(produce(state => {
state.cash += item.sellPrice;
state.inventory[itemId] -= 1;
}));
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);
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);
const savedState = loadGame(slot)
if (savedState) {
set(savedState);
set(savedState)
}
}
}));
},
}))

View file

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

View file

@ -1,29 +1,29 @@
import { GameState } from '../types';
import { GameState } from '../types'
const SAVE_SLOT_PREFIX = 'dionysian_idle_save_';
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;
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;
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);
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;
console.error('Failed to load game:', error)
return null
}
};
}
export const hasSaveInSlot = (slot: number): boolean => {
return !!localStorage.getItem(`${SAVE_SLOT_PREFIX}${slot}`);
};
return !!localStorage.getItem(`${SAVE_SLOT_PREFIX}${slot}`)
}