Set up prettier.
This commit is contained in:
parent
39309aa9e7
commit
d75e9f3cbb
5
.prettierrc
Normal file
5
.prettierrc
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
||||||
|
|
||||||
17
package-lock.json
generated
17
package-lock.json
generated
|
|
@ -24,6 +24,7 @@
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"@wyw-in-js/vite": "^0.6.0",
|
"@wyw-in-js/vite": "^0.6.0",
|
||||||
|
"prettier": "3.5.3",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^6.0.1"
|
"vite": "^6.0.1"
|
||||||
}
|
}
|
||||||
|
|
@ -1628,6 +1629,22 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/prettier": {
|
||||||
|
"version": "3.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
|
||||||
|
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin/prettier.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"type-check": "tsc --noEmit",
|
"type-check": "tsc --noEmit",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"lint": "prettier --write src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@linaria/core": "^6.3.0",
|
"@linaria/core": "^6.3.0",
|
||||||
|
|
@ -26,6 +27,7 @@
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"@wyw-in-js/vite": "^0.6.0",
|
"@wyw-in-js/vite": "^0.6.0",
|
||||||
|
"prettier": "3.5.3",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^6.0.1"
|
"vite": "^6.0.1"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
68
src/App.tsx
68
src/App.tsx
|
|
@ -1,47 +1,53 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react'
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from './components/CustomTabs';
|
import {
|
||||||
import { useGameTick } from './hooks/useGameTick';
|
Tabs,
|
||||||
import Field from './components/Field';
|
TabsContent,
|
||||||
import Warehouse from './components/Warehouse';
|
TabsList,
|
||||||
import Market from './components/Market';
|
TabsTrigger,
|
||||||
import { ActionCooldown } from './components/ActionCooldown';
|
} from './components/CustomTabs'
|
||||||
import { useGameStore } from './store/useGameStore';
|
import { useGameTick } from './hooks/useGameTick'
|
||||||
import { hasSaveInSlot } from './utils/saveSystem';
|
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();
|
const { saveToSlot, loadFromSlot } = useGameStore()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
const slot = {
|
const slot =
|
||||||
'1': 1,
|
{
|
||||||
'2': 2,
|
'1': 1,
|
||||||
'3': 3,
|
'2': 2,
|
||||||
'!': 1,
|
'3': 3,
|
||||||
'@': 2,
|
'!': 1,
|
||||||
'#': 3
|
'@': 2,
|
||||||
}[e.key] ?? null
|
'#': 3,
|
||||||
|
}[e.key] ?? null
|
||||||
|
|
||||||
if (slot !== null) {
|
if (slot !== null) {
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
saveToSlot(slot);
|
saveToSlot(slot)
|
||||||
console.log(`Game saved to slot ${slot}`);
|
console.log(`Game saved to slot ${slot}`)
|
||||||
} else if (hasSaveInSlot(slot)) {
|
} else if (hasSaveInSlot(slot)) {
|
||||||
loadFromSlot(slot);
|
loadFromSlot(slot)
|
||||||
console.log(`Game loaded from slot ${slot}`);
|
console.log(`Game loaded from slot ${slot}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -59,7 +65,9 @@ function App() {
|
||||||
<TabsTrigger value="fields">Fields</TabsTrigger>
|
<TabsTrigger value="fields">Fields</TabsTrigger>
|
||||||
<TabsTrigger value="warehouse">Warehouse</TabsTrigger>
|
<TabsTrigger value="warehouse">Warehouse</TabsTrigger>
|
||||||
<TabsTrigger value="market">Market</TabsTrigger>
|
<TabsTrigger value="market">Market</TabsTrigger>
|
||||||
<TabsTrigger value="temple" disabled>Temple</TabsTrigger>
|
<TabsTrigger value="temple" disabled>
|
||||||
|
Temple
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="fields">
|
<TabsContent value="fields">
|
||||||
<Field />
|
<Field />
|
||||||
|
|
@ -73,7 +81,7 @@ function App() {
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
export default App
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,35 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
export interface TabProps {
|
export interface TabProps {
|
||||||
value: string;
|
value: string
|
||||||
children: React.ReactNode;
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TabsProps {
|
export interface TabsProps {
|
||||||
defaultValue: string;
|
defaultValue: string
|
||||||
children: React.ReactNode;
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TabsListProps {
|
export interface TabsListProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TabsContentProps {
|
export interface TabsContentProps {
|
||||||
value: string;
|
value: string
|
||||||
children: React.ReactNode;
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TabsTriggerProps {
|
export interface TabsTriggerProps {
|
||||||
value: string;
|
value: string
|
||||||
children: React.ReactNode;
|
children: React.ReactNode
|
||||||
disabled?: boolean;
|
disabled?: boolean
|
||||||
onClick?: () => void;
|
onClick?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabsContainerStyle: React.CSSProperties = {
|
const tabsContainerStyle: React.CSSProperties = {
|
||||||
width: '100%'
|
width: '100%',
|
||||||
};
|
}
|
||||||
|
|
||||||
const tabsListContainerStyle: React.CSSProperties = {
|
const tabsListContainerStyle: React.CSSProperties = {
|
||||||
display: 'inline-flex',
|
display: 'inline-flex',
|
||||||
|
|
@ -39,10 +39,13 @@ const tabsListContainerStyle: React.CSSProperties = {
|
||||||
borderRadius: '0.5rem',
|
borderRadius: '0.5rem',
|
||||||
backgroundColor: '#f4f4f5',
|
backgroundColor: '#f4f4f5',
|
||||||
padding: '0.25rem',
|
padding: '0.25rem',
|
||||||
color: '#71717a'
|
color: '#71717a',
|
||||||
};
|
}
|
||||||
|
|
||||||
const getTabButtonStyle = (active: boolean, disabled: boolean): React.CSSProperties => ({
|
const getTabButtonStyle = (
|
||||||
|
active: boolean,
|
||||||
|
disabled: boolean,
|
||||||
|
): React.CSSProperties => ({
|
||||||
display: 'inline-flex',
|
display: 'inline-flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
|
@ -57,68 +60,71 @@ const getTabButtonStyle = (active: boolean, disabled: boolean): React.CSSPropert
|
||||||
boxShadow: active ? '0 1px 3px rgba(0, 0, 0, 0.1)' : 'none',
|
boxShadow: active ? '0 1px 3px rgba(0, 0, 0, 0.1)' : 'none',
|
||||||
opacity: disabled ? 0.5 : 1,
|
opacity: disabled ? 0.5 : 1,
|
||||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||||
border: 'none'
|
border: 'none',
|
||||||
});
|
})
|
||||||
|
|
||||||
const tabContentStyle: React.CSSProperties = {
|
const tabContentStyle: React.CSSProperties = {
|
||||||
marginTop: '0.5rem',
|
marginTop: '0.5rem',
|
||||||
outline: 'none'
|
outline: 'none',
|
||||||
};
|
}
|
||||||
|
|
||||||
const TabsContext = React.createContext<{
|
const TabsContext = React.createContext<{
|
||||||
value: string;
|
value: string
|
||||||
setValue: (value: string) => void;
|
setValue: (value: string) => void
|
||||||
}>({
|
}>({
|
||||||
value: '',
|
value: '',
|
||||||
setValue: () => {},
|
setValue: () => {},
|
||||||
});
|
})
|
||||||
|
|
||||||
export const Tabs: React.FC<TabsProps> = ({ defaultValue, children }) => {
|
export const Tabs: React.FC<TabsProps> = ({ defaultValue, children }) => {
|
||||||
const [value, setValue] = useState(defaultValue);
|
const [value, setValue] = useState(defaultValue)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabsContext.Provider value={{ value, setValue }}>
|
<TabsContext.Provider value={{ value, setValue }}>
|
||||||
<div style={tabsContainerStyle}>{children}</div>
|
<div style={tabsContainerStyle}>{children}</div>
|
||||||
</TabsContext.Provider>
|
</TabsContext.Provider>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export const TabsList: React.FC<TabsListProps> = ({ children, style }) => {
|
export const TabsList: React.FC<TabsListProps> = ({ children, style }) => {
|
||||||
return <div style={{...tabsListContainerStyle, ...style}}>{children}</div>;
|
return <div style={{ ...tabsListContainerStyle, ...style }}>{children}</div>
|
||||||
};
|
}
|
||||||
|
|
||||||
export const TabsTrigger: React.FC<TabsTriggerProps> = ({
|
export const TabsTrigger: React.FC<TabsTriggerProps> = ({
|
||||||
value,
|
value,
|
||||||
children,
|
children,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
onClick
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
const { value: selectedValue, setValue } = React.useContext(TabsContext);
|
const { value: selectedValue, setValue } = React.useContext(TabsContext)
|
||||||
const active = selectedValue === value;
|
const active = selectedValue === value
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
setValue(value);
|
setValue(value)
|
||||||
if (onClick) onClick();
|
if (onClick) onClick()
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
style={getTabButtonStyle(active, disabled)}
|
style={getTabButtonStyle(active, disabled)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export const TabsContent: React.FC<TabsContentProps> = ({ value, children }) => {
|
export const TabsContent: React.FC<TabsContentProps> = ({
|
||||||
const { value: selectedValue } = React.useContext(TabsContext);
|
value,
|
||||||
|
children,
|
||||||
if (selectedValue !== value) return null;
|
}) => {
|
||||||
|
const { value: selectedValue } = React.useContext(TabsContext)
|
||||||
return <div style={tabContentStyle}>{children}</div>;
|
|
||||||
};
|
if (selectedValue !== value) return null
|
||||||
|
|
||||||
|
return <div style={tabContentStyle}>{children}</div>
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,50 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react'
|
||||||
import { useGameStore } from '../store/useGameStore';
|
import { useGameStore } from '../store/useGameStore'
|
||||||
import { CROPS } from '../constants';
|
import { CROPS } from '../constants'
|
||||||
|
|
||||||
const fieldContainerStyle: React.CSSProperties = {
|
const fieldContainerStyle: React.CSSProperties = {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: '1rem',
|
gap: '1rem',
|
||||||
padding: '1rem'
|
padding: '1rem',
|
||||||
};
|
}
|
||||||
|
|
||||||
const titleStyle: React.CSSProperties = {
|
const titleStyle: React.CSSProperties = {
|
||||||
fontSize: '1.5rem',
|
fontSize: '1.5rem',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
marginBottom: '1rem',
|
marginBottom: '1rem',
|
||||||
textAlign: 'center'
|
textAlign: 'center',
|
||||||
};
|
}
|
||||||
|
|
||||||
const cropSelectionContainerStyle: React.CSSProperties = {
|
const cropSelectionContainerStyle: React.CSSProperties = {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
gap: '0.5rem',
|
gap: '0.5rem',
|
||||||
marginBottom: '1rem',
|
marginBottom: '1rem',
|
||||||
justifyContent: 'center'
|
justifyContent: 'center',
|
||||||
};
|
}
|
||||||
|
|
||||||
const cropSelectionLabelStyle: React.CSSProperties = {
|
const cropSelectionLabelStyle: React.CSSProperties = {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
marginBottom: '0.5rem'
|
marginBottom: '0.5rem',
|
||||||
};
|
}
|
||||||
|
|
||||||
const getCropButtonStyle = (active: boolean): React.CSSProperties => ({
|
const getCropButtonStyle = (active: boolean): React.CSSProperties => ({
|
||||||
padding: '0.5rem 1rem',
|
padding: '0.5rem 1rem',
|
||||||
borderRadius: '0.25rem',
|
borderRadius: '0.25rem',
|
||||||
border: '1px solid #ccc',
|
border: '1px solid #ccc',
|
||||||
backgroundColor: active ? '#4ade80' : '#f3f4f6',
|
backgroundColor: active ? '#4ade80' : '#f3f4f6',
|
||||||
cursor: 'pointer'
|
cursor: 'pointer',
|
||||||
});
|
})
|
||||||
|
|
||||||
const actionsContainerStyle: React.CSSProperties = {
|
const actionsContainerStyle: React.CSSProperties = {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: '0.5rem',
|
gap: '0.5rem',
|
||||||
marginBottom: '1rem',
|
marginBottom: '1rem',
|
||||||
justifyContent: 'center'
|
justifyContent: 'center',
|
||||||
};
|
}
|
||||||
|
|
||||||
const speedSelectorContainerStyle: React.CSSProperties = {
|
const speedSelectorContainerStyle: React.CSSProperties = {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|
@ -54,26 +54,26 @@ const speedSelectorContainerStyle: React.CSSProperties = {
|
||||||
padding: '0.5rem',
|
padding: '0.5rem',
|
||||||
backgroundColor: '#f9fafb',
|
backgroundColor: '#f9fafb',
|
||||||
borderRadius: '0.5rem',
|
borderRadius: '0.5rem',
|
||||||
border: '1px solid #e5e7eb'
|
border: '1px solid #e5e7eb',
|
||||||
};
|
}
|
||||||
|
|
||||||
const speedSelectorLabelStyle: React.CSSProperties = {
|
const speedSelectorLabelStyle: React.CSSProperties = {
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
marginBottom: '0.5rem'
|
marginBottom: '0.5rem',
|
||||||
};
|
}
|
||||||
|
|
||||||
const speedButtonsContainerStyle: React.CSSProperties = {
|
const speedButtonsContainerStyle: React.CSSProperties = {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: '0.5rem'
|
gap: '0.5rem',
|
||||||
};
|
}
|
||||||
|
|
||||||
const getSpeedButtonStyle = (active: boolean): React.CSSProperties => ({
|
const getSpeedButtonStyle = (active: boolean): React.CSSProperties => ({
|
||||||
padding: '0.5rem 1rem',
|
padding: '0.5rem 1rem',
|
||||||
borderRadius: '0.25rem',
|
borderRadius: '0.25rem',
|
||||||
border: '1px solid #ccc',
|
border: '1px solid #ccc',
|
||||||
backgroundColor: active ? '#4ade80' : '#f3f4f6',
|
backgroundColor: active ? '#4ade80' : '#f3f4f6',
|
||||||
cursor: 'pointer'
|
cursor: 'pointer',
|
||||||
});
|
})
|
||||||
|
|
||||||
const getActionButtonStyle = (disabled: boolean): React.CSSProperties => ({
|
const getActionButtonStyle = (disabled: boolean): React.CSSProperties => ({
|
||||||
padding: '0.5rem 1rem',
|
padding: '0.5rem 1rem',
|
||||||
|
|
@ -81,8 +81,8 @@ const getActionButtonStyle = (disabled: boolean): React.CSSProperties => ({
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
backgroundColor: disabled ? '#9ca3af' : '#22c55e',
|
backgroundColor: disabled ? '#9ca3af' : '#22c55e',
|
||||||
cursor: disabled ? 'not-allowed' : 'pointer'
|
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||||
});
|
})
|
||||||
|
|
||||||
const getFieldGridStyle = (size: number): React.CSSProperties => ({
|
const getFieldGridStyle = (size: number): React.CSSProperties => ({
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
|
|
@ -91,8 +91,8 @@ const getFieldGridStyle = (size: number): React.CSSProperties => ({
|
||||||
gap: '0.5rem',
|
gap: '0.5rem',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
maxWidth: '600px',
|
maxWidth: '600px',
|
||||||
margin: '0 auto'
|
margin: '0 auto',
|
||||||
});
|
})
|
||||||
|
|
||||||
const getPlotStyle = (bgColor: string): React.CSSProperties => ({
|
const getPlotStyle = (bgColor: string): React.CSSProperties => ({
|
||||||
border: '1px solid #78350f',
|
border: '1px solid #78350f',
|
||||||
|
|
@ -103,8 +103,8 @@ const getPlotStyle = (bgColor: string): React.CSSProperties => ({
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
backgroundColor: bgColor
|
backgroundColor: bgColor,
|
||||||
});
|
})
|
||||||
|
|
||||||
const getMoistureIndicatorStyle = (level: number): React.CSSProperties => ({
|
const getMoistureIndicatorStyle = (level: number): React.CSSProperties => ({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|
@ -114,69 +114,72 @@ const getMoistureIndicatorStyle = (level: number): React.CSSProperties => ({
|
||||||
height: `${Math.min(level * 100, 100)}%`,
|
height: `${Math.min(level * 100, 100)}%`,
|
||||||
backgroundColor: 'rgba(147, 197, 253, 0.3)',
|
backgroundColor: 'rgba(147, 197, 253, 0.3)',
|
||||||
transition: 'height 0.3s',
|
transition: 'height 0.3s',
|
||||||
zIndex: 1
|
zIndex: 1,
|
||||||
});
|
})
|
||||||
|
|
||||||
const getMaturityProgressContainerStyle = (): React.CSSProperties => ({
|
const getMaturityProgressContainerStyle = (): React.CSSProperties => ({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: '10px', // Moved up from the bottom to separate from moisture indicator
|
bottom: '10px', // Moved up from the bottom to separate from moisture indicator
|
||||||
left: 0,
|
left: 0,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '10px', // Increased height for better visibility
|
height: '10px', // Increased height for better visibility
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.3)', // Darker background for better contrast
|
backgroundColor: 'rgba(0, 0, 0, 0.3)', // Darker background for better contrast
|
||||||
zIndex: 5, // Increased z-index to ensure it's above other elements
|
zIndex: 5, // Increased z-index to ensure it's above other elements
|
||||||
border: '1px solid #000' // Added border for better visibility
|
border: '1px solid #000', // Added border for better visibility
|
||||||
});
|
})
|
||||||
|
|
||||||
const getMaturityProgressBarStyle = (progress: number, total: number): React.CSSProperties => ({
|
const getMaturityProgressBarStyle = (
|
||||||
|
progress: number,
|
||||||
|
total: number,
|
||||||
|
): React.CSSProperties => ({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
width: `${Math.min((progress / total) * 100, 100)}%`,
|
width: `${Math.min((progress / total) * 100, 100)}%`,
|
||||||
height: '10px', // Increased height to match container
|
height: '10px', // Increased height to match container
|
||||||
backgroundColor: '#ff5722', // Brighter orange color for better visibility
|
backgroundColor: '#ff5722', // Brighter orange color for better visibility
|
||||||
transition: 'width 0.3s',
|
transition: 'width 0.3s',
|
||||||
zIndex: 6, // Increased z-index to ensure it's above the container
|
zIndex: 6, // Increased z-index to ensure it's above the container
|
||||||
boxShadow: '0 0 3px rgba(0, 0, 0, 0.5)' // Added shadow for better visibility
|
boxShadow: '0 0 3px rgba(0, 0, 0, 0.5)', // Added shadow for better visibility
|
||||||
});
|
})
|
||||||
|
|
||||||
const FieldComponent: React.FC = () => {
|
const FieldComponent: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
plots,
|
plots,
|
||||||
fieldSize,
|
fieldSize,
|
||||||
plant,
|
plant,
|
||||||
water,
|
water,
|
||||||
harvest,
|
harvest,
|
||||||
assignCrop,
|
assignCrop,
|
||||||
gameSpeed,
|
gameSpeed,
|
||||||
setGameSpeed,
|
setGameSpeed,
|
||||||
actionCooldown
|
actionCooldown,
|
||||||
} = useGameStore();
|
} = useGameStore()
|
||||||
|
|
||||||
const [selectedCrop, setSelectedCrop] = useState<string | null>(null);
|
const [selectedCrop, setSelectedCrop] = useState<string | null>(null)
|
||||||
|
|
||||||
const handlePlotClick = (row: number, col: number) => {
|
const handlePlotClick = (row: number, col: number) => {
|
||||||
if (selectedCrop) {
|
if (selectedCrop) {
|
||||||
assignCrop(row, col, selectedCrop);
|
assignCrop(row, col, selectedCrop)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const getBgColor = (hasCrop: boolean, isMature: boolean) => {
|
const getBgColor = (hasCrop: boolean, isMature: boolean) => {
|
||||||
if (isMature) return "#22c55e";
|
if (isMature) return '#22c55e'
|
||||||
if (hasCrop) return "#86efac";
|
if (hasCrop) return '#86efac'
|
||||||
return "#92400e";
|
return '#92400e'
|
||||||
};
|
}
|
||||||
|
|
||||||
const availableSpeeds = [1, 2, 4, 8, 16, 32, 64]
|
const availableSpeeds = [1, 2, 4, 8, 16, 32, 64]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={fieldContainerStyle}>
|
<div style={fieldContainerStyle}>
|
||||||
<h2 style={titleStyle}>Fields</h2>
|
<h2 style={titleStyle}>Fields</h2>
|
||||||
|
|
||||||
<div style={speedSelectorContainerStyle}>
|
<div style={speedSelectorContainerStyle}>
|
||||||
<p style={speedSelectorLabelStyle}>Game Speed:</p>
|
<p style={speedSelectorLabelStyle}>Game Speed:</p>
|
||||||
<div style={speedButtonsContainerStyle}>
|
<div style={speedButtonsContainerStyle}>
|
||||||
{availableSpeeds.map(speed => (
|
{availableSpeeds.map((speed) => (
|
||||||
<button
|
<button
|
||||||
style={getSpeedButtonStyle(gameSpeed === speed)}
|
style={getSpeedButtonStyle(gameSpeed === speed)}
|
||||||
onClick={() => setGameSpeed(speed)}
|
onClick={() => setGameSpeed(speed)}
|
||||||
|
|
@ -187,10 +190,10 @@ const FieldComponent: React.FC = () => {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={cropSelectionContainerStyle}>
|
<div style={cropSelectionContainerStyle}>
|
||||||
<p style={cropSelectionLabelStyle}>Select a crop to plant:</p>
|
<p style={cropSelectionLabelStyle}>Select a crop to plant:</p>
|
||||||
{Object.values(CROPS).map(crop => (
|
{Object.values(CROPS).map((crop) => (
|
||||||
<button
|
<button
|
||||||
key={crop.id}
|
key={crop.id}
|
||||||
style={getCropButtonStyle(selectedCrop === crop.id)}
|
style={getCropButtonStyle(selectedCrop === crop.id)}
|
||||||
|
|
@ -200,59 +203,71 @@ const FieldComponent: React.FC = () => {
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={actionsContainerStyle}>
|
<div style={actionsContainerStyle}>
|
||||||
<button
|
<button
|
||||||
style={getActionButtonStyle(actionCooldown > 0)}
|
style={getActionButtonStyle(actionCooldown > 0)}
|
||||||
onClick={plant}
|
onClick={plant}
|
||||||
disabled={actionCooldown > 0}
|
disabled={actionCooldown > 0}
|
||||||
>
|
>
|
||||||
Plant Crop
|
Plant Crop
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
style={getActionButtonStyle(actionCooldown > 0)}
|
style={getActionButtonStyle(actionCooldown > 0)}
|
||||||
onClick={water}
|
onClick={water}
|
||||||
disabled={actionCooldown > 0}
|
disabled={actionCooldown > 0}
|
||||||
>
|
>
|
||||||
Water Crop
|
Water Crop
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
style={getActionButtonStyle(actionCooldown > 0)}
|
style={getActionButtonStyle(actionCooldown > 0)}
|
||||||
onClick={harvest}
|
onClick={harvest}
|
||||||
disabled={actionCooldown > 0}
|
disabled={actionCooldown > 0}
|
||||||
>
|
>
|
||||||
Harvest Crop
|
Harvest Crop
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={getFieldGridStyle(fieldSize)}>
|
<div style={getFieldGridStyle(fieldSize)}>
|
||||||
{plots.slice(0, fieldSize).map((row, rowIndex) =>
|
{plots.slice(0, fieldSize).map((row, rowIndex) =>
|
||||||
row.slice(0, fieldSize).map((plot, colIndex) => {
|
row.slice(0, fieldSize).map((plot, colIndex) => {
|
||||||
const hasCrop = !!plot.current;
|
const hasCrop = !!plot.current
|
||||||
const isMature = !!(plot.current?.mature);
|
const isMature = !!plot.current?.mature
|
||||||
const bgColor = getBgColor(hasCrop, isMature);
|
const bgColor = getBgColor(hasCrop, isMature)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`${rowIndex}-${colIndex}`}
|
key={`${rowIndex}-${colIndex}`}
|
||||||
style={getPlotStyle(bgColor)}
|
style={getPlotStyle(bgColor)}
|
||||||
onClick={() => handlePlotClick(rowIndex, colIndex)}
|
onClick={() => handlePlotClick(rowIndex, colIndex)}
|
||||||
>
|
>
|
||||||
{plot.intended && !plot.current && <div>🌱 {CROPS[plot.intended]?.name}</div>}
|
{plot.intended && !plot.current && (
|
||||||
{plot.current && <div>{plot.current.mature ? '🌿' : '🌱'} {CROPS[plot.current.cropId]?.name}</div>}
|
<div>🌱 {CROPS[plot.intended]?.name}</div>
|
||||||
|
)}
|
||||||
|
{plot.current && (
|
||||||
|
<div>
|
||||||
|
{plot.current.mature ? '🌿' : '🌱'}{' '}
|
||||||
|
{CROPS[plot.current.cropId]?.name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div style={getMoistureIndicatorStyle(plot.moisture)} />
|
<div style={getMoistureIndicatorStyle(plot.moisture)} />
|
||||||
{plot.current && !plot.current.mature && (
|
{plot.current && !plot.current.mature && (
|
||||||
<div style={getMaturityProgressContainerStyle()}>
|
<div style={getMaturityProgressContainerStyle()}>
|
||||||
<div style={getMaturityProgressBarStyle(plot.current.progress, CROPS[plot.current.cropId].growthTicks)} />
|
<div
|
||||||
|
style={getMaturityProgressBarStyle(
|
||||||
|
plot.current.progress,
|
||||||
|
CROPS[plot.current.cropId].growthTicks,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
})
|
}),
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldComponent;
|
export default FieldComponent
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,59 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react'
|
||||||
|
|
||||||
interface FloatingMessageProps {
|
interface FloatingMessageProps {
|
||||||
message: string;
|
message: string
|
||||||
startPosition: { x: number, y: number };
|
startPosition: { x: number; y: number }
|
||||||
onComplete: () => void;
|
onComplete: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const FloatingMessage: React.FC<FloatingMessageProps> = ({ message, startPosition, onComplete }) => {
|
const FloatingMessage: React.FC<FloatingMessageProps> = ({
|
||||||
const [position, setPosition] = useState({ y: startPosition.y });
|
message,
|
||||||
const [opacity, setOpacity] = useState(1);
|
startPosition,
|
||||||
|
onComplete,
|
||||||
|
}) => {
|
||||||
|
const [position, setPosition] = useState({ y: startPosition.y })
|
||||||
|
const [opacity, setOpacity] = useState(1)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now()
|
||||||
const duration = 1000; // 1 second animation
|
const duration = 1000 // 1 second animation
|
||||||
|
|
||||||
const animate = () => {
|
const animate = () => {
|
||||||
const elapsed = Date.now() - startTime;
|
const elapsed = Date.now() - startTime
|
||||||
const progress = Math.min(elapsed / duration, 1);
|
const progress = Math.min(elapsed / duration, 1)
|
||||||
|
|
||||||
setPosition({ y: startPosition.y - progress * 50 });
|
setPosition({ y: startPosition.y - progress * 50 })
|
||||||
setOpacity(1 - progress);
|
setOpacity(1 - progress)
|
||||||
|
|
||||||
if (progress < 1) {
|
if (progress < 1) {
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate)
|
||||||
} else {
|
} else {
|
||||||
onComplete();
|
onComplete()
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const animationFrame = requestAnimationFrame(animate);
|
const animationFrame = requestAnimationFrame(animate)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelAnimationFrame(animationFrame);
|
cancelAnimationFrame(animationFrame)
|
||||||
};
|
}
|
||||||
}, [startPosition, onComplete]);
|
}, [startPosition, onComplete])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div
|
||||||
position: 'absolute',
|
style={{
|
||||||
left: startPosition.x,
|
position: 'absolute',
|
||||||
top: position.y,
|
left: startPosition.x,
|
||||||
opacity,
|
top: position.y,
|
||||||
fontWeight: 'bold',
|
opacity,
|
||||||
pointerEvents: 'none',
|
fontWeight: 'bold',
|
||||||
zIndex: 100
|
pointerEvents: 'none',
|
||||||
}}>
|
zIndex: 100,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{message}
|
{message}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FloatingMessage;
|
export default FloatingMessage
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react'
|
||||||
import { useGameStore } from '../store/useGameStore';
|
import { useGameStore } from '../store/useGameStore'
|
||||||
import { MARKET_ITEMS } from '../constants';
|
import { MARKET_ITEMS } from '../constants'
|
||||||
import FloatingMessage from './FloatingMessage';
|
import FloatingMessage from './FloatingMessage'
|
||||||
import { styled } from '@linaria/react';
|
import { styled } from '@linaria/react'
|
||||||
|
|
||||||
const MarketContainer = styled.div`
|
const MarketContainer = styled.div`
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
`;
|
`
|
||||||
|
|
||||||
const Title = styled.h2`
|
const Title = styled.h2`
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
`;
|
`
|
||||||
|
|
||||||
const CashDisplay = styled.div`
|
const CashDisplay = styled.div`
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
@ -21,124 +21,135 @@ const CashDisplay = styled.div`
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
background-color: #dcfce7;
|
background-color: #dcfce7;
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
`;
|
`
|
||||||
|
|
||||||
const CashAmount = styled.h3`
|
const CashAmount = styled.h3`
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
`;
|
`
|
||||||
|
|
||||||
const SectionTitle = styled.h3`
|
const SectionTitle = styled.h3`
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
`;
|
`
|
||||||
|
|
||||||
const MarketTable = styled.table`
|
const MarketTable = styled.table`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: separate;
|
border-collapse: separate;
|
||||||
border-spacing: 0 0.5rem;
|
border-spacing: 0 0.5rem;
|
||||||
`;
|
`
|
||||||
|
|
||||||
const MarketItemRow = styled.tr`
|
const MarketItemRow = styled.tr`
|
||||||
background-color: #f3f4f6;
|
background-color: #f3f4f6;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
`;
|
`
|
||||||
|
|
||||||
const MarketItemCell = styled.td`
|
const MarketItemCell = styled.td`
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
`;
|
`
|
||||||
|
|
||||||
const TableHeader = styled.th`
|
const TableHeader = styled.th`
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
`;
|
`
|
||||||
|
|
||||||
const ItemIcon = styled.span`
|
const ItemIcon = styled.span`
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
`;
|
`
|
||||||
|
|
||||||
const ActionButton = styled.button<{ disabled?: boolean }>`
|
const ActionButton = styled.button<{ disabled?: boolean }>`
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
background-color: ${props => props.disabled ? '#9ca3af' : '#3b82f6'};
|
background-color: ${(props) => (props.disabled ? '#9ca3af' : '#3b82f6')};
|
||||||
color: white;
|
color: white;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: ${props => props.disabled ? 'not-allowed' : 'pointer'};
|
cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')};
|
||||||
border: none;
|
border: none;
|
||||||
`;
|
`
|
||||||
|
|
||||||
interface FloatingMessageData {
|
interface FloatingMessageData {
|
||||||
id: string;
|
id: string
|
||||||
message: string;
|
message: string
|
||||||
position: { x: number, y: number };
|
position: { x: number; y: number }
|
||||||
}
|
}
|
||||||
|
|
||||||
const MarketComponent: React.FC = () => {
|
const MarketComponent: React.FC = () => {
|
||||||
const { inventory, cash, buyItem, sellItem } = useGameStore();
|
const { inventory, cash, buyItem, sellItem } = useGameStore()
|
||||||
const [floatingMessages, setFloatingMessages] = useState<FloatingMessageData[]>([]);
|
const [floatingMessages, setFloatingMessages] = useState<
|
||||||
|
FloatingMessageData[]
|
||||||
|
>([])
|
||||||
|
|
||||||
const handleBuy = (itemId: string, e: React.MouseEvent) => {
|
const handleBuy = (itemId: string, e: React.MouseEvent) => {
|
||||||
const item = MARKET_ITEMS[itemId];
|
const item = MARKET_ITEMS[itemId]
|
||||||
if (!item || item.buyPrice === null || cash < item.buyPrice) return;
|
if (!item || item.buyPrice === null || cash < item.buyPrice) return
|
||||||
|
|
||||||
buyItem(itemId);
|
buyItem(itemId)
|
||||||
|
|
||||||
const rect = (e.target as HTMLElement).getBoundingClientRect();
|
const rect = (e.target as HTMLElement).getBoundingClientRect()
|
||||||
const position = { x: rect.left + rect.width / 2, y: rect.top };
|
const position = { x: rect.left + rect.width / 2, y: rect.top }
|
||||||
|
|
||||||
setFloatingMessages([
|
setFloatingMessages([
|
||||||
...floatingMessages,
|
...floatingMessages,
|
||||||
{
|
{
|
||||||
id: `buy-${itemId}-${Date.now()}`,
|
id: `buy-${itemId}-${Date.now()}`,
|
||||||
message: `+1 ${item.emoji} ${item.name}`,
|
message: `+1 ${item.emoji} ${item.name}`,
|
||||||
position
|
position,
|
||||||
}
|
},
|
||||||
]);
|
])
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleSell = (itemId: string, e: React.MouseEvent) => {
|
const handleSell = (itemId: string, e: React.MouseEvent) => {
|
||||||
const item = MARKET_ITEMS[itemId];
|
const item = MARKET_ITEMS[itemId]
|
||||||
if (!item || item.sellPrice === null || !inventory[itemId] || inventory[itemId] <= 0) return;
|
if (
|
||||||
|
!item ||
|
||||||
sellItem(itemId);
|
item.sellPrice === null ||
|
||||||
|
!inventory[itemId] ||
|
||||||
const rect = (e.target as HTMLElement).getBoundingClientRect();
|
inventory[itemId] <= 0
|
||||||
const position = { x: rect.left + rect.width / 2, y: rect.top };
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
sellItem(itemId)
|
||||||
|
|
||||||
|
const rect = (e.target as HTMLElement).getBoundingClientRect()
|
||||||
|
const position = { x: rect.left + rect.width / 2, y: rect.top }
|
||||||
|
|
||||||
setFloatingMessages([
|
setFloatingMessages([
|
||||||
...floatingMessages,
|
...floatingMessages,
|
||||||
{
|
{
|
||||||
id: `sell-${itemId}-${Date.now()}`,
|
id: `sell-${itemId}-${Date.now()}`,
|
||||||
message: `-1 ${item.emoji} ${item.name}`,
|
message: `-1 ${item.emoji} ${item.name}`,
|
||||||
position
|
position,
|
||||||
}
|
},
|
||||||
]);
|
])
|
||||||
};
|
}
|
||||||
|
|
||||||
const removeFloatingMessage = (messageId: string) => {
|
const removeFloatingMessage = (messageId: string) => {
|
||||||
setFloatingMessages(floatingMessages.filter(msg => msg.id !== messageId));
|
setFloatingMessages(floatingMessages.filter((msg) => msg.id !== messageId))
|
||||||
};
|
}
|
||||||
|
|
||||||
const sellableItems = Object.entries(MARKET_ITEMS)
|
const sellableItems = Object.entries(MARKET_ITEMS)
|
||||||
.filter(([_, item]) => item.sellPrice !== null && inventory[item.id] && inventory[item.id] > 0)
|
.filter(
|
||||||
.map(([_, item]) => item);
|
([_, item]) =>
|
||||||
|
item.sellPrice !== null && inventory[item.id] && inventory[item.id] > 0,
|
||||||
|
)
|
||||||
|
.map(([_, item]) => item)
|
||||||
|
|
||||||
const buyableItems = Object.entries(MARKET_ITEMS)
|
const buyableItems = Object.entries(MARKET_ITEMS)
|
||||||
.filter(([_, item]) => item.buyPrice !== null)
|
.filter(([_, item]) => item.buyPrice !== null)
|
||||||
.map(([_, item]) => item);
|
.map(([_, item]) => item)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MarketContainer>
|
<MarketContainer>
|
||||||
<Title>Market</Title>
|
<Title>Market</Title>
|
||||||
|
|
||||||
<CashDisplay>
|
<CashDisplay>
|
||||||
<CashAmount>Cash: ${cash.toFixed(2)}</CashAmount>
|
<CashAmount>Cash: ${cash.toFixed(2)}</CashAmount>
|
||||||
</CashDisplay>
|
</CashDisplay>
|
||||||
|
|
||||||
{sellableItems.length > 0 && (
|
{sellableItems.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<SectionTitle>Sell Items</SectionTitle>
|
<SectionTitle>Sell Items</SectionTitle>
|
||||||
|
|
@ -152,7 +163,7 @@ const MarketComponent: React.FC = () => {
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{sellableItems.map(item => (
|
{sellableItems.map((item) => (
|
||||||
<MarketItemRow key={`sell-${item.id}`}>
|
<MarketItemRow key={`sell-${item.id}`}>
|
||||||
<MarketItemCell>
|
<MarketItemCell>
|
||||||
<ItemIcon>{item.emoji}</ItemIcon>
|
<ItemIcon>{item.emoji}</ItemIcon>
|
||||||
|
|
@ -161,7 +172,7 @@ const MarketComponent: React.FC = () => {
|
||||||
<MarketItemCell>${item.sellPrice}</MarketItemCell>
|
<MarketItemCell>${item.sellPrice}</MarketItemCell>
|
||||||
<MarketItemCell>{inventory[item.id] || 0}</MarketItemCell>
|
<MarketItemCell>{inventory[item.id] || 0}</MarketItemCell>
|
||||||
<MarketItemCell>
|
<MarketItemCell>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
onClick={(e) => handleSell(item.id, e)}
|
onClick={(e) => handleSell(item.id, e)}
|
||||||
disabled={!inventory[item.id] || inventory[item.id] <= 0}
|
disabled={!inventory[item.id] || inventory[item.id] <= 0}
|
||||||
>
|
>
|
||||||
|
|
@ -174,7 +185,7 @@ const MarketComponent: React.FC = () => {
|
||||||
</MarketTable>
|
</MarketTable>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SectionTitle>Buy Items</SectionTitle>
|
<SectionTitle>Buy Items</SectionTitle>
|
||||||
<MarketTable>
|
<MarketTable>
|
||||||
<thead>
|
<thead>
|
||||||
|
|
@ -185,7 +196,7 @@ const MarketComponent: React.FC = () => {
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{buyableItems.map(item => (
|
{buyableItems.map((item) => (
|
||||||
<MarketItemRow key={`buy-${item.id}`}>
|
<MarketItemRow key={`buy-${item.id}`}>
|
||||||
<MarketItemCell>
|
<MarketItemCell>
|
||||||
<ItemIcon>{item.emoji}</ItemIcon>
|
<ItemIcon>{item.emoji}</ItemIcon>
|
||||||
|
|
@ -193,7 +204,7 @@ const MarketComponent: React.FC = () => {
|
||||||
</MarketItemCell>
|
</MarketItemCell>
|
||||||
<MarketItemCell>${item.buyPrice}</MarketItemCell>
|
<MarketItemCell>${item.buyPrice}</MarketItemCell>
|
||||||
<MarketItemCell>
|
<MarketItemCell>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
onClick={(e) => handleBuy(item.id, e)}
|
onClick={(e) => handleBuy(item.id, e)}
|
||||||
disabled={item.buyPrice ? cash < item.buyPrice : true}
|
disabled={item.buyPrice ? cash < item.buyPrice : true}
|
||||||
>
|
>
|
||||||
|
|
@ -204,8 +215,8 @@ const MarketComponent: React.FC = () => {
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</MarketTable>
|
</MarketTable>
|
||||||
|
|
||||||
{floatingMessages.map(msg => (
|
{floatingMessages.map((msg) => (
|
||||||
<FloatingMessage
|
<FloatingMessage
|
||||||
key={msg.id}
|
key={msg.id}
|
||||||
message={msg.message}
|
message={msg.message}
|
||||||
|
|
@ -214,7 +225,7 @@ const MarketComponent: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</MarketContainer>
|
</MarketContainer>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default MarketComponent;
|
export default MarketComponent
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,47 @@
|
||||||
import React from 'react';
|
import React from 'react'
|
||||||
import { useGameStore } from '../store/useGameStore';
|
import { useGameStore } from '../store/useGameStore'
|
||||||
import { CROPS } from '../constants';
|
import { CROPS } from '../constants'
|
||||||
|
|
||||||
const warehouseContainerStyle: React.CSSProperties = {
|
const warehouseContainerStyle: React.CSSProperties = {
|
||||||
padding: '1rem'
|
padding: '1rem',
|
||||||
};
|
}
|
||||||
|
|
||||||
const titleStyle: React.CSSProperties = {
|
const titleStyle: React.CSSProperties = {
|
||||||
fontSize: '1.5rem',
|
fontSize: '1.5rem',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
marginBottom: '1rem',
|
marginBottom: '1rem',
|
||||||
textAlign: 'center'
|
textAlign: 'center',
|
||||||
};
|
}
|
||||||
|
|
||||||
const cashDisplayStyle: React.CSSProperties = {
|
const cashDisplayStyle: React.CSSProperties = {
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
marginBottom: '1.5rem',
|
marginBottom: '1.5rem',
|
||||||
padding: '1rem',
|
padding: '1rem',
|
||||||
backgroundColor: '#dcfce7',
|
backgroundColor: '#dcfce7',
|
||||||
borderRadius: '0.375rem'
|
borderRadius: '0.375rem',
|
||||||
};
|
}
|
||||||
|
|
||||||
const cashAmountStyle: React.CSSProperties = {
|
const cashAmountStyle: React.CSSProperties = {
|
||||||
fontSize: '1.25rem',
|
fontSize: '1.25rem',
|
||||||
fontWeight: 500
|
fontWeight: 500,
|
||||||
};
|
}
|
||||||
|
|
||||||
const inventoryTitleStyle: React.CSSProperties = {
|
const inventoryTitleStyle: React.CSSProperties = {
|
||||||
fontSize: '1.25rem',
|
fontSize: '1.25rem',
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
marginBottom: '0.5rem'
|
marginBottom: '0.5rem',
|
||||||
};
|
}
|
||||||
|
|
||||||
const getInventoryGridStyle = (): React.CSSProperties => {
|
const getInventoryGridStyle = (): React.CSSProperties => {
|
||||||
const style: React.CSSProperties = {
|
const style: React.CSSProperties = {
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: 'repeat(2, 1fr)',
|
gridTemplateColumns: 'repeat(2, 1fr)',
|
||||||
gap: '1rem',
|
gap: '1rem',
|
||||||
marginTop: '1rem'
|
marginTop: '1rem',
|
||||||
};
|
}
|
||||||
|
|
||||||
return style;
|
return style
|
||||||
};
|
}
|
||||||
|
|
||||||
const inventoryItemStyle: React.CSSProperties = {
|
const inventoryItemStyle: React.CSSProperties = {
|
||||||
backgroundColor: '#f3f4f6',
|
backgroundColor: '#f3f4f6',
|
||||||
|
|
@ -51,67 +51,68 @@ const inventoryItemStyle: React.CSSProperties = {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
textAlign: 'center'
|
textAlign: 'center',
|
||||||
};
|
}
|
||||||
|
|
||||||
const itemIconStyle: React.CSSProperties = {
|
const itemIconStyle: React.CSSProperties = {
|
||||||
fontSize: '1.5rem',
|
fontSize: '1.5rem',
|
||||||
marginBottom: '0.5rem'
|
marginBottom: '0.5rem',
|
||||||
};
|
}
|
||||||
|
|
||||||
const itemNameStyle: React.CSSProperties = {
|
const itemNameStyle: React.CSSProperties = {
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
marginBottom: '0.25rem'
|
marginBottom: '0.25rem',
|
||||||
};
|
}
|
||||||
|
|
||||||
const formatItemName = (id: string) => {
|
const formatItemName = (id: string) => {
|
||||||
if (id.endsWith('_seed')) {
|
if (id.endsWith('_seed')) {
|
||||||
const cropId = id.replace('_seed', '');
|
const cropId = id.replace('_seed', '')
|
||||||
return `${CROPS[cropId]?.name || cropId} Seeds`;
|
return `${CROPS[cropId]?.name || cropId} Seeds`
|
||||||
}
|
}
|
||||||
|
|
||||||
return CROPS[id]?.name || id;
|
return CROPS[id]?.name || id
|
||||||
};
|
}
|
||||||
|
|
||||||
const getItemIcon = (id: string) => {
|
const getItemIcon = (id: string) => {
|
||||||
if (id.endsWith('_seed')) {
|
if (id.endsWith('_seed')) {
|
||||||
return '🌰';
|
return '🌰'
|
||||||
}
|
}
|
||||||
|
|
||||||
const cropIcons: Record<string, string> = {
|
const cropIcons: Record<string, string> = {
|
||||||
celery: '🥬',
|
celery: '🥬',
|
||||||
corn: '🌽',
|
corn: '🌽',
|
||||||
};
|
}
|
||||||
|
|
||||||
return cropIcons[id] || '📦';
|
return cropIcons[id] || '📦'
|
||||||
};
|
}
|
||||||
|
|
||||||
const WarehouseComponent: React.FC = () => {
|
const WarehouseComponent: React.FC = () => {
|
||||||
const { inventory, cash } = useGameStore();
|
const { inventory, cash } = useGameStore()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={warehouseContainerStyle}>
|
<div style={warehouseContainerStyle}>
|
||||||
<h2 style={titleStyle}>Warehouse</h2>
|
<h2 style={titleStyle}>Warehouse</h2>
|
||||||
|
|
||||||
<div style={cashDisplayStyle}>
|
<div style={cashDisplayStyle}>
|
||||||
<h3 style={cashAmountStyle}>Cash: ${cash.toFixed(2)}</h3>
|
<h3 style={cashAmountStyle}>Cash: ${cash.toFixed(2)}</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 style={inventoryTitleStyle}>Inventory:</h3>
|
<h3 style={inventoryTitleStyle}>Inventory:</h3>
|
||||||
|
|
||||||
<div style={getInventoryGridStyle()}>
|
<div style={getInventoryGridStyle()}>
|
||||||
{Object.entries(inventory).map(([id, count]) => (
|
{Object.entries(inventory).map(
|
||||||
count > 0 && (
|
([id, count]) =>
|
||||||
<div key={id} style={inventoryItemStyle}>
|
count > 0 && (
|
||||||
<div style={itemIconStyle}>{getItemIcon(id)}</div>
|
<div key={id} style={inventoryItemStyle}>
|
||||||
<h4 style={itemNameStyle}>{formatItemName(id)}</h4>
|
<div style={itemIconStyle}>{getItemIcon(id)}</div>
|
||||||
<p>Quantity: {count}</p>
|
<h4 style={itemNameStyle}>{formatItemName(id)}</h4>
|
||||||
</div>
|
<p>Quantity: {count}</p>
|
||||||
)
|
</div>
|
||||||
))}
|
),
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default WarehouseComponent;
|
export default WarehouseComponent
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,70 @@
|
||||||
import { CropDefinitions } from '../types';
|
import { CropDefinitions } from '../types'
|
||||||
|
|
||||||
export const INITIAL_CASH = 50;
|
export const INITIAL_CASH = 50
|
||||||
export const INITIAL_FIELD_SIZE = 4;
|
export const INITIAL_FIELD_SIZE = 4
|
||||||
export const COOLDOWN_DURATION = 2000; // 2 seconds in milliseconds
|
export const COOLDOWN_DURATION = 2000 // 2 seconds in milliseconds
|
||||||
export const TICK_INTERVAL = 12000; // 12 seconds in milliseconds
|
export const TICK_INTERVAL = 12000 // 12 seconds in milliseconds
|
||||||
export const GAME_SPEEDS = {
|
export const GAME_SPEEDS = {
|
||||||
NORMAL: 1, FAST: 5, SUPER_FAST: 10
|
NORMAL: 1,
|
||||||
};
|
FAST: 5,
|
||||||
export const INITIAL_GAME_SPEED = GAME_SPEEDS.NORMAL;
|
SUPER_FAST: 10,
|
||||||
|
}
|
||||||
|
export const INITIAL_GAME_SPEED = GAME_SPEEDS.NORMAL
|
||||||
|
|
||||||
export const FIELD_UPGRADE_COSTS = [100, 1000, 10000, 100000];
|
export const FIELD_UPGRADE_COSTS = [100, 1000, 10000, 100000]
|
||||||
|
|
||||||
export const CROPS: CropDefinitions = {
|
export const CROPS: CropDefinitions = {
|
||||||
celery: {
|
celery: {
|
||||||
id: 'celery',
|
id: 'celery',
|
||||||
name: 'Celery',
|
name: 'Celery',
|
||||||
growthTicks: 36,
|
growthTicks: 36,
|
||||||
waterPerTick: 1/20,
|
waterPerTick: 1 / 20,
|
||||||
yield: 1,
|
yield: 1,
|
||||||
yieldType: 'celery'
|
yieldType: 'celery',
|
||||||
},
|
},
|
||||||
corn: {
|
corn: {
|
||||||
id: 'corn',
|
id: 'corn',
|
||||||
name: 'Corn',
|
name: 'Corn',
|
||||||
growthTicks: 120,
|
growthTicks: 120,
|
||||||
waterPerTick: 1/40,
|
waterPerTick: 1 / 40,
|
||||||
yield: 5,
|
yield: 5,
|
||||||
yieldType: 'corn'
|
yieldType: 'corn',
|
||||||
}
|
},
|
||||||
};
|
}
|
||||||
|
|
||||||
export const INITIAL_INVENTORY = {
|
export const INITIAL_INVENTORY = {
|
||||||
'celery_seed': 8
|
celery_seed: 8,
|
||||||
};
|
}
|
||||||
|
|
||||||
export interface MarketItem {
|
export interface MarketItem {
|
||||||
id: string;
|
id: string
|
||||||
name: string;
|
name: string
|
||||||
emoji: string;
|
emoji: string
|
||||||
buyPrice: number | null; // null means not buyable
|
buyPrice: number | null // null means not buyable
|
||||||
sellPrice: number | null; // null means not sellable
|
sellPrice: number | null // null means not sellable
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MARKET_ITEMS: Record<string, MarketItem> = {
|
export const MARKET_ITEMS: Record<string, MarketItem> = {
|
||||||
'celery_seed': { id: 'celery_seed', name: 'Celery Seeds', emoji: '🌰', buyPrice: 1, sellPrice: null },
|
celery_seed: {
|
||||||
'corn_seed': { id: 'corn_seed', name: 'Corn Seeds', emoji: '🌰', buyPrice: 5, sellPrice: null },
|
id: 'celery_seed',
|
||||||
'celery': { id: 'celery', name: 'Celery', emoji: '🥬', buyPrice: null, sellPrice: 5 },
|
name: 'Celery Seeds',
|
||||||
'corn': { id: 'corn', name: 'Corn', emoji: '🌽', buyPrice: null, sellPrice: 8 },
|
emoji: '🌰',
|
||||||
};
|
buyPrice: 1,
|
||||||
|
sellPrice: null,
|
||||||
|
},
|
||||||
|
corn_seed: {
|
||||||
|
id: 'corn_seed',
|
||||||
|
name: 'Corn Seeds',
|
||||||
|
emoji: '🌰',
|
||||||
|
buyPrice: 5,
|
||||||
|
sellPrice: null,
|
||||||
|
},
|
||||||
|
celery: {
|
||||||
|
id: 'celery',
|
||||||
|
name: 'Celery',
|
||||||
|
emoji: '🥬',
|
||||||
|
buyPrice: null,
|
||||||
|
sellPrice: 5,
|
||||||
|
},
|
||||||
|
corn: { id: 'corn', name: 'Corn', emoji: '🌽', buyPrice: null, sellPrice: 8 },
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,16 @@ import { useGameStore } from '../store/useGameStore'
|
||||||
import { TICK_INTERVAL } from '../constants'
|
import { TICK_INTERVAL } from '../constants'
|
||||||
|
|
||||||
export const useGameTick = () => {
|
export const useGameTick = () => {
|
||||||
const tick = useGameStore(state => state.tick)
|
const tick = useGameStore((state) => state.tick)
|
||||||
const gameSpeed = useGameStore(state => state.gameSpeed)
|
const gameSpeed = useGameStore((state) => state.gameSpeed)
|
||||||
const intervalRef = useRef<number | null>(null)
|
const intervalRef = useRef<number | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (intervalRef.current !== null) {
|
if (intervalRef.current !== null) {
|
||||||
clearInterval(intervalRef.current)
|
clearInterval(intervalRef.current)
|
||||||
}
|
}
|
||||||
|
|
||||||
const adjustedInterval = TICK_INTERVAL / gameSpeed;
|
const adjustedInterval = TICK_INTERVAL / gameSpeed
|
||||||
intervalRef.current = window.setInterval(() => {
|
intervalRef.current = window.setInterval(() => {
|
||||||
tick()
|
tick()
|
||||||
}, adjustedInterval)
|
}, adjustedInterval)
|
||||||
|
|
@ -23,4 +23,4 @@ export const useGameTick = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [tick, gameSpeed])
|
}, [tick, gameSpeed])
|
||||||
};
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,9 @@ git /* Base styles */
|
||||||
|
|
||||||
/* Global styles */
|
/* Global styles */
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
font-family:
|
||||||
|
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
|
||||||
|
Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
@ -33,4 +35,3 @@ body {
|
||||||
* {
|
* {
|
||||||
box-sizing: inherit;
|
box-sizing: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,46 @@
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand'
|
||||||
import { produce } from 'immer';
|
import { produce } from 'immer'
|
||||||
import {
|
import {
|
||||||
INITIAL_CASH,
|
INITIAL_CASH,
|
||||||
INITIAL_FIELD_SIZE,
|
INITIAL_FIELD_SIZE,
|
||||||
FIELD_UPGRADE_COSTS,
|
FIELD_UPGRADE_COSTS,
|
||||||
INITIAL_INVENTORY,
|
INITIAL_INVENTORY,
|
||||||
CROPS,
|
CROPS,
|
||||||
INITIAL_GAME_SPEED,
|
INITIAL_GAME_SPEED,
|
||||||
COOLDOWN_DURATION,
|
COOLDOWN_DURATION,
|
||||||
MARKET_ITEMS
|
MARKET_ITEMS,
|
||||||
} from '../constants';
|
} from '../constants'
|
||||||
import { GameState, PlotState } from '../types';
|
import { GameState, PlotState } from '../types'
|
||||||
import { saveGame, loadGame } from '../utils/saveSystem';
|
import { saveGame, loadGame } from '../utils/saveSystem'
|
||||||
|
|
||||||
const initializeField = (size: number): PlotState[][] => {
|
const initializeField = (size: number): PlotState[][] => {
|
||||||
return Array(size).fill(0).map(() =>
|
return Array(size)
|
||||||
Array(size).fill(0).map(() => ({
|
.fill(0)
|
||||||
moisture: 0
|
.map(() =>
|
||||||
}))
|
Array(size)
|
||||||
);
|
.fill(0)
|
||||||
};
|
.map(() => ({
|
||||||
|
moisture: 0,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const useGameStore = create<GameState & {
|
export const useGameStore = create<
|
||||||
plant: () => void;
|
GameState & {
|
||||||
water: () => void;
|
plant: () => void
|
||||||
harvest: () => void;
|
water: () => void
|
||||||
tick: () => void;
|
harvest: () => void
|
||||||
assignCrop: (row: number, col: number, cropId: string) => void;
|
tick: () => void
|
||||||
upgradeField: () => void;
|
assignCrop: (row: number, col: number, cropId: string) => void
|
||||||
setGameSpeed: (speed: number) => void;
|
upgradeField: () => void
|
||||||
setActionCooldown: (cooldown: number) => void;
|
setGameSpeed: (speed: number) => void
|
||||||
buyItem: (itemId: string) => void;
|
setActionCooldown: (cooldown: number) => void
|
||||||
sellItem: (itemId: string) => void;
|
buyItem: (itemId: string) => void
|
||||||
saveToSlot: (slot: number) => void;
|
sellItem: (itemId: string) => void
|
||||||
loadFromSlot: (slot: number) => void;
|
saveToSlot: (slot: number) => void
|
||||||
}>((set, get) => ({
|
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,
|
||||||
|
|
@ -46,37 +52,41 @@ export const useGameStore = create<GameState & {
|
||||||
tickCount: 0,
|
tickCount: 0,
|
||||||
|
|
||||||
assignCrop: (row, col, cropId) => {
|
assignCrop: (row, col, cropId) => {
|
||||||
set(produce(state => {
|
set(
|
||||||
state.plots[row][col].intended = cropId;
|
produce((state) => {
|
||||||
}));
|
state.plots[row][col].intended = cropId
|
||||||
|
}),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
plant: () => {
|
plant: () => {
|
||||||
const { plots, inventory, actionCooldown } = get();
|
const { plots, inventory, actionCooldown } = get()
|
||||||
|
|
||||||
if (actionCooldown > 0) {
|
if (actionCooldown > 0) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let row = 0; row < plots.length; row++) {
|
for (let row = 0; row < plots.length; row++) {
|
||||||
for (let col = 0; col < plots[row].length; col++) {
|
for (let col = 0; col < plots[row].length; col++) {
|
||||||
const plot = plots[row][col];
|
const plot = plots[row][col]
|
||||||
|
|
||||||
if (plot.intended && !plot.current) {
|
if (plot.intended && !plot.current) {
|
||||||
const seedId = `${plot.intended}_seed`;
|
const seedId = `${plot.intended}_seed`
|
||||||
|
|
||||||
if (inventory[seedId] && inventory[seedId] > 0) {
|
if (inventory[seedId] && inventory[seedId] > 0) {
|
||||||
set(produce(state => {
|
set(
|
||||||
state.plots[row][col].current = {
|
produce((state) => {
|
||||||
cropId: plot.intended!,
|
state.plots[row][col].current = {
|
||||||
progress: 0,
|
cropId: plot.intended!,
|
||||||
mature: false
|
progress: 0,
|
||||||
};
|
mature: false,
|
||||||
|
}
|
||||||
state.inventory[seedId] = state.inventory[seedId] - 1;
|
|
||||||
state.actionCooldown = COOLDOWN_DURATION;
|
state.inventory[seedId] = state.inventory[seedId] - 1
|
||||||
}));
|
state.actionCooldown = COOLDOWN_DURATION
|
||||||
return;
|
}),
|
||||||
|
)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -84,163 +94,203 @@ export const useGameStore = create<GameState & {
|
||||||
},
|
},
|
||||||
|
|
||||||
water: () => {
|
water: () => {
|
||||||
const { plots, actionCooldown } = get();
|
const { plots, actionCooldown } = get()
|
||||||
|
|
||||||
if (actionCooldown > 0) {
|
if (actionCooldown > 0) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let driestRow = -1;
|
let driestRow = -1
|
||||||
let driestCol = -1;
|
let driestCol = -1
|
||||||
let lowestMoisture = 1;
|
let lowestMoisture = 1
|
||||||
|
|
||||||
for (let row = 0; row < plots.length; row++) {
|
for (let row = 0; row < plots.length; row++) {
|
||||||
for (let col = 0; col < plots[row].length; col++) {
|
for (let col = 0; col < plots[row].length; col++) {
|
||||||
if (plots[row][col].current && plots[row][col].moisture < lowestMoisture) {
|
if (
|
||||||
lowestMoisture = plots[row][col].moisture;
|
plots[row][col].current &&
|
||||||
driestRow = row;
|
plots[row][col].moisture < lowestMoisture
|
||||||
driestCol = col;
|
) {
|
||||||
|
lowestMoisture = plots[row][col].moisture
|
||||||
|
driestRow = row
|
||||||
|
driestCol = col
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (driestRow >= 0 && driestCol >= 0) {
|
if (driestRow >= 0 && driestCol >= 0) {
|
||||||
set(produce(state => {
|
set(
|
||||||
state.plots[driestRow][driestCol].moisture = 1;
|
produce((state) => {
|
||||||
state.actionCooldown = COOLDOWN_DURATION;
|
state.plots[driestRow][driestCol].moisture = 1
|
||||||
}));
|
state.actionCooldown = COOLDOWN_DURATION
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
harvest: () => {
|
harvest: () => {
|
||||||
const { plots, actionCooldown } = get();
|
const { plots, actionCooldown } = get()
|
||||||
|
|
||||||
if (actionCooldown > 0) {
|
if (actionCooldown > 0) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let row = 0; row < plots.length; row++) {
|
for (let row = 0; row < plots.length; row++) {
|
||||||
for (let col = 0; col < plots[row].length; col++) {
|
for (let col = 0; col < plots[row].length; col++) {
|
||||||
const plot = plots[row][col];
|
const plot = plots[row][col]
|
||||||
|
|
||||||
if (plot.current && plot.current.mature) {
|
if (plot.current && plot.current.mature) {
|
||||||
const crop = CROPS[plot.current.cropId];
|
const crop = CROPS[plot.current.cropId]
|
||||||
const yieldItem = crop.yieldType;
|
const yieldItem = crop.yieldType
|
||||||
const yieldAmount = crop.yield;
|
const yieldAmount = crop.yield
|
||||||
|
|
||||||
set(produce(state => {
|
set(
|
||||||
state.plots[row][col].current = undefined;
|
produce((state) => {
|
||||||
state.inventory[yieldItem] = (state.inventory[yieldItem] || 0) + yieldAmount;
|
state.plots[row][col].current = undefined
|
||||||
state.actionCooldown = COOLDOWN_DURATION;
|
state.inventory[yieldItem] =
|
||||||
}));
|
(state.inventory[yieldItem] || 0) + yieldAmount
|
||||||
return;
|
state.actionCooldown = COOLDOWN_DURATION
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
tick: () => {
|
tick: () => {
|
||||||
set(produce(state => {
|
set(
|
||||||
// Update plots
|
produce((state) => {
|
||||||
state.plots.forEach((row: PlotState[], rowIndex: number) => {
|
// Update plots
|
||||||
row.forEach((plot: PlotState, colIndex: number) => {
|
state.plots.forEach((row: PlotState[], rowIndex: number) => {
|
||||||
if (!plot.current || plot.current.mature) {
|
row.forEach((plot: PlotState, colIndex: number) => {
|
||||||
return;
|
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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: () => {
|
upgradeField: () => {
|
||||||
set(produce(state => {
|
set(
|
||||||
if (state.fieldSize >= state.maxFieldSize) {
|
produce((state) => {
|
||||||
return;
|
if (state.fieldSize >= state.maxFieldSize) {
|
||||||
}
|
return
|
||||||
|
}
|
||||||
const upgradeIndex = state.fieldSize - INITIAL_FIELD_SIZE;
|
|
||||||
const cost = state.fieldUpgradeCosts[upgradeIndex];
|
const upgradeIndex = state.fieldSize - INITIAL_FIELD_SIZE
|
||||||
|
const cost = state.fieldUpgradeCosts[upgradeIndex]
|
||||||
if (state.cash < cost) {
|
|
||||||
return;
|
if (state.cash < cost) {
|
||||||
}
|
return
|
||||||
|
}
|
||||||
const newSize = state.fieldSize + 1;
|
|
||||||
|
const newSize = state.fieldSize + 1
|
||||||
state.cash = state.cash - cost;
|
|
||||||
state.fieldSize = newSize;
|
state.cash = state.cash - cost
|
||||||
state.plots = initializeField(newSize);
|
state.fieldSize = newSize
|
||||||
}));
|
state.plots = initializeField(newSize)
|
||||||
|
}),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
setGameSpeed: (speed) => {
|
setGameSpeed: (speed) => {
|
||||||
set(produce(state => {
|
set(
|
||||||
state.gameSpeed = speed;
|
produce((state) => {
|
||||||
}));
|
state.gameSpeed = speed
|
||||||
|
}),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
setActionCooldown: (cooldown) => {
|
setActionCooldown: (cooldown) => {
|
||||||
set(produce(state => {
|
set(
|
||||||
state.actionCooldown = cooldown;
|
produce((state) => {
|
||||||
}));
|
state.actionCooldown = cooldown
|
||||||
|
}),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
buyItem: (itemId) => {
|
buyItem: (itemId) => {
|
||||||
const { cash } = get();
|
const { cash } = get()
|
||||||
const item = MARKET_ITEMS[itemId];
|
const item = MARKET_ITEMS[itemId]
|
||||||
|
|
||||||
if (!item || item.buyPrice === null) {
|
if (!item || item.buyPrice === null) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cash < item.buyPrice) {
|
if (cash < item.buyPrice) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
set(produce(state => {
|
set(
|
||||||
state.cash -= item.buyPrice;
|
produce((state) => {
|
||||||
state.inventory[itemId] = (state.inventory[itemId] || 0) + 1;
|
state.cash -= item.buyPrice
|
||||||
}));
|
state.inventory[itemId] = (state.inventory[itemId] || 0) + 1
|
||||||
|
}),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
sellItem: (itemId) => {
|
sellItem: (itemId) => {
|
||||||
const { inventory } = get();
|
const { inventory } = get()
|
||||||
const item = MARKET_ITEMS[itemId];
|
const item = MARKET_ITEMS[itemId]
|
||||||
|
|
||||||
if (!item || item.sellPrice === null || !inventory[itemId] || inventory[itemId] <= 0) {
|
if (
|
||||||
return;
|
!item ||
|
||||||
|
item.sellPrice === null ||
|
||||||
|
!inventory[itemId] ||
|
||||||
|
inventory[itemId] <= 0
|
||||||
|
) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
set(produce(state => {
|
set(
|
||||||
state.cash += item.sellPrice;
|
produce((state) => {
|
||||||
state.inventory[itemId] -= 1;
|
state.cash += item.sellPrice
|
||||||
}));
|
state.inventory[itemId] -= 1
|
||||||
|
}),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
saveToSlot: (slot: number) => {
|
saveToSlot: (slot: number) => {
|
||||||
const state = get();
|
const state = get()
|
||||||
const { plant, water, harvest, tick, assignCrop, upgradeField, setGameSpeed, setActionCooldown, buyItem, sellItem, saveToSlot, loadFromSlot, ...gameState } = state;
|
const {
|
||||||
saveGame(slot, gameState);
|
plant,
|
||||||
|
water,
|
||||||
|
harvest,
|
||||||
|
tick,
|
||||||
|
assignCrop,
|
||||||
|
upgradeField,
|
||||||
|
setGameSpeed,
|
||||||
|
setActionCooldown,
|
||||||
|
buyItem,
|
||||||
|
sellItem,
|
||||||
|
saveToSlot,
|
||||||
|
loadFromSlot,
|
||||||
|
...gameState
|
||||||
|
} = state
|
||||||
|
saveGame(slot, gameState)
|
||||||
},
|
},
|
||||||
|
|
||||||
loadFromSlot: (slot: number) => {
|
loadFromSlot: (slot: number) => {
|
||||||
const savedState = loadGame(slot);
|
const savedState = loadGame(slot)
|
||||||
if (savedState) {
|
if (savedState) {
|
||||||
set(savedState);
|
set(savedState)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}));
|
}))
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
export const saveGame = (slot: number, state: GameState) => {
|
||||||
try {
|
try {
|
||||||
const saveData = JSON.stringify(state);
|
const saveData = JSON.stringify(state)
|
||||||
localStorage.setItem(`${SAVE_SLOT_PREFIX}${slot}`, saveData);
|
localStorage.setItem(`${SAVE_SLOT_PREFIX}${slot}`, saveData)
|
||||||
return true;
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save game:', error);
|
console.error('Failed to save game:', error)
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export const loadGame = (slot: number): GameState | null => {
|
export const loadGame = (slot: number): GameState | null => {
|
||||||
try {
|
try {
|
||||||
const saveData = localStorage.getItem(`${SAVE_SLOT_PREFIX}${slot}`);
|
const saveData = localStorage.getItem(`${SAVE_SLOT_PREFIX}${slot}`)
|
||||||
if (!saveData) return null;
|
if (!saveData) return null
|
||||||
return JSON.parse(saveData);
|
return JSON.parse(saveData)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load game:', error);
|
console.error('Failed to load game:', error)
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export const hasSaveInSlot = (slot: number): boolean => {
|
export const hasSaveInSlot = (slot: number): boolean => {
|
||||||
return !!localStorage.getItem(`${SAVE_SLOT_PREFIX}${slot}`);
|
return !!localStorage.getItem(`${SAVE_SLOT_PREFIX}${slot}`)
|
||||||
};
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue