UNPKG

@jeremyckahn/farmhand

Version:
287 lines (254 loc) 7.09 kB
import { faHeart } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import Tooltip from '@mui/material/Tooltip/index.js' import Typography from '@mui/material/Typography/index.js' import classNames from 'classnames' import { array, bool, func, object, string } from 'prop-types' import React, { Component } from 'react' import { Tweenable } from 'shifty' import { random } from '../../common/utils.js' import { LEFT, RIGHT } from '../../constants.js' import { pixel } from '../../img/index.js' import { getCowDisplayName, getCowImage } from '../../utils/index.js' // Only moves the cow within the middle 80% of the pen const randomPosition = () => 10 + random() * 80 export class Cow extends Component { state = { cowImage: pixel, isTransitioning: false, moveDirection: RIGHT, rotate: 0, showHugAnimation: false, x: randomPosition(), y: randomPosition(), } /** @type {null | NodeJS.Timeout} */ repositionTimeoutId = null /** @type {null | NodeJS.Timeout} */ animateHugTimeoutId = null tweenable = new Tweenable() isComponentMounted = false static flipAnimationDuration = 1000 static transitionAnimationDuration = 3000 // This MUST be kept in sync with $hug-animation-duration in CowPen.sass. static hugAnimationDuration = 750 get waitVariance() { return 2000 * this.props.cowInventory.length } componentDidUpdate(prevProps) { if ( this.props.isSelected && !prevProps.isSelected && this.repositionTimeoutId !== null ) { clearTimeout(this.repositionTimeoutId) } if (!this.props.isSelected && prevProps.isSelected) { this.scheduleMove() } if ( this.props.cow.happinessBoostsToday > prevProps.cow.happinessBoostsToday && !this.state.showHugAnimation ) { this.setState({ showHugAnimation: true }) this.animateHugTimeoutId = setTimeout(() => { if (this.isComponentMounted) { this.setState({ showHugAnimation: false }) } }, Cow.hugAnimationDuration) } } move = async () => { const newX = randomPosition() const { moveDirection: oldDirection, x, y } = this.state const newDirection = newX < this.state.x ? LEFT : RIGHT if (this.isComponentMounted) { this.setState({ moveDirection: newDirection, }) } if (oldDirection !== newDirection) { /** * @param {{ rotate: number }} param0 */ const render = ({ rotate }) => { if (this.isComponentMounted) { this.setState({ rotate }) } } try { const duration = Cow.flipAnimationDuration const easing = 'swingTo' if (newDirection === LEFT) { await this.tweenable.tween({ from: { rotate: 0, }, to: { rotate: 180, }, easing, duration, // @ts-expect-error render, }) } else { await this.tweenable.tween({ from: { rotate: 180, }, to: { rotate: 0, }, easing, duration, // @ts-expect-error render, }) } } catch (e) { // The tween was cancelled by the component unmounting return } } if (this.isComponentMounted) { this.setState({ isTransitioning: true, }) } try { await this.tweenable.tween({ from: { x, y }, to: { x: newX, y: randomPosition() }, duration: Cow.transitionAnimationDuration, render: ({ x, y }) => { if (this.isComponentMounted) { this.setState({ x, y }) } }, easing: 'linear', }) } catch (e) { // The tween was cancelled by the component unmounting return } if (this.isComponentMounted) { this.setState({ isTransitioning: false }) } this.scheduleMove() } repositionTimeoutHandler = () => { this.repositionTimeoutId = null this.move() } scheduleMove = () => { if (this.props.isSelected) { return } this.repositionTimeoutId = setTimeout( this.repositionTimeoutHandler, random() * this.waitVariance ) } componentDidMount() { this.isComponentMounted = true this.scheduleMove() ;(async () => { const cowImage = await getCowImage(this.props.cow) if (!this.isComponentMounted) return this.setState({ cowImage: cowImage }) })() } componentWillUnmount() { ;[this.repositionTimeoutId, this.animateHugTimeoutId].forEach( id => typeof id === 'number' && clearTimeout(id) ) this.isComponentMounted = false this.tweenable.cancel() } render() { const { props: { allowCustomPeerCowNames, cow, handleCowClick, playerId, isSelected, }, state: { cowImage, isTransitioning, rotate, showHugAnimation, x, y }, } = this const cowDisplayName = getCowDisplayName( cow, playerId, allowCustomPeerCowNames ) return ( <div className={classNames('cow', { 'is-transitioning': isTransitioning, 'is-selected': isSelected, 'is-loaded': cowImage !== pixel, })} onClick={() => handleCowClick(cow)} style={{ left: `${x}%`, top: `${y}%`, }} > {isSelected && ( <p className="visually_hidden">{cowDisplayName} is selected</p> )} <Tooltip {...{ arrow: true, placement: 'top', title: <Typography>{cowDisplayName}</Typography>, open: isSelected, PopperProps: { disablePortal: true, }, }} > <div {...{ style: { transform: `rotateY(${rotate}deg)` } }}> <img {...{ src: cowImage, }} alt={cowDisplayName} /> <FontAwesomeIcon {...{ className: classNames('animation', { 'is-animating': showHugAnimation, }), icon: faHeart, }} /> </div> </Tooltip> <ol {...{ className: 'happiness-boosts-today' }}> {new Array(this.props.cow.happinessBoostsToday) .fill(undefined) .map((_, i) => ( <li {...{ key: i }}> <FontAwesomeIcon {...{ icon: faHeart, }} /> </li> ))} </ol> </div> ) } } Cow.propTypes = { allowCustomPeerCowNames: bool.isRequired, cow: object.isRequired, cowInventory: array.isRequired, handleCowClick: func.isRequired, playerId: string.isRequired, isSelected: bool.isRequired, }