@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.
Files changed (29) hide show
  1. package/dist/assets/{index-B03HVYBS.js → index-C9kG86vs.js} +11 -11
  2. package/dist/assets/{index-B03HVYBS.js.map → index-C9kG86vs.js.map} +1 -1
  3. package/dist/index.html +1 -1
  4. package/dist/service-worker.js +1 -1
  5. package/dist/service-worker.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/components/Cellar/Keg.tsx +4 -1
  8. package/src/components/ContextPane/ContextPane.test.tsx +57 -0
  9. package/src/components/ContextPane/ContextPane.tsx +38 -2
  10. package/src/components/Farmhand/FarmhandService.test.ts +23 -0
  11. package/src/components/Farmhand/FarmhandService.ts +12 -16
  12. package/src/components/Item/Item.test.tsx +29 -0
  13. package/src/components/Item/Item.tsx +14 -2
  14. package/src/components/Navigation/Navigation.test.tsx +0 -3
  15. package/src/components/Navigation/Navigation.tsx +3 -34
  16. package/src/components/OnlinePeersView/OnlinePeer/OnlinePeer.tsx +1 -2
  17. package/src/data/items.ts +17 -0
  18. package/src/farmhand.d.ts +1 -0
  19. package/src/game-logic/reducers/harvestPlot.test.ts +18 -0
  20. package/src/game-logic/reducers/harvestPlot.ts +5 -6
  21. package/src/game-logic/reducers/modifyCow.test.ts +33 -0
  22. package/src/game-logic/reducers/modifyCow.ts +4 -9
  23. package/src/game-logic/reducers/purchaseItem.test.ts +129 -0
  24. package/src/game-logic/reducers/purchaseItem.ts +9 -2
  25. package/src/utils/getItemById.test.ts +18 -0
  26. package/src/utils/getItemById.ts +14 -0
  27. package/src/utils/getPlotImage.ts +1 -3
  28. package/src/utils/transformStateDataForImport.test.ts +9 -0
  29. package/src/utils/transformStateDataForImport.ts +9 -12
@@ -1,4 +1,6 @@
1
1
  import { INFINITE_STORAGE_LIMIT } from '../../constants.js'
2
+ import { huggingMachine } from '../../data/items.js'
3
+ import { generateCow } from '../../utils/generateCow.js'
2
4
  import { testItem, testState } from '../../test-utils/index.js'
3
5
 
4
6
  import { purchaseItem } from './purchaseItem.js'
@@ -81,5 +83,132 @@ describe('purchaseItem', () => {
81
83
  })
82
84
  })
83
85
  })
