Compare commits

..

2 commits

Author SHA1 Message Date
Ryan Lanny Jenkins 00db07e637 Move to a tool bar style interaction for the fields view. 2025-05-18 13:10:35 -05:00
Ryan Lanny Jenkins b38cc9762e Add the basis of the fertility system. 2025-05-18 12:53:49 -05:00
7 changed files with 288 additions and 139 deletions

View file

@ -1,6 +1,7 @@
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'
import { FieldTool, PlotState } from '../types'
const fieldContainerStyle: React.CSSProperties = { const fieldContainerStyle: React.CSSProperties = {
display: 'flex', display: 'flex',
@ -16,6 +17,24 @@ const titleStyle: React.CSSProperties = {
textAlign: 'center', 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 = { const cropSelectionContainerStyle: React.CSSProperties = {
display: 'flex', display: 'flex',
flexWrap: 'wrap', flexWrap: 'wrap',
@ -39,13 +58,6 @@ const getCropButtonStyle = (active: boolean): React.CSSProperties => ({
cursor: 'pointer', cursor: 'pointer',
}) })
const actionsContainerStyle: React.CSSProperties = {
display: 'flex',
gap: '0.5rem',
marginBottom: '1rem',
justifyContent: 'center',
}
const speedSelectorContainerStyle: React.CSSProperties = { const speedSelectorContainerStyle: React.CSSProperties = {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
@ -75,15 +87,6 @@ const getSpeedButtonStyle = (active: boolean): React.CSSProperties => ({
cursor: 'pointer', cursor: 'pointer',
}) })
const getActionButtonStyle = (disabled: boolean): React.CSSProperties => ({
padding: '0.5rem 1rem',
borderRadius: '0.25rem',
fontWeight: 'bold',
color: 'white',
backgroundColor: disabled ? '#9ca3af' : '#22c55e',
cursor: disabled ? 'not-allowed' : 'pointer',
})
const getFieldGridStyle = (size: number): React.CSSProperties => ({ const getFieldGridStyle = (size: number): React.CSSProperties => ({
display: 'grid', display: 'grid',
gridTemplateColumns: `repeat(${size}, 1fr)`, gridTemplateColumns: `repeat(${size}, 1fr)`,
@ -119,13 +122,13 @@ const getMoistureIndicatorStyle = (level: number): React.CSSProperties => ({
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',
left: 0, left: 0,
width: '100%', width: '100%',
height: '10px', // Increased height for better visibility height: '10px',
backgroundColor: 'rgba(0, 0, 0, 0.3)', // Darker background for better contrast backgroundColor: 'rgba(0, 0, 0, 0.3)',
zIndex: 5, // Increased z-index to ensure it's above other elements zIndex: 5,
border: '1px solid #000', // Added border for better visibility border: '1px solid #000',
}) })
const getMaturityProgressBarStyle = ( const getMaturityProgressBarStyle = (
@ -136,13 +139,117 @@ const getMaturityProgressBarStyle = (
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',
backgroundColor: '#ff5722', // Brighter orange color for better visibility backgroundColor: '#ff5722',
transition: 'width 0.3s', transition: 'width 0.3s',
zIndex: 6, // Increased z-index to ensure it's above the container zIndex: 6,
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)',
}) })
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 FieldComponent: React.FC = () => {
const { const {
plots, plots,
@ -157,21 +264,63 @@ const FieldComponent: React.FC = () => {
} = useGameStore() } = useGameStore()
const [selectedCrop, setSelectedCrop] = useState<string | null>(null) 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) => { const handlePlotClick = (row: number, col: number) => {
if (selectedCrop) { if (actionCooldown > 0) return
assignCrop(row, col, selectedCrop)
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 'inspect':
setInspectedPlot({ plot: plots[row][col], row, col })
break
} }
} }
const getBgColor = (hasCrop: boolean, isMature: boolean) => { const getBgColor = (
hasCrop: boolean,
isMature: boolean,
fertility: number,
) => {
if (isMature) return '#22c55e' if (isMature) return '#22c55e'
if (hasCrop) return '#86efac' if (hasCrop) return '#86efac'
return '#92400e'
// 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 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: 'inspect', label: 'Inspect', icon: '🔍' },
]
return ( return (
<div style={fieldContainerStyle}> <div style={fieldContainerStyle}>
<h2 style={titleStyle}>Fields</h2> <h2 style={titleStyle}>Fields</h2>
@ -191,49 +340,40 @@ const FieldComponent: React.FC = () => {
</div> </div>
</div> </div>
<div style={cropSelectionContainerStyle}> <div style={toolbarStyle}>
<p style={cropSelectionLabelStyle}>Select a crop to plant:</p> {tools.map((tool) => (
{Object.values(CROPS).map((crop) => (
<button <button
key={crop.id} key={tool.id}
style={getCropButtonStyle(selectedCrop === crop.id)} style={getToolButtonStyle(selectedTool === tool.id)}
onClick={() => setSelectedCrop(crop.id)} onClick={() => setSelectedTool(tool.id)}
> >
{crop.name} <span>{tool.icon}</span>
<span>{tool.label}</span>
</button> </button>
))} ))}
</div> </div>
<div style={actionsContainerStyle}> {selectedTool === 'mark' && (
<button <div style={cropSelectionContainerStyle}>
style={getActionButtonStyle(actionCooldown > 0)} <p style={cropSelectionLabelStyle}>Select a crop to mark:</p>
onClick={plant} {Object.values(CROPS).map((crop) => (
disabled={actionCooldown > 0} <button
> key={crop.id}
Plant Crop style={getCropButtonStyle(selectedCrop === crop.id)}
</button> onClick={() => setSelectedCrop(crop.id)}
<button >
style={getActionButtonStyle(actionCooldown > 0)} {crop.name}
onClick={water} </button>
disabled={actionCooldown > 0} ))}
> </div>
Water Crop )}
</button>
<button
style={getActionButtonStyle(actionCooldown > 0)}
onClick={harvest}
disabled={actionCooldown > 0}
>
Harvest Crop
</button>
</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, plot.fertility)
return ( return (
<div <div
@ -266,6 +406,13 @@ const FieldComponent: React.FC = () => {
}), }),
)} )}
</div> </div>
{inspectedPlot && (
<PlotInfoModal
plot={inspectedPlot.plot}
onClose={() => setInspectedPlot(null)}
/>
)}
</div> </div>
) )
} }

