Skip to content

Commit

Permalink
feat(closes #491): Winemaking (#497)
Browse files Browse the repository at this point in the history
* feat(#491): stand up winemaking tab

* feat(#491): [wip] scaffold getWinesAvailableToMake

* refactor(types): define GRAPE cropFamily

* feat(#491): [wip] compute grapes sold

* refactor(#491): use grape factory function

* docs(#491): define wine type

* feat(#491): define grape varieties

* feat(#491): calculate wines available to make

* test(#491): validate getWineVarietiesAvailableToMake

* feat(#491): add wine art

* feat(#491): use @lstebner's wine art

* feat(#491): wire up WineRecipeList

* docs: improve types for FarmhandContext

* docs(types): define remaining FarmhandContext props

* feat(#491): scaffold and wire up WineRecipe

* feat(#491): render wine images in recipe

* feat(#491): show days needed to mature wine

* refactor(#491): use service for wine logic

* feat(#491): show grapes required for wine

* feat(#491): show number of required grapes in inventory

* docs: improve context typing

* docs: add types for debounced handlers

* feat(#491): add flour art

* feat(#491): update flour art

* feat(#491): add yeast art

* feat(#491): improve flour art

* feat(#491): improve yeast art

* feat(#491): define flour and yeast recipes

* feat(#491): require yeast for wine production

* feat(#491): make yeast and flour the ingredients for bread

* refactor: move cellar inventory calculation to service

* refactor(#491): move keg generation to CellarService

* feat(#491): show wine instances in cellar

* feat(#491): define getMaxWineYield

* feat(#491): disable winemaking button when none can be made

* refactor(#491): move doesCellarSpaceRemain to cellarService

* feat(#491): accept wine quantity

* feat(#491): [wip] start on wine keg creation

* chore(#491): add wineId to grape type

* feat(#491): define wine recipes

* refactor: use better variable names

* feat(#491): add wine to cellar

* feat(#491): set daysUntilMature for wine

* feat(#491): don't spoil wine kegs

* feat(#491): implement getWineValue

* feat(#491): present wine value

* feat(#491): sell wine for appropriate price

* feat(#491): add more info about kegs

* feat(#491): use compound interest for wine value

* refactor(#491): move getMaxWineYield to wineService

* refactor(#491): roll wine value calculation into getKegValue

* refactor: simplify getKegValue

* test(#491): validate getMaxWineYield

* test(#491): validate generateKeg

* test: validate getKegValue

* test(#491): validate makeWine

* test(#491): validate presentation of days to mature for wine

* refactor(makeWine): derive wine variety

* test(#491): validate presentation of wine requirements

* feat(#491): show available yeast

* test(#491): validate presentation of current wines in cellar

* test(#491): [wip] validate disabling of Make button

* test(#491): [wip] validate yeast multiplier requirement presentation

* feat(QuantityInput): select all text when focusing input instead of clearing it out

* feat(#491): show yeast requirements for specified quantity

* chore(sellItem): import types

* feat(stats): sort items sold by name

* feat(#491): improve cellar copy

* fix(#491): use correct tab props

* refactor(#491): make cropVariety type-safe

* fix(#491): consume accurate amount of yeast

* test(#491): validate that wine kegs do not spoil
  • Loading branch information
jeremyckahn authored Jun 9, 2024
1 parent 8731681 commit 1c061ce
Show file tree
Hide file tree
Showing 61 changed files with 1,946 additions and 215 deletions.
3 changes: 3 additions & 0 deletions src/components/Cellar/Cellar.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Tabs from '@mui/material/Tabs'

import { CellarInventoryTabPanel } from './CellarInventoryTabPanel'
import { FermentationTabPanel } from './FermentationTabPanel'
import { WinemakingTabPanel } from './WinemakingTabPanel'
import { a11yProps } from './TabPanel'

import './Cellar.sass'
Expand All @@ -20,9 +21,11 @@ export const Cellar = () => {
>
<Tab {...{ label: 'Cellar Inventory', ...a11yProps(0) }} />
<Tab {...{ label: 'Fermentation', ...a11yProps(1) }} />
<Tab {...{ label: 'Winemaking', ...a11yProps(2) }} />
</Tabs>
<CellarInventoryTabPanel index={0} currentTab={currentTab} />
<FermentationTabPanel index={1} currentTab={currentTab} />
<WinemakingTabPanel index={2} currentTab={currentTab} />
</div>
)
}
Expand Down
17 changes: 15 additions & 2 deletions src/components/Cellar/CellarInventoryTabPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import CardContent from '@mui/material/CardContent'
import ReactMarkdown from 'react-markdown'

import FarmhandContext from '../Farmhand/Farmhand.context'
import { KEG_INTEREST_RATE, PURCHASEABLE_CELLARS } from '../../constants'
import {
KEG_INTEREST_RATE,
PURCHASEABLE_CELLARS,
WINE_GROWTH_TIMELINE_CAP,
WINE_INTEREST_RATE,
} from '../../constants'

import { integerString } from '../../utils'

Expand Down Expand Up @@ -54,7 +59,15 @@ export const CellarInventoryTabPanel = ({ index, currentTab }) => {
{...{
linkTarget: '_blank',
className: 'markdown',
source: `This is your inventory of cellar kegs. Keg contents take time to reach maturity before they can be sold. Once they reach maturity, keg contents become higher in quality and their value compounds at a rate of ${KEG_INTEREST_RATE}% a day.`,
source: `This is your inventory of Cellar kegs.
Keg contents take time to reach maturity before they can be sold. After they reach maturity, keg contents become higher in quality over time and their value grows.
Kegs that contain fermented crops compound in value at a rate of ${KEG_INTEREST_RATE}% a day but have an increasing chance of spoiling.
Kegs that contain wine compound in value at a rate of ${WINE_INTEREST_RATE}% for up to ${integerString(
WINE_GROWTH_TIMELINE_CAP
)} days and never spoil.`,
}}
/>
</CardContent>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Cellar/FermentationTabPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const FermentationTabPanel = ({ index, currentTab }) => (
linkTarget: '_blank',
className: 'markdown',
source:
'Some items can be fermented. Fermented items become much more valuable over time!',
'Some items can be fermented and become much more valuable over time.',
}}
/>
</CardContent>
Expand Down
25 changes: 19 additions & 6 deletions src/components/Cellar/Keg.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import CardActions from '@mui/material/CardActions'
import Button from '@mui/material/Button'

import { itemsMap } from '../../data/maps'
import { items } from '../../img'
import { items, wines } from '../../img'

import FarmhandContext from '../Farmhand/Farmhand.context'
import { getKegValue } from '../../utils/getKegValue'
Expand All @@ -18,6 +18,8 @@ import AnimatedNumber from '../AnimatedNumber'

import './Keg.sass'
import { getKegSpoilageRate } from '../../utils/getKegSpoilageRate'
import { wineService } from '../../services/wine'
import { cellarService } from '../../services/cellar'

/**
* @param {Object} props
Expand All @@ -41,7 +43,11 @@ export function Keg({ keg }) {
} = useContext(FarmhandContext)

const item = itemsMap[keg.itemId]
const fermentationRecipeName = FERMENTED_CROP_NAME`${item}`

let imageSrc = items[item.id]

// @ts-expect-error
let recipeName = FERMENTED_CROP_NAME`${item}`

const handleSellClick = () => {
handleSellKegClick(keg)
Expand All @@ -55,19 +61,24 @@ export function Keg({ keg }) {
const kegValue =
getKegValue(keg) * getSalePriceMultiplier(completedAchievements)

if (wineService.isWineRecipe(item)) {
imageSrc = wines[item.variety]
recipeName = item.name
}

const spoilageRate = getKegSpoilageRate(keg)
const spoilageRateDisplayValue = Number((spoilageRate * 100).toPrecision(2))

return (
<Card className="Keg">
<CardHeader
title={fermentationRecipeName}
title={recipeName}
avatar={
<img
{...{
src: items[item.id],
src: imageSrc,
}}
alt={fermentationRecipeName}
alt={recipeName}
/>
}
subheader={
Expand All @@ -81,7 +92,9 @@ export function Keg({ keg }) {
{...{ number: kegValue, formatter: moneyString }}
/>
</p>
<p>Potential for spoilage: {spoilageRateDisplayValue}%</p>
{cellarService.doesKegSpoil(keg) && (
<p>Potential for spoilage: {spoilageRateDisplayValue}%</p>
)}
</>
) : (
<p>Days until ready: {keg.daysUntilMature}</p>
Expand Down
38 changes: 38 additions & 0 deletions src/components/Cellar/WinemakingTabPanel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react'
import { number } from 'prop-types'
import Divider from '@mui/material/Divider'
import Card from '@mui/material/Card'
import CardContent from '@mui/material/CardContent'
import ReactMarkdown from 'react-markdown'

import { WineRecipeList } from '../WineRecipeList/WineRecipeList'

import { TabPanel } from './TabPanel'

export const WinemakingTabPanel = ({ index, currentTab }) => (
<TabPanel value={currentTab} index={index}>
<WineRecipeList />
<Divider />
<ul className="card-list">
<li>
<Card>
<CardContent>
<ReactMarkdown
{...{
linkTarget: '_blank',
className: 'markdown',
source:
'Grapes can be made into wine. Wine becomes very valuable in time and never spoils.',
}}
/>
</CardContent>
</Card>
</li>
</ul>
</TabPanel>
)

WinemakingTabPanel.propTypes = {
currentTab: number.isRequired,
index: number.isRequired,
}
32 changes: 31 additions & 1 deletion src/components/Farmhand/Farmhand.context.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
/**
* @typedef {import('../../').farmhand.item} farmhand.item
* @typedef {import('../../').farmhand.levelEntitlements} farmhand.levelEntitlements
* @typedef {import('./Farmhand').farmhand.state} farmhand.state
*/
import { createContext } from 'react'

const FarmhandContext = createContext()
// eslint-disable-next-line no-unused-vars
import uiEventHandlers from '../../handlers/ui-events'

/**
* @type {import('react').Context<{
* gameState: farmhand.state & {
* blockInput: boolean,
* features: Record<string, boolean>,
* fieldToolInventory: farmhand.item[],
* isChatAvailable: boolean,
* levelEntitlements: farmhand.levelEntitlements,
* plantableCropInventory: farmhand.item[],
* playerInventory: farmhand.item[],
* playerInventoryQuantities: Record<string, number>,
* shopInventory: farmhand.item[],
* viewList: string[],
* viewTitle: string,
* }
* handlers: uiEventHandlers & { debounced: uiEventHandlers }
* }>}
*/
// @ts-expect-error
const FarmhandContext = createContext({
gameState: {},
handlers: {},
})

export default FarmhandContext
32 changes: 20 additions & 12 deletions src/components/Farmhand/Farmhand.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,14 @@ const { CLEANUP, HARVEST, MINE, OBSERVE, WATER, PLANT } = fieldMode
const emptyObject = Object.freeze({})

/*!
* @param {Array.<{ id: farmhand.item, quantity: number }>} inventory
* @param {Object.<number>} valueAdjustments
* @returns {Array.<farmhand.item>}
* @param {{ id: farmhand.item['id'], quantity: number }} inventory
* @param {Record<string, number>} valueAdjustments
* @returns {farmhand.item[]}
*/
export const computePlayerInventory = memoize((inventory, valueAdjustments) =>
export const computePlayerInventory = memoize((
/** @type {{ id: farmhand.item['id'], quantity: number }[]} */ inventory,
/** @type {Record<string, number>} */ valueAdjustments
) =>
inventory.map(({ quantity, id }) => ({
quantity,
...itemsMap[id],
Expand All @@ -144,10 +147,12 @@ export const computePlayerInventory = memoize((inventory, valueAdjustments) =>
)

/*!
* @param {Array.<{ id: farmhand.item }>} inventory
* @returns {Array.<{ id: farmhand.item }>}
* @param {farmhand.item[]} inventory
* @returns {{ id: farmhand.item }[]}
*/
export const getFieldToolInventory = memoize(inventory =>
export const getFieldToolInventory = memoize((
/** @type {Array.<farmhand.item>} */ inventory
) =>
inventory
.filter(({ id }) => {
const { enablesFieldMode } = itemsMap[id]
Expand All @@ -158,10 +163,12 @@ export const getFieldToolInventory = memoize(inventory =>
)

/*!
* @param {Array.<{ id: farmhand.item }>} inventory
* @param {farmhand.item[]} inventory
* @returns {Array.<{ id: farmhand.item }>}
*/
export const getPlantableCropInventory = memoize(inventory =>
export const getPlantableCropInventory = memoize((
/** @type {farmhand.item[]} */ inventory
) =>
inventory
.filter(({ id }) => itemsMap[id].isPlantableCrop)
.map(({ id }) => itemsMap[id])
Expand Down Expand Up @@ -235,9 +242,10 @@ const applyPriceEvents = (valueAdjustments, priceCrashes, priceSurges) => {
* @property {boolean} isAwaitingNetworkRequest
* @property {boolean} isCombineEnabled
* @property {boolean} isMenuOpen
* @property {Object} itemsSold Keys are items IDs, values are the number of
* that item sold. The numbers in this map are inclusive of the corresponding
* ones in cellarItemsSold and represent the grand total of each item sold.
* @property {Record<farmhand.item['id'], number>} itemsSold Keys are items
* IDs, values are the number of that item sold. The numbers in this map are
* inclusive of the corresponding ones in cellarItemsSold and represent the
* grand total of each item sold.
* @property {Object} cellarItemsSold Keys are items IDs, values are the number
* of that cellar item sold. The numbers in this map represent a subset of the
* corresponding ones in itemsSold. cellarItemsSold is intended to be used for
Expand Down
3 changes: 3 additions & 0 deletions src/components/Farmhand/Farmhand.sass
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ body

@mixin markdown-styles
.markdown
p
margin: 1em 0

ul li
list-style: disc
margin-left: 1em
Expand Down
4 changes: 4 additions & 0 deletions src/components/Farmhand/FarmhandReducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ export class FarmhandReducers extends Component {
throw new Error('Unimplemented')
}
/** @type BoundReducer */
makeWine() {
throw new Error('Unimplemented')
}
/** @type BoundReducer */
modifyCow() {
throw new Error('Unimplemented')
}
Expand Down
7 changes: 5 additions & 2 deletions src/components/Farmhand/helpers/getInventoryQuantities.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
/**
* @typedef {import('../../../').farmhand.item} farmhand.item
*/
import { itemsMap } from '../../../data/maps'

const itemIds = Object.keys(itemsMap)

/**
* @param {Array.<{ id: farmhand.item, quantity: number }>} inventory
* @returns {Object.<string, number>}
* @param {Array.<{ id: farmhand.item['id'], quantity: number }>} inventory
*/
export const getInventoryQuantities = inventory => {
/** @type {Record<string, number>} */
const quantities = {}

for (const itemId of itemIds) {
Expand Down
44 changes: 10 additions & 34 deletions src/components/FermentationRecipeList/FermentationRecipe.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,52 +12,24 @@ import Button from '@mui/material/Button'

import { PURCHASEABLE_CELLARS } from '../../constants'
import { items } from '../../img'
import { doesCellarSpaceRemain } from '../../utils/doesCellarSpaceRemain'
import { getMaxYieldOfFermentationRecipe } from '../../utils/getMaxYieldOfFermentationRecipe'
import { getSaltRequirementsForFermentationRecipe } from '../../utils/getSaltRequirementsForFermentationRecipe'
import { FERMENTED_CROP_NAME } from '../../templates'
import QuantityInput from '../QuantityInput'
import FarmhandContext from '../Farmhand/Farmhand.context'
import { fermentableItemsMap, itemsMap } from '../../data/maps'

import './FermentationRecipe.sass'
import { itemsMap } from '../../data/maps'
import { cellarService } from '../../services/cellar'
import { getInventoryQuantityMap } from '../../utils/getInventoryQuantityMap'
import { integerString } from '../../utils'
import AnimatedNumber from '../AnimatedNumber'
import { memoize } from '../../utils/memoize'

/**
* @type {function(keg[], item):number}
*/
const getRecipesInstancesInCellar = memoize(
/**
* @param {keg[]} cellarInventory
* @param {item} item
* @returns number
*/
(cellarInventory, item) => {
return cellarInventory.filter(keg => keg.itemId === item.id).length
},
{ cacheSize: Object.keys(fermentableItemsMap).length }
)
import './FermentationRecipe.sass'

/**
* @param {Object} props
* @param {item} props.item
*/
export const FermentationRecipe = ({ item }) => {
/**
* @type {{
* gameState: {
* inventory: Array.<item>,
* cellarInventory: Array.<keg>,
* purchasedCellar: number
* },
* handlers: {
* handleMakeFermentationRecipeClick: function(item, number)
* }
* }}
*/
const {
gameState: { inventory, cellarInventory, purchasedCellar },
handlers: { handleMakeFermentationRecipeClick },
Expand All @@ -66,8 +38,11 @@ export const FermentationRecipe = ({ item }) => {
const [quantity, setQuantity] = useState(1)

const inventoryQuantityMap = getInventoryQuantityMap(inventory)
// @ts-expect-error
const fermentationRecipeName = FERMENTED_CROP_NAME`${item}`
const { space: cellarSize } = PURCHASEABLE_CELLARS.get(purchasedCellar)
const { space: cellarSize } = PURCHASEABLE_CELLARS.get(purchasedCellar) ?? {
space: 0,
}

useEffect(() => {
setQuantity(
Expand All @@ -84,7 +59,8 @@ export const FermentationRecipe = ({ item }) => {
}, [cellarInventory, cellarSize, inventory, item, quantity])

const canBeMade =
quantity > 0 && doesCellarSpaceRemain(cellarInventory, purchasedCellar)
quantity > 0 &&
cellarService.doesCellarSpaceRemain(cellarInventory, purchasedCellar)

const handleMakeFermentationRecipe = () => {
if (canBeMade) {
Expand All @@ -99,7 +75,7 @@ export const FermentationRecipe = ({ item }) => {
cellarSize
)

const recipeInstancesInCellar = getRecipesInstancesInCellar(
const recipeInstancesInCellar = cellarService.getItemInstancesInCellar(
cellarInventory,
item
)
Expand Down
Loading

0 comments on commit 1c061ce

Please sign in to comment.