Skip to content

Commit

Permalink
feat: first pieces for The Forest (#475)
Browse files Browse the repository at this point in the history
* feat: first pieces for The Forest

add the beginnings of the forest behind a feature flag

  - add feature flag
  - add new view and insert into navigation
  - add forest unlock to levelEntitlements
  - add some strings for forest notifications
  - add some constants for forest sizes
  - add purchasable upgrade to shop for forest upgrades
  - move `unlockTool` to be a reducer and add missing unit tests
  - refactor `Shop` unit tests to use RTL

---------

Co-authored-by: Jeremy Kahn <jeremyckahn@gmail.com>
  • Loading branch information
lstebner and jeremyckahn authored Jan 24, 2024
1 parent de83ce7 commit 6968f21
Show file tree
Hide file tree
Showing 25 changed files with 376 additions and 60 deletions.
1 change: 1 addition & 0 deletions .env.development.local
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
REACT_APP_API_ROOT=http://localhost:3001/
REACT_APP_ENABLE_KEGS=true
REACT_APP_ENABLE_FOREST=true

# Silence warnings from dev server
# https://stackoverflow.com/a/70834076
Expand Down
20 changes: 18 additions & 2 deletions src/components/Farmhand/Farmhand.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
* @typedef {import("../../index").farmhand.item} farmhand.item
* @typedef {import("../../index").farmhand.cow} farmhand.cow
* @typedef {import("../../index").farmhand.cowBreedingPen} farmhand.cowBreedingPen
* @typedef {import("../../index").farmhand.forestForageable} farmhand.forestForageable
* @typedef {import("../../index").farmhand.keg} farmhand.keg
* @typedef {import("../../index").farmhand.plantedTree} farmhand.plantedTree
* @typedef {import("../../index").farmhand.plotContent} farmhand.plotContent
* @typedef {import("../../index").farmhand.peerMessage} farmhand.peerMessage
* @typedef {import("../../index").farmhand.peerMetadata} farmhand.peerMetadata
Expand Down Expand Up @@ -60,6 +62,7 @@ import { levelAchieved } from '../../utils/levelAchieved'
import {
computeMarketPositions,
createNewField,
createNewForest,
doesMenuObstructStage,
generateCow,
getAvailableShopInventory,
Expand Down Expand Up @@ -116,7 +119,7 @@ import {
SERVER_ERROR,
UPDATE_AVAILABLE,
} from '../../strings'
import { endpoints, rtcConfig, trackerUrls } from '../../config'
import { endpoints, features, rtcConfig, trackerUrls } from '../../config'

import { scarecrow } from '../../data/items'

Expand Down Expand Up @@ -214,6 +217,7 @@ const applyPriceEvents = (valueAdjustments, priceCrashes, priceSurges) => {
* @property {number} experience
* @property {string} farmName
* @property {(?farmhand.plotContent)[][]} field
* @property {(farmhand.plantedTree | farmhand.forestForageable | null)[][]} forest
* @property {farmhand.fieldMode} fieldMode
* @property {Function?} getCowAccept https://github.com/dmotz/trystero#receiver
* @property {Function?} getCowReject https://github.com/dmotz/trystero#receiver
Expand Down Expand Up @@ -272,6 +276,7 @@ const applyPriceEvents = (valueAdjustments, priceCrashes, priceSurges) => {
* @property {number} purchasedCowPen
* @property {number} purchasedCellar
* @property {number} purchasedField
* @property {number} purchasedForest
* @property {number} purchasedSmelter
* @property {number} profitabilityStreak
* @property {number} record7dayProfitAverage
Expand All @@ -287,6 +292,7 @@ const applyPriceEvents = (valueAdjustments, priceCrashes, priceSurges) => {
* @property {boolean} showHomeScreen Option to show the Home Screen
* @property {boolean} showNotifications
* @property {farmhand.stageFocusType} stageFocus
* indicating if the stage has been unlocked
* @property {Array.<farmhand.notification>} todaysNotifications
* @property {number} todaysLosses Should always be a negative number.
* @property {Object} todaysPurchases Keys are item names, values are their
Expand Down Expand Up @@ -364,13 +370,17 @@ export default class Farmhand extends FarmhandReducers {
}

get viewList() {
const { CELLAR, COW_PEN, HOME, WORKSHOP } = stageFocusType
const { CELLAR, COW_PEN, HOME, WORKSHOP, FOREST } = stageFocusType
const viewList = [...STANDARD_VIEW_LIST]

if (this.state.showHomeScreen) {
viewList.unshift(HOME)
}

if (this.isForestUnlocked && features.FOREST) {
viewList.push(FOREST)
}

if (this.state.purchasedCowPen) {
viewList.push(COW_PEN)
}
Expand Down Expand Up @@ -409,6 +419,10 @@ export default class Farmhand extends FarmhandReducers {
return isOnline && room !== DEFAULT_ROOM
}

get isForestUnlocked() {
return this.levelEntitlements.stageFocusType[stageFocusType.FOREST]
}

/**
* @returns {farmhand.state}
*/
Expand Down Expand Up @@ -437,6 +451,7 @@ export default class Farmhand extends FarmhandReducers {
farmName: 'Unnamed',
field: createNewField(),
fieldMode: OBSERVE,
forest: createNewForest(),
getCowAccept: noop,
getCowReject: noop,
getCowTradeRequest: noop,
Expand Down Expand Up @@ -490,6 +505,7 @@ export default class Farmhand extends FarmhandReducers {
purchasedCowPen: 0,
purchasedCellar: 0,
purchasedField: 0,
purchasedForest: 0,
purchasedSmelter: 0,
sendCowTradeRequest: noop,
showHomeScreen: true,
Expand Down
4 changes: 4 additions & 0 deletions src/components/Farmhand/FarmhandReducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ export class FarmhandReducers extends Component {
throw new Error('Unimplemented')
}
/** @type BoundReducer */
purchaseForest() {
throw new Error('Unimplemented')
}
/** @type BoundReducer */
purchaseItem() {
throw new Error('Unimplemented')
}
Expand Down
5 changes: 5 additions & 0 deletions src/components/Forest/Forest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import React from 'react'

export const Forest = () => {
return <div>'welcome to da forest'</div>
}
1 change: 1 addition & 0 deletions src/components/Forest/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Forest } from './Forest'
25 changes: 24 additions & 1 deletion src/components/Shop/Shop.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,15 @@ import {
} from '../../utils'
import { memoize } from '../../utils/memoize'
import { items } from '../../img'
import { itemType, toolType } from '../../enums'
import { itemType, stageFocusType, toolType } from '../../enums'
import {
INFINITE_STORAGE_LIMIT,
PURCHASEABLE_CELLARS,
PURCHASEABLE_COMBINES,
PURCHASEABLE_COMPOSTERS,
PURCHASEABLE_COW_PENS,
PURCHASEABLE_FIELD_SIZES,
PURCHASABLE_FOREST_SIZES,
PURCHASEABLE_SMELTERS,
STORAGE_EXPANSION_AMOUNT,
} from '../../constants'
Expand Down Expand Up @@ -60,15 +61,18 @@ export const Shop = ({
handleCowPenPurchase,
handleCellarPurchase,
handleFieldPurchase,
handleForestPurchase,
handleSmelterPurchase,
handleStorageExpansionPurchase,
inventoryLimit,
levelEntitlements,
money,
purchasedCombine,
purchasedComposter,
purchasedCowPen,
purchasedCellar,
purchasedField,
purchasedForest,
purchasedSmelter,
shopInventory,
toolLevels,
Expand All @@ -79,6 +83,9 @@ export const Shop = ({

const { seeds, fieldTools } = categorizeShopInventory(shopInventory)

const isForestUnlocked =
levelEntitlements.stageFocusType[stageFocusType.FOREST]

return (
<div className="Shop">
<Tabs
Expand Down Expand Up @@ -197,6 +204,22 @@ export const Shop = ({
/>
</li>
) : null}
{features.FOREST && isForestUnlocked ? (
<li>
<TierPurchase
{...{
onBuyClick: handleForestPurchase,
maxedOutPlaceholder:
"You've purchased the largest forest available!",
purchasedTier: purchasedForest,
renderTierLabel: ({ columns, price, rows }) =>
`${dollarString(price)}: ${columns} x ${rows}`,
tiers: PURCHASABLE_FOREST_SIZES,
title: 'Expand Forest',
}}
/>
</li>
) : null}
<li>
<TierPurchase
{...{
Expand Down
64 changes: 38 additions & 26 deletions src/components/Shop/Shop.test.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,50 @@
import React from 'react'
import { shallow } from 'enzyme'
import { render, screen } from '@testing-library/react'

import Inventory from '../Inventory'
import { INFINITE_STORAGE_LIMIT } from '../../constants'
import { noop } from '../../utils/noop'

import { Shop } from './Shop'
import FarmhandContext from '../Farmhand/Farmhand.context'

let component
import Shop from './Shop'

beforeEach(() => {
component = shallow(
<Shop
{...{
handleCombinePurchase: noop,
handleCowPenPurchase: noop,
handleCellarPurchase: noop,
handleFieldPurchase: noop,
handleStorageExpansionPurchase: noop,
inventoryLimit: INFINITE_STORAGE_LIMIT,
money: 0,
purchasedCombine: 0,
purchasedCowPen: 0,
purchasedCellar: 0,
purchasedSmelter: 0,
purchasedField: 0,
shopInventory: [],
toolLevels: {},
valueAdjustments: {},
}}
/>
const gameState = {
inventoryLimit: INFINITE_STORAGE_LIMIT,
levelEntitlements: {
stageFocusType: {},
},
money: 0,
purchasedCombine: 0,
purchasedCowPen: 0,
purchasedCellar: 0,
purchasedSmelter: 0,
purchasedField: 0,
shopInventory: [],
toolLevels: {},
valueAdjustments: {},
}

const handlers = {
handleCombinePurchase: noop,
handleCowPenPurchase: noop,
handleCellarPurchase: noop,
handleFieldPurchase: noop,
handleStorageExpansionPurchase: noop,
}

render(
<FarmhandContext.Provider value={{ gameState, handlers }}>
<Shop />
</FarmhandContext.Provider>
)
})

test('renders shop inventory', () => {
expect(component.find(Inventory)).toHaveLength(2)
describe('<Shop />', () => {
test.each(['Seeds', 'Supplies', 'Upgrades'])(
'the %s tab exists',
tabLabel => {
expect(screen.getByText(tabLabel)).toBeInTheDocument()
}
)
})
2 changes: 2 additions & 0 deletions src/components/Stage/Stage.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { array, arrayOf, string } from 'prop-types'

import FarmhandContext from '../Farmhand/Farmhand.context'
import Field from '../Field'
import { Forest } from '../Forest'
import Home from '../Home'
import CowPen from '../CowPen'
import Shop from '../Shop'
Expand Down Expand Up @@ -52,6 +53,7 @@ export const Stage = ({ field, stageFocus, viewTitle }) => {
}}
/>
)}
{stageFocus === stageFocusType.FOREST && <Forest />}
{stageFocus === stageFocusType.SHOP && <Shop />}
{stageFocus === stageFocusType.COW_PEN && <CowPen />}
{stageFocus === stageFocusType.WORKSHOP && <Workshop />}
Expand Down
18 changes: 18 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,20 @@ export const PURCHASEABLE_FIELD_SIZES = freeze(
])
)

export const INITIAL_FOREST_WIDTH = 4
export const INITIAL_FOREST_HEIGHT = 1

/**
* @type Map<number, farmhand.purchasableFieldSize>
*/
export const PURCHASABLE_FOREST_SIZES = freeze(
new Map([
[1, { columns: 4, rows: 2, price: 100_000 }],
[2, { columns: 4, rows: 3, price: 200_000 }],
[3, { columns: 4, rows: 4, price: 300_000 }],
])
)

export const LARGEST_PURCHASABLE_FIELD_SIZE = /** @type {farmhand.purchaseableFieldSize} */ (PURCHASEABLE_FIELD_SIZES.get(
PURCHASEABLE_FIELD_SIZES.size
))
Expand Down Expand Up @@ -130,6 +144,7 @@ export const PRICE_EVENT_STANDARD_DURATION_DECREASE = 1
export const STAGE_TITLE_MAP = {
[stageFocusType.HOME]: 'Home',
[stageFocusType.FIELD]: 'Field',
[stageFocusType.FOREST]: 'Forest',
[stageFocusType.SHOP]: 'Shop',
[stageFocusType.COW_PEN]: 'Cows',
[stageFocusType.WORKSHOP]: 'Workshop',
Expand Down Expand Up @@ -157,6 +172,7 @@ export const PERSISTED_STATE_KEYS = [
'experience',
'farmName',
'field',
'forest',
'historicalDailyLosses',
'historicalDailyRevenue',
'historicalValueAdjustments',
Expand All @@ -181,6 +197,7 @@ export const PERSISTED_STATE_KEYS = [
'purchasedCowPen',
'purchasedCellar',
'purchasedField',
'purchasedForest',
'purchasedSmelter',
'record7dayProfitAverage',
'recordProfitabilityStreak',
Expand Down Expand Up @@ -280,6 +297,7 @@ export const EXPERIENCE_VALUES = {
COW_TRADED: 1,
FERMENTATION_RECIPE_MADE: 1,
FIELD_EXPANDED: 5,
FOREST_EXPANDED: 10,
FORGE_RECIPE_MADE: 3,
ITEM_SOLD: 1,
KEG_SOLD: 2,
Expand Down
6 changes: 5 additions & 1 deletion src/data/levels.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { toolType } from '../enums'
import { stageFocusType, toolType } from '../enums'

import * as items from './items'
import * as recipes from './recipes'
Expand Down Expand Up @@ -45,6 +45,10 @@ levels[14] = {
unlocksShopItem: items.potatoSeed.id,
}

levels[15] = {
unlocksStageFocusType: stageFocusType.FOREST,
}

levels[16] = {
unlocksShopItem: items.onionSeed.id,
}
Expand Down
1 change: 1 addition & 0 deletions src/enums.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export const stageFocusType = enumify([
'NONE', // Used for testing
'HOME',
'FIELD',
'FOREST',
'SHOP',
'COW_PEN',
'INVENTORY',
Expand Down
2 changes: 2 additions & 0 deletions src/game-logic/reducers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export * from './purchaseCow'
export * from './purchaseCowPen'
export * from './purchaseCellar'
export * from './purchaseField'
export * from './purchaseForest'
export * from './purchaseItem'
export * from './purchaseSmelter'
export * from './purchaseStorageExpansion'
Expand All @@ -64,6 +65,7 @@ export * from './setScarecrow'
export * from './setSprinkler'
export * from './showNotification'
export * from './spawnWeeds'
export * from './unlockTool'
export * from './updateAchievements'
export * from './updateFinancialRecords'
export * from './updateInventoryRecordsForNextDay'
Expand Down
Loading

0 comments on commit 6968f21

Please sign in to comment.