@jeremyckahn/farmhand 1.24.0 → 1.24.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{index-BZUcLFKn.js → index-C9kG86vs.js} +11 -11
- package/dist/assets/{index-BZUcLFKn.js.map → index-C9kG86vs.js.map} +1 -1
- package/dist/index.html +1 -1
- package/dist/service-worker.js +1 -1
- package/dist/service-worker.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Cellar/Keg.tsx +4 -1
- package/src/components/ContextPane/ContextPane.test.tsx +57 -0
- package/src/components/ContextPane/ContextPane.tsx +38 -2
- package/src/components/Farmhand/Farmhand.tsx +8 -1
- package/src/components/Farmhand/FarmhandService.test.ts +23 -0
- package/src/components/Farmhand/FarmhandService.ts +12 -16
- package/src/components/Farmhand/useFarmhand.ts +16 -4
- package/src/components/Item/Item.test.tsx +29 -0
- package/src/components/Item/Item.tsx +14 -2
- package/src/components/Navigation/Navigation.test.tsx +0 -3
- package/src/components/Navigation/Navigation.tsx +3 -34
- package/src/components/OnlinePeersView/OnlinePeer/OnlinePeer.tsx +1 -2
- package/src/data/items.ts +17 -0
- package/src/farmhand.d.ts +1 -0
- package/src/game-logic/reducers/harvestPlot.test.ts +18 -0
- package/src/game-logic/reducers/harvestPlot.ts +5 -6
- package/src/game-logic/reducers/modifyCow.test.ts +33 -0
- package/src/game-logic/reducers/modifyCow.ts +4 -9
- package/src/game-logic/reducers/purchaseItem.test.ts +129 -0
- package/src/game-logic/reducers/purchaseItem.ts +9 -2
- package/src/shell/bootup.test.ts +31 -0
- package/src/utils/getItemById.test.ts +18 -0
- package/src/utils/getItemById.ts +14 -0
- package/src/utils/getPlotImage.ts +1 -3
- package/src/utils/transformStateDataForImport.test.ts +9 -0
- package/src/utils/transformStateDataForImport.ts +9 -12
package/dist/index.html
CHANGED
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
gtag('config', analyticsID)
|
|
76
76
|
})()
|
|
77
77
|
</script>
|
|
78
|
-
<script type="module" crossorigin src="./assets/index-
|
|
78
|
+
<script type="module" crossorigin src="./assets/index-C9kG86vs.js"></script>
|
|
79
79
|
<link rel="stylesheet" crossorigin href="./assets/index-CaYdR-lC.css">
|
|
80
80
|
<link rel="manifest" href="./manifest.webmanifest"></head>
|
|
81
81
|
<body>
|
package/dist/service-worker.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,o)=>{const r=e||("document"in self?document.currentScript.src:"")||location.href;if(s[r])return;let d={};const l=e=>i(e,r),t={module:{uri:r},exports:d,require:l};s[r]=Promise.all(n.map(e=>t[e]||l(e))).then(e=>(o(...e),d))}}define(["./workbox-2fbc6a65"],function(e){"use strict";self.addEventListener("message",e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()}),e.precacheAndRoute([{url:"index.html",revision:"
|
|
1
|
+
if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,o)=>{const r=e||("document"in self?document.currentScript.src:"")||location.href;if(s[r])return;let d={};const l=e=>i(e,r),t={module:{uri:r},exports:d,require:l};s[r]=Promise.all(n.map(e=>t[e]||l(e))).then(e=>(o(...e),d))}}define(["./workbox-2fbc6a65"],function(e){"use strict";self.addEventListener("message",e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()}),e.precacheAndRoute([{url:"index.html",revision:"f5ce8c2d4c7358bc1ce3bb4838506540"},{url:"electron.js",revision:"b0456826f47539f6c1957c3add5eb959"},{url:"assets/workbox-window.prod.es5-Bd17z0YL.js",revision:null},{url:"assets/index-CaYdR-lC.css",revision:null},{url:"assets/index-C9kG86vs.js",revision:null},{url:"favicon.ico",revision:"445bb19d4a8fd46eb433dd43cbdce691"},{url:"logo192.png",revision:"70746d790b8990639d1390337d77d81a"},{url:"logo512.png",revision:"b044ab46b3f33f0e2d313d961bf1f654"},{url:"logoMaskable.png",revision:"23be58ccf16df9f43658aad3da30056d"},{url:"manifest.webmanifest",revision:"3ad071a00582451c9cdeb827b37b9ebe"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("index.html")))});
|
|
2
2
|
//# sourceMappingURL=service-worker.js.map
|
|
3
3
|
//# sourceMappingURL=service-worker.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service-worker.js","sources":["../../../../../tmp/
|
|
1
|
+
{"version":3,"file":"service-worker.js","sources":["../../../../../tmp/5a47feca681fd14fa867c579d860224e/service-worker.js"],"sourcesContent":["import {precacheAndRoute as workbox_precaching_precacheAndRoute} from '/home/runner/work/farmhand/farmhand/node_modules/workbox-precaching/precacheAndRoute.mjs';\nimport {cleanupOutdatedCaches as workbox_precaching_cleanupOutdatedCaches} from '/home/runner/work/farmhand/farmhand/node_modules/workbox-precaching/cleanupOutdatedCaches.mjs';\nimport {registerRoute as workbox_routing_registerRoute} from '/home/runner/work/farmhand/farmhand/node_modules/workbox-routing/registerRoute.mjs';\nimport {NavigationRoute as workbox_routing_NavigationRoute} from '/home/runner/work/farmhand/farmhand/node_modules/workbox-routing/NavigationRoute.mjs';\nimport {createHandlerBoundToURL as workbox_precaching_createHandlerBoundToURL} from '/home/runner/work/farmhand/farmhand/node_modules/workbox-precaching/createHandlerBoundToURL.mjs';/**\n * Welcome to your Workbox-powered service worker!\n *\n * You'll need to register this file in your web app.\n * See https://goo.gl/nhQhGp\n *\n * The rest of the code is auto-generated. Please don't update this file\n * directly; instead, make changes to your Workbox build configuration\n * and re-run your build process.\n * See https://goo.gl/2aRDsh\n */\n\n\n\n\nself.addEventListener('message', (event) => {\n if (event.data && event.data.type === 'SKIP_WAITING') {\n self.skipWaiting();\n }\n});\n\n/**\n * The precacheAndRoute() method efficiently caches and responds to\n * requests for URLs in the manifest.\n * See https://goo.gl/S9QRab\n */\nworkbox_precaching_precacheAndRoute([\n {\n \"url\": \"index.html\",\n \"revision\": \"f5ce8c2d4c7358bc1ce3bb4838506540\"\n },\n {\n \"url\": \"electron.js\",\n \"revision\": \"b0456826f47539f6c1957c3add5eb959\"\n },\n {\n \"url\": \"assets/workbox-window.prod.es5-Bd17z0YL.js\",\n \"revision\": null\n },\n {\n \"url\": \"assets/index-CaYdR-lC.css\",\n \"revision\": null\n },\n {\n \"url\": \"assets/index-C9kG86vs.js\",\n \"revision\": null\n },\n {\n \"url\": \"favicon.ico\",\n \"revision\": \"445bb19d4a8fd46eb433dd43cbdce691\"\n },\n {\n \"url\": \"logo192.png\",\n \"revision\": \"70746d790b8990639d1390337d77d81a\"\n },\n {\n \"url\": \"logo512.png\",\n \"revision\": \"b044ab46b3f33f0e2d313d961bf1f654\"\n },\n {\n \"url\": \"logoMaskable.png\",\n \"revision\": \"23be58ccf16df9f43658aad3da30056d\"\n },\n {\n \"url\": \"manifest.webmanifest\",\n \"revision\": \"3ad071a00582451c9cdeb827b37b9ebe\"\n }\n], {});\nworkbox_precaching_cleanupOutdatedCaches();workbox_routing_registerRoute(new workbox_routing_NavigationRoute(workbox_precaching_createHandlerBoundToURL(\"index.html\")));\n\n\n"],"names":["self","addEventListener","event","data","type","skipWaiting","workbox_precaching_precacheAndRoute","url","revision","workbox_precaching_cleanupOutdatedCaches","workbox_routing_registerRoute","workbox_routing_NavigationRoute","workbox_precaching_createHandlerBoundToURL"],"mappings":"inBAmBAA,KAAKC,iBAAiB,UAAYC,IAC5BA,EAAMC,MAA4B,iBAApBD,EAAMC,KAAKC,MAC3BJ,KAAKK,gBASTC,EAAAA,iBAAoC,CAClC,CACEC,IAAO,aACPC,SAAY,oCAEd,CACED,IAAO,cACPC,SAAY,oCAEd,CACED,IAAO,6CACPC,SAAY,MAEd,CACED,IAAO,4BACPC,SAAY,MAEd,CACED,IAAO,2BACPC,SAAY,MAEd,CACED,IAAO,cACPC,SAAY,oCAEd,CACED,IAAO,cACPC,SAAY,oCAEd,CACED,IAAO,cACPC,SAAY,oCAEd,CACED,IAAO,mBACPC,SAAY,oCAEd,CACED,IAAO,uBACPC,SAAY,qCAEb,CAAA,GACHC,EAAAA,wBAA2CC,EAAAA,cAA8B,IAAIC,EAAAA,gBAAgCC,0BAA2C"}
|
package/package.json
CHANGED
|
@@ -24,7 +24,10 @@ export function Keg({ keg }: { keg: farmhand.keg }) {
|
|
|
24
24
|
|
|
25
25
|
const item = itemsMap[keg.itemId]
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
if (!item) {
|
|
28
|
+
throw new Error(`Keg references unknown item id: ${keg.itemId}`)
|
|
29
|
+
}
|
|
30
|
+
|
|
28
31
|
let imageSrc = items[item.id as keyof typeof items]
|
|
29
32
|
|
|
30
33
|
const recipeName = getKegDisplayName(item)
|
|
@@ -24,6 +24,8 @@ vi.mock('../Inventory/index.js', () => ({
|
|
|
24
24
|
const defaultProps = {
|
|
25
25
|
playerInventory: [],
|
|
26
26
|
stageFocus: stageFocusType.NONE,
|
|
27
|
+
inventory: [],
|
|
28
|
+
inventoryLimit: INFINITE_STORAGE_LIMIT,
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
const baseItemProps = {
|
|
@@ -69,6 +71,61 @@ describe('ContextPane', () => {
|
|
|
69
71
|
expect(screen.getByText('Corn')).toBeInTheDocument()
|
|
70
72
|
})
|
|
71
73
|
|
|
74
|
+
test('displays inventory capacity for a limited inventory', () => {
|
|
75
|
+
const inventory = [
|
|
76
|
+
testItem({ id: 'carrot', name: 'Carrot', quantity: 5 }),
|
|
77
|
+
testItem({ id: 'corn', name: 'Corn', quantity: 3 }),
|
|
78
|
+
] as unknown as farmhand.state['inventory']
|
|
79
|
+
|
|
80
|
+
render(
|
|
81
|
+
<ContextPane
|
|
82
|
+
{...defaultProps}
|
|
83
|
+
inventory={inventory}
|
|
84
|
+
inventoryLimit={10}
|
|
85
|
+
/>
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
const capacity = screen.getByText('Capacity: 8 / 10')
|
|
89
|
+
|
|
90
|
+
expect(capacity).toBeInTheDocument()
|
|
91
|
+
expect(capacity).not.toHaveClass('is-inventory-full')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('marks inventory capacity as full when the limit is reached', () => {
|
|
95
|
+
const inventory = [
|
|
96
|
+
testItem({ id: 'carrot', name: 'Carrot', quantity: 5 }),
|
|
97
|
+
testItem({ id: 'corn', name: 'Corn', quantity: 5 }),
|
|
98
|
+
] as unknown as farmhand.state['inventory']
|
|
99
|
+
|
|
100
|
+
render(
|
|
101
|
+
<ContextPane
|
|
102
|
+
{...defaultProps}
|
|
103
|
+
inventory={inventory}
|
|
104
|
+
inventoryLimit={10}
|
|
105
|
+
/>
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
expect(screen.getByText('Capacity: 10 / 10')).toHaveClass(
|
|
109
|
+
'is-inventory-full'
|
|
110
|
+
)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('does not display inventory capacity for unlimited storage', () => {
|
|
114
|
+
const inventory = [
|
|
115
|
+
testItem({ id: 'carrot', name: 'Carrot', quantity: 5 }),
|
|
116
|
+
] as unknown as farmhand.state['inventory']
|
|
117
|
+
|
|
118
|
+
render(
|
|
119
|
+
<ContextPane
|
|
120
|
+
{...defaultProps}
|
|
121
|
+
inventory={inventory}
|
|
122
|
+
inventoryLimit={INFINITE_STORAGE_LIMIT}
|
|
123
|
+
/>
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
expect(screen.queryByText(/Capacity:/)).not.toBeInTheDocument()
|
|
127
|
+
})
|
|
128
|
+
|
|
72
129
|
describe('conditional UI based on stageFocus', () => {
|
|
73
130
|
test('shows inventory view when stageFocus is SHOP', () => {
|
|
74
131
|
const playerInventory = [
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import React, { memo } from 'react'
|
|
2
|
+
import classNames from 'classnames'
|
|
2
3
|
|
|
3
4
|
import FarmhandContext from '../Farmhand/Farmhand.context.js'
|
|
4
5
|
import Inventory from '../Inventory/index.js'
|
|
5
6
|
import CowPenContextMenu from '../CowPenContextMenu/index.js'
|
|
6
7
|
import FarmhandShuffleContextMenu from '../FarmhandShuffleContextMenu/index.js'
|
|
7
8
|
import { stageFocusType } from '../../enums.js'
|
|
8
|
-
import { Div } from '../Elements/index.js'
|
|
9
|
+
import { Div, H3 } from '../Elements/index.js'
|
|
9
10
|
import { centerTabsSx } from '../../styles/sx.js'
|
|
11
|
+
import { colors } from '../../styles/tokens.js'
|
|
12
|
+
|
|
13
|
+
import { integerString } from '../../utils/integerString.js'
|
|
14
|
+
import { doesInventorySpaceRemain } from '../../utils/doesInventorySpaceRemain.js'
|
|
15
|
+
import { inventorySpaceConsumed } from '../../utils/inventorySpaceConsumed.js'
|
|
16
|
+
import { INFINITE_STORAGE_LIMIT } from '../../constants.js'
|
|
10
17
|
|
|
11
18
|
export const PlayerInventory = memo<{ playerInventory: farmhand.item[] }>(
|
|
12
19
|
/**
|
|
@@ -29,10 +36,19 @@ player's inventory.
|
|
|
29
36
|
export const ContextPane = ({
|
|
30
37
|
playerInventory,
|
|
31
38
|
stageFocus,
|
|
39
|
+
inventory,
|
|
40
|
+
inventoryLimit,
|
|
32
41
|
}: {
|
|
33
42
|
playerInventory: farmhand.item[]
|
|
34
43
|
stageFocus: string
|
|
44
|
+
inventory: farmhand.state['inventory']
|
|
45
|
+
inventoryLimit: number
|
|
35
46
|
}) => {
|
|
47
|
+
const isInventoryFull = !doesInventorySpaceRemain({
|
|
48
|
+
inventory,
|
|
49
|
+
inventoryLimit,
|
|
50
|
+
})
|
|
51
|
+
|
|
36
52
|
return (
|
|
37
53
|
<Div
|
|
38
54
|
className="ContextPane"
|
|
@@ -40,6 +56,8 @@ export const ContextPane = ({
|
|
|
40
56
|
...centerTabsSx,
|
|
41
57
|
margin: 0,
|
|
42
58
|
'& h2': { margin: '0.5em 0 1em', textAlign: 'center' },
|
|
59
|
+
'& .inventory-info': { fontSize: '1em' },
|
|
60
|
+
'& .inventory-title': { marginBottom: '0.5em' },
|
|
43
61
|
}}
|
|
44
62
|
>
|
|
45
63
|
{stageFocus === stageFocusType.COW_PEN ? (
|
|
@@ -48,7 +66,25 @@ export const ContextPane = ({
|
|
|
48
66
|
<FarmhandShuffleContextMenu />
|
|
49
67
|
) : (
|
|
50
68
|
<>
|
|
51
|
-
<h2>Inventory</h2>
|
|
69
|
+
<h2 className="inventory-title">Inventory</h2>
|
|
70
|
+
|
|
71
|
+
{inventoryLimit > INFINITE_STORAGE_LIMIT && (
|
|
72
|
+
<H3
|
|
73
|
+
{...{
|
|
74
|
+
className: classNames('inventory-info', {
|
|
75
|
+
'is-inventory-full': isInventoryFull,
|
|
76
|
+
}),
|
|
77
|
+
}}
|
|
78
|
+
sx={{
|
|
79
|
+
color: isInventoryFull ? colors.error : undefined,
|
|
80
|
+
textAlign: 'center',
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
Capacity: {integerString(inventorySpaceConsumed(inventory))} /{' '}
|
|
84
|
+
{integerString(inventoryLimit)}
|
|
85
|
+
</H3>
|
|
86
|
+
)}
|
|
87
|
+
|
|
52
88
|
{/*
|
|
53
89
|
// NOTE: Weird ignore comment syntax and formatting is needed here.
|
|
54
90
|
// See: https://stackoverflow.com/a/56913087/470685 */}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from '../PixelIcon/index.js'
|
|
19
19
|
|
|
20
20
|
import { Z_INDEX } from '../../constants.js'
|
|
21
|
+
import { stageFocusType } from '../../enums.js'
|
|
21
22
|
import theme, { blueStripeBg } from '../../mui-theme.js'
|
|
22
23
|
import { Div } from '../Elements/index.js'
|
|
23
24
|
import { fillSx } from '../../styles/sx.js'
|
|
@@ -77,6 +78,12 @@ const Farmhand = ({
|
|
|
77
78
|
isChatAvailable,
|
|
78
79
|
} = useFarmhand(props)
|
|
79
80
|
|
|
81
|
+
// Ending the day mid-match is only blocked on the Shuffle stage itself;
|
|
82
|
+
// a match left in progress must not hide the button everywhere else.
|
|
83
|
+
const isEndDayHidden =
|
|
84
|
+
state.stageFocus === stageFocusType.FARMHAND_SHUFFLE &&
|
|
85
|
+
gameState.farmhandShuffle.isMatchInProgress
|
|
86
|
+
|
|
80
87
|
return (
|
|
81
88
|
<GlobalHotKeys
|
|
82
89
|
allowChanges={true}
|
|
@@ -268,7 +275,7 @@ const Farmhand = ({
|
|
|
268
275
|
</Fab>
|
|
269
276
|
</div>
|
|
270
277
|
</div>
|
|
271
|
-
{!
|
|
278
|
+
{!isEndDayHidden && (
|
|
272
279
|
<Tooltip
|
|
273
280
|
placement="left"
|
|
274
281
|
title={
|
|
@@ -33,6 +33,29 @@ describe('FarmhandService', () => {
|
|
|
33
33
|
})
|
|
34
34
|
})
|
|
35
35
|
|
|
36
|
+
describe('inventory containing an unknown item id', () => {
|
|
37
|
+
const inventory = [{ quantity: 1, id: 'not-a-real-item' }]
|
|
38
|
+
const message = 'Unknown item id: not-a-real-item'
|
|
39
|
+
|
|
40
|
+
test('computePlayerInventory throws an informative error', () => {
|
|
41
|
+
expect(() =>
|
|
42
|
+
FarmhandService.computePlayerInventory(inventory, {})
|
|
43
|
+
).toThrow(message)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('getFieldToolInventory throws an informative error', () => {
|
|
47
|
+
expect(() => FarmhandService.getFieldToolInventory(inventory)).toThrow(
|
|
48
|
+
message
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('getPlantableCropInventory throws an informative error', () => {
|
|
53
|
+
expect(() =>
|
|
54
|
+
FarmhandService.getPlantableCropInventory(inventory)
|
|
55
|
+
).toThrow(message)
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
36
59
|
describe('getFieldToolInventory', () => {
|
|
37
60
|
test('selects field tools from inventory', () => {
|
|
38
61
|
const fieldToolInventory = FarmhandService.getFieldToolInventory([
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { itemsMap } from '../../data/maps.js'
|
|
2
2
|
import { fieldMode, itemType } from '../../enums.js'
|
|
3
|
+
import { getItemById } from '../../utils/getItemById.js'
|
|
3
4
|
import { getItemCurrentValue } from '../../utils/getItemCurrentValue.js'
|
|
4
5
|
import { memoize } from '../../utils/memoize.js'
|
|
5
6
|
|
|
@@ -12,23 +13,22 @@ export class FarmhandService {
|
|
|
12
13
|
inventory: farmhand.state['inventory'],
|
|
13
14
|
valueAdjustments: Record<string, number>
|
|
14
15
|
): farmhand.item[] =>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
valueAdjustments
|
|
22
|
-
|
|
23
|
-
})
|
|
16
|
+
inventory.map(({ quantity, id }: { quantity: number; id: string }) => {
|
|
17
|
+
const item = getItemById(id)
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
quantity,
|
|
21
|
+
...item,
|
|
22
|
+
value: getItemCurrentValue(item, valueAdjustments),
|
|
23
|
+
}
|
|
24
|
+
})
|
|
24
25
|
)
|
|
25
26
|
|
|
26
27
|
static getFieldToolInventory = memoize(
|
|
27
28
|
(inventory: farmhand.state['inventory']): farmhand.item[] =>
|
|
28
29
|
inventory
|
|
29
30
|
.filter(({ id }: { id: string }) => {
|
|
30
|
-
|
|
31
|
-
const item = itemsMap[id as keyof typeof itemsMap]
|
|
31
|
+
const item = getItemById(id)
|
|
32
32
|
|
|
33
33
|
return (
|
|
34
34
|
typeof item.enablesFieldMode === 'string' &&
|
|
@@ -48,11 +48,7 @@ export class FarmhandService {
|
|
|
48
48
|
static getPlantableCropInventory = memoize(
|
|
49
49
|
(inventory: farmhand.state['inventory']): farmhand.item[] =>
|
|
50
50
|
inventory
|
|
51
|
-
.filter(
|
|
52
|
-
({ id }: { id: string }) =>
|
|
53
|
-
// TODO: Add a defensive check to verify itemsMap[id] exists before accessing isPlantableCrop
|
|
54
|
-
itemsMap[id as keyof typeof itemsMap].isPlantableCrop
|
|
55
|
-
)
|
|
51
|
+
.filter(({ id }: { id: string }) => getItemById(id).isPlantableCrop)
|
|
56
52
|
.map(({ id, quantity }: { id: string; quantity: number }) => ({
|
|
57
53
|
...itemsMap[id as keyof typeof itemsMap],
|
|
58
54
|
quantity,
|
|
@@ -532,10 +532,14 @@ export const useFarmhand = (props: FarmhandProps) => {
|
|
|
532
532
|
|
|
533
533
|
const keyHandlers = useMemo(() => {
|
|
534
534
|
const map: Record<string, () => void> = {
|
|
535
|
-
// The End Day button is hidden
|
|
536
|
-
// progress (see Farmhand.tsx), so its hotkey must not
|
|
535
|
+
// The End Day button is hidden on the Farmhand Shuffle stage while a
|
|
536
|
+
// match is in progress (see Farmhand.tsx), so its hotkey must not
|
|
537
|
+
// bypass that.
|
|
537
538
|
incrementDay: () => {
|
|
538
|
-
if (!
|
|
539
|
+
if (!(
|
|
540
|
+
state.stageFocus === stageFocusType.FARMHAND_SHUFFLE &&
|
|
541
|
+
state.farmhandShuffle.isMatchInProgress
|
|
542
|
+
)) {
|
|
539
543
|
incrementDay()
|
|
540
544
|
}
|
|
541
545
|
},
|
|
@@ -587,6 +591,7 @@ export const useFarmhand = (props: FarmhandProps) => {
|
|
|
587
591
|
focusPreviousView,
|
|
588
592
|
handlers,
|
|
589
593
|
state.farmhandShuffle.isMatchInProgress,
|
|
594
|
+
state.stageFocus,
|
|
590
595
|
state.toolLevels,
|
|
591
596
|
viewList,
|
|
592
597
|
clearPersistedData,
|
|
@@ -863,7 +868,14 @@ export const useFarmhand = (props: FarmhandProps) => {
|
|
|
863
868
|
}
|
|
864
869
|
|
|
865
870
|
showInventoryFullNotifications(prevState, state)
|
|
866
|
-
|
|
871
|
+
|
|
872
|
+
// prevState.hasBooted is false only for the render where the persisted
|
|
873
|
+
// game is restored on page load. Diffing against that pre-boot state
|
|
874
|
+
// (which has no learned recipes) would report every recipe the player has
|
|
875
|
+
// ever learned as newly learned, on every page load.
|
|
876
|
+
if (prevState.hasBooted) {
|
|
877
|
+
showRecipeLearnedNotifications(prevState)
|
|
878
|
+
}
|
|
867
879
|
}, [
|
|
868
880
|
state,
|
|
869
881
|
prevState,
|
|
@@ -3,8 +3,10 @@ import { render, screen, waitFor, within } from '@testing-library/react'
|
|
|
3
3
|
import userEvent from '@testing-library/user-event'
|
|
4
4
|
|
|
5
5
|
import { testItem } from '../../test-utils/index.js'
|
|
6
|
+
import { testState } from '../../test-utils/index.js'
|
|
6
7
|
import { season } from '../../enums.js'
|
|
7
8
|
import { carrotSeed } from '../../data/crops/index.js'
|
|
9
|
+
import { huggingMachine } from '../../data/items.js'
|
|
8
10
|
|
|
9
11
|
import { INFINITE_STORAGE_LIMIT } from '../../constants.js'
|
|
10
12
|
|
|
@@ -74,6 +76,33 @@ describe('Item', () => {
|
|
|
74
76
|
})
|
|
75
77
|
})
|
|
76
78
|
|
|
79
|
+
test('limits the purchase quantity using the item maximum', async () => {
|
|
80
|
+
render(
|
|
81
|
+
<Item
|
|
82
|
+
{...{
|
|
83
|
+
...props,
|
|
84
|
+
adjustedValue: 500,
|
|
85
|
+
item: huggingMachine,
|
|
86
|
+
money: 10_000,
|
|
87
|
+
purchaseState: testState({
|
|
88
|
+
inventory: [],
|
|
89
|
+
inventoryLimit: INFINITE_STORAGE_LIMIT,
|
|
90
|
+
purchasedCowPen: 1,
|
|
91
|
+
}),
|
|
92
|
+
}}
|
|
93
|
+
/>
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
const quantity = screen.getByRole('textbox')
|
|
97
|
+
const increment = screen.getByRole('button', { name: 'Increment' })
|
|
98
|
+
|
|
99
|
+
for (let clickCount = 0; clickCount < 9; clickCount += 1) {
|
|
100
|
+
await userEvent.click(increment)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
expect(quantity).toHaveValue('10')
|
|
104
|
+
})
|
|
105
|
+
|
|
77
106
|
describe('prices', () => {
|
|
78
107
|
beforeEach(() => {
|
|
79
108
|
render(<Item {...{ ...props, money: 100 }} />)
|
|
@@ -117,6 +117,7 @@ export interface ItemProps {
|
|
|
117
117
|
adjustedValue?: number
|
|
118
118
|
previousDayAdjustedValue?: number | null
|
|
119
119
|
maxQuantityPlayerCanPurchase?: number
|
|
120
|
+
purchaseState?: farmhand.state
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
export const Item = ({
|
|
@@ -137,6 +138,7 @@ export const Item = ({
|
|
|
137
138
|
playerInventoryQuantities,
|
|
138
139
|
showQuantity,
|
|
139
140
|
valueAdjustments,
|
|
141
|
+
purchaseState,
|
|
140
142
|
|
|
141
143
|
item: {
|
|
142
144
|
description,
|
|
@@ -163,7 +165,10 @@ export const Item = ({
|
|
|
163
165
|
0,
|
|
164
166
|
Math.min(
|
|
165
167
|
Math.floor(money / adjustedValue),
|
|
166
|
-
inventorySpaceRemaining({ inventory, inventoryLimit })
|
|
168
|
+
inventorySpaceRemaining({ inventory, inventoryLimit }),
|
|
169
|
+
purchaseState && item?.getMaxPurchaseQuantity
|
|
170
|
+
? item.getMaxPurchaseQuantity(purchaseState)
|
|
171
|
+
: Infinity
|
|
167
172
|
)
|
|
168
173
|
),
|
|
169
174
|
}: ItemProps) => {
|
|
@@ -461,7 +466,14 @@ export default function Consumer(props: Partial<ItemProps>) {
|
|
|
461
466
|
return (
|
|
462
467
|
<FarmhandContext.Consumer>
|
|
463
468
|
{({ gameState, handlers }) => (
|
|
464
|
-
<Item
|
|
469
|
+
<Item
|
|
470
|
+
{...{
|
|
471
|
+
...gameState,
|
|
472
|
+
...handlers,
|
|
473
|
+
...props,
|
|
474
|
+
purchaseState: gameState,
|
|
475
|
+
}}
|
|
476
|
+
/>
|
|
465
477
|
)}
|
|
466
478
|
</FarmhandContext.Consumer>
|
|
467
479
|
)
|
|
@@ -4,7 +4,6 @@ import userEvent from '@testing-library/user-event'
|
|
|
4
4
|
import { vi } from 'vitest'
|
|
5
5
|
|
|
6
6
|
import { dialogView, stageFocusType } from '../../enums.js'
|
|
7
|
-
import { INFINITE_STORAGE_LIMIT } from '../../constants.js'
|
|
8
7
|
import { noop } from '../../utils/noop.js'
|
|
9
8
|
import FarmhandContext, {
|
|
10
9
|
createContextData,
|
|
@@ -27,8 +26,6 @@ const defaultProps = {
|
|
|
27
26
|
handleOnlineToggleChange: noop,
|
|
28
27
|
handleRoomChange: noop,
|
|
29
28
|
handleViewChange: noop,
|
|
30
|
-
inventory: [],
|
|
31
|
-
inventoryLimit: INFINITE_STORAGE_LIMIT,
|
|
32
29
|
itemsSold: {},
|
|
33
30
|
isChatAvailable: false,
|
|
34
31
|
isDialogViewOpen: false,
|
|
@@ -27,21 +27,15 @@ import {
|
|
|
27
27
|
} from '../PixelIcon/index.js'
|
|
28
28
|
|
|
29
29
|
import { MAX_ROOM_NAME_LENGTH } from '../../common/constants.js'
|
|
30
|
-
import {
|
|
31
|
-
DEFAULT_ROOM,
|
|
32
|
-
INFINITE_STORAGE_LIMIT,
|
|
33
|
-
STAGE_TITLE_MAP,
|
|
34
|
-
} from '../../constants.js'
|
|
30
|
+
import { DEFAULT_ROOM, STAGE_TITLE_MAP } from '../../constants.js'
|
|
35
31
|
import { dialogView } from '../../enums.js'
|
|
36
|
-
import { doesInventorySpaceRemain } from '../../utils/doesInventorySpaceRemain.js'
|
|
37
32
|
import { integerString } from '../../utils/integerString.js'
|
|
38
|
-
import { inventorySpaceConsumed } from '../../utils/inventorySpaceConsumed.js'
|
|
39
33
|
import FarmhandContext from '../Farmhand/Farmhand.context.js'
|
|
40
34
|
|
|
41
|
-
import { breakpoints,
|
|
35
|
+
import { breakpoints, fonts } from '../../styles/tokens.js'
|
|
42
36
|
import AccountingView from '../AccountingView/index.js'
|
|
43
37
|
import AchievementsView from '../AchievementsView/index.js'
|
|
44
|
-
import {
|
|
38
|
+
import { Header } from '../Elements/index.js'
|
|
45
39
|
import KeybindingsView from '../KeybindingsView/index.js'
|
|
46
40
|
import LogView from '../LogView/index.js'
|
|
47
41
|
import OnlinePeersView from '../OnlinePeersView/index.js'
|
|
@@ -260,8 +254,6 @@ export const Navigation = ({
|
|
|
260
254
|
handleOnlineToggleChange,
|
|
261
255
|
handleRoomChange,
|
|
262
256
|
handleViewChange,
|
|
263
|
-
inventory,
|
|
264
|
-
inventoryLimit,
|
|
265
257
|
isChatAvailable,
|
|
266
258
|
isDialogViewOpen,
|
|
267
259
|
isOnline,
|
|
@@ -286,8 +278,6 @@ export const Navigation = ({
|
|
|
286
278
|
handleOnlineToggleChange: (checked: boolean) => void
|
|
287
279
|
handleRoomChange: (newRoom: string) => void
|
|
288
280
|
handleViewChange: (event: any) => void
|
|
289
|
-
inventory: farmhand.state['inventory']
|
|
290
|
-
inventoryLimit: number
|
|
291
281
|
isChatAvailable: boolean
|
|
292
282
|
isDialogViewOpen: boolean
|
|
293
283
|
isOnline: boolean
|
|
@@ -369,7 +359,6 @@ export const Navigation = ({
|
|
|
369
359
|
position: 'relative',
|
|
370
360
|
},
|
|
371
361
|
},
|
|
372
|
-
'& .inventory-info': { fontSize: '1em' },
|
|
373
362
|
'& .MuiSelect-select': { padding: '1em' },
|
|
374
363
|
'& .chat-placeholder': {
|
|
375
364
|
fontSize: '0.75em',
|
|
@@ -396,26 +385,6 @@ export const Navigation = ({
|
|
|
396
385
|
room,
|
|
397
386
|
}}
|
|
398
387
|
/>
|
|
399
|
-
{inventoryLimit > INFINITE_STORAGE_LIMIT && (
|
|
400
|
-
<H3
|
|
401
|
-
{...{
|
|
402
|
-
className: classNames('inventory-info', {
|
|
403
|
-
'is-inventory-full': !doesInventorySpaceRemain({
|
|
404
|
-
inventory,
|
|
405
|
-
inventoryLimit,
|
|
406
|
-
}),
|
|
407
|
-
}),
|
|
408
|
-
}}
|
|
409
|
-
sx={{
|
|
410
|
-
color: !doesInventorySpaceRemain({ inventory, inventoryLimit })
|
|
411
|
-
? colors.error
|
|
412
|
-
: undefined,
|
|
413
|
-
}}
|
|
414
|
-
>
|
|
415
|
-
Inventory: {integerString(inventorySpaceConsumed(inventory))} /{' '}
|
|
416
|
-
{integerString(inventoryLimit)}
|
|
417
|
-
</H3>
|
|
418
|
-
)}
|
|
419
388
|
|
|
420
389
|
<Select
|
|
421
390
|
variant="standard"
|
|
@@ -30,8 +30,7 @@ const OnlinePeer = ({
|
|
|
30
30
|
subheader: (
|
|
31
31
|
<div>
|
|
32
32
|
<p>Day: {integerString(dayCount)}</p>
|
|
33
|
-
|
|
34
|
-
<p>Level: {integerString(levelAchieved(experience ?? 0))}</p>
|
|
33
|
+
<p>Level: {integerString(levelAchieved(experience))}</p>
|
|
35
34
|
<p>Money: {moneyString(money)}</p>
|
|
36
35
|
</div>
|
|
37
36
|
),
|
package/src/data/items.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
COW_FEED_ITEM_ID,
|
|
4
4
|
HUGGING_MACHINE_ITEM_ID,
|
|
5
5
|
INITIAL_SPRINKLER_RANGE,
|
|
6
|
+
PURCHASEABLE_COW_PENS,
|
|
6
7
|
} from '../constants.js'
|
|
7
8
|
|
|
8
9
|
const { freeze } = Object
|
|
@@ -197,6 +198,22 @@ export const huggingMachine: farmhand.item = freeze({
|
|
|
197
198
|
name: 'Hugging Machine',
|
|
198
199
|
type: HUGGING_MACHINE,
|
|
199
200
|
value: 500,
|
|
201
|
+
getMaxPurchaseQuantity: (state: farmhand.state) => {
|
|
202
|
+
const huggingMachinesInInventory =
|
|
203
|
+
state.inventory.find(i => i.id === HUGGING_MACHINE_ITEM_ID)?.quantity ?? 0
|
|
204
|
+
|
|
205
|
+
const huggingMachinesInUse = state.cowInventory.filter(
|
|
206
|
+
cow => cow.isUsingHuggingMachine
|
|
207
|
+
).length
|
|
208
|
+
|
|
209
|
+
const cowCapacity =
|
|
210
|
+
PURCHASEABLE_COW_PENS.get(state.purchasedCowPen)?.cows ?? 0
|
|
211
|
+
|
|
212
|
+
return Math.max(
|
|
213
|
+
0,
|
|
214
|
+
cowCapacity - huggingMachinesInInventory - huggingMachinesInUse
|
|
215
|
+
)
|
|
216
|
+
},
|
|
200
217
|
})
|
|
201
218
|
|
|
202
219
|
/**
|
package/src/farmhand.d.ts
CHANGED
|
@@ -63,6 +63,7 @@ declare namespace farmhand {
|
|
|
63
63
|
// ore - see applyDestructionYield in reducers/helpers.tsx for the
|
|
64
64
|
// shared logic that applies it.
|
|
65
65
|
destructionYield?: { itemId: string; quantity: number }
|
|
66
|
+
getMaxPurchaseQuantity?: (state: state) => number
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
interface seedItem extends item {
|
|
@@ -99,6 +99,24 @@ describe('harvestPlot', () => {
|
|
|
99
99
|
})
|
|
100
100
|
})
|
|
101
101
|
|
|
102
|
+
describe('inventory has less space than the harvest yield', () => {
|
|
103
|
+
test('only counts the crops that were actually received', () => {
|
|
104
|
+
const inputState = testState({
|
|
105
|
+
field: [[testCrop({ itemId: 'sample-crop-1', daysWatered: 4 })]],
|
|
106
|
+
inventory: [{ id: 'sample-crop-2', quantity: 4 }],
|
|
107
|
+
inventoryLimit: 5,
|
|
108
|
+
toolLevels: toolLevelsBronze,
|
|
109
|
+
})
|
|
110
|
+
// The bronze scythe yields 2 crops but only 1 slot is free
|
|
111
|
+
const { cropsHarvested } = harvestPlot(inputState, 0, 0)
|
|
112
|
+
|
|
113
|
+
expect(cropsHarvested).toEqual({
|
|
114
|
+
...testState().cropsHarvested,
|
|
115
|
+
SAMPLE_CROP_TYPE_1: 1,
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
102
120
|
describe('there is insufficient inventory space', () => {
|
|
103
121
|
test('no-ops', () => {
|
|
104
122
|
const inputState = testState({
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from '../../enums.js'
|
|
8
8
|
import { itemsMap } from '../../data/maps.js'
|
|
9
9
|
import { doesInventorySpaceRemain } from '../../utils/doesInventorySpaceRemain.js'
|
|
10
|
+
import { inventorySpaceRemaining } from '../../utils/inventorySpaceRemaining.js'
|
|
10
11
|
import { getCropLifeStage } from '../../utils/getCropLifeStage.js'
|
|
11
12
|
import { getPlotContentType } from '../../utils/getPlotContentType.js'
|
|
12
13
|
import { getSeedItemIdFromFinalStageCropItemId } from '../../utils/getSeedItemIdFromFinalStageCropItemId.js'
|
|
@@ -43,7 +44,10 @@ function getHarvestedQuantity(state: farmhand.state): number {
|
|
|
43
44
|
amount = 1
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
// Cap the yield to the space actually available so that callers only count
|
|
48
|
+
// crops the player received, not crops that addItemToInventory would
|
|
49
|
+
// silently drop because the inventory is nearly full.
|
|
50
|
+
return Math.min(amount, inventorySpaceRemaining(state))
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
function harvestCrops(
|
|
@@ -94,11 +98,6 @@ function harvestCrops(
|
|
|
94
98
|
|
|
95
99
|
const { cropsHarvested } = state
|
|
96
100
|
|
|
97
|
-
// TODO: cropsHarvested is incremented by the full harvestedQuantity even
|
|
98
|
-
// when addItemToInventory silently caps how much is actually added because
|
|
99
|
-
// the inventory is nearly full, so this counter can over-count relative to
|
|
100
|
-
// what the player actually received. See the equivalent fix applied to
|
|
101
|
-
// treeFruitsHarvested in harvestForestPlot.ts for the pattern to follow.
|
|
102
101
|
return {
|
|
103
102
|
...state,
|
|
104
103
|
cropsHarvested: {
|