86
+
87
+ test('respects an item maximum purchase quantity', () => {
88
+ const item = testItem({
89
+ id: 'sample-item-1',
90
+ getMaxPurchaseQuantity: () => 2,
91
+ })
92
+
93
+ expect(purchaseItem(state, item, 5)).toMatchObject({
94
+ inventory: [{ id: 'sample-item-1', quantity: 2 }],
95
+ todaysPurchases: { 'sample-item-1': 2 },
96
+ money: 8,
97
+ })
98
+ })
99
+
100
+ test('does not purchase when an item maximum is negative', () => {
101
+ const item = testItem({
102
+ id: 'sample-item-1',
103
+ getMaxPurchaseQuantity: () => -2,
104
+ })
105
+
106
+ expect(purchaseItem(state, item, 1)).toMatchObject({
107
+ inventory: [],
108
+ todaysPurchases: {},
109
+ money: 10,
110
+ })
111
+ })
112
+
113
+ describe('the item has a maximum purchase quantity', () => {
114
+ test.each([
115
+ [1, 10],
116
+ [2, 20],
117
+ [3, 30],
118
+ ])(
119
+ 'limits Hugging Machines to the Cow Pen capacity of %i cows',
120
+ (purchasedCowPen, cowCapacity) => {
121
+ state = testState({
122
+ inventory: [],
123
+ money: 10_000,
124
+ purchasedCowPen,
125
+ valueAdjustments: { [huggingMachine.id]: 1 },
126
+ })
127
+
128
+ expect(huggingMachine.getMaxPurchaseQuantity?.(state)).toEqual(
129
+ cowCapacity
130
+ )
131
+ }
132
+ )
133
+
134
+ test('limits the purchase to the remaining Cow Pen capacity', () => {
135
+ state = testState({
136
+ inventory: [{ id: huggingMachine.id, quantity: 9 }],
137
+ money: 10_000,
138
+ purchasedCowPen: 1,
139
+ valueAdjustments: { [huggingMachine.id]: 1 },
140
+ })
141
+
142
+ expect(purchaseItem(state, huggingMachine, 2)).toMatchObject({
143
+ inventory: [{ id: huggingMachine.id, quantity: 10 }],
144
+ todaysPurchases: { [huggingMachine.id]: 1 },
145
+ money: 9_500,
146
+ pendingPeerMessages: [
147
+ { message: 'purchased 1 unit of Hugging Machine.' },
148
+ ],
149
+ })
150
+ })
151
+
152
+ test('does not purchase a Hugging Machine when capacity is full', () => {
153
+ state = testState({
154
+ inventory: [{ id: huggingMachine.id, quantity: 10 }],
155
+ money: 10_000,
156
+ purchasedCowPen: 1,
157
+ valueAdjustments: { [huggingMachine.id]: 1 },
158
+ })
159
+
160
+ expect(purchaseItem(state, huggingMachine, 1)).toMatchObject({
161
+ inventory: [{ id: huggingMachine.id, quantity: 10 }],
162
+ todaysPurchases: {},
163
+ money: 10_000,
164
+ })
165
+ })
166
+
167
+ test('does not purchase a Hugging Machine when already over capacity', () => {
168
+ state = testState({
169
+ inventory: [{ id: huggingMachine.id, quantity: 11 }],
170
+ money: 10_000,
171
+ purchasedCowPen: 1,
172
+ valueAdjustments: { [huggingMachine.id]: 1 },
173
+ })
174
+
175
+ expect(purchaseItem(state, huggingMachine, 1)).toMatchObject({
176
+ inventory: [{ id: huggingMachine.id, quantity: 11 }],
177
+ todaysPurchases: {},
178
+ money: 10_000,
179
+ })
180
+ })
181
+
182
+ test('does not purchase a Hugging Machine when all capacity is in use', () => {
183
+ state = testState({
184
+ cowInventory: Array.from({ length: 6 }, () =>
185
+ generateCow({ isUsingHuggingMachine: true })
186
+ ),
187
+ inventory: [{ id: huggingMachine.id, quantity: 4 }],
188
+ money: 10_000,
189
+ purchasedCowPen: 1,
190
+ valueAdjustments: { [huggingMachine.id]: 1 },
191
+ })
192
+
193
+ expect(purchaseItem(state, huggingMachine, 1)).toMatchObject({
194
+ inventory: [{ id: huggingMachine.id, quantity: 4 }],
195
+ todaysPurchases: {},
196
+ money: 10_000,
197
+ })
198
+ })
199
+
200
+ test('does not purchase a Hugging Machine without a Cow Pen', () => {
201
+ state = testState({
202
+ money: 10_000,
203
+ valueAdjustments: { [huggingMachine.id]: 1 },
204
+ })
205
+
206
+ expect(purchaseItem(state, huggingMachine, 1)).toMatchObject({
207
+ inventory: [],
208
+ todaysPurchases: {},
209
+ money: 10_000,
210
+ })
211
+ })
212
+ })
84
213
  })
85
214
  })
