@jeremyckahn/farmhand 1.20.2 → 1.20.3
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-dORrBwcH.js → index-tSySi_OA.js} +6 -6
- package/dist/assets/{index-dORrBwcH.js.map → index-tSySi_OA.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/Achievement/Achievement.test.tsx +66 -0
- package/src/components/Achievement/Achievement.tsx +44 -20
- package/src/components/ProgressBar/ProgressBar.tsx +12 -3
- package/src/data/achievements.test.ts +64 -1
- package/src/data/achievements.ts +54 -32
- package/src/data/recipes.ts +2 -2
- package/src/farmhand.d.ts +7 -0
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-tSySi_OA.js"></script>
|
|
79
79
|
<link rel="stylesheet" crossorigin href="./assets/index-CNcrTYN-.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:"5d027ca75fa72ddc050fadee8c7bab00"},{url:"electron.js",revision:"b0456826f47539f6c1957c3add5eb959"},{url:"assets/workbox-window.prod.es5-Bd17z0YL.js",revision:null},{url:"assets/index-tSySi_OA.js",revision:null},{url:"assets/index-CNcrTYN-.css",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/8b70c0006c9c68a30a0afc3bb9013cc3/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\": \"5d027ca75fa72ddc050fadee8c7bab00\"\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-tSySi_OA.js\",\n \"revision\": null\n },\n {\n \"url\": \"assets/index-CNcrTYN-.css\",\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,2BACPC,SAAY,MAEd,CACED,IAAO,4BACPC,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
|
@@ -43,4 +43,70 @@ describe('<Achievement />', () => {
|
|
|
43
43
|
screen.getByText(new RegExp(achievementObject.rewardDescription))
|
|
44
44
|
).toBeInTheDocument()
|
|
45
45
|
})
|
|
46
|
+
|
|
47
|
+
test('does not render a progress bar when the achievement has no getProgress', () => {
|
|
48
|
+
expect(document.querySelector('.ProgressBar')).not.toBeInTheDocument()
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('<Achievement /> with getProgress', () => {
|
|
53
|
+
let achievementObject: farmhand.achievement
|
|
54
|
+
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
achievementObject = {
|
|
57
|
+
description: 'the best achievement',
|
|
58
|
+
id: 'achievement-1',
|
|
59
|
+
name: 'achievement one',
|
|
60
|
+
rewardDescription: 'the greatest reward',
|
|
61
|
+
condition: () => false,
|
|
62
|
+
reward: state => state,
|
|
63
|
+
getProgress: () => ({ currentValue: 25, goal: 100 }),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const farmhandContextValue = createContextData()
|
|
67
|
+
|
|
68
|
+
render(
|
|
69
|
+
<FarmhandContext.Provider value={farmhandContextValue}>
|
|
70
|
+
<Achievement
|
|
71
|
+
achievement={achievementObject}
|
|
72
|
+
completedAchievements={{}}
|
|
73
|
+
/>
|
|
74
|
+
</FarmhandContext.Provider>
|
|
75
|
+
)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('renders a progress bar', () => {
|
|
79
|
+
expect(document.querySelector('.ProgressBar')).toBeInTheDocument()
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe('<Achievement /> with getProgress and isComplete', () => {
|
|
84
|
+
let achievementObject: farmhand.achievement
|
|
85
|
+
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
achievementObject = {
|
|
88
|
+
description: 'the best achievement',
|
|
89
|
+
id: 'achievement-1',
|
|
90
|
+
name: 'achievement one',
|
|
91
|
+
rewardDescription: 'the greatest reward',
|
|
92
|
+
condition: () => false,
|
|
93
|
+
reward: state => state,
|
|
94
|
+
getProgress: () => ({ currentValue: 100, goal: 100 }),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const farmhandContextValue = createContextData()
|
|
98
|
+
|
|
99
|
+
render(
|
|
100
|
+
<FarmhandContext.Provider value={farmhandContextValue}>
|
|
101
|
+
<Achievement
|
|
102
|
+
achievement={achievementObject}
|
|
103
|
+
completedAchievements={{ 'achievement-1': true }}
|
|
104
|
+
/>
|
|
105
|
+
</FarmhandContext.Provider>
|
|
106
|
+
)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('does not render a progress bar once the achievement is complete', () => {
|
|
110
|
+
expect(document.querySelector('.ProgressBar')).not.toBeInTheDocument()
|
|
111
|
+
})
|
|
46
112
|
})
|
|
@@ -9,35 +9,58 @@ import BeenhereIcon from '@mui/icons-material/Beenhere.js'
|
|
|
9
9
|
import { bool, object, shape, string } from 'prop-types'
|
|
10
10
|
|
|
11
11
|
import FarmhandContext from '../Farmhand/Farmhand.context.js'
|
|
12
|
+
import { Div } from '../Elements/index.js'
|
|
13
|
+
import ProgressBar from '../ProgressBar/index.js'
|
|
12
14
|
|
|
13
15
|
const Achievement = ({
|
|
14
|
-
achievement
|
|
16
|
+
achievement,
|
|
15
17
|
completedAchievements,
|
|
18
|
+
gameState,
|
|
16
19
|
|
|
17
|
-
isComplete = Boolean(completedAchievements[id]),
|
|
20
|
+
isComplete = Boolean(completedAchievements[achievement.id]),
|
|
18
21
|
}: {
|
|
19
22
|
achievement: farmhand.achievement
|
|
20
23
|
completedAchievements: Partial<Record<string, boolean>>
|
|
24
|
+
gameState: farmhand.state
|
|
21
25
|
isComplete?: boolean
|
|
22
|
-
}) =>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
>
|
|
29
|
-
<CardHeader
|
|
26
|
+
}) => {
|
|
27
|
+
const { description, name, rewardDescription } = achievement
|
|
28
|
+
const progress = achievement.getProgress?.(gameState)
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Card
|
|
30
32
|
{...{
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
className: classNames('Achievement', { 'is-complete': isComplete }),
|
|
34
|
+
}}
|
|
35
|
+
sx={{
|
|
36
|
+
'& .MuiSvgIcon-root': { color: isComplete ? '#13b747' : '#666' },
|
|
34
37
|
}}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
>
|
|
39
|
+
<CardHeader
|
|
40
|
+
{...{
|
|
41
|
+
avatar: isComplete ? <BeenhereIcon /> : <AssignmentLateIcon />,
|
|
42
|
+
title: name,
|
|
43
|
+
subheader: <p>Reward: {rewardDescription}</p>,
|
|
44
|
+
}}
|
|
45
|
+
/>
|
|
46
|
+
<CardContent>
|
|
47
|
+
<p>{description}</p>
|
|
48
|
+
{!isComplete && progress && (
|
|
49
|
+
<Div sx={{ marginTop: '1em' }}>
|
|
50
|
+
<ProgressBar
|
|
51
|
+
{...{
|
|
52
|
+
percent: Math.min(
|
|
53
|
+
100,
|
|
54
|
+
(progress.currentValue / progress.goal) * 100
|
|
55
|
+
),
|
|
56
|
+
}}
|
|
57
|
+
/>
|
|
58
|
+
</Div>
|
|
59
|
+
)}
|
|
60
|
+
</CardContent>
|
|
61
|
+
</Card>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
41
64
|
|
|
42
65
|
Achievement.propTypes = {
|
|
43
66
|
achievement: shape({
|
|
@@ -47,6 +70,7 @@ Achievement.propTypes = {
|
|
|
47
70
|
rewardDescription: string.isRequired,
|
|
48
71
|
}).isRequired,
|
|
49
72
|
completedAchievements: object.isRequired,
|
|
73
|
+
gameState: object.isRequired,
|
|
50
74
|
isComplete: bool,
|
|
51
75
|
}
|
|
52
76
|
|
|
@@ -58,7 +82,7 @@ export default function Consumer(props: {
|
|
|
58
82
|
return (
|
|
59
83
|
<FarmhandContext.Consumer>
|
|
60
84
|
{({ gameState, handlers }) => (
|
|
61
|
-
<Achievement {...{ ...gameState, ...handlers, ...props }} />
|
|
85
|
+
<Achievement {...{ ...gameState, ...handlers, ...props, gameState }} />
|
|
62
86
|
)}
|
|
63
87
|
</FarmhandContext.Consumer>
|
|
64
88
|
)
|
|
@@ -79,12 +79,21 @@ const ProgressBar = ({ percent }: { percent: number }) => {
|
|
|
79
79
|
}, [percent, prefersReducedMotion])
|
|
80
80
|
|
|
81
81
|
return (
|
|
82
|
-
<Div
|
|
82
|
+
<Div
|
|
83
|
+
className="ProgressBar"
|
|
84
|
+
sx={{
|
|
85
|
+
margin: '0 auto',
|
|
86
|
+
display: 'flex',
|
|
87
|
+
alignItems: 'center',
|
|
88
|
+
gap: '0.5em',
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
83
91
|
<Div
|
|
84
92
|
className="progress-wrapper"
|
|
85
93
|
sx={{
|
|
94
|
+
flexGrow: 1,
|
|
86
95
|
height: '1em',
|
|
87
|
-
background: '
|
|
96
|
+
background: 'rgba(0, 0, 0, 0.15)',
|
|
88
97
|
borderRadius: '0.5em',
|
|
89
98
|
overflow: 'hidden',
|
|
90
99
|
}}
|
|
@@ -100,7 +109,7 @@ const ProgressBar = ({ percent }: { percent: number }) => {
|
|
|
100
109
|
sx={{ height: '100%' }}
|
|
101
110
|
></Div>
|
|
102
111
|
</Div>
|
|
103
|
-
<P sx={{ lineHeight: '
|
|
112
|
+
<P sx={{ lineHeight: '1em', minWidth: '3em', textAlign: 'right' }}>
|
|
104
113
|
<span>{displayedProgress}%</span>
|
|
105
114
|
</P>
|
|
106
115
|
</Div>
|
|
@@ -5,9 +5,32 @@ import { toolLevel, toolType } from '../enums.js'
|
|
|
5
5
|
|
|
6
6
|
import { INFINITE_STORAGE_LIMIT } from '../constants.js'
|
|
7
7
|
|
|
8
|
-
import { achievementsMap } from './achievements.js'
|
|
8
|
+
import { achievementsMap, progressAchievement } from './achievements.js'
|
|
9
9
|
import { carrot } from './crops/index.js'
|
|
10
10
|
|
|
11
|
+
describe('progressAchievement', () => {
|
|
12
|
+
test('condition is false and getProgress reflects currentValue below goal', () => {
|
|
13
|
+
const { condition, getProgress } = progressAchievement(10, () => 9)
|
|
14
|
+
|
|
15
|
+
expect(condition({} as any)).toEqual(false)
|
|
16
|
+
expect(getProgress?.({} as any)).toEqual({ currentValue: 9, goal: 10 })
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('condition is true and getProgress reflects currentValue at goal', () => {
|
|
20
|
+
const { condition, getProgress } = progressAchievement(10, () => 10)
|
|
21
|
+
|
|
22
|
+
expect(condition({} as any)).toEqual(true)
|
|
23
|
+
expect(getProgress?.({} as any)).toEqual({ currentValue: 10, goal: 10 })
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('condition is true and getProgress reflects currentValue above goal', () => {
|
|
27
|
+
const { condition, getProgress } = progressAchievement(10, () => 11)
|
|
28
|
+
|
|
29
|
+
expect(condition({} as any)).toEqual(true)
|
|
30
|
+
expect(getProgress?.({} as any)).toEqual({ currentValue: 11, goal: 10 })
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
11
34
|
describe('harvest-crop', () => {
|
|
12
35
|
describe('condition', () => {
|
|
13
36
|
let inputState: any
|
|
@@ -85,6 +108,18 @@ describe.each(iAmRichVariants)(
|
|
|
85
108
|
|
|
86
109
|
expect(achievement.condition(state)).toEqual(false)
|
|
87
110
|
})
|
|
111
|
+
|
|
112
|
+
test('reports progress toward the revenue goal', () => {
|
|
113
|
+
const achievement = achievementsMap[id]
|
|
114
|
+
const state = {
|
|
115
|
+
revenue: Number(goal) - 1,
|
|
116
|
+
} as any
|
|
117
|
+
|
|
118
|
+
expect(achievement.getProgress?.(state)).toEqual({
|
|
119
|
+
currentValue: Number(goal) - 1,
|
|
120
|
+
goal: Number(goal),
|
|
121
|
+
})
|
|
122
|
+
})
|
|
88
123
|
})
|
|
89
124
|
}
|
|
90
125
|
)
|
|
@@ -173,6 +208,13 @@ describe('orchardist', () => {
|
|
|
173
208
|
|
|
174
209
|
expect(saplings).toEqual({ id: 'apple-sapling', quantity: 50 })
|
|
175
210
|
})
|
|
211
|
+
|
|
212
|
+
test('reports progress toward the fruits-picked goal', () => {
|
|
213
|
+
expect(achievement.getProgress?.(state)).toEqual({
|
|
214
|
+
currentValue: 999,
|
|
215
|
+
goal: 1000,
|
|
216
|
+
})
|
|
217
|
+
})
|
|
176
218
|
})
|
|
177
219
|
|
|
178
220
|
describe('piemaker', () => {
|
|
@@ -218,6 +260,13 @@ describe('piemaker', () => {
|
|
|
218
260
|
|
|
219
261
|
expect(state.money).toEqual(100_000)
|
|
220
262
|
})
|
|
263
|
+
|
|
264
|
+
test('reports progress toward the pies-made goal', () => {
|
|
265
|
+
expect(achievement.getProgress?.(state)).toEqual({
|
|
266
|
+
currentValue: 99,
|
|
267
|
+
goal: 100,
|
|
268
|
+
})
|
|
269
|
+
})
|
|
221
270
|
})
|
|
222
271
|
|
|
223
272
|
describe('deforestation', () => {
|
|
@@ -251,6 +300,13 @@ describe('deforestation', () => {
|
|
|
251
300
|
|
|
252
301
|
expect(wood).toEqual({ id: 'wood', quantity: 500 })
|
|
253
302
|
})
|
|
303
|
+
|
|
304
|
+
test('reports progress toward the trees-chopped goal', () => {
|
|
305
|
+
expect(achievement.getProgress?.(state)).toEqual({
|
|
306
|
+
currentValue: 249,
|
|
307
|
+
goal: 250,
|
|
308
|
+
})
|
|
309
|
+
})
|
|
254
310
|
})
|
|
255
311
|
|
|
256
312
|
describe('landscaper', () => {
|
|
@@ -290,4 +346,11 @@ describe('landscaper', () => {
|
|
|
290
346
|
|
|
291
347
|
expect(rainbowMulch).toEqual({ id: 'rainbow-mulch', quantity: 25 })
|
|
292
348
|
})
|
|
349
|
+
|
|
350
|
+
test('reports progress toward the mulch-applied goal', () => {
|
|
351
|
+
expect(achievement.getProgress?.(state)).toEqual({
|
|
352
|
+
currentValue: 99,
|
|
353
|
+
goal: 100,
|
|
354
|
+
})
|
|
355
|
+
})
|
|
293
356
|
})
|
package/src/data/achievements.ts
CHANGED
|
@@ -48,11 +48,24 @@ const PIE_RECIPE_IDS = Object.values(recipesMap)
|
|
|
48
48
|
.filter(recipe => recipe.isPie)
|
|
49
49
|
.map(recipe => recipe.id)
|
|
50
50
|
|
|
51
|
-
const sumOfPiesMade = (
|
|
52
|
-
|
|
51
|
+
const sumOfPiesMade = memoize(
|
|
52
|
+
(recipesMade: Partial<Record<string, number>>) =>
|
|
53
|
+
PIE_RECIPE_IDS.reduce((sum, id) => sum + (recipesMade[id] || 0), 0),
|
|
54
|
+
{}
|
|
55
|
+
)
|
|
53
56
|
|
|
54
57
|
const cowFeed = itemsMap[COW_FEED_ITEM_ID]
|
|
55
58
|
|
|
59
|
+
// Derives condition and getProgress from a single source of truth so that an
|
|
60
|
+
// achievement's "complete" state and its progress bar can never disagree.
|
|
61
|
+
export const progressAchievement = (
|
|
62
|
+
goal: number,
|
|
63
|
+
getCurrentValue: (state: farmhand.state) => number
|
|
64
|
+
): Pick<farmhand.achievement, 'condition' | 'getProgress'> => ({
|
|
65
|
+
condition: state => getCurrentValue(state) >= goal,
|
|
66
|
+
getProgress: state => ({ currentValue: getCurrentValue(state), goal }),
|
|
67
|
+
})
|
|
68
|
+
|
|
56
69
|
const achievements: farmhand.achievement[] = [
|
|
57
70
|
((reward = 100) => ({
|
|
58
71
|
id: 'plant-crop',
|
|
@@ -112,7 +125,7 @@ const achievements: farmhand.achievement[] = [
|
|
|
112
125
|
goal
|
|
113
126
|
)}. Proven farmers get access to the Crop Price Guide, an invaluable tool for making better buying and selling decisions!`,
|
|
114
127
|
rewardDescription: 'Crop Price Guide',
|
|
115
|
-
|
|
128
|
+
...progressAchievement(goal, state => state.revenue),
|
|
116
129
|
// Reward is a no-op for this achievement. The value of
|
|
117
130
|
// state.completedAchievements['unlock-crop-price-guide'] (which is
|
|
118
131
|
// automatically set to `true` in the achievement processing logic) is used
|
|
@@ -130,15 +143,18 @@ const achievements: farmhand.achievement[] = [
|
|
|
130
143
|
reward: state => addItemToInventory(state, cowFeed, reward, true),
|
|
131
144
|
}))(),
|
|
132
145
|
|
|
133
|
-
((reward = 100) => ({
|
|
146
|
+
((reward = 100, goal = Object.keys(standardCowColors).length) => ({
|
|
134
147
|
id: 'purchase-all-cow-colors',
|
|
135
148
|
name: 'Cows of Many Colors',
|
|
136
149
|
description: 'Show that you love all cows and purchase one of every color.',
|
|
137
150
|
rewardDescription: `${reward} units of ${cowFeed.name}`,
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
151
|
+
...progressAchievement(
|
|
152
|
+
goal,
|
|
153
|
+
state =>
|
|
154
|
+
Object.values(standardCowColors).filter(
|
|
155
|
+
color => (state.cowColorsPurchased[color] || 0) > 0
|
|
156
|
+
).length
|
|
157
|
+
),
|
|
142
158
|
reward: state => addItemToInventory(state, cowFeed, reward, true),
|
|
143
159
|
}))(),
|
|
144
160
|
|
|
@@ -159,7 +175,7 @@ const achievements: farmhand.achievement[] = [
|
|
|
159
175
|
itemsMap['jackolantern'].name
|
|
160
176
|
}. That's enough to fill a whole pumpkin patch!`,
|
|
161
177
|
rewardDescription: `${reward} units of ${itemsMap['scarecrow'].name}`,
|
|
162
|
-
|
|
178
|
+
...progressAchievement(goal, state => state.itemsSold.jackolantern || 0),
|
|
163
179
|
reward: state =>
|
|
164
180
|
addItemToInventory(state, itemsMap['scarecrow'], reward, true),
|
|
165
181
|
}))(),
|
|
@@ -169,12 +185,13 @@ const achievements: farmhand.achievement[] = [
|
|
|
169
185
|
name: `Daily profit: ${dollarString(goal)}`,
|
|
170
186
|
description: `Earn ${dollarString(goal)} of profit in a single day.`,
|
|
171
187
|
rewardDescription: `${reward} units of ${itemsMap['fertilizer'].name}`,
|
|
172
|
-
|
|
188
|
+
...progressAchievement(goal, state =>
|
|
173
189
|
getProfitRecord(
|
|
174
190
|
state.recordSingleDayProfit,
|
|
175
191
|
state.todaysRevenue,
|
|
176
192
|
state.todaysLosses
|
|
177
|
-
)
|
|
193
|
+
)
|
|
194
|
+
),
|
|
178
195
|
reward: state =>
|
|
179
196
|
addItemToInventory(state, itemsMap['fertilizer'], reward, true),
|
|
180
197
|
}))(),
|
|
@@ -184,12 +201,13 @@ const achievements: farmhand.achievement[] = [
|
|
|
184
201
|
name: `Daily profit: ${dollarString(goal)}`,
|
|
185
202
|
description: `Earn ${dollarString(goal)} of profit in a single day.`,
|
|
186
203
|
rewardDescription: `${reward} units of ${itemsMap['onion-seed'].name}`,
|
|
187
|
-
|
|
204
|
+
...progressAchievement(goal, state =>
|
|
188
205
|
getProfitRecord(
|
|
189
206
|
state.recordSingleDayProfit,
|
|
190
207
|
state.todaysRevenue,
|
|
191
208
|
state.todaysLosses
|
|
192
|
-
)
|
|
209
|
+
)
|
|
210
|
+
),
|
|
193
211
|
reward: state =>
|
|
194
212
|
addItemToInventory(state, itemsMap['onion-seed'], reward, true),
|
|
195
213
|
}))(),
|
|
@@ -199,12 +217,13 @@ const achievements: farmhand.achievement[] = [
|
|
|
199
217
|
name: `Daily profit: ${dollarString(goal)}`,
|
|
200
218
|
description: `Earn ${dollarString(goal)} of profit in a single day.`,
|
|
201
219
|
rewardDescription: `${reward} units of ${itemsMap['tomato-seed'].name}`,
|
|
202
|
-
|
|
220
|
+
...progressAchievement(goal, state =>
|
|
203
221
|
getProfitRecord(
|
|
204
222
|
state.recordSingleDayProfit,
|
|
205
223
|
state.todaysRevenue,
|
|
206
224
|
state.todaysLosses
|
|
207
|
-
)
|
|
225
|
+
)
|
|
226
|
+
),
|
|
208
227
|
reward: state =>
|
|
209
228
|
addItemToInventory(state, itemsMap['tomato-seed'], reward, true),
|
|
210
229
|
}))(),
|
|
@@ -214,7 +233,7 @@ const achievements: farmhand.achievement[] = [
|
|
|
214
233
|
name: `7-day profit average: ${dollarString(goal)}`,
|
|
215
234
|
description: `Reach a 7-day profit average of ${dollarString(goal)}.`,
|
|
216
235
|
rewardDescription: `${reward} units of ${rewardItem.name}`,
|
|
217
|
-
|
|
236
|
+
...progressAchievement(goal, state => state.record7dayProfitAverage),
|
|
218
237
|
reward: state => addItemToInventory(state, rewardItem, reward, true),
|
|
219
238
|
}))(),
|
|
220
239
|
|
|
@@ -223,7 +242,7 @@ const achievements: farmhand.achievement[] = [
|
|
|
223
242
|
name: `7-day profit average: ${dollarString(goal)}`,
|
|
224
243
|
description: `Reach a 7-day profit average of ${dollarString(goal)}.`,
|
|
225
244
|
rewardDescription: `${reward} units of ${rewardItem.name}`,
|
|
226
|
-
|
|
245
|
+
...progressAchievement(goal, state => state.record7dayProfitAverage),
|
|
227
246
|
reward: state => addItemToInventory(state, rewardItem, reward, true),
|
|
228
247
|
}))(),
|
|
229
248
|
|
|
@@ -232,7 +251,7 @@ const achievements: farmhand.achievement[] = [
|
|
|
232
251
|
name: `7-day profit average: ${dollarString(goal)}`,
|
|
233
252
|
description: `Reach a 7-day profit average of ${dollarString(goal)}.`,
|
|
234
253
|
rewardDescription: `${reward} units of ${rewardItem.name}`,
|
|
235
|
-
|
|
254
|
+
...progressAchievement(goal, state => state.record7dayProfitAverage),
|
|
236
255
|
reward: state => addItemToInventory(state, rewardItem, reward, true),
|
|
237
256
|
}))(),
|
|
238
257
|
|
|
@@ -241,7 +260,7 @@ const achievements: farmhand.achievement[] = [
|
|
|
241
260
|
name: `7-day profit average: ${dollarString(goal)}`,
|
|
242
261
|
description: `Reach a 7-day profit average of ${dollarString(goal)}.`,
|
|
243
262
|
rewardDescription: `${reward} units of ${rewardItem.name}`,
|
|
244
|
-
|
|
263
|
+
...progressAchievement(goal, state => state.record7dayProfitAverage),
|
|
245
264
|
reward: state => addItemToInventory(state, rewardItem, reward, true),
|
|
246
265
|
}))(),
|
|
247
266
|
|
|
@@ -250,7 +269,7 @@ const achievements: farmhand.achievement[] = [
|
|
|
250
269
|
name: `7-day profit average: ${dollarString(goal)}`,
|
|
251
270
|
description: `Reach a 7-day profit average of ${dollarString(goal)}.`,
|
|
252
271
|
rewardDescription: `${reward} units of ${rewardItem.name}`,
|
|
253
|
-
|
|
272
|
+
...progressAchievement(goal, state => state.record7dayProfitAverage),
|
|
254
273
|
reward: state => addItemToInventory(state, rewardItem, reward, true),
|
|
255
274
|
}))(),
|
|
256
275
|
|
|
@@ -259,7 +278,7 @@ const achievements: farmhand.achievement[] = [
|
|
|
259
278
|
name: `7-day profit average: ${dollarString(goal)}`,
|
|
260
279
|
description: `Reach a 7-day profit average of ${dollarString(goal)}.`,
|
|
261
280
|
rewardDescription: `${reward} units of ${rewardItem.name}`,
|
|
262
|
-
|
|
281
|
+
...progressAchievement(goal, state => state.record7dayProfitAverage),
|
|
263
282
|
reward: state => addItemToInventory(state, rewardItem, reward, true),
|
|
264
283
|
}))(),
|
|
265
284
|
|
|
@@ -273,7 +292,7 @@ const achievements: farmhand.achievement[] = [
|
|
|
273
292
|
name: `Dairy Master`,
|
|
274
293
|
description: `Sell ${integerString(goal)} units of ${goalItem.name}.`,
|
|
275
294
|
rewardDescription: `${integerString(reward)} ${rewardItem.name} units`,
|
|
276
|
-
|
|
295
|
+
...progressAchievement(goal, state => state.itemsSold[goalItem.id] || 0),
|
|
277
296
|
reward: state => addItemToInventory(state, rewardItem, reward, true),
|
|
278
297
|
}))(),
|
|
279
298
|
|
|
@@ -287,7 +306,7 @@ const achievements: farmhand.achievement[] = [
|
|
|
287
306
|
name: `A Big Average Rainbow`,
|
|
288
307
|
description: `Sell ${integerString(goal)} units of ${goalItem.name}.`,
|
|
289
308
|
rewardDescription: `${integerString(reward)} ${rewardItem.name} units`,
|
|
290
|
-
|
|
309
|
+
...progressAchievement(goal, state => state.itemsSold[goalItem.id] || 0),
|
|
291
310
|
reward: state => addItemToInventory(state, rewardItem, reward, true),
|
|
292
311
|
}))(),
|
|
293
312
|
|
|
@@ -296,7 +315,7 @@ const achievements: farmhand.achievement[] = [
|
|
|
296
315
|
name: `Burger Master`,
|
|
297
316
|
description: `Sell ${integerString(goal)} ${goalItem.name} units.`,
|
|
298
317
|
rewardDescription: `${integerString(reward)} additional inventory spaces`,
|
|
299
|
-
|
|
318
|
+
...progressAchievement(goal, state => state.itemsSold[goalItem.id] || 0),
|
|
300
319
|
reward: state => ({
|
|
301
320
|
...state,
|
|
302
321
|
inventoryLimit: state.inventoryLimit + reward,
|
|
@@ -310,7 +329,7 @@ const achievements: farmhand.achievement[] = [
|
|
|
310
329
|
rewardDescription: `All sales receive a ${percentageString(
|
|
311
330
|
I_AM_RICH_BONUSES[0]
|
|
312
331
|
)} bonus`,
|
|
313
|
-
|
|
332
|
+
...progressAchievement(goal, state => state.revenue),
|
|
314
333
|
reward: state => state,
|
|
315
334
|
}))(),
|
|
316
335
|
|
|
@@ -321,7 +340,7 @@ const achievements: farmhand.achievement[] = [
|
|
|
321
340
|
rewardDescription: `All sales receive a ${percentageString(
|
|
322
341
|
I_AM_RICH_BONUSES[1]
|
|
323
342
|
)} bonus`,
|
|
324
|
-
|
|
343
|
+
...progressAchievement(goal, state => state.revenue),
|
|
325
344
|
reward: state => state,
|
|
326
345
|
}))(),
|
|
327
346
|
|
|
@@ -332,7 +351,7 @@ const achievements: farmhand.achievement[] = [
|
|
|
332
351
|
rewardDescription: `All sales receive a ${percentageString(
|
|
333
352
|
I_AM_RICH_BONUSES[2]
|
|
334
353
|
)} bonus`,
|
|
335
|
-
|
|
354
|
+
...progressAchievement(goal, state => state.revenue),
|
|
336
355
|
reward: state => state,
|
|
337
356
|
}))(),
|
|
338
357
|
|
|
@@ -364,8 +383,9 @@ const achievements: farmhand.achievement[] = [
|
|
|
364
383
|
name: 'Orchardist',
|
|
365
384
|
description: `Pick ${integerString(goal)} fruits from trees in the Forest.`,
|
|
366
385
|
rewardDescription: `${reward} units of ${itemsMap['apple-sapling'].name}`,
|
|
367
|
-
|
|
368
|
-
sumOfPartialRecordValues(state.treeFruitsHarvested)
|
|
386
|
+
...progressAchievement(goal, state =>
|
|
387
|
+
sumOfPartialRecordValues(state.treeFruitsHarvested)
|
|
388
|
+
),
|
|
369
389
|
reward: state =>
|
|
370
390
|
addItemToInventory(state, itemsMap['apple-sapling'], reward, true),
|
|
371
391
|
}))(),
|
|
@@ -375,7 +395,7 @@ const achievements: farmhand.achievement[] = [
|
|
|
375
395
|
name: 'Piemaker',
|
|
376
396
|
description: `Make ${integerString(goal)} pies.`,
|
|
377
397
|
rewardDescription: dollarString(reward),
|
|
378
|
-
|
|
398
|
+
...progressAchievement(goal, state => sumOfPiesMade(state.recipesMade)),
|
|
379
399
|
reward: state => addMoney(state, reward),
|
|
380
400
|
}))(),
|
|
381
401
|
|
|
@@ -384,7 +404,7 @@ const achievements: farmhand.achievement[] = [
|
|
|
384
404
|
name: 'Deforestation',
|
|
385
405
|
description: `Chop down ${integerString(goal)} trees in the Forest.`,
|
|
386
406
|
rewardDescription: `${reward} units of ${itemsMap['wood'].name}`,
|
|
387
|
-
|
|
407
|
+
...progressAchievement(goal, state => state.treesChopped),
|
|
388
408
|
reward: state => addItemToInventory(state, itemsMap['wood'], reward, true),
|
|
389
409
|
}))(),
|
|
390
410
|
|
|
@@ -395,7 +415,9 @@ const achievements: farmhand.achievement[] = [
|
|
|
395
415
|
goal
|
|
396
416
|
)} bags of mulch (of any type) on trees in the Forest.`,
|
|
397
417
|
rewardDescription: `${reward} units of ${itemsMap['rainbow-mulch'].name}`,
|
|
398
|
-
|
|
418
|
+
...progressAchievement(goal, state =>
|
|
419
|
+
sumOfPartialRecordValues(state.mulchApplied)
|
|
420
|
+
),
|
|
399
421
|
reward: state =>
|
|
400
422
|
addItemToInventory(state, itemsMap['rainbow-mulch'], reward, true),
|
|
401
423
|
}))(),
|
package/src/data/recipes.ts
CHANGED
|
@@ -780,7 +780,7 @@ export const compost = convertToRecipe({
|
|
|
780
780
|
id: 'compost',
|
|
781
781
|
name: 'Compost',
|
|
782
782
|
ingredients: {
|
|
783
|
-
[items.weed.id]:
|
|
783
|
+
[items.weed.id]: 10,
|
|
784
784
|
},
|
|
785
785
|
condition: state =>
|
|
786
786
|
state.purchasedComposter > 0 &&
|
|
@@ -797,7 +797,7 @@ export const fertilizer: farmhand.item = convertToRecipe({
|
|
|
797
797
|
id: 'fertilizer',
|
|
798
798
|
name: 'Fertilizer',
|
|
799
799
|
ingredients: {
|
|
800
|
-
[compost.id]:
|
|
800
|
+
[compost.id]: 3,
|
|
801
801
|
},
|
|
802
802
|
condition: state =>
|
|
803
803
|
state.purchasedComposter > 0 && (state.itemsSold[compost.id] || 0) >= 10,
|
package/src/farmhand.d.ts
CHANGED
|
@@ -281,6 +281,12 @@ declare namespace farmhand {
|
|
|
281
281
|
|
|
282
282
|
type achievementCondition = (state: state, prevState?: state) => boolean
|
|
283
283
|
type achievementReward = (state: state) => state
|
|
284
|
+
type achievementProgress = (
|
|
285
|
+
state: state
|
|
286
|
+
) => {
|
|
287
|
+
currentValue: number
|
|
288
|
+
goal: number
|
|
289
|
+
}
|
|
284
290
|
|
|
285
291
|
interface achievement {
|
|
286
292
|
id: string
|
|
@@ -289,6 +295,7 @@ declare namespace farmhand {
|
|
|
289
295
|
rewardDescription: string
|
|
290
296
|
condition: achievementCondition
|
|
291
297
|
reward: achievementReward
|
|
298
|
+
getProgress?: achievementProgress
|
|
292
299
|
}
|
|
293
300
|
|
|
294
301
|
interface level {
|