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
+}