@@ -13,7 +13,14 @@ export const purchaseItem = (
13
13
  howMany: number = 1
14
14
  ): farmhand.state => {
15
15
  const { money, todaysPurchases, valueAdjustments } = state
16
- const numberOfItemsToAdd = Math.min(howMany, inventorySpaceRemaining(state))
16
+
17
+ const getMaxPurchaseQuantity =
18
+ item.getMaxPurchaseQuantity?.(state) ?? Infinity
19
+
20
+ const numberOfItemsToAdd = Math.max(
21
+ 0,
22
+ Math.min(howMany, inventorySpaceRemaining(state), getMaxPurchaseQuantity)
23
+ )
17
24
 
18
25
  if (numberOfItemsToAdd === 0) {
19
26
  return state
@@ -28,7 +35,7 @@ export const purchaseItem = (
28
35
 
29
36
  state = prependPendingPeerMessage(
30
37
  state,
31
- PURCHASED_ITEM_PEER_NOTIFICATION('', howMany, item)
38
+ PURCHASED_ITEM_PEER_NOTIFICATION('', numberOfItemsToAdd, item)
32
39
  )
33
40
 
34
41
  return addItemToInventory(
@@ -0,0 +1,18 @@
1
+ import { carrot } from '../data/items.js'
2
+
3
+ import { getItemById } from './getItemById.js'
4
+
5
+ vitest.mock('../data/maps.js')
6
+ vitest.mock('../data/items.js')
7
+
8
+ describe('getItemById', () => {
9
+ test('returns the item for a known id', () => {
10
+ expect(getItemById('carrot')).toEqual(carrot)
11
+ })
12
+
13
+ test('throws an informative error for an unknown id', () => {
14
+ expect(() => getItemById('not-a-real-item')).toThrow(
15
+ 'Unknown item id: not-a-real-item'
16
+ )
17
+ })
18
+ })
@@ -0,0 +1,14 @@
1
+ import { itemsMap } from '../data/maps.js'
2
+
3
+ /**
4
+ * @throws If no item exists for the provided id.
5
+ */
6
+ export const getItemById = (id: string): farmhand.item => {
7
+ const item = itemsMap[id as keyof typeof itemsMap]
8
+
9
+ if (!item) {
10
+ throw new Error(`Unknown item id: ${id}`)
11
+ }
12
+
13
+ return item
14
+ }
@@ -64,9 +64,7 @@ export const getPlotImage = (
64
64
 
65
65
  if (getPlotContentType(plotContents) === itemType.WEED) {
66
66
  const weedColors = ['yellow', 'orange', 'pink']
67
- // TODO: Handle negative coordinates by using Math.abs(x * y) to avoid
68
- // negative modulo index.
69
- const color = weedColors[(x * y) % weedColors.length]
67
+ const color = weedColors[Math.abs(x * y) % weedColors.length]
70
68
 
71
69
  return (itemImages as Record<string, string>)[`weed-${color}`]
72
70
  }
@@ -27,6 +27,15 @@ describe('transformStateDataForImport', () => {
27
27
  expect(sanitizedState).toEqual(stateWithoutVersion)
28
28
  })
29
29
 
30
+ test('it does not throw if cowBreedingPen and cowInventory are missing', () => {
31
+ delete state.cowBreedingPen
32
+ delete state.cowInventory
33
+
34
+ const sanitizedState = transformStateDataForImport(state as any)
35
+
36
+ expect(sanitizedState.cowBreedingPen).toBeUndefined()
37
+ })
38
+
30
39
  test('it calculates experience from itemsSold if experience is 0', () => {
31
40
  state.experience = 0
32
41
  state.itemsSold = {
@@ -34,20 +34,17 @@ export const transformStateDataForImport = (
34
34
  // cowInventory, but at least one player's game somehow got into that state.
35
35
  // This block detects such an invalid state and corrects it.
36
36
  {
37
- // TODO: Add defensive check safeguards for sanitizedState.cowBreedingPen
38
- // and sanitizedState.cowInventory to prevent TypeError crashes during
39
- // corrupt/legacy state imports.
40
- const { cowId1, cowId2 } =
41
- sanitizedState.cowBreedingPen as farmhand.state['cowBreedingPen']
37
+ const { cowId1, cowId2 } = (sanitizedState.cowBreedingPen ?? {}) as Partial<
38
+ farmhand.state['cowBreedingPen']
39
+ >
42
40
 
43
- const cowPenIdMap = (sanitizedState.cowInventory as farmhand.cow[]).reduce(
44
- (acc: Record<string, farmhand.cow>, cow: farmhand.cow) => {
45
- acc[cow.id] = cow
41
+ const cowPenIdMap = (
42
+ (sanitizedState.cowInventory ?? []) as farmhand.cow[]
43
+ ).reduce((acc: Record<string, farmhand.cow>, cow: farmhand.cow) => {
44
+ acc[cow.id] = cow
46
45
 
47
- return acc
48
- },
49
- {}
50
- )
46
+ return acc
47
+ }, {})
51
48
 
52
49
  const isCowInBreedingPenMissingFromInventory = [cowId1, cowId2].some(
53
50
  cowId => {