DionysianIdle/src/components/Field.tsx

425 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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