@jeremyckahn/farmhand
Version:
A farming game
415 lines (388 loc) • 11.9 kB
JavaScript
import React, { useState, useEffect } from 'react'
import classNames from 'classnames'
import AccountBalanceIcon from '@mui/icons-material/AccountBalance.js'
import AssessmentIcon from '@mui/icons-material/Assessment.js'
import BeenhereIcon from '@mui/icons-material/Beenhere.js'
import BookIcon from '@mui/icons-material/Book.js'
import Button from '@mui/material/Button/index.js'
import Dialog from '@mui/material/Dialog/index.js'
import DialogActions from '@mui/material/DialogActions/index.js'
import DialogContent from '@mui/material/DialogContent/index.js'
import DialogTitle from '@mui/material/DialogTitle/index.js'
import Fab from '@mui/material/Fab/index.js'
import FlashOnIcon from '@mui/icons-material/FlashOn.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 MenuItem from '@mui/material/MenuItem/index.js'
import Select from '@mui/material/Select/index.js'
import SettingsIcon from '@mui/icons-material/Settings.js'
import Switch from '@mui/material/Switch/index.js'
import TextField from '@mui/material/TextField/index.js'
import Tooltip from '@mui/material/Tooltip/index.js'
import Typography from '@mui/material/Typography/index.js'
import { array, bool, func, number, string } from 'prop-types'
import FarmhandContext from '../Farmhand/Farmhand.context.js'
import {
doesInventorySpaceRemain,
integerString,
inventorySpaceConsumed,
} from '../../utils/index.js'
import { dialogView } from '../../enums.js'
import {
DEFAULT_ROOM,
INFINITE_STORAGE_LIMIT,
STAGE_TITLE_MAP,
} from '../../constants.js'
import { MAX_ROOM_NAME_LENGTH } from '../../common/constants.js'
import AccountingView from '../AccountingView/index.js'
import AchievementsView from '../AchievementsView/index.js'
import LogView from '../LogView/index.js'
import OnlinePeersView from '../OnlinePeersView/index.js'
import PriceEventView from '../PriceEventView/index.js'
import SettingsView from '../SettingsView/index.js'
import StatsView from '../StatsView/index.js'
import KeybindingsView from '../KeybindingsView/index.js'
import DayAndProgressContainer from './DayAndProgressContainer.js'
import './Navigation.sass'
const FarmNameDisplay = ({ farmName, handleFarmNameUpdate }) => {
const [displayedFarmName, setDisplayedFarmName] = useState(farmName)
useEffect(() => {
setDisplayedFarmName(farmName)
}, [farmName, setDisplayedFarmName])
return (
<h2 className="farm-name">
<TextField
variant="standard"
{...{
inputProps: {
maxLength: 12,
},
onChange: ({ target: { value } }) => {
setDisplayedFarmName(value)
},
onBlur: ({ target: { value } }) => {
handleFarmNameUpdate(value)
},
placeholder: 'Farm Name',
value: displayedFarmName,
}}
/>{' '}
Farm
</h2>
)
}
const OnlineControls = ({
activePlayers,
handleActivePlayerButtonClick,
handleChatRoomOpenStateChange,
handleOnlineToggleChange,
handleRoomChange,
isChatAvailable,
isOnline,
room,
}) => {
const [displayedRoom, setDisplayedRoom] = useState(room)
useEffect(() => {
setDisplayedRoom(room)
}, [room, setDisplayedRoom])
const handleChatButtonClick = () => {
handleChatRoomOpenStateChange(true)
}
const submitRoomNameChange = () => {
handleRoomChange(displayedRoom)
}
/**
* @param {boolean} checked
*/
const handleSwitchChange = checked => {
submitRoomNameChange()
handleOnlineToggleChange(checked)
}
return (
<>
<FormControl
variant="standard"
{...{ className: 'online-control-container', component: 'fieldset' }}
>
<FormGroup {...{ className: 'toggle-container' }}>
<FormControlLabel
control={
<Switch
color="primary"
checked={isOnline}
onChange={(_, checked) => {
handleSwitchChange(checked)
}}
name="use-alternate-end-day-button-position"
/>
}
label="Play online"
/>
</FormGroup>
<TextField
{...{
className: 'room-name',
inputProps: {
maxLength: MAX_ROOM_NAME_LENGTH,
},
label: 'Room name',
onChange: ({ target: { value } }) => {
setDisplayedRoom(value)
},
onKeyUp: ({ key }) => {
if (key === 'Enter') {
submitRoomNameChange()
}
},
value: displayedRoom,
variant: 'outlined',
}}
/>
</FormControl>
{activePlayers && (
<>
<Button
{...{
color: 'primary',
onClick: handleActivePlayerButtonClick,
variant: 'contained',
}}
>
Connected players: {integerString(activePlayers)}
</Button>
{isChatAvailable ? (
<Button
variant="contained"
color="primary"
onClick={handleChatButtonClick}
>
Open chat
</Button>
) : (
<Typography className="chat-placeholder">
Chat is available for online rooms other than "{DEFAULT_ROOM}"
</Typography>
)}
</>
)}
</>
)
}
const {
FARMERS_LOG,
PRICE_EVENTS,
STATS,
ACHIEVEMENTS,
ACCOUNTING,
SETTINGS,
ONLINE_PEERS,
// Has no UI trigger
KEYBINDINGS,
} = dialogView
// The labels here must be kept in sync with mappings in initInputHandlers in
// Farmhand.js.
const dialogTriggerTextMap = {
[FARMERS_LOG]: "Open Farmer's Log (l)",
[PRICE_EVENTS]: 'See Price Events (e)',
[STATS]: 'View your stats (s)',
[ACHIEVEMENTS]: 'View Achievements (a)',
[ACCOUNTING]: 'View Bank Account (b)',
[SETTINGS]: 'View Settings (comma)',
}
const dialogTitleMap = {
[FARMERS_LOG]: "Farmer's Log",
[PRICE_EVENTS]: 'Price Events',
[STATS]: 'Farm Stats',
[ACHIEVEMENTS]: 'Achievements',
[ACCOUNTING]: 'Bank Account',
[SETTINGS]: 'Settings',
[ONLINE_PEERS]: 'Active Players',
// Has no UI trigger
[KEYBINDINGS]: 'Keyboard Shortcuts',
}
const dialogContentMap = {
[FARMERS_LOG]: <LogView />,
[PRICE_EVENTS]: <PriceEventView />,
[STATS]: <StatsView />,
[ACHIEVEMENTS]: <AchievementsView />,
[ACCOUNTING]: <AccountingView />,
[SETTINGS]: <SettingsView />,
[ONLINE_PEERS]: <OnlinePeersView />,
// Has no UI trigger
[KEYBINDINGS]: <KeybindingsView />,
}
export const Navigation = ({
activePlayers,
blockInput,
currentDialogView,
farmName,
handleActivePlayerButtonClick,
handleChatRoomOpenStateChange,
handleClickDialogViewButton,
handleCloseDialogView,
handleDialogViewExited,
handleFarmNameUpdate,
handleOnlineToggleChange,
handleRoomChange,
handleViewChange,
inventory,
inventoryLimit,
isChatAvailable,
isDialogViewOpen,
isOnline,
room,
stageFocus,
viewList,
currentDialogViewLowerCase = currentDialogView.toLowerCase(),
modalTitleId = `${currentDialogViewLowerCase}-modal-title`,
modalContentId = `${currentDialogViewLowerCase}-modal-content`,
}) => {
return (
<header className="Navigation">
<h1>Farmhand</h1>
<p className="version">
v{import.meta.env?.VITE_FARMHAND_PACKAGE_VERSION}
</p>
<FarmNameDisplay {...{ farmName, handleFarmNameUpdate }} />
<DayAndProgressContainer />
<OnlineControls
{...{
activePlayers,
handleActivePlayerButtonClick,
handleChatRoomOpenStateChange,
handleOnlineToggleChange,
handleRoomChange,
isChatAvailable,
isOnline,
room,
}}
/>
{inventoryLimit > INFINITE_STORAGE_LIMIT && (
<h3
{...{
className: classNames('inventory-info', {
// @ts-expect-error
'is-inventory-full': !doesInventorySpaceRemain({
inventory,
inventoryLimit,
}),
}),
}}
>
Inventory: {integerString(inventorySpaceConsumed(inventory))} /{' '}
{integerString(inventoryLimit)}
</h3>
)}
<Select
variant="standard"
{...{
className: 'view-select',
onChange: handleViewChange,
value: stageFocus,
}}
>
{viewList.map((view, i) => (
<MenuItem {...{ key: view, value: view }}>
{i + 1}: {STAGE_TITLE_MAP[view]}
</MenuItem>
))}
</Select>
<div className="button-array">
{[
{ dialogView: FARMERS_LOG, Icon: BookIcon },
{ dialogView: PRICE_EVENTS, Icon: FlashOnIcon },
{ dialogView: STATS, Icon: AssessmentIcon },
{ dialogView: ACHIEVEMENTS, Icon: BeenhereIcon },
{ dialogView: ACCOUNTING, Icon: AccountBalanceIcon },
{ dialogView: SETTINGS, Icon: SettingsIcon },
].map(({ dialogView, Icon }) => (
<Tooltip
{...{
arrow: true,
key: dialogView,
placement: 'top',
title: dialogTriggerTextMap[dialogView],
}}
>
<Fab
{...{
'aria-label': dialogTriggerTextMap[dialogView],
color: 'primary',
onClick: () => handleClickDialogViewButton(dialogView),
}}
>
<Icon />
</Fab>
</Tooltip>
))}
</div>
{/*
This Dialog gets the Farmhand class because it renders outside of the root
Farmhand component. This explicit class maintains style consistency.
*/}
<Dialog
{...{
className: classNames('Farmhand', { 'block-input': blockInput }),
fullWidth: true,
maxWidth: 'xs',
onClose: handleCloseDialogView,
open: isDialogViewOpen,
TransitionProps: {
onExited: handleDialogViewExited,
},
}}
aria-describedby={modalTitleId}
aria-labelledby={modalContentId}
>
<DialogTitle {...{ id: modalTitleId }}>
{dialogTitleMap[currentDialogView]}
</DialogTitle>
<DialogContent {...{ id: modalContentId }}>
{dialogContentMap[currentDialogView]}
</DialogContent>
<DialogActions>
<Button onClick={handleCloseDialogView} color="primary" autoFocus>
Close
</Button>
</DialogActions>
</Dialog>
</header>
)
}
Navigation.propTypes = {
activePlayers: number,
blockInput: bool.isRequired,
farmName: string.isRequired,
handleClickDialogViewButton: func.isRequired,
handleChatRoomOpenStateChange: func.isRequired,
handleActivePlayerButtonClick: func.isRequired,
handleCloseDialogView: func.isRequired,
handleDialogViewExited: func.isRequired,
handleFarmNameUpdate: func.isRequired,
handleOnlineToggleChange: func.isRequired,
handleRoomChange: func.isRequired,
handleViewChange: func.isRequired,
inventory: array.isRequired,
inventoryLimit: number.isRequired,
isChatAvailable: bool.isRequired,
isDialogViewOpen: bool.isRequired,
isOnline: bool.isRequired,
stageFocus: string.isRequired,
viewList: array.isRequired,
}
export default function Consumer(props) {
return (
<FarmhandContext.Consumer>
{({ gameState, handlers }) => (
<Navigation
{...{
...gameState,
...handlers,
...props,
}}
/>
)}
</FarmhandContext.Consumer>
)
}