diff --git a/.env.development.local b/.env.development.local index 09062c3e0..18006bc20 100644 --- a/.env.development.local +++ b/.env.development.local @@ -1,2 +1,3 @@ REACT_APP_API_ROOT=http://localhost:3001/ REACT_APP_ENABLE_KEGS=true +REACT_APP_ENABLE_EXPERIENCE=true diff --git a/src/components/Farmhand/Farmhand.js b/src/components/Farmhand/Farmhand.js index aec94fdf5..978d766a2 100644 --- a/src/components/Farmhand/Farmhand.js +++ b/src/components/Farmhand/Farmhand.js @@ -60,7 +60,6 @@ import { computeMarketPositions, createNewField, doesMenuObstructStage, - farmProductsSold, generateCow, getAvailableShopInventory, getItemCurrentValue, @@ -381,7 +380,7 @@ export default class Farmhand extends FarmhandReducers { get levelEntitlements() { return getLevelEntitlements( - levelAchieved(farmProductsSold(this.state.itemsSold)) + levelAchieved({ itemsSold: this.state.itemsSold }) ) } @@ -425,6 +424,7 @@ export default class Farmhand extends FarmhandReducers { cowTradeTimeoutId: -1, cropsHarvested: {}, dayCount: 0, + experience: 0, farmName: 'Unnamed', field: createNewField(), fieldMode: OBSERVE, @@ -497,6 +497,7 @@ export default class Farmhand extends FarmhandReducers { [toolType.WATERING_CAN]: toolLevel.DEFAULT, }, useAlternateEndDayButtonPosition: false, + useLegacyLevelingSystem: true, valueAdjustments: {}, version: process.env.REACT_APP_VERSION ?? '', } diff --git a/src/components/Field/Field.js b/src/components/Field/Field.js index f4c8dca7c..5396c3266 100644 --- a/src/components/Field/Field.js +++ b/src/components/Field/Field.js @@ -18,12 +18,7 @@ import Plot from '../Plot' import QuickSelect from '../QuickSelect' import { fieldMode } from '../../enums' import tools from '../../data/tools' -import { - doesInventorySpaceRemain, - farmProductsSold, - levelAchieved, - nullArray, -} from '../../utils' +import { doesInventorySpaceRemain, levelAchieved, nullArray } from '../../utils' import { getLevelEntitlements } from '../../utils/getLevelEntitlements' import './Field.sass' @@ -74,7 +69,7 @@ export const isInHoverRange = ({ switch (fieldMode) { case SET_SPRINKLER: hoveredPlotRangeSizeToRender = getLevelEntitlements( - levelAchieved(farmProductsSold(itemsSold)) + levelAchieved({ itemsSold }) ).sprinklerRange break diff --git a/src/components/Navigation/Navigation.js b/src/components/Navigation/Navigation.js index a08f63940..aef2dbf84 100644 --- a/src/components/Navigation/Navigation.js +++ b/src/components/Navigation/Navigation.js @@ -225,7 +225,7 @@ export const Navigation = ({ viewList, totalFarmProductsSold = farmProductsSold(itemsSold), - currentLevel = levelAchieved(totalFarmProductsSold), + currentLevel = levelAchieved({ itemsSold }), levelPercent = scaleNumber( totalFarmProductsSold, farmProductSalesVolumeNeededForLevel(currentLevel), diff --git a/src/components/OnlinePeersView/OnlinePeer/OnlinePeer.js b/src/components/OnlinePeersView/OnlinePeer/OnlinePeer.js index 4a520e836..31a6658b1 100644 --- a/src/components/OnlinePeersView/OnlinePeer/OnlinePeer.js +++ b/src/components/OnlinePeersView/OnlinePeer/OnlinePeer.js @@ -7,12 +7,7 @@ import CardHeader from '@material-ui/core/CardHeader' import CowCard from '../../CowCard' import { moneyString } from '../../../utils/moneyString' -import { - getPlayerName, - farmProductsSold, - integerString, - levelAchieved, -} from '../../../utils' +import { getPlayerName, integerString, levelAchieved } from '../../../utils' import './OnlinePeer.sass' @@ -28,10 +23,7 @@ const OnlinePeer = ({ subheader: (

Day: {integerString(dayCount)}

-

- Level:{' '} - {integerString(levelAchieved(farmProductsSold(itemsSold)))} -

+

Level: {integerString(levelAchieved({ itemsSold }))}

