From 9e626f92dd06e872cc271a960fe2ce4b88f91d22 Mon Sep 17 00:00:00 2001 From: Jeremy Kahn Date: Sat, 29 Apr 2023 13:42:40 -0500 Subject: [PATCH] feat: [closes #408] Salt (#410) * refactor: make ResourceFactory private static variables fully private * feat: add salt rock art * feat: implement Salt Rock mining * feat: make ore respawn logic more challenging * refactor: improve ore code typing * refactor: improve factory types * fix: remove Twitter links - Badgen no longer supports Twitter badges. * docs: explain interface system * feat: add salt art * feat: improve salt rock art * feat: add salt recipe * feat: add salt to various recipes * test: improve expectation for minePlot * refactor(StoneFactory): simplify generate method * refactor(file naming): rename salt-rock.js to saltRock.js --- README.md | 2 +- src/components/Home/Home.js | 2 +- src/constants.js | 3 +- src/data/items.js | 10 +++- src/data/ores/bronzeOre.js | 1 + src/data/ores/coal.js | 1 + src/data/ores/goldOre.js | 1 + src/data/ores/index.js | 1 + src/data/ores/ironOre.js | 1 + src/data/ores/saltRock.js | 19 ++++++++ src/data/ores/silverOre.js | 1 + src/data/ores/stone.js | 1 + src/data/recipes.js | 23 ++++++++++ src/factories/CoalFactory.js | 8 ++-- src/factories/OreFactory.js | 8 +++- src/factories/ResourceFactory.js | 56 +++++++++++------------ src/factories/StoneFactory.js | 46 ++++++++----------- src/factories/StoneFactory.test.js | 16 +++---- src/game-logic/reducers/minePlot.js | 25 +++++----- src/game-logic/reducers/minePlot.test.js | 4 +- src/img/dishes/salt.piskel | 1 + src/img/dishes/salt.png | Bin 0 -> 279 bytes src/img/index.js | 4 ++ src/img/ores/salt-rock.piskel | 1 + src/img/ores/salt-rock.png | Bin 0 -> 321 bytes src/index.js | 1 + src/interfaces/Factory.js | 12 +++++ src/interfaces/README.md | 3 ++ 28 files changed, 165 insertions(+), 86 deletions(-) create mode 100644 src/data/ores/saltRock.js create mode 100644 src/img/dishes/salt.piskel create mode 100644 src/img/dishes/salt.png create mode 100644 src/img/ores/salt-rock.piskel create mode 100644 src/img/ores/salt-rock.png create mode 100644 src/interfaces/Factory.js create mode 100644 src/interfaces/README.md diff --git a/README.md b/README.md index 0fee77407..941963a6b 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Community links: - Discord link: [![Discord](https://img.shields.io/discord/714539345050075176?label=farmhand)](https://discord.gg/6cHEZ9H) -- Twitter link: [![@farmhandgame](https://badgen.net/twitter/follow/farmhandgame)](https://twitter.com/farmhandgame) +- Twitter link: [@farmhandgame](https://badgen.net/twitter/follow/farmhandgame) - Reddit link: [![r/FarmhandGame](https://img.shields.io/reddit/subreddit-subscribers/FarmhandGame?style=social)](https://www.reddit.com/r/FarmhandGame/) Storefront links: diff --git a/src/components/Home/Home.js b/src/components/Home/Home.js index 0bd8048d7..dcf34d97a 100644 --- a/src/components/Home/Home.js +++ b/src/components/Home/Home.js @@ -87,7 +87,7 @@ const Home = ({ source: ` Hi, you're playing **Farmhand**! This is an open source game project created by [Jeremy Kahn](https://github.com/jeremyckahn). The project has evolved over time and is now developed with the support of [a community of contributors](https://github.com/jeremyckahn/farmhand/blob/develop/CONTRIBUTORS.md). -[![Source code](https://badgen.net/badge/icon/github?icon=github&label)](https://github.com/jeremyckahn/farmhand) [![Discord](https://img.shields.io/discord/714539345050075176?label=farmhand+discord)](https://discord.gg/6cHEZ9H) [![@farmhandgame](https://badgen.net/twitter/follow/farmhandgame)](https://twitter.com/farmhandgame) [![r/FarmhandGame](https://img.shields.io/reddit/subreddit-subscribers/FarmhandGame?style=social)](https://www.reddit.com/r/FarmhandGame/) +[![Source code](https://badgen.net/badge/icon/github?icon=github&label)](https://github.com/jeremyckahn/farmhand) [![Discord](https://img.shields.io/discord/714539345050075176?label=farmhand+discord)](https://discord.gg/6cHEZ9H) [![r/FarmhandGame](https://img.shields.io/reddit/subreddit-subscribers/FarmhandGame?style=social)](https://www.reddit.com/r/FarmhandGame/) [Twitter: @farmhandgame](https://badgen.net/twitter/follow/farmhandgame) Farmhand is a resource management game that puts a farm in your hand. It is designed to be both desktop and mobile-friendly and fun for 30 seconds or 30 minutes at a time. Can you build a thriving farming business? Give it a try and find out! diff --git a/src/constants.js b/src/constants.js index 49c08b3ad..adfeb13c0 100644 --- a/src/constants.js +++ b/src/constants.js @@ -218,7 +218,8 @@ export const RESOURCE_SPAWN_CHANCE = 0.3 // todo: migrate these to an object called RESOURCE_SPAWN_CHANCES once reducers is refactored export const ORE_SPAWN_CHANCE = 0.25 export const COAL_SPAWN_CHANCE = 0.15 -export const STONE_SPAWN_CHANCE = 0.6 +export const STONE_SPAWN_CHANCE = 0.4 +export const SALT_ROCK_SPAWN_CHANCE = 0.3 // if spawning ore, which kind? // note: these values end up being used relative to each other diff --git a/src/data/items.js b/src/data/items.js index 0f87dd201..e841c21e6 100644 --- a/src/data/items.js +++ b/src/data/items.js @@ -82,7 +82,15 @@ export const weed = freeze({ type: WEED, }) -export { bronzeOre, coal, goldOre, ironOre, silverOre, stone } from './ores' +export { + bronzeOre, + coal, + goldOre, + ironOre, + silverOre, + stone, + saltRock, +} from './ores' //////////////////////////////////////// // diff --git a/src/data/ores/bronzeOre.js b/src/data/ores/bronzeOre.js index fb64771fa..57410a89c 100644 --- a/src/data/ores/bronzeOre.js +++ b/src/data/ores/bronzeOre.js @@ -1,3 +1,4 @@ +/** @typedef {import("../../index").farmhand.item} farmhand.item */ import { itemType } from '../../enums' import { BRONZE_SPAWN_CHANCE } from '../../constants' diff --git a/src/data/ores/coal.js b/src/data/ores/coal.js index 7295ab308..aaa75394c 100644 --- a/src/data/ores/coal.js +++ b/src/data/ores/coal.js @@ -1,3 +1,4 @@ +/** @typedef {import("../../index").farmhand.item} farmhand.item */ import { itemType } from '../../enums' import { COAL_SPAWN_CHANCE } from '../../constants' diff --git a/src/data/ores/goldOre.js b/src/data/ores/goldOre.js index 816dfb370..dead2506e 100644 --- a/src/data/ores/goldOre.js +++ b/src/data/ores/goldOre.js @@ -1,3 +1,4 @@ +/** @typedef {import("../../index").farmhand.item} farmhand.item */ import { itemType } from '../../enums' import { GOLD_SPAWN_CHANCE } from '../../constants' diff --git a/src/data/ores/index.js b/src/data/ores/index.js index b184f2f44..c2a77635a 100644 --- a/src/data/ores/index.js +++ b/src/data/ores/index.js @@ -4,3 +4,4 @@ export { goldOre } from './goldOre' export { ironOre } from './ironOre' export { silverOre } from './silverOre' export { stone } from './stone' +export { saltRock } from './saltRock' diff --git a/src/data/ores/ironOre.js b/src/data/ores/ironOre.js index f19f952fc..d3f54ccc8 100644 --- a/src/data/ores/ironOre.js +++ b/src/data/ores/ironOre.js @@ -1,3 +1,4 @@ +/** @typedef {import("../../index").farmhand.item} farmhand.item */ import { itemType } from '../../enums' import { IRON_SPAWN_CHANCE } from '../../constants' diff --git a/src/data/ores/saltRock.js b/src/data/ores/saltRock.js new file mode 100644 index 000000000..2b07c4a5a --- /dev/null +++ b/src/data/ores/saltRock.js @@ -0,0 +1,19 @@ +/** @typedef {import("../../index").farmhand.item} farmhand.item */ +import { itemType } from '../../enums' +import { SALT_ROCK_SPAWN_CHANCE } from '../../constants' + +const { freeze } = Object + +/** + * @property farmhand.module:items.saltRock + * @type {farmhand.item} + */ +export const saltRock = freeze({ + description: 'A large chunk of salt.', + doesPriceFluctuate: true, + id: 'salt-rock', + name: 'Salt Rock', + spawnChance: SALT_ROCK_SPAWN_CHANCE, + type: itemType.STONE, + value: 10, +}) diff --git a/src/data/ores/silverOre.js b/src/data/ores/silverOre.js index a1c34ebce..b329b2cd9 100644 --- a/src/data/ores/silverOre.js +++ b/src/data/ores/silverOre.js @@ -1,3 +1,4 @@ +/** @typedef {import("../../index").farmhand.item} farmhand.item */ import { itemType } from '../../enums' import { SILVER_SPAWN_CHANCE } from '../../constants' diff --git a/src/data/ores/stone.js b/src/data/ores/stone.js index ccf95d933..9636613b2 100644 --- a/src/data/ores/stone.js +++ b/src/data/ores/stone.js @@ -1,3 +1,4 @@ +/** @typedef {import("../../index").farmhand.item} farmhand.item */ import { itemType } from '../../enums' import { STONE_SPAWN_CHANCE } from '../../constants' diff --git a/src/data/recipes.js b/src/data/recipes.js index 31e72db04..44e89e402 100644 --- a/src/data/recipes.js +++ b/src/data/recipes.js @@ -3,6 +3,7 @@ */ import { itemType, fieldMode, recipeType } from '../enums' import { RECIPE_INGREDIENT_VALUE_MULTIPLIER } from '../constants' +import { features } from '../config' import * as items from './items' import baseItemsMap from './items-map' @@ -28,6 +29,23 @@ const itemify = recipe => { return item } +/** + * @property farmhand.module:recipes.salt + * @type {farmhand.item} + */ +export const salt = itemify({ + id: 'salt', + name: 'Salt', + ingredients: { + [items.saltRock.id]: 1, + }, + condition: state => state.itemsSold[items.saltRock.id] >= 30, + description: features.KEGS + ? 'Useful for seasoning food and fermentation.' + : 'Useful for seasoning food.', + recipeType: recipeType.KITCHEN, +}) + /** * @property farmhand.module:recipes.bread * @type {farmhand.recipe} @@ -181,6 +199,7 @@ export const frenchOnionSoup = itemify({ ingredients: { [items.onion.id]: 5, [cheese.id]: 2, + [salt.id]: 2, }, condition: state => state.itemsSold[items.onion.id] >= 15 && state.itemsSold[cheese.id] >= 10, @@ -307,6 +326,7 @@ export const hotSauce = itemify({ name: 'Hot Sauce', ingredients: { [items.jalapeno.id]: 10, + [salt.id]: 1, }, condition: state => state.itemsSold[items.jalapeno.id] >= 50, recipeType: recipeType.KITCHEN, @@ -408,6 +428,7 @@ export const garlicFries = itemify({ [items.potato.id]: 5, [items.garlic.id]: 3, [vegetableOil.id]: 1, + [salt.id]: 2, }, condition: state => state.itemsSold[items.potato.id] >= 50 && @@ -512,6 +533,7 @@ export const sweetPotatoFries = itemify({ ingredients: { [items.sweetPotato.id]: 10, [vegetableOil.id]: 1, + [salt.id]: 1, }, condition: state => state.itemsSold[items.sweetPotato.id] >= 100, recipeType: recipeType.KITCHEN, @@ -529,6 +551,7 @@ export const onionRings = itemify({ [vegetableOil.id]: 1, [items.wheat.id]: 5, [soyMilk.id]: 1, + [salt.id]: 3, }, condition: state => state.itemsSold[items.onion.id] >= 50 && diff --git a/src/factories/CoalFactory.js b/src/factories/CoalFactory.js index 58eacfefa..dbc451051 100644 --- a/src/factories/CoalFactory.js +++ b/src/factories/CoalFactory.js @@ -1,14 +1,16 @@ -import { chooseRandom } from '../utils' +/** @typedef {import("../index").farmhand.item} farmhand.item */ import { coal, stone } from '../data/ores' +import { Factory } from '../interfaces/Factory' +import { chooseRandom } from '../utils' /** * Resource factory used for spawning coal * @constructor */ -export default class CoalFactory { +export default class CoalFactory extends Factory { /** * Generate resources - * @returns {Array} an array of coal and stone resources + * @returns {Array.} an array of coal and stone resources */ generate() { let spawns = [] diff --git a/src/factories/OreFactory.js b/src/factories/OreFactory.js index 6be8d771a..1dc0fce5c 100644 --- a/src/factories/OreFactory.js +++ b/src/factories/OreFactory.js @@ -1,4 +1,6 @@ +/** @typedef {import("../index").farmhand.item} farmhand.item */ import { goldOre, ironOre, bronzeOre, silverOre } from '../data/ores' +import { Factory } from '../interfaces/Factory' import { randomChoice } from '../utils' const SPAWNABLE_ORES = [goldOre, ironOre, bronzeOre, silverOre] @@ -7,8 +9,10 @@ const SPAWNABLE_ORES = [goldOre, ironOre, bronzeOre, silverOre] * Resource factory used for spawning ores * @constructor */ -export default class OreFactory { +export default class OreFactory extends Factory { constructor() { + super() + this.oreOptions = [] for (let o of SPAWNABLE_ORES) { this.oreOptions.push({ @@ -20,7 +24,7 @@ export default class OreFactory { /** * Generate resources - * @returns {Array} an array of ore resources + * @returns {Array.} an array of ore resources */ generate() { return [this.spawn()] diff --git a/src/factories/ResourceFactory.js b/src/factories/ResourceFactory.js index 029373a18..39a003008 100644 --- a/src/factories/ResourceFactory.js +++ b/src/factories/ResourceFactory.js @@ -1,3 +1,5 @@ +/** @typedef {import("../index").farmhand.item} farmhand.item */ +/** @typedef {import("../enums").itemType} farmhand.itemType */ import { itemType, toolLevel } from '../enums' import { RESOURCE_SPAWN_CHANCE, @@ -6,13 +8,26 @@ import { STONE_SPAWN_CHANCE, } from '../constants' import { randomChoice } from '../utils' - import { randomNumberService } from '../common/services/randomNumber' +// eslint-disable-next-line no-unused-vars +import { Factory } from '../interfaces/Factory' import OreFactory from './OreFactory' import CoalFactory from './CoalFactory' import StoneFactory from './StoneFactory' +/** + * Object for private cache of factory instances + * @type {Record.} + */ +const factoryInstances = {} + +/** + * Var for caching reference to instance of ResourceFactory + * @type {?ResourceFactory} + */ +let instance = null + /** * Used for spawning mined resources * @constructor @@ -26,36 +41,23 @@ export default class ResourceFactory { ] } - /** - * Object for internal cache of factory instances - * @static - */ - static _factoryInstances = {} - - /** - * Var for caching reference to instance of ResourceFactory - * @static - */ - static _instance = null - /** * Retrieve a reusable instance of ResourceFactory * @returns {ResourceFactory} * @static */ static instance() { - if (!ResourceFactory._instance) { - ResourceFactory._instance = new ResourceFactory() + if (!instance) { + instance = new ResourceFactory() } - return ResourceFactory._instance + return instance } /** * Generate an instance for specific factory - * @param {Number} type - an item type from itemType enum - * @returns {?Factory} returns a factory if one exists for type, default return is null - * @static + * @param {farmhand.itemType} type + * @returns {?Factory} A factory if one exists for type, default return is null */ static generateFactoryInstance(type) { switch (type) { @@ -76,26 +78,24 @@ export default class ResourceFactory { /** * Retrieve a specific factory for generating resources. Will create and cache * a factory instance for reuse. - * @param {Number} type - an item type from itemType enum - * @return {Factory} - * @static + * @returns {?Factory} */ static getFactoryForItemType = type => { - if (!ResourceFactory._factoryInstances[type]) { - ResourceFactory._factoryInstances[ - type - ] = ResourceFactory.generateFactoryInstance(type) + if (!factoryInstances[type]) { + factoryInstances[type] = ResourceFactory.generateFactoryInstance(type) } - return ResourceFactory._factoryInstances[type] + return factoryInstances[type] } /** * Use dice roll and resource factories to generate resources at random - * @returns {Array} array of resource objects + * @returns {Array.} array of resource objects */ generateResources(shovelLevel) { + /** @type {Array.} */ let resources = [] + let spawnChance = RESOURCE_SPAWN_CHANCE switch (shovelLevel) { diff --git a/src/factories/StoneFactory.js b/src/factories/StoneFactory.js index f09fb0355..b862fc509 100644 --- a/src/factories/StoneFactory.js +++ b/src/factories/StoneFactory.js @@ -1,43 +1,37 @@ -import { coal, stone } from '../data/ores' -import { COAL_SPAWN_CHANCE } from '../constants' +/** @typedef {import("../index").farmhand.item} farmhand.item */ import { randomNumberService } from '../common/services/randomNumber' +import { Factory } from '../interfaces/Factory' +import { coal, saltRock, stone } from '../data/ores' +import { + COAL_SPAWN_CHANCE, + SALT_ROCK_SPAWN_CHANCE, + STONE_SPAWN_CHANCE, +} from '../constants' + +const spawnableResources = [ + [stone, STONE_SPAWN_CHANCE], + [saltRock, SALT_ROCK_SPAWN_CHANCE], + [coal, COAL_SPAWN_CHANCE], +] /** * Resource factory used for spawning stone * @constructor */ -export default class StoneFactory { +export default class StoneFactory extends Factory { /** * Generate resources - * @returns {Array} an array of stone and coal resources + * @returns {Array.} an array of stone and coal resources */ generate() { let resources = [] - resources.push(this.spawnStone()) - - if (randomNumberService.isRandomNumberLessThan(COAL_SPAWN_CHANCE)) { - resources.push(this.spawnCoal()) + for (const [resource, spawnChance] of spawnableResources) { + if (randomNumberService.isRandomNumberLessThan(spawnChance)) { + resources.push(resource) + } } return resources } - - /** - * Spawn a piece of stone - * @returns {Object} stone item - * @private - */ - spawnStone() { - return stone - } - - /** - * Spawn a piece of coal - * @returns {Object} coal item - * @private - */ - spawnCoal() { - return coal - } } diff --git a/src/factories/StoneFactory.test.js b/src/factories/StoneFactory.test.js index 0e67e1f30..6072cd143 100644 --- a/src/factories/StoneFactory.test.js +++ b/src/factories/StoneFactory.test.js @@ -1,5 +1,5 @@ import { randomNumberService } from '../common/services/randomNumber' -import { coal, stone } from '../data/ores' +import { coal, stone, saltRock } from '../data/ores' import StoneFactory from './StoneFactory' @@ -15,17 +15,15 @@ describe('StoneFactory', () => { stoneFactory = new StoneFactory() }) - test('it generates stones', () => { - const resources = stoneFactory.generate() - - expect(resources[0]).toEqual(stone) - }) - - test('it generates a coal along with the stone when random chance passes', () => { + test('it generates resources', () => { randomNumberService.isRandomNumberLessThan.mockReturnValue(true) + const resources = stoneFactory.generate() - expect(resources).toEqual([stone, coal]) + expect(resources).toHaveLength(3) + expect(resources).toContain(stone) + expect(resources).toContain(saltRock) + expect(resources).toContain(coal) }) }) }) diff --git a/src/game-logic/reducers/minePlot.js b/src/game-logic/reducers/minePlot.js index 2e6147f81..dc2cdcfe1 100644 --- a/src/game-logic/reducers/minePlot.js +++ b/src/game-logic/reducers/minePlot.js @@ -33,29 +33,28 @@ export const minePlot = (state, x, y) => { const spawnedResources = ResourceFactory.instance().generateResources( shovelLevel ) - let spawnedOre = null + const [spawnedResource] = spawnedResources let daysUntilClear = chooseRandom(daysUntilClearPeriods) - if (spawnedResources.length) { - // even when multiple resources are spawned, the first one is ok to use - // for all subsequent logic - spawnedOre = spawnedResources[0] + if (spawnedResource) { + const spawnChances = spawnedResources.map(({ spawnChance }) => spawnChance) + const minSpawnChance = Math.min(...spawnChances) - // if ore was spawned, add up to 10 days to the time to clear - // at random, based loosely on the spawnChance meant to make - // rarer ores take longer to cooldown - daysUntilClear += Math.round(random() * (1 - spawnedOre.spawnChance) * 10) + // if a resource was spawned, add up to 10 days to the time to clear at + // random, based loosely on the minimum spawnChance meant to make rarer + // resources take longer to cooldown + daysUntilClear += Math.round(random() * (1 - minSpawnChance) * 10) + } - for (let resource of spawnedResources) { - state = addItemToInventory(state, resource) - } + for (let resource of spawnedResources) { + state = addItemToInventory(state, resource) } state = modifyFieldPlotAt(state, x, y, () => { return { isShoveled: true, daysUntilClear, - oreId: spawnedOre ? spawnedOre.id : null, + oreId: spawnedResource?.id ?? null, } }) diff --git a/src/game-logic/reducers/minePlot.test.js b/src/game-logic/reducers/minePlot.test.js index 4fa1b63ef..36efdbadf 100644 --- a/src/game-logic/reducers/minePlot.test.js +++ b/src/game-logic/reducers/minePlot.test.js @@ -1,3 +1,4 @@ +import { randomNumberService } from '../../common/services/randomNumber' import { goldOre } from '../../data/ores' import { ResourceFactory } from '../../factories' import { toolType, toolLevel } from '../../enums' @@ -18,6 +19,7 @@ describe('minePlot', () => { } jest.spyOn(ResourceFactory, 'instance') + jest.spyOn(randomNumberService, 'generateRandomNumber').mockReturnValue(1) ResourceFactory.instance.mockReturnValue({ generateResources: () => [goldOre], @@ -35,7 +37,7 @@ describe('minePlot', () => { }) test('sets the days until clear', () => { - expect(gameState.field[0][0].daysUntilClear > 0).toEqual(true) + expect(gameState.field[0][0].daysUntilClear).toEqual(12) }) test('adds the spawned ore to the inventory', () => { diff --git a/src/img/dishes/salt.piskel b/src/img/dishes/salt.piskel new file mode 100644 index 000000000..fd9a96299 --- /dev/null +++ b/src/img/dishes/salt.piskel @@ -0,0 +1 @@ +{"modelVersion":2,"piskel":{"name":"salt","description":"","fps":12,"height":24,"width":24,"layers":["{\"name\":\"Layer 1\",\"opacity\":1,\"frameCount\":1,\"chunks\":[{\"layout\":[[0]],\"base64PNG\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAA3klEQVRIS2NkoDFgpLH5DANqwX8yfYfiaFw++K++6TjJ5t/0s4TpgZuLzQKyDQc5CmoJTgvIDRYGmI8JWlBz4S7JQdNioEyaBVcPKxFtibbtPQaqWLAuB2Jn0BRUu6liAczwR48eMcjJyaFYQrEFMMPRwwzmE4osABkOczXVLcDlcpBFyEFFsQ9gQYHsG+SIpsgCWLCADEdPPTA5qliAL1OQZQHRuQyqkOSMhmyB0R9lDPvOsaAWJ3SzAL2gA7kMvbj+T25hB/UmRvGPtT4gNQ6wOBRuxIDWyWR4BFMLAHg9rRn+GIpBAAAAAElFTkSuQmCC\"}]}"]}} \ No newline at end of file diff --git a/src/img/dishes/salt.png b/src/img/dishes/salt.png new file mode 100644 index 0000000000000000000000000000000000000000..597716ceb61e1373ceb78d6e058b0c987ca6857d GIT binary patch literal 279 zcmV+y0qFjTP)yQyLU%~;0 z(ZdYp6N)SV84hzb)h)o~a*##HK}E3-U_pWG8{~+l$O2fffDA`ZJuo?{Sb#56kYxeU zxeTh1). Ideally JSDoc or TypeScript would be used to achieve a more proper interface/implementation system. However, TypeScript is not currently available in this project and JSDoc doesn't have good editor integration for interfaces. So, classes with empty method definitions are used instead.