425 lines
11 KiB
TypeScript
425 lines
11 KiB
TypeScript
import React, { useState } from 'react'
|
||
import { useGameStore } from '../store/useGameStore'
|
||
import { CROPS } from '../constants'
|
||
import { FieldTool, PlotState } from '../types'
|
||
|
||
const fieldContainerStyle: React.CSSProperties = {
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
gap: '1rem',
|
||
padding: '1rem',
|
||
}
|
||
|
||
const titleStyle: React.CSSProperties = {
|
||
fontSize: '1.5rem',
|
||
fontWeight: 'bold',
|
||
marginBottom: '1rem',
|
||
textAlign: 'center',
|
||
}
|
||
|
||
const toolbarStyle: React.CSSProperties = {
|
||
display: 'flex',
|
||
gap: '0.5rem',
|
||
marginBottom: '1rem',
|
||
justifyContent: 'center',
|
||
}
|
||
|
||
const getToolButtonStyle = (active: boolean): React.CSSProperties => ({
|
||
padding: '0.5rem 1rem',
|
||
borderRadius: '0.25rem',
|
||
border: '1px solid #ccc',
|
||
backgroundColor: active ? '#4ade80' : '#f3f4f6',
|
||
cursor: 'pointer',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '0.5rem',
|
||
})
|
||
|
||
const cropSelectionContainerStyle: React.CSSProperties = {
|
||
display: 'flex',
|
||
flexWrap: 'wrap',
|
||
gap: '0.5rem',
|
||
marginBottom: '1rem',
|
||
justifyContent: 'center',
|
||
}
|
||
|
||
const cropSelectionLabelStyle: React.CSSProperties = {
|
||
width: '100%',
|
||
textAlign: 'center',
|
||
fontWeight: 500,
|
||
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',
|
||
})
|
||
|
||
const speedSelectorContainerStyle: React.CSSProperties = {
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
alignItems: 'center',
|
||
marginBottom: '1rem',
|
||
padding: '0.5rem',
|
||
backgroundColor: '#f9fafb',
|
||
borderRadius: '0.5rem',
|
||
border: '1px solid #e5e7eb',
|
||
}
|
||
|
||
const speedSelectorLabelStyle: React.CSSProperties = {
|
||
fontWeight: 500,
|
||
marginBottom: '0.5rem',
|
||
}
|
||
|
||
const speedButtonsContainerStyle: React.CSSProperties = {
|
||
display: 'flex',
|
||
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',
|
||
})
|
||
|
||
const getFieldGridStyle = (size: number): React.CSSProperties => ({
|
||
display: 'grid',
|
||
gridTemplateColumns: `repeat(${size}, 1fr)`,
|
||
gridTemplateRows: `repeat(${size}, 1fr)`,
|
||
gap: '0.5rem',
|
||
width: '100%',
|
||
maxWidth: '600px',
|
||
margin: '0 auto',
|
||
})
|
||
|
||
const getPlotStyle = (bgColor: string): React.CSSProperties => ({
|
||
border: '1px solid #78350f',
|
||
aspectRatio: '1',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
position: 'relative',
|
||
cursor: 'pointer',
|
||
backgroundColor: bgColor,
|
||
})
|
||
|
||
const getMoistureIndicatorStyle = (level: number): React.CSSProperties => ({
|
||
position: 'absolute',
|
||
bottom: 0,
|
||
left: 0,
|
||
width: '100%',
|
||
height: `${Math.min(level * 100, 100)}%`,
|
||
backgroundColor: 'rgba(147, 197, 253, 0.3)',
|
||
transition: 'height 0.3s',
|
||
zIndex: 1,
|
||
})
|
||
|
||
const getMaturityProgressContainerStyle = (): React.CSSProperties => ({
|
||
position: 'absolute',
|
||
bottom: '10px',
|
||
left: 0,
|
||
width: '100%',
|
||
height: '10px',
|
||
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||
zIndex: 5,
|
||
border: '1px solid #000',
|
||
})
|
||
|
||
const getMaturityProgressBarStyle = (
|
||
progress: number,
|
||
total: number,
|
||
): React.CSSProperties => ({
|
||
position: 'absolute',
|
||
bottom: 0,
|
||
left: 0,
|
||
width: `${Math.min((progress / total) * 100, 100)}%`,
|
||
height: '10px',
|
||
backgroundColor: '#ff5722',
|
||
transition: 'width 0.3s',
|
||
zIndex: 6,
|
||
boxShadow: '0 0 3px rgba(0, 0, 0, 0.5)',
|
||
})
|
||
|
||
const modalOverlayStyle: React.CSSProperties = {
|
||
position: 'fixed',
|
||
top: 0,
|
||
left: 0,
|
||
right: 0,
|
||
bottom: 0,
|
||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||
display: 'flex',
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
zIndex: 1000,
|
||
}
|
||
|
||
const modalContentStyle: React.CSSProperties = {
|
||
backgroundColor: 'white',
|
||
padding: '2rem',
|
||
borderRadius: '0.5rem',
|
||
maxWidth: '400px',
|
||
width: '90%',
|
||
position: 'relative',
|
||
}
|
||
|
||
const modalCloseButtonStyle: React.CSSProperties = {
|
||
position: 'absolute',
|
||
top: '0.5rem',
|
||
right: '0.5rem',
|
||
padding: '0.5rem',
|
||
border: 'none',
|
||
background: 'none',
|
||
cursor: 'pointer',
|
||
fontSize: '1.5rem',
|
||
}
|
||
|
||
const plotInfoStyle: React.CSSProperties = {
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
gap: '1rem',
|
||
}
|
||
|
||
const plotInfoItemStyle: React.CSSProperties = {
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
padding: '0.5rem',
|
||
backgroundColor: '#f3f4f6',
|
||
borderRadius: '0.25rem',
|
||
}
|
||
|
||
interface PlotInfoModalProps {
|
||
plot: PlotState
|
||
onClose: () => void
|
||
}
|
||
|
||
const PlotInfoModal: React.FC<PlotInfoModalProps> = ({ plot, onClose }) => {
|
||
const formatPercentage = (value: number) => `${Math.round(value * 100)}%`
|
||
|
||
return (
|
||
<div style={modalOverlayStyle} onClick={onClose}>
|
||
<div style={modalContentStyle} onClick={(e) => e.stopPropagation()}>
|
||
<button style={modalCloseButtonStyle} onClick={onClose}>
|
||
×
|
||
</button>
|
||
<h3 style={{ marginTop: 0 }}>Plot Information</h3>
|
||
<div style={plotInfoStyle}>
|
||
<div style={plotInfoItemStyle}>
|
||
<span>Current Crop:</span>
|
||
<span>
|
||
{plot.current
|
||
? `${CROPS[plot.current.cropId].name}${
|
||
plot.current.mature ? ' (Mature)' : ''
|
||
}`
|
||
: 'None'}
|
||
</span>
|
||
</div>
|
||
<div style={plotInfoItemStyle}>
|
||
<span>Intended Crop:</span>
|
||
<span>{plot.intended ? CROPS[plot.intended].name : 'None'}</span>
|
||
</div>
|
||
<div style={plotInfoItemStyle}>
|
||
<span>Water Level:</span>
|
||
<span>{formatPercentage(plot.moisture)}</span>
|
||
</div>
|
||
<div style={plotInfoItemStyle}>
|
||
<span>Fertility Level:</span>
|
||
<span>{formatPercentage(plot.fertility)}</span>
|
||
</div>
|
||
{plot.current && !plot.current.mature && (
|
||
<div style={plotInfoItemStyle}>
|
||
<span>Growth Progress:</span>
|
||
<span>
|
||
{formatPercentage(
|
||
plot.current.progress /
|
||
CROPS[plot.current.cropId].growthTicks,
|
||
)}
|
||
</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const FieldComponent: React.FC = () => {
|
||
const {
|
||
plots,
|
||
fieldSize,
|
||
plant,
|
||
water,
|
||
harvest,
|
||
remove,
|
||
assignCrop,
|
||
gameSpeed,
|
||
setGameSpeed,
|
||
actionCooldown,
|
||
} = useGameStore()
|
||
|
||
const [selectedCrop, setSelectedCrop] = useState<string | null>(null)
|
||
const [selectedTool, setSelectedTool] = useState<FieldTool>('mark')
|
||
const [inspectedPlot, setInspectedPlot] = useState<{
|
||
plot: PlotState
|
||
row: number
|
||
col: number
|
||
} | null>(null)
|
||
|
||
const handlePlotClick = (row: number, col: number) => {
|
||
if (actionCooldown > 0) return
|
||
|
||
switch (selectedTool) {
|
||
case 'mark':
|
||
if (selectedCrop) {
|
||
assignCrop(row, col, selectedCrop)
|
||
}
|
||
break
|
||
case 'plant':
|
||
plant(row, col)
|
||
break
|
||
case 'water':
|
||
water(row, col)
|
||
break
|
||
case 'harvest':
|
||
harvest(row, col)
|
||
break
|
||
case 'remove':
|
||
remove(row, col)
|
||
break
|
||
case 'inspect':
|
||
setInspectedPlot({ plot: plots[row][col], row, col })
|
||
break
|
||
}
|
||
}
|
||
|
||
const getBgColor = (
|
||
hasCrop: boolean,
|
||
isMature: boolean,
|
||
fertility: number,
|
||
) => {
|
||
if (isMature) return '#22c55e'
|
||
if (hasCrop) return '#86efac'
|
||
|
||
// For empty plots, show fertility level through color
|
||
// Convert fertility (0-1) to a color between light grey-brown and dark brown
|
||
const r = Math.floor(146 + (73 - 146) * fertility) // 146 to 73
|
||
const g = Math.floor(131 + (47 - 131) * fertility) // 131 to 47
|
||
const b = Math.floor(120 + (23 - 120) * fertility) // 120 to 23
|
||
return `rgb(${r}, ${g}, ${b})`
|
||
}
|
||
|
||
const availableSpeeds = [1, 2, 4, 8, 16, 32, 64]
|
||
|
||
const tools: { id: FieldTool; label: string; icon: string }[] = [
|
||
{ id: 'mark', label: 'Mark', icon: '🎯' },
|
||
{ id: 'plant', label: 'Plant', icon: '🌱' },
|
||
{ id: 'water', label: 'Water', icon: '💧' },
|
||
{ id: 'harvest', label: 'Harvest', icon: '✂️' },
|
||
{ id: 'remove', label: 'Remove', icon: '🗑️' },
|
||
{ id: 'inspect', label: 'Inspect', icon: '🔍' },
|
||
]
|
||
|
||
return (
|
||
<div style={fieldContainerStyle}>
|
||
<h2 style={titleStyle}>Fields</h2>
|
||
|
||
<div style={speedSelectorContainerStyle}>
|
||
<p style={speedSelectorLabelStyle}>Game Speed:</p>
|
||
<div style={speedButtonsContainerStyle}>
|
||
{availableSpeeds.map((speed) => (
|
||
<button
|
||
style={getSpeedButtonStyle(gameSpeed === speed)}
|
||
onClick={() => setGameSpeed(speed)}
|
||
key={speed}
|
||
>
|
||
{speed}x
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div style={toolbarStyle}>
|
||
{tools.map((tool) => (
|
||
<button
|
||
key={tool.id}
|
||
style={getToolButtonStyle(selectedTool === tool.id)}
|
||
onClick={() => setSelectedTool(tool.id)}
|
||
>
|
||
<span>{tool.icon}</span>
|
||
<span>{tool.label}</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{selectedTool === 'mark' && (
|
||
<div style={cropSelectionContainerStyle}>
|
||
<p style={cropSelectionLabelStyle}>Select a crop to mark:</p>
|
||
{Object.values(CROPS).map((crop) => (
|
||
<button
|
||
key={crop.id}
|
||
style={getCropButtonStyle(selectedCrop === crop.id)}
|
||
onClick={() => setSelectedCrop(crop.id)}
|
||
>
|
||
{crop.name}
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
<div style={getFieldGridStyle(fieldSize)}>
|
||
{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, plot.fertility)
|
||
|
||
return (
|
||
<div
|
||
key={`${rowIndex}-${colIndex}`}
|
||
style={getPlotStyle(bgColor)}
|
||
onClick={() => handlePlotClick(rowIndex, colIndex)}
|
||
>
|
||
{plot.intended && !plot.current && (
|
||
<div>🌱 {CROPS[plot.intended]?.name}</div>
|
||
)}
|
||
{plot.current && (
|
||
<div>
|
||
{plot.current.mature ? '🌿' : '🌱'}{' '}
|
||
{CROPS[plot.current.cropId]?.name}
|
||
</div>
|
||
)}
|
||
<div style={getMoistureIndicatorStyle(plot.moisture)} />
|
||
{plot.current && !plot.current.mature && (
|
||
<div style={getMaturityProgressContainerStyle()}>
|
||
<div
|
||
style={getMaturityProgressBarStyle(
|
||
plot.current.progress,
|
||
CROPS[plot.current.cropId].growthTicks,
|
||
)}
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}),
|
||
)}
|
||
</div>
|
||
|
||
{inspectedPlot && (
|
||
<PlotInfoModal
|
||
plot={inspectedPlot.plot}
|
||
onClose={() => setInspectedPlot(null)}
|
||
/>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default FieldComponent
|