Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(closes #491): Winemaking #497

Merged
merged 91 commits into from
Jun 9, 2024
Merged
Show file tree
Hide file tree
Changes from 88 commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
56d646e
feat(#491): stand up winemaking tab
jeremyckahn Apr 12, 2024
4f539f1
feat(#491): [wip] scaffold getWinesAvailableToMake
jeremyckahn Apr 12, 2024
69bb4f2
refactor(types): define GRAPE cropFamily
jeremyckahn Apr 13, 2024
7f4263f
feat(#491): [wip] compute grapes sold
jeremyckahn Apr 14, 2024
b59b3b7
refactor(#491): use grape factory function
jeremyckahn Apr 15, 2024
3be7f63
docs(#491): define wine type
jeremyckahn Apr 15, 2024
88486fb
feat(#491): define grape varieties
jeremyckahn Apr 16, 2024
fc45ea6
feat(#491): calculate wines available to make
jeremyckahn Apr 16, 2024
c42fb04
test(#491): validate getWineVarietiesAvailableToMake
jeremyckahn Apr 17, 2024
d876cd1
feat(#491): add wine art
jeremyckahn Apr 18, 2024
e6e329f
feat(#491): use @lstebner's wine art
jeremyckahn Apr 18, 2024
b3317c2
feat(#491): wire up WineRecipeList
jeremyckahn Apr 19, 2024
c77a1bd
docs: improve types for FarmhandContext
jeremyckahn Apr 19, 2024
8030154
docs(types): define remaining FarmhandContext props
jeremyckahn Apr 19, 2024
1c3a7c6
feat(#491): scaffold and wire up WineRecipe
jeremyckahn Apr 20, 2024
af3c22c
feat(#491): render wine images in recipe
jeremyckahn Apr 20, 2024
c8e4717
feat(#491): show days needed to mature wine
jeremyckahn Apr 20, 2024
83e2d09
refactor(#491): use service for wine logic
jeremyckahn Apr 23, 2024
6d35209
feat(#491): show grapes required for wine
jeremyckahn Apr 23, 2024
0de2853
feat(#491): show number of required grapes in inventory
jeremyckahn Apr 23, 2024
d278586
docs: improve context typing
jeremyckahn Apr 23, 2024
232a194
docs: add types for debounced handlers
jeremyckahn Apr 23, 2024
2ae9488
feat(#491): add flour art
jeremyckahn May 1, 2024
e601d70
feat(#491): update flour art
jeremyckahn May 3, 2024
27d092a
feat(#491): add yeast art
jeremyckahn May 3, 2024
dba8558
feat(#491): improve flour art
jeremyckahn May 3, 2024
a4af19a
feat(#491): improve yeast art
jeremyckahn May 5, 2024
10706df
feat(#491): define flour and yeast recipes
jeremyckahn May 5, 2024
6327bfe
feat(#491): require yeast for wine production
jeremyckahn May 6, 2024
601def4
feat(#491): make yeast and flour the ingredients for bread
jeremyckahn May 6, 2024
99eb059
refactor: cleanup
jeremyckahn May 6, 2024
c405b2f
refactor: move cellar inventory calculation to service
jeremyckahn May 6, 2024
f6e89af
refactor(#491): move keg generation to CellarService
jeremyckahn May 7, 2024
936d683
feat(#491): show wine instances in cellar
jeremyckahn May 8, 2024
d425cfb
feat(#491): define getMaxWineYield
jeremyckahn May 8, 2024
a2aaf39
feat(#491): disable winemaking button when none can be made
jeremyckahn May 8, 2024
29bf3e1
refactor(#491): move doesCellarSpaceRemain to cellarService
jeremyckahn May 8, 2024
dd13988
feat(#491): accept wine quantity
jeremyckahn May 9, 2024
1b68960
feat(#491): [wip] start on wine keg creation
jeremyckahn May 9, 2024
cf0e410
chore(#491): add wineId to grape type
jeremyckahn May 10, 2024
893d700
feat(#491): define wine recipes
jeremyckahn May 11, 2024
13f1a8b
fix: update test mock
jeremyckahn May 11, 2024
d615005
refactor: use better variable names
jeremyckahn May 11, 2024
054bf12
feat(#491): add wine to cellar
jeremyckahn May 11, 2024
4d5e727
feat(#491): set daysUntilMature for wine
jeremyckahn May 12, 2024
bce2d89
feat(#491): don't spoil wine kegs
jeremyckahn May 13, 2024
b901115
refactor: remove circular import dependency
jeremyckahn May 14, 2024
a6bcaa2
feat(#491): implement getWineValue
jeremyckahn May 14, 2024
af6d7fb
feat(#491): present wine value
jeremyckahn May 14, 2024
7fd5320
feat(#491): sell wine for appropriate price
jeremyckahn May 14, 2024
d354d17
feat(#491): add more info about kegs
jeremyckahn May 16, 2024
58f3d8d
fix: types
jeremyckahn May 16, 2024
4745a92
feat(#491): use compound interest for wine value
jeremyckahn May 16, 2024
67964a4
fix: use consistent markdown <p> margin
jeremyckahn May 16, 2024
7878537
refactor(#491): move getMaxWineYield to wineService
jeremyckahn May 18, 2024
81695f4
refactor(#491): roll wine value calculation into getKegValue
jeremyckahn May 18, 2024
ca14982
refactor: simplify getKegValue
jeremyckahn May 18, 2024
850bbdb
test(#491): validate getMaxWineYield
jeremyckahn May 18, 2024
9ecacea
fix: show correct number of wines in cellar
jeremyckahn May 18, 2024
22de019
test(#491): validate generateKeg
jeremyckahn May 19, 2024
e723a05
test: [wip] validate getKegValue
jeremyckahn May 20, 2024
fc0ee34
test: validate more code paths in getKegValue
jeremyckahn May 20, 2024
36d19f8
fix: make immature kegs worth nothing
jeremyckahn May 20, 2024
5e444bb
test(#491): validate makeWine
jeremyckahn May 20, 2024
85aa293
refactor: simplify cellar tests
jeremyckahn May 20, 2024
ef7dc40
test(#491): validate presentation of days to mature for wine
jeremyckahn May 21, 2024
5539a97
refactor: improve test label
jeremyckahn May 21, 2024
fa66de1
refactor(makeWine): derive wine variety
jeremyckahn May 21, 2024
981a9b5
test(#491): validate presentation of wine requirements
jeremyckahn May 22, 2024
04b51d4
feat(#491): show available yeast
jeremyckahn May 22, 2024
185703a
test(#491): validate presentation of current wines in cellar
jeremyckahn May 22, 2024
1e1e970
test(#491): [wip] validate disabling of Make button
jeremyckahn May 22, 2024
6ff5f88
fix(#491): [wip] account for insufficient grape inventory
jeremyckahn May 22, 2024
226c0a8
fix(#491): enable make button when player has enough grapes
jeremyckahn May 23, 2024
45e8112
fix(#491): consume correct amount of grapes when making wine
jeremyckahn May 23, 2024
8304456
test(#491): [wip] validate yeast multiplier requirement presentation
jeremyckahn May 24, 2024
7211f5a
feat(QuantityInput): select all text when focusing input instead of c…
jeremyckahn May 24, 2024
c902529
feat(#491): show yeast requirements for specified quantity
jeremyckahn May 24, 2024
a8bead1
chore(sellItem): import types
jeremyckahn May 25, 2024
162d68e
feat(stats): sort items sold by name
jeremyckahn May 25, 2024
a0e119f
feat(#491): improve cellar copy
jeremyckahn May 26, 2024
9a076a6
fix(#491): use correct tab props
jeremyckahn May 26, 2024
811cad0
refactor: cleanup
jeremyckahn May 26, 2024
a33cef6
refactor(#491): make cropVariety type-safe
jeremyckahn May 26, 2024
6ebc711
fix(#491): consume accurate amount of yeast
jeremyckahn May 26, 2024
6457d17
test(#491): validate that wine kegs do not spoil
jeremyckahn May 26, 2024
2b6a8be
refactor: simplify type annotations
jeremyckahn May 26, 2024
cc2b3c0
refactor: cleanup
jeremyckahn May 26, 2024
b95e3b5
refactor(#491): use config object for getWineMaxYield
jeremyckahn Jun 9, 2024
ff79785
refactor: use clear variable name for grapeVarietyToGrapeItemMap
jeremyckahn Jun 9, 2024
23f6137
refactor(#491): improve keg spoilage logic abstractions
jeremyckahn Jun 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
24 changes: 18 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,7 @@ import AnimatedNumber from '../AnimatedNumber'

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

/**
* @param {Object} props
Expand All @@ -41,7 +42,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 +60,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 +91,9 @@ export function Keg({ keg }) {
{...{ number: kegValue, formatter: moneyString }}
/>
</p>
<p>Potential for spoilage: {spoilageRateDisplayValue}%</p>
{!wineService.isWineRecipe(item) && (
jeremyckahn marked this conversation as resolved.
Show resolved Hide resolved
<p>Potential for spoilage: {spoilageRateDisplayValue}%</p>
jeremyckahn marked this conversation as resolved.
Show resolved Hide resolved
)}
</>
) : (
<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
Loading