diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..ba4e1a9 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "semi": false, + "singleQuote": true +} + diff --git a/package-lock.json b/package-lock.json index b919825..bb54a0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 50d7aec..321f904 100644 --- a/package.json +++ b/package.json @@ -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" } diff --git a/src/App.tsx b/src/App.tsx index 5f7c60d..e681d3d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() { Fields Warehouse Market - Temple + + Temple + @@ -73,7 +81,7 @@ function App() { - ); + ) } export default App diff --git a/src/components/ActionCooldown.tsx b/src/components/ActionCooldown.tsx index 4dbd629..4028a97 100644 --- a/src/components/ActionCooldown.tsx +++ b/src/components/ActionCooldown.tsx @@ -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 ( - ); -}; + ) +} diff --git a/src/components/CustomTabs.tsx b/src/components/CustomTabs.tsx index 277a53a..69c1150 100644 --- a/src/components/CustomTabs.tsx +++ b/src/components/CustomTabs.tsx @@ -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 = ({ defaultValue, children }) => { - const [value, setValue] = useState(defaultValue); + const [value, setValue] = useState(defaultValue) return (
{children}
- ); -}; + ) +} export const TabsList: React.FC = ({ children, style }) => { - return
{children}
; -}; + return
{children}
+} -export const TabsTrigger: React.FC = ({ - value, - children, +export const TabsTrigger: React.FC = ({ + 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 ( - - ); -}; + ) +} -export const TabsContent: React.FC = ({ value, children }) => { - const { value: selectedValue } = React.useContext(TabsContext); - - if (selectedValue !== value) return null; - - return
{children}
; -}; +export const TabsContent: React.FC = ({ + value, + children, +}) => { + const { value: selectedValue } = React.useContext(TabsContext) + + if (selectedValue !== value) return null + + return
{children}
+} diff --git a/src/components/Field.tsx b/src/components/Field.tsx index 925eb0c..12d2b8d 100644 --- a/src/components/Field.tsx +++ b/src/components/Field.tsx @@ -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(null); + const [selectedCrop, setSelectedCrop] = useState(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 (

Fields

- +

Game Speed:

- {availableSpeeds.map(speed => ( + {availableSpeeds.map((speed) => (
- +

Select a crop to plant:

- {Object.values(CROPS).map(crop => ( + {Object.values(CROPS).map((crop) => ( ))}
- +
- - -
- +
- {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 ( -
handlePlotClick(rowIndex, colIndex)} > - {plot.intended && !plot.current &&
🌱 {CROPS[plot.intended]?.name}
} - {plot.current &&
{plot.current.mature ? '🌿' : '🌱'} {CROPS[plot.current.cropId]?.name}
} + {plot.intended && !plot.current && ( +
🌱 {CROPS[plot.intended]?.name}
+ )} + {plot.current && ( +
+ {plot.current.mature ? '🌿' : '🌱'}{' '} + {CROPS[plot.current.cropId]?.name} +
+ )}
{plot.current && !plot.current.mature && (
-
+
)}
- ); - }) + ) + }), )}
- ); -}; + ) +} -export default FieldComponent; +export default FieldComponent diff --git a/src/components/FloatingMessage.tsx b/src/components/FloatingMessage.tsx index fb56d1a..4891cc2 100644 --- a/src/components/FloatingMessage.tsx +++ b/src/components/FloatingMessage.tsx @@ -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 = ({ message, startPosition, onComplete }) => { - const [position, setPosition] = useState({ y: startPosition.y }); - const [opacity, setOpacity] = useState(1); - +const FloatingMessage: React.FC = ({ + 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 ( -
+
{message}
- ); -}; + ) +} -export default FloatingMessage; +export default FloatingMessage diff --git a/src/components/Market.tsx b/src/components/Market.tsx index 6dc3e1a..60a4cd2 100644 --- a/src/components/Market.tsx +++ b/src/components/Market.tsx @@ -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([]); + 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 ( Market - + Cash: ${cash.toFixed(2)} - + {sellableItems.length > 0 && ( <> Sell Items @@ -152,7 +163,7 @@ const MarketComponent: React.FC = () => { - {sellableItems.map(item => ( + {sellableItems.map((item) => ( {item.emoji} @@ -161,7 +172,7 @@ const MarketComponent: React.FC = () => { ${item.sellPrice} {inventory[item.id] || 0} - handleSell(item.id, e)} disabled={!inventory[item.id] || inventory[item.id] <= 0} > @@ -174,7 +185,7 @@ const MarketComponent: React.FC = () => { )} - + Buy Items @@ -185,7 +196,7 @@ const MarketComponent: React.FC = () => { - {buyableItems.map(item => ( + {buyableItems.map((item) => ( {item.emoji} @@ -193,7 +204,7 @@ const MarketComponent: React.FC = () => { ${item.buyPrice} - handleBuy(item.id, e)} disabled={item.buyPrice ? cash < item.buyPrice : true} > @@ -204,8 +215,8 @@ const MarketComponent: React.FC = () => { ))} - - {floatingMessages.map(msg => ( + + {floatingMessages.map((msg) => ( { /> ))} - ); -}; + ) +} -export default MarketComponent; +export default MarketComponent diff --git a/src/components/Warehouse.tsx b/src/components/Warehouse.tsx index f394acd..fd435d2 100644 --- a/src/components/Warehouse.tsx +++ b/src/components/Warehouse.tsx @@ -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 = { celery: '🥬', corn: '🌽', - }; - - return cropIcons[id] || '📦'; -}; + } + + return cropIcons[id] || '📦' +} const WarehouseComponent: React.FC = () => { - const { inventory, cash } = useGameStore(); + const { inventory, cash } = useGameStore() return (

Warehouse

- +

Cash: ${cash.toFixed(2)}

- +

Inventory:

- +
- {Object.entries(inventory).map(([id, count]) => ( - count > 0 && ( -
-
{getItemIcon(id)}
-

{formatItemName(id)}

-

Quantity: {count}

-
- ) - ))} + {Object.entries(inventory).map( + ([id, count]) => + count > 0 && ( +
+
{getItemIcon(id)}
+

{formatItemName(id)}

+

Quantity: {count}

+
+ ), + )}
- ); -}; + ) +} -export default WarehouseComponent; +export default WarehouseComponent diff --git a/src/constants/index.ts b/src/constants/index.ts index 5b9fd6c..f35c2a3 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -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 = { - '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 }, +} diff --git a/src/hooks/useGameTick.ts b/src/hooks/useGameTick.ts index 5b7f385..f92dbec 100644 --- a/src/hooks/useGameTick.ts +++ b/src/hooks/useGameTick.ts @@ -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(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]) -}; +} diff --git a/src/index.css b/src/index.css index 3074cf8..849010f 100644 --- a/src/index.css +++ b/src/index.css @@ -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; } - diff --git a/src/store/useGameStore.ts b/src/store/useGameStore.ts index de94914..9f48ed9 100644 --- a/src/store/useGameStore.ts +++ b/src/store/useGameStore.ts @@ -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 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 { - 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 { - 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) } - } -})); + }, +})) diff --git a/src/types/index.ts b/src/types/index.ts index fb12142..6670f81 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -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; - fieldSize: number; - maxFieldSize: number; - fieldUpgradeCosts: number[]; - plots: PlotState[][]; - gameSpeed: number; - actionCooldown: number; + cash: number + inventory: Record + 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 + } } diff --git a/src/utils/saveSystem.ts b/src/utils/saveSystem.ts index 0f045c9..1d1446b 100644 --- a/src/utils/saveSystem.ts +++ b/src/utils/saveSystem.ts @@ -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}`); -}; \ No newline at end of file + return !!localStorage.getItem(`${SAVE_SLOT_PREFIX}${slot}`) +}