UNPKG

@jeremyckahn/farmhand

Version:
474 lines (439 loc) 12.1 kB
import React, { memo, useEffect, useState } from 'react' import { array, bool, element, func, number, object, string } from 'prop-types' import Fab from '@mui/material/Fab/index.js' import FormControl from '@mui/material/FormControl/index.js' import FormControlLabel from '@mui/material/FormControlLabel/index.js' import FormGroup from '@mui/material/FormGroup/index.js' import Switch from '@mui/material/Switch/index.js' import Slider from '@mui/material/Slider/index.js' import ZoomInIcon from '@mui/icons-material/ZoomIn.js' import ZoomOutIcon from '@mui/icons-material/ZoomOut.js' import Tooltip from '@mui/material/Tooltip/index.js' import Typography from '@mui/material/Typography/index.js' import { TransformWrapper, TransformComponent } from 'react-zoom-pan-pinch' import { GlobalHotKeys } from 'react-hotkeys' import classNames from 'classnames' import FarmhandContext from '../Farmhand/Farmhand.context.js' import Plot from '../Plot/index.js' import QuickSelect from '../QuickSelect/index.js' import { fieldMode } from '../../enums.js' import tools from '../../data/tools.js' import { levelAchieved } from '../../utils/levelAchieved.js' import { doesInventorySpaceRemain, nullArray } from '../../utils/index.js' import { getLevelEntitlements } from '../../utils/getLevelEntitlements.js' import './Field.sass' const { CLEANUP, FERTILIZE, HARVEST, MINE, OBSERVE, PLANT, SET_SCARECROW, SET_SPRINKLER, WATER, } = fieldMode const zoomKeyMap = { zoomIn: ['=', 'plus'], zoomOut: '-', } const fieldKeyMap = { selectWateringCan: tools.wateringCan.fieldKey, selectScythe: tools.scythe.fieldKey, selectHoe: tools.hoe.fieldKey, } if (tools.shovel) { fieldKeyMap.selectShovel = tools.shovel.fieldKey } export const isInHoverRange = ({ experience, fieldMode, hoveredPlotRangeSize, hoveredPlot: { x: hoveredPlotX, y: hoveredPlotY }, x, y, }) => { // If hoveredPlotX is null, assume that hoveredPlotY is as well. // If fieldMode === OBSERVE, nothing is in hover range. if (hoveredPlotX == null || fieldMode === OBSERVE) { return false } let hoveredPlotRangeSizeToRender = hoveredPlotRangeSize switch (fieldMode) { case SET_SPRINKLER: hoveredPlotRangeSizeToRender = getLevelEntitlements( levelAchieved(experience) ).sprinklerRange break case SET_SCARECROW: hoveredPlotRangeSizeToRender = Number.MAX_SAFE_INTEGER break default: } const squareSize = 2 * hoveredPlotRangeSizeToRender const rangeFloorX = hoveredPlotX - hoveredPlotRangeSizeToRender const rangeFloorY = hoveredPlotY - hoveredPlotRangeSizeToRender const rangeCeilingX = rangeFloorX + squareSize const rangeCeilingY = rangeFloorY + squareSize return ( x >= rangeFloorX && x <= rangeCeilingX && y >= rangeFloorY && y <= rangeCeilingY ) } export const MemoPlot = memo( /** * @param {object} props * @param {number} props.experience * @param {string} props.fieldMode * @param {object} props.hoveredPlot * @param {number} props.hoveredPlotRangeSize * @param {object} props.plotContent * @param {function} props.setHoveredPlot * @param {number} props.x * @param {number} props.y */ props => { const { hoveredPlot, plotContent, setHoveredPlot, x, y } = props return ( <Plot {...{ hoveredPlot, isInHoverRange: isInHoverRange(props), plotContent, setHoveredPlot, x, y, }} /> ) }, (prev, next) => { if (isInHoverRange(prev) !== isInHoverRange(next)) { return false } return ( prev.plotContent === next.plotContent && prev.hoveredPlotRangeSize === next.hoveredPlotRangeSize ) } ) // @ts-expect-error MemoPlot.propTypes = { experience: number.isRequired, fieldMode: string.isRequired, hoveredPlot: object.isRequired, hoveredPlotRangeSize: number.isRequired, plotContent: object, setHoveredPlot: func.isRequired, x: number.isRequired, y: number.isRequired, } export const FieldContentWrapper = ({ fieldContent, previousScale, resetTransform, scale, zoomIn, zoomOut, }) => { useEffect(() => { if (scale === 1 && previousScale !== 1) { resetTransform() } }, [scale, previousScale, resetTransform]) return ( <> <GlobalHotKeys {...{ keyMap: zoomKeyMap, handlers: { zoomIn, zoomOut, }, }} /> <TransformComponent>{fieldContent}</TransformComponent> <div className="fab-buttons zoom-controls zoom-in-wrapper"> <Tooltip {...{ placement: 'top', title: 'Zoom In', }} > <Fab {...{ 'aria-label': 'Zoom In', color: 'primary', onClick: zoomIn, }} > <ZoomInIcon /> </Fab> </Tooltip> </div> <div className="fab-buttons zoom-controls zoom-out-wrapper"> <Tooltip {...{ placement: 'top', title: 'Zoom Out', }} > <Fab {...{ 'aria-label': 'Zoom Out', color: 'primary', onClick: zoomOut, }} > <ZoomOutIcon /> </Fab> </Tooltip> </div> </> ) } FieldContentWrapper.propTypes = { fieldContent: element.isRequired, } export const FieldContent = ({ columns, experience, field, fieldMode, handleCombineEnabledChange, hoveredPlot, hoveredPlotRangeSize, isCombineEnabled, purchasedCombine, rows, setHoveredPlot, }) => ( <> <div {...{ className: 'row-wrapper', onMouseLeave: () => setHoveredPlot({ x: null, y: null }), }} > {nullArray(rows).map((_null, y) => ( <div className="row" key={y}> {nullArray(columns).map( (_null, x, arr, plotContent = field[y][x]) => ( <MemoPlot key={x} {...{ experience, fieldMode, hoveredPlot, hoveredPlotRangeSize, plotContent, setHoveredPlot, x, y, }} /> ) )} </div> ))} </div> {purchasedCombine ? ( <FormControl variant="standard" component="fieldset"> <FormGroup> <FormControlLabel control={ <Switch color="primary" checked={isCombineEnabled} onChange={handleCombineEnabledChange} name="is-combine-enabled" /> } label="Automatically harvest crops at the start of every day" /> </FormGroup> </FormControl> ) : null} </> ) FieldContent.propTypes = { columns: number.isRequired, experience: number.isRequired, field: array.isRequired, fieldMode: string.isRequired, handleCombineEnabledChange: func.isRequired, hoveredPlot: object.isRequired, hoveredPlotRangeSize: number.isRequired, isCombineEnabled: bool.isRequired, purchasedCombine: number.isRequired, rows: number.isRequired, setHoveredPlot: func.isRequired, } const adjustableRangeFieldModes = new Set([ CLEANUP, FERTILIZE, HARVEST, MINE, PLANT, WATER, ]) const RangeSliderValueLabelComponent = ({ children, open, value }) => ( <Tooltip {...{ open, placement: 'top', title: ( <Typography> Range: {value} x {value} </Typography> ), }} > {children} </Tooltip> ) export const Field = props => { const { field, fieldMode, handleFieldActionRangeChange, hoveredPlotRangeSize, inventory, inventoryLimit, purchasedField, } = props const [hoveredPlot, setHoveredPlot] = useState({ x: null, y: null }) const [currentScale, setCurrentScale] = useState(1) const [fieldActionRange, setFieldActionRange] = useState(hoveredPlotRangeSize) useEffect(() => { setFieldActionRange(hoveredPlotRangeSize) }, [hoveredPlotRangeSize]) const handleFieldActionRangeSliderChange = value => { setFieldActionRange(value) handleFieldActionRangeChange(value) } return ( <> <GlobalHotKeys {...{ keyMap: fieldKeyMap, // Handlers are defined in Farmhand.js's initInputHandlers. }} /> <div {...{ className: classNames('Field', { 'cleanup-mode': fieldMode === CLEANUP, 'fertilize-mode': fieldMode === FERTILIZE, 'harvest-mode': fieldMode === HARVEST, 'mine-mode': fieldMode === MINE, // @ts-expect-error - Unnecessary properties are omitted out of convenience 'is-inventory-full': !doesInventorySpaceRemain({ inventory, inventoryLimit, }), 'plant-mode': fieldMode === PLANT, 'set-scarecrow-mode': fieldMode === SET_SCARECROW, 'set-sprinkler-mode': fieldMode === SET_SPRINKLER, 'water-mode': fieldMode === WATER, }), 'data-purchased-field': purchasedField, 'data-testid': 'field', }} > <TransformWrapper {...{ options: { limitToBounds: false, }, reset: { animationTime: 0, }, pan: { disabled: currentScale <= 1, }, // These 0s prevent NREs within react-zoom-pan-pinch, but also // disable zoom animations. zoomIn: { animationTime: 0, }, zoomOut: { animationTime: 0, }, onZoomChange: ({ scale }) => { // If setCurrentScale with scale < 1 is called here, it causes a // reference error within react-zoom-pan-pinch. if (scale >= 1) { setCurrentScale(scale) } }, wheel: { disabled: true, }, doubleClick: { disabled: true }, }} > {transformProps => ( <FieldContentWrapper {...{ ...transformProps, fieldContent: ( <FieldContent {...{ ...props, hoveredPlot, setHoveredPlot }} /> ), }} /> )} </TransformWrapper> {adjustableRangeFieldModes.has(fieldMode) && ( <div className="slider-wrapper"> <Slider {...{ marks: true, max: field.length - 1, min: 0, onChange: (e, value) => handleFieldActionRangeSliderChange(value), step: 1, value: fieldActionRange, valueLabelDisplay: 'auto', valueLabelFormat: value => `${value * 2 + 1}`, components: { ValueLabel: RangeSliderValueLabelComponent, }, }} /> </div> )} <QuickSelect /> </div> </> ) } Field.propTypes = { columns: number.isRequired, experience: number.isRequired, field: array.isRequired, fieldMode: string.isRequired, handleCombineEnabledChange: func.isRequired, handleFieldActionRangeChange: func.isRequired, hoveredPlotRangeSize: number.isRequired, inventory: array.isRequired, inventoryLimit: number.isRequired, isCombineEnabled: bool.isRequired, purchasedCombine: number.isRequired, purchasedField: number.isRequired, rows: number.isRequired, } export default function Consumer(props) { return ( <FarmhandContext.Consumer> {({ gameState, handlers }) => ( <Field {...{ ...gameState, ...handlers, ...props }} /> )} </FarmhandContext.Consumer> ) }