Money: {moneyString(money)}

), diff --git a/src/components/OnlinePeersView/OnlinePeersView.js b/src/components/OnlinePeersView/OnlinePeersView.js index 36afcc03a..d07075b61 100644 --- a/src/components/OnlinePeersView/OnlinePeersView.js +++ b/src/components/OnlinePeersView/OnlinePeersView.js @@ -9,7 +9,7 @@ import { array, number, object, string } from 'prop-types' import BailOutErrorBoundary from '../BailOutErrorBoundary' -import { getPlayerName, farmProductsSold, levelAchieved } from '../../utils' +import { getPlayerName, levelAchieved } from '../../utils' import FarmhandContext from '../Farmhand/Farmhand.context' import CowCard from '../CowCard' @@ -58,7 +58,7 @@ const OnlinePeersView = ({ {sortBy(populatedPeers, [ peerId => // Use negative value to reverse sort order - -levelAchieved(farmProductsSold(peers[peerId].itemsSold || 0)), + -levelAchieved({ itemsSold: peers[peerId].itemsSold || 0 }), ]).map(peerId => ( diff --git a/src/components/SettingsView/SettingsView.js b/src/components/SettingsView/SettingsView.js index 34af40d08..391d3d331 100644 --- a/src/components/SettingsView/SettingsView.js +++ b/src/components/SettingsView/SettingsView.js @@ -31,6 +31,7 @@ const SettingsView = ({ handleShowHomeScreenChange, showNotifications, useAlternateEndDayButtonPosition, + useLegacyLevelingSystem, showHomeScreen, }) => { const [isClearDataDialogOpen, setIsClearDataDialogOpen] = useState(false) @@ -99,6 +100,17 @@ const SettingsView = ({ } label="Display custom names for cows received from other players" /> + console.log('todo: save change')} + name="use-legacy-leveling-system" + /> + } + label="Use legacy leveling system (items sold)" + /> @@ -210,6 +222,7 @@ SettingsView.propTypes = { showHomeScreen: bool.isRequired, showNotifications: bool.isRequired, useAlternateEndDayButtonPosition: bool.isRequired, + useLegacyLevelingSystem: bool, } export default function Consumer(props) { diff --git a/src/components/StatsView/StatsView.js b/src/components/StatsView/StatsView.js index b330bf346..bd7d1675f 100644 --- a/src/components/StatsView/StatsView.js +++ b/src/components/StatsView/StatsView.js @@ -51,7 +51,7 @@ const StatsView = ({ todaysRevenue, totalFarmProductsSold = farmProductsSold(itemsSold), - currentLevel = levelAchieved(totalFarmProductsSold), + currentLevel = levelAchieved({ itemsSold }), }) => (
diff --git a/src/constants.js b/src/constants.js index 096db8f74..be09f9490 100644 --- a/src/constants.js +++ b/src/constants.js @@ -143,6 +143,7 @@ export const PERSISTED_STATE_KEYS = [ 'cowsTraded', 'cropsHarvested', 'dayCount', + 'experience', 'farmName', 'field', 'historicalDailyLosses', diff --git a/src/game-logic/reducers/addExperience.js b/src/game-logic/reducers/addExperience.js new file mode 100644 index 000000000..3932d601e --- /dev/null +++ b/src/game-logic/reducers/addExperience.js @@ -0,0 +1,12 @@ +import { processLevelUp } from './processLevelUp' + +export const addExperience = (state, amount) => { + const newExperience = state.experience + amount + + state = processLevelUp(state, state.oldLevel) + + return { + ...state, + experience: newExperience, + } +} diff --git a/src/game-logic/reducers/generatePriceEvents.js b/src/game-logic/reducers/generatePriceEvents.js index de02d3a4a..ecc99ac27 100644 --- a/src/game-logic/reducers/generatePriceEvents.js +++ b/src/game-logic/reducers/generatePriceEvents.js @@ -1,5 +1,4 @@ import { - farmProductsSold, filterItemIdsToSeeds, getPriceEventForCrop, getRandomUnlockedCrop, @@ -29,7 +28,7 @@ export const generatePriceEvents = state => { // less-than check. if (random() < PRICE_EVENT_CHANCE) { const { items: unlockedItems } = getLevelEntitlements( - levelAchieved(farmProductsSold(state.itemsSold)) + levelAchieved({ itemsSold: state.itemsSold }) ) const cropItem = getRandomUnlockedCrop( diff --git a/src/game-logic/reducers/processLevelUp.js b/src/game-logic/reducers/processLevelUp.js index 3bc37daef..a65be2a17 100644 --- a/src/game-logic/reducers/processLevelUp.js +++ b/src/game-logic/reducers/processLevelUp.js @@ -1,6 +1,5 @@ import { levels } from '../../data/levels' import { - farmProductsSold, getRandomLevelUpReward, getRandomLevelUpRewardQuantity, levelAchieved, @@ -20,7 +19,7 @@ import { showNotification } from './showNotification' */ export const processLevelUp = (state, oldLevel) => { const { itemsSold, selectedItemId } = state - const newLevel = levelAchieved(farmProductsSold(itemsSold)) + const newLevel = levelAchieved({ itemsSold }) // Loop backwards so that the notifications appear in descending order. for (let i = newLevel; i > oldLevel; i--) { @@ -48,7 +47,7 @@ export const processLevelUp = (state, oldLevel) => { selectedItemId === SPRINKLER_ITEM_ID ) { const { sprinklerRange } = getLevelEntitlements( - levelAchieved(farmProductsSold(itemsSold)) + levelAchieved({ itemsSold }) ) if (sprinklerRange > state.hoveredPlotRangeSize) { diff --git a/src/game-logic/reducers/processSprinklers.js b/src/game-logic/reducers/processSprinklers.js index 92ff18ba2..e828b5562 100644 --- a/src/game-logic/reducers/processSprinklers.js +++ b/src/game-logic/reducers/processSprinklers.js @@ -1,10 +1,5 @@ import { itemType } from '../../enums' -import { - farmProductsSold, - getPlotContentType, - getRangeCoords, - levelAchieved, -} from '../../utils' +import { getPlotContentType, getRangeCoords, levelAchieved } from '../../utils' import { getLevelEntitlements } from '../../utils/getLevelEntitlements' import { setWasWatered } from './helpers' @@ -19,9 +14,7 @@ export const processSprinklers = state => { const crops = new Map() let modifiedField = [...field] - const { sprinklerRange } = getLevelEntitlements( - levelAchieved(farmProductsSold(itemsSold)) - ) + const { sprinklerRange } = getLevelEntitlements(levelAchieved({ itemsSold })) field.forEach((row, plotY) => { row.forEach((plot, plotX) => { diff --git a/src/game-logic/reducers/sellItem.js b/src/game-logic/reducers/sellItem.js index 8feb6a42c..c0abee79f 100644 --- a/src/game-logic/reducers/sellItem.js +++ b/src/game-logic/reducers/sellItem.js @@ -1,7 +1,6 @@ import { itemsMap } from '../../data/maps' import { castToMoney, - farmProductsSold, getAdjustedItemValue, getResaleValue, getSalePriceMultiplier, @@ -39,7 +38,7 @@ export const sellItem = (state, { id }, howMany = 1) => { money: initialMoney, valueAdjustments, } = state - const oldLevel = levelAchieved(farmProductsSold(itemsSold)) + const oldLevel = levelAchieved({ itemsSold }) let { loanBalance } = state const adjustedItemValue = isItemSoldInShop(item) diff --git a/src/game-logic/reducers/sellKeg.js b/src/game-logic/reducers/sellKeg.js index 7cfcc04dc..49ef053a0 100644 --- a/src/game-logic/reducers/sellKeg.js +++ b/src/game-logic/reducers/sellKeg.js @@ -7,7 +7,6 @@ import { itemsMap } from '../../data/maps' import { castToMoney, - farmProductsSold, getSalePriceMultiplier, levelAchieved, moneyTotal, @@ -38,7 +37,7 @@ export const sellKeg = (state, keg) => { itemsSold, money: initialMoney, } = state - const oldLevel = levelAchieved(farmProductsSold(itemsSold)) + const oldLevel = levelAchieved({ itemsSold }) let { loanBalance } = state let saleValue = 0 diff --git a/src/utils/farmProductsSold.js b/src/utils/farmProductsSold.js new file mode 100644 index 000000000..ad44d7c19 --- /dev/null +++ b/src/utils/farmProductsSold.js @@ -0,0 +1,17 @@ +import { itemsMap } from '../data/maps' + +import { memoize } from './memoize' +import { isItemAFarmProduct } from './isItemAFarmProduct' + +export const farmProductsSold = memoize( + /** + * @param {Record} itemsSold + * @returns {number} + */ + itemsSold => + Object.entries(itemsSold).reduce( + (sum, [itemId, numberSold]) => + sum + (isItemAFarmProduct(itemsMap[itemId]) ? numberSold : 0), + 0 + ) +) diff --git a/src/utils/index.js b/src/utils/index.js index 5b2a44370..e4139bde6 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -289,24 +289,6 @@ export const getPlotContentType = ({ itemId }) => export const doesPlotContainCrop = plot => plot !== null && getPlotContentType(plot) === itemType.CROP -/** - * @param {farmhand.item} item - * @returns {boolean} - */ -export const isItemAGrownCrop = item => - Boolean(item.type === itemType.CROP && !item.growsInto) - -/** - * @param {farmhand.item} item - * @returns {boolean} - */ -export const isItemAFarmProduct = item => - Boolean( - isItemAGrownCrop(item) || - item.type === itemType.MILK || - item.type === itemType.CRAFTED_ITEM - ) - export const getLifeStageRange = memoize(( /** @type {farmhand.cropTimetable} */ cropTimetable ) => @@ -791,26 +773,6 @@ export const findCowById = memoize( (cowInventory, id) => cowInventory.find(cow => id === cow.id) ) -export const farmProductsSold = memoize( - /** - * @param {Record} itemsSold - * @returns {number} - */ - itemsSold => - Object.entries(itemsSold).reduce( - (sum, [itemId, numberSold]) => - sum + (isItemAFarmProduct(itemsMap[itemId]) ? numberSold : 0), - 0 - ) -) - -/** - * @param {number} farmProductsSold - * @returns {number} - */ -export const levelAchieved = farmProductsSold => - Math.floor(Math.sqrt(farmProductsSold) / 10) + 1 - /** * @param {number} targetLevel * @returns {number} @@ -1202,3 +1164,7 @@ export const isOctober = () => new Date().getMonth() === 9 export const isDecember = () => new Date().getMonth() === 11 export { default as totalIngredientsInRecipe } from './totalIngredientsInRecipe' +export { farmProductsSold } from './farmProductsSold' +export { isItemAFarmProduct } from './isItemAFarmProduct' +export { isItemAGrownCrop } from './isItemAGrownCrop' +export { levelAchieved } from './levelAchieved' diff --git a/src/utils/index.test.js b/src/utils/index.test.js index 6a75a6d2b..f05b3b1ae 100644 --- a/src/utils/index.test.js +++ b/src/utils/index.test.js @@ -768,12 +768,41 @@ describe('farmProductsSold', () => { }) describe('levelAchieved', () => { - test('calculates achieved level', () => { - expect(levelAchieved(0)).toEqual(1) - expect(levelAchieved(100)).toEqual(2) - expect(levelAchieved(150)).toEqual(2) - expect(levelAchieved(400)).toEqual(3) - expect(levelAchieved(980100)).toEqual(100) + const cases = [ + [1, 0], + [2, 100], + [2, 150], + [3, 400], + [100, 980100], + ] + + describe('with legacy system', () => { + test.each(cases)( + `returns level %p for %p items sold`, + (expectedLevel, numItemsSold) => { + const level = levelAchieved({ + itemsSold: { carrot: numItemsSold }, + useLegacyLevelingSystem: true, + }) + + expect(level).toEqual(expectedLevel) + } + ) + }) + + describe('with experience system', () => { + test.each(cases)( + `returns level %p for %p experience`, + (expectedLevel, experience) => { + const level = levelAchieved({ + experience, + features: { EXPERIENCE: true }, + useLegacyLevelingSystem: false, + }) + + expect(level).toEqual(expectedLevel) + } + ) }) }) diff --git a/src/utils/isItemAFarmProduct.js b/src/utils/isItemAFarmProduct.js new file mode 100644 index 000000000..c18887601 --- /dev/null +++ b/src/utils/isItemAFarmProduct.js @@ -0,0 +1,14 @@ +import { itemType } from '../enums' + +import { isItemAGrownCrop } from './isItemAGrownCrop' + +/** + * @param {farmhand.item} item + * @returns {boolean} + */ +export const isItemAFarmProduct = item => + Boolean( + isItemAGrownCrop(item) || + item.type === itemType.MILK || + item.type === itemType.CRAFTED_ITEM + ) diff --git a/src/utils/isItemAGrownCrop.js b/src/utils/isItemAGrownCrop.js new file mode 100644 index 000000000..990f8144c --- /dev/null +++ b/src/utils/isItemAGrownCrop.js @@ -0,0 +1,8 @@ +import { itemType } from '../enums' + +/** + * @param {farmhand.item} item + * @returns {boolean} + */ +export const isItemAGrownCrop = item => + Boolean(item.type === itemType.CROP && !item.growsInto) diff --git a/src/utils/levelAchieved.js b/src/utils/levelAchieved.js new file mode 100644 index 000000000..1e5fb8388 --- /dev/null +++ b/src/utils/levelAchieved.js @@ -0,0 +1,18 @@ +import { farmProductsSold } from './farmProductsSold' + +/** + * @param {number} farmProductsSold + * @returns {number} + */ +export function levelAchieved({ + itemsSold, + experience, + features = {}, + useLegacyLevelingSystem = true, +}) { + if (features.EXPERIENCE && !useLegacyLevelingSystem) { + return Math.floor(Math.sqrt(experience) / 10) + 1 + } + + return Math.floor(Math.sqrt(farmProductsSold(itemsSold)) / 10) + 1 +}