View file

@ -1,4 +1,9 @@
import React, { useEffect, useState, useImperativeHandle, forwardRef } from 'react' import React, {
useEffect,
useState,
useImperativeHandle,
forwardRef,
} from 'react'
interface Message { interface Message {
id: string id: string
@ -20,12 +25,12 @@ const FloatingMessages = forwardRef<FloatingMessagesHandle>((_, ref) => {
text, text,
position, position,
} }
setMessages(prev => [...prev, newMessage]) setMessages((prev) => [...prev, newMessage])
} },
})) }))
const handleAnimationEnd = (messageId: string) => { const handleAnimationEnd = (messageId: string) => {
setMessages(prev => prev.filter(msg => msg.id !== messageId)) setMessages((prev) => prev.filter((msg) => msg.id !== messageId))
} }
return ( return (
@ -52,7 +57,7 @@ const FloatingMessages = forwardRef<FloatingMessagesHandle>((_, ref) => {
} }
`} `}
</style> </style>
{messages.map(message => ( {messages.map((message) => (
<div <div
key={message.id} key={message.id}
className="floating-message" className="floating-message"

View file

@ -86,7 +86,7 @@ const MarketComponent: React.FC = () => {
floatingMessagesRef.current?.addMessage( floatingMessagesRef.current?.addMessage(
`+1 ${item.emoji} ${item.name}`, `+1 ${item.emoji} ${item.name}`,
position position,
) )
} }
@ -107,7 +107,7 @@ const MarketComponent: React.FC = () => {
floatingMessagesRef.current?.addMessage( floatingMessagesRef.current?.addMessage(
`-1 ${item.emoji} ${item.name}`, `-1 ${item.emoji} ${item.name}`,
position position,
) )
} }

View file

@ -21,6 +21,7 @@ export const CROPS: CropDefinitions = {
waterPerTick: 1 / 20, waterPerTick: 1 / 20,
yield: 1, yield: 1,
yieldType: 'celery', yieldType: 'celery',
fertilityDepletion: 0.1,
}, },
corn: { corn: {
id: 'corn', id: 'corn',
@ -29,6 +30,7 @@ export const CROPS: CropDefinitions = {
waterPerTick: 1 / 40, waterPerTick: 1 / 40,
yield: 5, yield: 5,
yieldType: 'corn', yieldType: 'corn',
fertilityDepletion: 0.3,
}, },
} }

View file

@ -10,7 +10,7 @@ import {
COOLDOWN_DURATION, COOLDOWN_DURATION,
MARKET_ITEMS, MARKET_ITEMS,
} from '../constants' } from '../constants'
import { GameState, PlotState } from '../types' import { GameState, PlotState, FieldTool } from '../types'
const initializeField = (size: number): PlotState[][] => { const initializeField = (size: number): PlotState[][] => {
return Array(size) return Array(size)
@ -20,6 +20,7 @@ const initializeField = (size: number): PlotState[][] => {
.fill(0) .fill(0)
.map(() => ({ .map(() => ({
moisture: 0, moisture: 0,
fertility: 1,
})), })),
) )
} }
@ -54,9 +55,9 @@ export const hasSaveInSlot = (slot: number): boolean => {
export const useGameStore = create< export const useGameStore = create<
GameState & { GameState & {
plant: () => void plant: (row: number, col: number) => void
water: () => void water: (row: number, col: number) => void
harvest: () => void harvest: (row: number, col: number) => void
tick: () => void tick: () => void
assignCrop: (row: number, col: number, cropId: string) => void assignCrop: (row: number, col: number, cropId: string) => void
upgradeField: () => void upgradeField: () => void
@ -86,101 +87,77 @@ export const useGameStore = create<
) )
}, },
plant: () => { plant: (row, col) => {
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++) { const plot = plots[row][col]
for (let col = 0; col < plots[row].length; col++) { if (plot.intended && !plot.current) {
const plot = plots[row][col] const seedId = `${plot.intended}_seed`
if (plot.intended && !plot.current) { if (inventory[seedId] && inventory[seedId] > 0) {
const seedId = `${plot.intended}_seed` set(
produce((state) => {
state.plots[row][col].current = {
cropId: plot.intended!,
progress: 0,
mature: false,
}
if (inventory[seedId] && inventory[seedId] > 0) { state.inventory[seedId] = state.inventory[seedId] - 1
set( state.actionCooldown = COOLDOWN_DURATION
produce((state) => { }),
state.plots[row][col].current = { )
cropId: plot.intended!,
progress: 0,
mature: false,
}
state.inventory[seedId] = state.inventory[seedId] - 1
state.actionCooldown = COOLDOWN_DURATION
}),
)
return
}
}
} }
} }
}, },
water: () => { water: (row, col) => {
const { plots, actionCooldown } = get() const { plots, actionCooldown } = get()
if (actionCooldown > 0) { if (actionCooldown > 0) {
return return
} }
let driestRow = -1 const plot = plots[row][col]
let driestCol = -1 if (plot.current && plot.moisture < 1) {
let lowestMoisture = 1
for (let row = 0; row < plots.length; row++) {
for (let col = 0; col < plots[row].length; col++) {
if (
plots[row][col].current &&
plots[row][col].moisture < lowestMoisture
) {
lowestMoisture = plots[row][col].moisture
driestRow = row
driestCol = col
}
}
}
if (driestRow >= 0 && driestCol >= 0) {
set( set(
produce((state) => { produce((state) => {
state.plots[driestRow][driestCol].moisture = 1 state.plots[row][col].moisture = 1
state.actionCooldown = COOLDOWN_DURATION state.actionCooldown = COOLDOWN_DURATION
}), }),
) )
} }
}, },
harvest: () => { harvest: (row, col) => {
const { plots, actionCooldown } = get() const { plots, actionCooldown } = get()
if (actionCooldown > 0) { if (actionCooldown > 0) {
return return
} }
for (let row = 0; row < plots.length; row++) { const plot = plots[row][col]
for (let col = 0; col < plots[row].length; col++) { if (plot.current && plot.current.mature) {
const plot = plots[row][col] const crop = CROPS[plot.current.cropId]
const yieldItem = crop.yieldType
const yieldAmount = crop.yield
if (plot.current && plot.current.mature) { set(
const crop = CROPS[plot.current.cropId] produce((state) => {
const yieldItem = crop.yieldType state.plots[row][col].current = undefined
const yieldAmount = crop.yield state.inventory[yieldItem] =
(state.inventory[yieldItem] || 0) + yieldAmount
set( state.plots[row][col].fertility = Math.max(
produce((state) => { 0,
state.plots[row][col].current = undefined state.plots[row][col].fertility - crop.fertilityDepletion,
state.inventory[yieldItem] =
(state.inventory[yieldItem] || 0) + yieldAmount
state.actionCooldown = COOLDOWN_DURATION
}),
) )
return state.actionCooldown = COOLDOWN_DURATION
} }),
} )
} }
}, },
@ -190,6 +167,14 @@ export const useGameStore = create<
// Update plots // Update plots
state.plots.forEach((row: PlotState[], rowIndex: number) => { state.plots.forEach((row: PlotState[], rowIndex: number) => {
row.forEach((plot: PlotState, colIndex: number) => { row.forEach((plot: PlotState, colIndex: number) => {
// Regenerate fertility every 100 ticks
if (state.tickCount % 100 === 0 && plot.fertility < 1) {
state.plots[rowIndex][colIndex].fertility = Math.min(
1,
plot.fertility + 0.1,
)
}
if (!plot.current || plot.current.mature) { if (!plot.current || plot.current.mature) {
return return
} }
@ -197,8 +182,15 @@ export const useGameStore = create<
const crop = CROPS[plot.current.cropId] const crop = CROPS[plot.current.cropId]
const waterNeeded = crop.waterPerTick const waterNeeded = crop.waterPerTick
if (plot.moisture >= waterNeeded) { // Only grow if fertility is above 0.2
const newProgress = plot.current.progress + 1 if (plot.moisture >= waterNeeded && plot.fertility >= 0.2) {
let growthRate = 1
// Half growth rate if fertility is between 0.2 and 0.5
if (plot.fertility < 0.5) {
growthRate = 0.5
}
const newProgress = plot.current.progress + growthRate
const mature = newProgress >= crop.growthTicks const mature = newProgress >= crop.growthTicks
state.plots[rowIndex][colIndex].moisture = state.plots[rowIndex][colIndex].moisture =
@ -257,7 +249,7 @@ export const useGameStore = create<
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 || item.buyPrice === undefined) {
return return
} }
@ -267,7 +259,7 @@ export const useGameStore = create<
set( set(
produce((state) => { produce((state) => {
state.cash -= item.buyPrice state.cash -= item.buyPrice!
state.inventory[itemId] = (state.inventory[itemId] || 0) + 1 state.inventory[itemId] = (state.inventory[itemId] || 0) + 1
}), }),
) )

View file

@ -1,5 +1,5 @@
import { useEffect } from "react" import { useEffect } from 'react'
import { hasSaveInSlot, useGameStore } from "./useGameStore" import { hasSaveInSlot, useGameStore } from './useGameStore'
export const useSaveSystem = () => { export const useSaveSystem = () => {
const { saveToSlot, loadFromSlot } = useGameStore() const { saveToSlot, loadFromSlot } = useGameStore()
@ -31,7 +31,6 @@ export const useSaveSystem = () => {
} }
} }
window.addEventListener('keydown', handleKeyDown) window.addEventListener('keydown', handleKeyDown)
const interval = setInterval(() => { const interval = setInterval(() => {
saveToSlot(0) saveToSlot(0)

View file

@ -5,6 +5,7 @@ export interface Crop {
waterPerTick: number waterPerTick: number
yield: number yield: number
yieldType: string yieldType: string
fertilityDepletion: number
} }
export interface CropDefinitions { export interface CropDefinitions {
@ -19,6 +20,7 @@ export interface PlotState {
mature: boolean mature: boolean
} }
moisture: number moisture: number
fertility: number
} }
export interface InventoryItem { export interface InventoryItem {
@ -26,6 +28,8 @@ export interface InventoryItem {
count: number count: number
} }
export type FieldTool = 'mark' | 'plant' | 'water' | 'harvest' | 'inspect'
export interface GameState { export interface GameState {
cash: number cash: number
inventory: Record<string, number> inventory: Record<string, number>