-
Notifications
You must be signed in to change notification settings - Fork 25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add experience part 1 #448
Conversation
- add 'experience' to farmhand state - add 'useLegacyLevelingSystem' to farmhand state and settings screen - update `levelAchieved` to be more flexible about how level is calculated based on which leveling system is being used - update all calls to `levelAchieved` - update utils unit tests for levelAchieved to test both systems - add 'EXPERIENCE' feature flag
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm still working through a fuller review, I just wanted to make my in-progress comments available.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for moving this to a new file!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a few more notes. I've gotten through all the files now. Great work here so far!
I haven't tested this out yet, but I will do so once this is promoted to "Ready for review."
const cases = [ | ||
[1, 0], | ||
[2, 100], | ||
[2, 150], | ||
[3, 400], | ||
[100, 980100], | ||
] | ||
|
||
describe('with legacy system', () => { | ||
test.each(cases)( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a much better approach! 👏
- hook up SettingsView for useLegacyLevelSystem toggle - persist useLegacyLevelSystem value - unit tests for addExperience and fix for oldLevel value - wording update for setting
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for following up with my previous review items! I found a few more things to discuss with the latest revision. Crucially, this is currently breaking new game sessions (I've called out where in the Farmhand
component) so we'll need to get that addressed by adding an initialValue
to the reduce
.
We're getting there!
it('will check for leveling up when experience is added', () => { | ||
addExperience(gameState, 100) | ||
|
||
expect(processLevelUp).toHaveBeenCalled() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally this unit test would not be concerned with implementation details like we're doing here. Could this test be changed to examine gameState
to detect whether the level-up was processed instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i have mixed feelings about this. not strong enough that I won't change it, but I do want to offer my perspective. i did things this way because I see this test as a unit test, and if it were to check that a level changed it would have to know about game logic conditions that cause the level to change (i.e. how much experience it takes to get from one level to another). the way i see, addExperience
is only concerned with making sure that processLevelUp
is invoked, which is what mocking it and checking for it to be called does. i think the place for doing a test on game logic belongs elsewhere, which is what might be missing from this PR, but i haven't found that spec yet 😄 .
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the way i see,
addExperience
is only concerned with making sure thatprocessLevelUp
is invoked
I think this is where our views diverge. Based on the semantics and API of this reducer, I would assume that addExperience
only cares about adding experience. The fact that level-ups are processed within this reducer seems like an implementation detail that could conceivably be handled outside of this function (though I'm not suggesting we do that).
We definitely do want to test that adding experience causes a level up, but perhaps this isn't the appropriate place to validate that? Specifically, I'm thinking that it may be better to validate that gaining experience results in a level-up in a higher-level React Test Library test. I'm thinking something similar to this:
farmhand/src/game-logic/item-sell.test.js
Lines 9 to 29 in f791e83
test('item in inventory can be sold', async () => { | |
const loadedState = saveDataStubFactory({ | |
inventory: [{ id: 'carrot-seed', quantity: 1 }], | |
}) | |
await farmhandStub({ | |
localforage: { | |
getItem: () => Promise.resolve(loadedState), | |
setItem: (_key, data) => Promise.resolve(data), | |
}, | |
}) | |
const menu = screen.getByRole('complementary') | |
const carrotSeedMenuItem = within(menu) | |
.getByText('Carrot Seed') | |
.closest('.Item') | |
const sellButton = within(carrotSeedMenuItem).getByText('Sell') | |
userEvent.click(sellButton) | |
expect(within(menu).queryByText('Carrot Seed')).not.toBeInTheDocument() | |
}) |
I realize that this is expanding the scope of what you set out to do here, and the current state isn't something I'd block this PR over or anything. It's just a difference of opinion. If you feel good about what we have here, then I'm okay with sticking with it. Just let me know which way you'd like to go. 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i actually think i'm with you on processing level ups as being the thing that is out of place here. so i think i'll remove that. it definitely feels like an unexpected side effect not that you've mentioned it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok i've removed it. i see that sellItem
is what actually does processLevelUp
but I don't see any tests in there explicitly checking for level up. i always forget that level
isn't part of farmhand state and that it's calculated when needed so testing that a level up was achieved is trickier than i expected (at least, without just mocking and checking for the call.) if you have suggestions on how to test it let me know and i am happy to add it to this PR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for making that change!
testing that a level up was achieved is trickier than i expected (at least, without just mocking and checking for the call.)
If we're speaking in terms of React Testing Library tests, I'd approach this by checking to see if the level up notification text is present on the page:
Lines 184 to 214 in d37d410
export const LEVEL_GAINED_NOTIFICATION = (_, newLevel, randomCropSeed) => { | |
let chunks = [ | |
`You reached **level ${newLevel}!** Way to go! | |
`, | |
] | |
const levelObject = levels[newLevel] | |
if (itemUnlockLevels[newLevel]) { | |
chunks.push( | |
`Now available in the shop: **${ | |
itemsMap[itemUnlockLevels[newLevel]].name | |
}**.` | |
) | |
} else if (levelObject && levelObject.increasesSprinklerRange) { | |
chunks.push(`Sprinkler range has increased.`) | |
} else if (randomCropSeed) { | |
chunks.push( | |
`You got **${getRandomLevelUpRewardQuantity(newLevel)} units of ${ | |
randomCropSeed.name | |
}** as a reward!` | |
) | |
} else if (levelObject && levelObject.unlocksTool) { | |
// todo: there is only one tool that can be unlocked currently, but this is a bit | |
// short-sighted if we ever introduce other tool unlocks | |
chunks.push(`You've unlocked a new tool for the field, The **Shovel**!`) | |
} | |
return chunks.join(' ') | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ahh yes that makes sense. the sellItem
test doesn't seem like the right place in that case (it isn't doing any rendering). i suppose we could check the notifications in state from there though?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it might be worth spinning up a new test file sibling to src/game-logic/item-sell.test.js
just for testing level-ups. This is game logic that cuts across functional units, so an integration test seems appropriate.
src/utils/index.js
Outdated
export { default as totalIngredientsInRecipe } from './totalIngredientsInRecipe' | ||
export { farmProductsSold } from './farmProductsSold' | ||
export { isItemAFarmProduct } from './isItemAFarmProduct' | ||
export { isItemAGrownCrop } from './isItemAGrownCrop' | ||
export { levelAchieved } from './levelAchieved' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for making this change and getting all of the import
s straightened out!
@@ -1,4 +1,4 @@ | |||
export default function totalIngredientsInRecipe(recipe, amount = 1) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh good call to update this. Thanks for switching to a named export!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code looks good to merge, but I still haven't had a chance to QA this (sorry about that!). I will approve once I'm able to run through everything!
it('will check for leveling up when experience is added', () => { | ||
addExperience(gameState, 100) | ||
|
||
expect(processLevelUp).toHaveBeenCalled() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the way i see,
addExperience
is only concerned with making sure thatprocessLevelUp
is invoked
I think this is where our views diverge. Based on the semantics and API of this reducer, I would assume that addExperience
only cares about adding experience. The fact that level-ups are processed within this reducer seems like an implementation detail that could conceivably be handled outside of this function (though I'm not suggesting we do that).
We definitely do want to test that adding experience causes a level up, but perhaps this isn't the appropriate place to validate that? Specifically, I'm thinking that it may be better to validate that gaining experience results in a level-up in a higher-level React Test Library test. I'm thinking something similar to this:
farmhand/src/game-logic/item-sell.test.js
Lines 9 to 29 in f791e83
test('item in inventory can be sold', async () => { | |
const loadedState = saveDataStubFactory({ | |
inventory: [{ id: 'carrot-seed', quantity: 1 }], | |
}) | |
await farmhandStub({ | |
localforage: { | |
getItem: () => Promise.resolve(loadedState), | |
setItem: (_key, data) => Promise.resolve(data), | |
}, | |
}) | |
const menu = screen.getByRole('complementary') | |
const carrotSeedMenuItem = within(menu) | |
.getByText('Carrot Seed') | |
.closest('.Item') | |
const sellButton = within(carrotSeedMenuItem).getByText('Sell') | |
userEvent.click(sellButton) | |
expect(within(menu).queryByText('Carrot Seed')).not.toBeInTheDocument() | |
}) |
I realize that this is expanding the scope of what you set out to do here, and the current state isn't something I'd block this PR over or anything. It's just a difference of opinion. If you feel good about what we have here, then I'm okay with sticking with it. Just let me know which way you'd like to go. 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did a bit of testing. Here's what I found:
the
experience
value in state should match the total number of items sold when loading a saved game that previously did not have experience
I imported one of my high-level save files and saw that experience
was 0
. I saved the game and refreshed the page, and experience
was appropriately updated. This wouldn't introduce instability if this PR was merged as-is, but I imagine that this would be perceived as a pretty significant bug once the Experience feature is complete and shipped. It seems like this issue should be addressed in this function:
Lines 941 to 952 in 89bb2ee
/** | |
* @param {farmhand.state} state | |
* @return {farmhand.state} | |
*/ | |
export const transformStateDataForImport = state => { | |
const sanitizedState = { ...state } | |
const rejectedKeys = ['version'] | |
rejectedKeys.forEach(rejectedKey => delete sanitizedState[rejectedKey]) | |
return sanitizedState | |
} |
experience
should increment by 1 for each item sold
This is working as expected. I noticed that experience
is also incremented when Weeds are sold. Is this intentional? I don't think we discussed this specifically, but it seems like that shouldn't be the case because Weeds do not currently count towards experience. What do you think?
Aside from this, things are looking good. Great work so far!
thanks for calling those things out @jeremyckahn . i've made some updates now to handle importing saves and making sure that experience is only rewarded for farm products sold. |
…o need to do it in Farmhand.js anymore
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks ready to merge! Thanks for sticking with this and addressing all the issues. Awesome work! 🚀
@@ -1046,3 +1047,52 @@ describe('getCowImage', () => { | |||
expect(image).toEqual(animals.cow.rainbow) | |||
}) | |||
}) | |||
|
|||
describe('transformStateDataForImport', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for setting up test cases for this function! 🙏
const FARM_PRODUCT_TYPES = [itemType.MILK, itemType.CRAFTED_ITEM] | ||
|
||
/** | ||
* @param {farmhand.item} item | ||
* @returns {boolean} | ||
*/ | ||
export const isItemAFarmProduct = item => | ||
Boolean( | ||
isItemAGrownCrop(item) || | ||
item.type === itemType.MILK || | ||
item.type === itemType.CRAFTED_ITEM | ||
) | ||
Boolean(isItemAGrownCrop(item) || FARM_PRODUCT_TYPES.includes(item.type)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an excellent refactor! 💯
What this PR does
The goal of this PR is to lay some groundwork for an experience based level system as outlined in #371
levelAchieved
to be more flexible about how level is calculated based on which leveling system is being usedlevelAchieved
experience
value to match number of items sold when loading a saved gameHow this change can be validated
This PR would be easiest to test if checked out and ran locally so that the feature flag can be flipped on and off
With the feature flag enabled:
With the flag disabled:
Regardless of flag enabled or disabled, these things should happen:
experience
value can be seen when inspecting the Farmhand root componentexperience
value in state should match the total number of items sold when loading a saved game that previously did not have experienceexperience
should increment by 1 for each item soldQuestions or concerns about this change
Additional information