Compare commits

..

No commits in common. "d75e9f3cbb1d42cda9642f9cb6e334fe912e4b97" and "d4dda5b3e482e61ce972c2cef22cc3ce716166fe" have entirely different histories.

16 changed files with 545 additions and 759 deletions

View file

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

17
package-lock.json generated
View file

@ -24,7 +24,6 @@
"@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"
} }
@ -1629,22 +1628,6 @@
"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,8 +7,7 @@
"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",
@ -27,7 +26,6 @@
"@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,60 +1,25 @@
import React, { useEffect } from 'react' import React from 'react';
import { import { Tabs, TabsContent, TabsList, TabsTrigger } from './components/CustomTabs';
Tabs, import { useGameTick } from './hooks/useGameTick';
TabsContent, import Field from './components/Field';
TabsList, import Warehouse from './components/Warehouse';
TabsTrigger, import Market from './components/Market';
} from './components/CustomTabs' import { ActionCooldown } from './components/ActionCooldown';
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>
@ -65,9 +30,7 @@ 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> <TabsTrigger value="temple" disabled>Temple</TabsTrigger>
Temple
</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="fields"> <TabsContent value="fields">
<Field /> <Field />
@ -81,7 +44,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,13 +39,10 @@ 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 = ( const getTabButtonStyle = (active: boolean, disabled: boolean): React.CSSProperties => ({
active: boolean,
disabled: boolean,
): React.CSSProperties => ({
display: 'inline-flex', display: 'inline-flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
@ -60,51 +57,51 @@ const getTabButtonStyle = (
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
@ -115,16 +112,13 @@ export const TabsTrigger: React.FC<TabsTriggerProps> = ({
> >
{children} {children}
</button> </button>
) );
} };
export const TabsContent: React.FC<TabsContentProps> = ({ export const TabsContent: React.FC<TabsContentProps> = ({ value, children }) => {
value, const { value: selectedValue } = React.useContext(TabsContext);
children,
}) => {
const { value: selectedValue } = React.useContext(TabsContext)
if (selectedValue !== value) return null if (selectedValue !== value) return null;
return <div style={tabContentStyle}>{children}</div> 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,8 +114,8 @@ 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',
@ -125,13 +125,10 @@ const getMaturityProgressContainerStyle = (): React.CSSProperties => ({
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 = ( const getMaturityProgressBarStyle = (progress: number, total: number): React.CSSProperties => ({
progress: number,
total: number,
): React.CSSProperties => ({
position: 'absolute', position: 'absolute',
bottom: 0, bottom: 0,
left: 0, left: 0,
@ -140,8 +137,8 @@ const getMaturityProgressBarStyle = (
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 {
@ -153,22 +150,22 @@ const FieldComponent: React.FC = () => {
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]
@ -179,7 +176,7 @@ const FieldComponent: React.FC = () => {
<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)}
@ -193,7 +190,7 @@ const FieldComponent: React.FC = () => {
<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)}
@ -231,9 +228,9 @@ const FieldComponent: React.FC = () => {
<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
@ -241,33 +238,21 @@ const FieldComponent: React.FC = () => {
style={getPlotStyle(bgColor)} style={getPlotStyle(bgColor)}
onClick={() => handlePlotClick(rowIndex, colIndex)} onClick={() => handlePlotClick(rowIndex, colIndex)}
> >
{plot.intended && !plot.current && ( {plot.intended && !plot.current && <div>🌱 {CROPS[plot.intended]?.name}</div>}
<div>🌱 {CROPS[plot.intended]?.name}</div> {plot.current && <div>{plot.current.mature ? '🌿' : '🌱'} {CROPS[plot.current.cropId]?.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 <div style={getMaturityProgressBarStyle(plot.current.progress, CROPS[plot.current.cropId].growthTicks)} />
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,59 +1,53 @@
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> = ({ const FloatingMessage: React.FC<FloatingMessageProps> = ({ message, startPosition, onComplete }) => {
message, const [position, setPosition] = useState({ y: startPosition.y });
startPosition, const [opacity, setOpacity] = useState(1);
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 <div style={{
style={{
position: 'absolute', position: 'absolute',
left: startPosition.x, left: startPosition.x,
top: position.y, top: position.y,
opacity, opacity,
fontWeight: 'bold', fontWeight: 'bold',
pointerEvents: 'none', pointerEvents: 'none',
zIndex: 100, 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,126 +21,115 @@ 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< const [floatingMessages, setFloatingMessages] = useState<FloatingMessageData[]>([]);
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 ( if (!item || item.sellPrice === null || !inventory[itemId] || inventory[itemId] <= 0) return;
!item ||
item.sellPrice === null ||
!inventory[itemId] ||
inventory[itemId] <= 0
)
return
sellItem(itemId) sellItem(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: `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( .filter(([_, item]) => item.sellPrice !== null && inventory[item.id] && inventory[item.id] > 0)
([_, item]) => .map(([_, 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>
@ -163,7 +152,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>
@ -196,7 +185,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>
@ -216,7 +205,7 @@ 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}
@ -225,7 +214,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,43 +51,43 @@ 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}>
@ -100,19 +100,18 @@ const WarehouseComponent: React.FC = () => {
<h3 style={inventoryTitleStyle}>Inventory:</h3> <h3 style={inventoryTitleStyle}>Inventory:</h3>
<div style={getInventoryGridStyle()}> <div style={getInventoryGridStyle()}>
{Object.entries(inventory).map( {Object.entries(inventory).map(([id, count]) => (
([id, count]) =>
count > 0 && ( count > 0 && (
<div key={id} style={inventoryItemStyle}> <div key={id} style={inventoryItemStyle}>
<div style={itemIconStyle}>{getItemIcon(id)}</div> <div style={itemIconStyle}>{getItemIcon(id)}</div>
<h4 style={itemNameStyle}>{formatItemName(id)}</h4> <h4 style={itemNameStyle}>{formatItemName(id)}</h4>
<p>Quantity: {count}</p> <p>Quantity: {count}</p>
</div> </div>
),
)}
</div>
</div>
) )
} ))}
</div>
</div>
);
};
export default WarehouseComponent export default WarehouseComponent;

View file

@ -1,70 +1,50 @@
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, NORMAL: 1, FAST: 5, SUPER_FAST: 10
FAST: 5, };
SUPER_FAST: 10, export const INITIAL_GAME_SPEED = GAME_SPEEDS.NORMAL;
}
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: { 'celery_seed': { id: 'celery_seed', name: 'Celery Seeds', emoji: '🌰', buyPrice: 1, sellPrice: null },
id: 'celery_seed', 'corn_seed': { id: 'corn_seed', name: 'Corn Seeds', emoji: '🌰', buyPrice: 5, sellPrice: null },
name: 'Celery Seeds', 'celery': { id: 'celery', name: 'Celery', emoji: '🥬', buyPrice: null, sellPrice: 5 },
emoji: '🌰', 'corn': { id: 'corn', name: 'Corn', emoji: '🌽', buyPrice: null, sellPrice: 8 },
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,8 +3,8 @@ 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(() => {
@ -12,7 +12,7 @@ export const useGameTick = () => {
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,9 +24,7 @@ git /* Base styles */
/* Global styles */ /* Global styles */
body { body {
font-family: font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
-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;
@ -35,3 +33,4 @@ body {
* { * {
box-sizing: inherit; box-sizing: inherit;
} }

View file

@ -1,5 +1,5 @@
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,
@ -8,39 +8,30 @@ import {
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) return Array(size).fill(0).map(() =>
.fill(0) Array(size).fill(0).map(() => ({
.map(() => moisture: 0
Array(size) }))
.fill(0) );
.map(() => ({ };
moisture: 0,
})),
)
}
export const useGameStore = create< export const useGameStore = create<GameState & {
GameState & { plant: () => void;
plant: () => void water: () => void;
water: () => void harvest: () => void;
harvest: () => void tick: () => void;
tick: () => void assignCrop: (row: number, col: number, cropId: string) => void;
assignCrop: (row: number, col: number, cropId: string) => void upgradeField: () => void;
upgradeField: () => void setGameSpeed: (speed: number) => void;
setGameSpeed: (speed: number) => void setActionCooldown: (cooldown: number) => void;
setActionCooldown: (cooldown: number) => void buyItem: (itemId: string) => void;
buyItem: (itemId: string) => void sellItem: (itemId: string) => void;
sellItem: (itemId: string) => void }>((set, get) => ({
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,
@ -49,44 +40,39 @@ export const useGameStore = create<
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( set(produce(state => {
produce((state) => { state.plots[row][col].intended = cropId;
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( set(produce(state => {
produce((state) => {
state.plots[row][col].current = { state.plots[row][col].current = {
cropId: plot.intended!, cropId: plot.intended!,
progress: 0, progress: 0,
mature: false, mature: false
} };
state.inventory[seedId] = state.inventory[seedId] - 1 state.inventory[seedId] = state.inventory[seedId] - 1;
state.actionCooldown = COOLDOWN_DURATION state.actionCooldown = COOLDOWN_DURATION;
}), }));
) return;
return
} }
} }
} }
@ -94,203 +80,153 @@ export const useGameStore = create<
}, },
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 ( if (plots[row][col].current && plots[row][col].moisture < lowestMoisture) {
plots[row][col].current && lowestMoisture = plots[row][col].moisture;
plots[row][col].moisture < lowestMoisture driestRow = row;
) { driestCol = col;
lowestMoisture = plots[row][col].moisture
driestRow = row
driestCol = col
} }
} }
} }
if (driestRow >= 0 && driestCol >= 0) { if (driestRow >= 0 && driestCol >= 0) {
set( set(produce(state => {
produce((state) => { state.plots[driestRow][driestCol].moisture = 1;
state.plots[driestRow][driestCol].moisture = 1 state.actionCooldown = COOLDOWN_DURATION;
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( set(produce(state => {
produce((state) => { state.plots[row][col].current = undefined;
state.plots[row][col].current = undefined state.inventory[yieldItem] = (state.inventory[yieldItem] || 0) + yieldAmount;
state.inventory[yieldItem] = state.actionCooldown = COOLDOWN_DURATION;
(state.inventory[yieldItem] || 0) + yieldAmount }));
state.actionCooldown = COOLDOWN_DURATION return;
}),
)
return
} }
} }
} }
}, },
tick: () => { tick: () => {
set( set(produce(state => {
produce((state) => { // Update cooldown
if (state.actionCooldown > 0) {
state.actionCooldown = Math.max(0, state.actionCooldown - (COOLDOWN_DURATION / 20));
}
// Update plots // Update plots
state.plots.forEach((row: PlotState[], rowIndex: number) => { state.plots.forEach((row: PlotState[], rowIndex: number) => {
row.forEach((plot: PlotState, colIndex: number) => { row.forEach((plot: PlotState, colIndex: number) => {
if (!plot.current || plot.current.mature) { if (!plot.current || plot.current.mature) {
return return;
} }
const crop = CROPS[plot.current.cropId] const crop = CROPS[plot.current.cropId];
const waterNeeded = crop.waterPerTick const waterNeeded = crop.waterPerTick;
if (plot.moisture >= waterNeeded) { if (plot.moisture >= waterNeeded) {
const newProgress = plot.current.progress + 1 const newProgress = plot.current.progress + 1;
const mature = newProgress >= crop.growthTicks const mature = newProgress >= crop.growthTicks;
state.plots[rowIndex][colIndex].moisture = state.plots[rowIndex][colIndex].moisture = plot.moisture - waterNeeded;
plot.moisture - waterNeeded state.plots[rowIndex][colIndex].current.progress = newProgress;
state.plots[rowIndex][colIndex].current.progress = newProgress state.plots[rowIndex][colIndex].current.mature = mature;
state.plots[rowIndex][colIndex].current.mature = mature
} }
}) });
}) });
}));
state.tickCount = state.tickCount + 1
}),
)
}, },
upgradeField: () => { upgradeField: () => {
set( set(produce(state => {
produce((state) => {
if (state.fieldSize >= state.maxFieldSize) { if (state.fieldSize >= state.maxFieldSize) {
return return;
} }
const upgradeIndex = state.fieldSize - INITIAL_FIELD_SIZE const upgradeIndex = state.fieldSize - INITIAL_FIELD_SIZE;
const cost = state.fieldUpgradeCosts[upgradeIndex] const cost = state.fieldUpgradeCosts[upgradeIndex];
if (state.cash < cost) { if (state.cash < cost) {
return return;
} }
const newSize = state.fieldSize + 1 const newSize = state.fieldSize + 1;
state.cash = state.cash - cost state.cash = state.cash - cost;
state.fieldSize = newSize state.fieldSize = newSize;
state.plots = initializeField(newSize) state.plots = initializeField(newSize);
}), }));
)
}, },
setGameSpeed: (speed) => { setGameSpeed: (speed) => {
set( set(produce(state => {
produce((state) => { state.gameSpeed = speed;
state.gameSpeed = speed }));
}),
)
}, },
setActionCooldown: (cooldown) => { setActionCooldown: (cooldown) => {
set( set(produce(state => {
produce((state) => { state.actionCooldown = cooldown;
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( set(produce(state => {
produce((state) => { state.cash -= item.buyPrice;
state.cash -= item.buyPrice state.inventory[itemId] = (state.inventory[itemId] || 0) + 1;
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 ( if (!item || item.sellPrice === null || !inventory[itemId] || inventory[itemId] <= 0) {
!item || return;
item.sellPrice === null ||
!inventory[itemId] ||
inventory[itemId] <= 0
) {
return
} }
set( set(produce(state => {
produce((state) => { state.cash += item.sellPrice;
state.cash += item.sellPrice state.inventory[itemId] -= 1;
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;
} };
} }

View file

@ -1,29 +0,0 @@
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}`)
}