@jeremyckahn/farmhand 1.24.1 → 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-B03HVYBS.js → index-C9kG86vs.js} +11 -11
- package/dist/assets/{index-B03HVYBS.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/FarmhandService.test.ts +23 -0
- package/src/components/Farmhand/FarmhandService.ts +12 -16
- 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/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 */}
|
|
@@ -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,
|
|
@@ -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: {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getCowStub } from '../../test-utils/stubs/cowStub.js'
|
|
2
|
+
import { testState } from '../../test-utils/index.js'
|
|
3
|
+
|
|
4
|
+
import { modifyCow } from './modifyCow.js'
|
|
5
|
+
|
|
6
|
+
describe('modifyCow', () => {
|
|
7
|
+
test('updates the matching cow', () => {
|
|
8
|
+
const cow1 = getCowStub({ id: 'cow-1', name: 'Bessie' })
|
|
9
|
+
const cow2 = getCowStub({ id: 'cow-2', name: 'Daisy' })
|
|
10
|
+
const state = testState({ cowInventory: [cow1, cow2] })
|
|
11
|
+
|
|
12
|
+
const { cowInventory } = modifyCow(state, 'cow-2', () => ({
|
|
13
|
+
name: 'Clover',
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
expect(cowInventory).toEqual([cow1, { ...cow2, name: 'Clover' }])
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('does not mutate the original cowInventory', () => {
|
|
20
|
+
const cow = getCowStub({ id: 'cow-1' })
|
|
21
|
+
const state = testState({ cowInventory: [cow] })
|
|
22
|
+
|
|
23
|
+
modifyCow(state, 'cow-1', () => ({ name: 'Clover' }))
|
|
24
|
+
|
|
25
|
+
expect(state.cowInventory).toEqual([cow])
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('no-ops if the cow does not exist', () => {
|
|
29
|
+
const state = testState({ cowInventory: [getCowStub({ id: 'cow-1' })] })
|
|
30
|
+
|
|
31
|
+
expect(modifyCow(state, 'missing', () => ({ name: 'x' }))).toBe(state)
|
|
32
|
+
})
|
|
33
|
+
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { findCowById } from '../../utils/findCowById.js'
|
|
2
|
+
|
|
2
3
|
/**
|
|
3
4
|
* @param fn Function that takes a cow and returns the modified cow or undefined.
|
|
4
5
|
*/
|
|
@@ -7,21 +8,15 @@ export const modifyCow = (
|
|
|
7
8
|
cowId: string,
|
|
8
9
|
fn: (cow: farmhand.cow) => Partial<farmhand.cow>
|
|
9
10
|
): farmhand.state => {
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
// TODO: Use the findCowById util here.
|
|
13
|
-
const cow = cowInventory.find(({ id }) => id === cowId)
|
|
11
|
+
const cow = findCowById(state.cowInventory, cowId)
|
|
14
12
|
|
|
15
13
|
if (!cow) {
|
|
16
14
|
return state
|
|
17
15
|
}
|
|
18
16
|
|
|
17
|
+
const cowInventory = [...state.cowInventory]
|
|
19
18
|
const cowIndex = cowInventory.indexOf(cow)
|
|
20
19
|
|
|
21
|
-
if (cowIndex === -1) {
|
|
22
|
-
return state
|
|
23
|
-
}
|
|
24
|
-
|
|
25
20
|
cowInventory[cowIndex] = {
|
|
26
21
|
...cow,
|
|
27
22
|
...fn(cow),
|