diff --git a/package-lock.json b/package-lock.json index 5d9f66fdf..2c7543ea4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@jeremyckahn/farmhand", - "version": "1.17.4", + "version": "1.17.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@jeremyckahn/farmhand", - "version": "1.17.4", + "version": "1.17.5", "license": "GPL-2.0-or-later", "dependencies": { "@fortawesome/fontawesome-svg-core": "^1.2.27", @@ -56,7 +56,7 @@ "shifty": "^3.0.1", "source-map-explorer": "^2.3.1", "stream": "npm:stream-browserify@^3.0.0", - "trystero": "^0.11.8", + "trystero": "^0.13.0", "typeface-francois-one": "0.0.71", "typeface-public-sans": "^1.1.4", "url": "^0.11.0", @@ -32255,8 +32255,9 @@ "license": "MIT" }, "node_modules/trystero": { - "version": "0.11.8", - "license": "MIT", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/trystero/-/trystero-0.13.0.tgz", + "integrity": "sha512-KfsMQ6bpRm7o8PNkh3nGGDBZ+n9XpF0RAet0PynIVpMTKTQ8xS2yILpohJCiYCMnf8yypvVblSIwXSfOdrLVig==", "dependencies": { "firebase": "^9.6.5", "ipfs-core": "0.9.0", @@ -56054,7 +56055,9 @@ "dev": true }, "trystero": { - "version": "0.11.8", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/trystero/-/trystero-0.13.0.tgz", + "integrity": "sha512-KfsMQ6bpRm7o8PNkh3nGGDBZ+n9XpF0RAet0PynIVpMTKTQ8xS2yILpohJCiYCMnf8yypvVblSIwXSfOdrLVig==", "requires": { "firebase": "^9.6.5", "ipfs-core": "0.9.0", diff --git a/package.json b/package.json index 21c83e1f8..354208b7d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@jeremyckahn/farmhand", - "version": "1.17.4", + "version": "1.17.5", "publishConfig": { "access": "public" }, @@ -132,7 +132,7 @@ "shifty": "^3.0.1", "source-map-explorer": "^2.3.1", "stream": "npm:stream-browserify@^3.0.0", - "trystero": "^0.11.8", + "trystero": "^0.13.0", "typeface-francois-one": "0.0.71", "typeface-public-sans": "^1.1.4", "url": "^0.11.0", diff --git a/src/components/Farmhand/Farmhand.js b/src/components/Farmhand/Farmhand.js index 9f35b23b7..aec94fdf5 100644 --- a/src/components/Farmhand/Farmhand.js +++ b/src/components/Farmhand/Farmhand.js @@ -5,6 +5,7 @@ * @typedef {import("../../index").farmhand.keg} farmhand.keg * @typedef {import("../../index").farmhand.plotContent} farmhand.plotContent * @typedef {import("../../index").farmhand.peerMessage} farmhand.peerMessage + * @typedef {import("../../index").farmhand.peerMetadata} farmhand.peerMetadata * @typedef {import("../../index").farmhand.priceEvent} farmhand.priceEvent * @typedef {import("../../index").farmhand.notification} farmhand.notification * @typedef {import("../../enums").cowColors} farmhand.cowColors @@ -247,7 +248,8 @@ const applyPriceEvents = (valueAdjustments, priceCrashes, priceSurges) => { * @property {farmhand.notification?} latestNotification * @property {Array.} newDayNotifications * @property {Array.} notificationLog - * @property {Object} peers Keys are (Trystero) peer ids, values are their respective metadata or null. + * @property {Record} peers Keys are (Trystero) + * peer ids, values are their respective metadata or null. * @property {Object?} peerRoom See https://github.com/dmotz/trystero * @property {farmhand.peerMessage[]} pendingPeerMessages An array of messages * to be sent to the Trystero peer room upon the next broadcast. @@ -707,6 +709,7 @@ export default class Farmhand extends FarmhandReducers { const [sendPeerMetadata, getPeerMetadata] = peerRoom.makeAction( 'peerMetadata' ) + getPeerMetadata(( /** @type {[object, string]} */ ...args @@ -715,18 +718,21 @@ export default class Farmhand extends FarmhandReducers { const [sendCowTradeRequest, getCowTradeRequest] = peerRoom.makeAction( 'cowTrade' ) + getCowTradeRequest(( /** @type {[object, string]} */ ...args ) => handleCowTradeRequest(this, ...args)) const [sendCowAccept, getCowAccept] = peerRoom.makeAction('cowAccept') + getCowAccept(( /** @type {[object, string]} */ ...args ) => handleCowTradeRequestAccept(this, ...args)) const [sendCowReject, getCowReject] = peerRoom.makeAction('cowReject') + getCowReject(( /** @type {[object]} */ ...args @@ -892,12 +898,15 @@ export default class Farmhand extends FarmhandReducers { this.scheduleHeartbeat() + const trackerRedundancy = 4 + this.setState({ activePlayers, peerRoom: joinRoom( { appId: process.env.REACT_APP_NAME, trackerUrls, + trackerRedundancy, rtcConfig, }, room diff --git a/src/components/LogView/LogView.js b/src/components/LogView/LogView.js index 9e7f44fca..97277f3b1 100644 --- a/src/components/LogView/LogView.js +++ b/src/components/LogView/LogView.js @@ -14,7 +14,16 @@ export const LogView = ({ notificationLog, todaysNotifications }) => (
    {todaysNotifications.map(({ message, onClick, severity }) => (
  • - +
  • diff --git a/src/components/NotificationSystem/NotificationSystem.js b/src/components/NotificationSystem/NotificationSystem.js index bc834f1b6..a14f053f2 100644 --- a/src/components/NotificationSystem/NotificationSystem.js +++ b/src/components/NotificationSystem/NotificationSystem.js @@ -18,6 +18,9 @@ export const snackbarProviderContentCallback = ( key, onClick, severity, + style: { + cursor: onClick ? 'pointer' : 'default', + }, }} > diff --git a/src/game-logic/reducers/showNotification.js b/src/game-logic/reducers/showNotification.js index 8c02be4f9..79b15c05e 100644 --- a/src/game-logic/reducers/showNotification.js +++ b/src/game-logic/reducers/showNotification.js @@ -1,6 +1,7 @@ /** * @typedef {import("../../components/Farmhand/Farmhand").farmhand.state} state - * @typedef {import("@material-ui/lab/Alert").Color} alertSeverity + * @typedef {import('@material-ui/lab/Alert').Color} alertSeverity + * @typedef {import('@material-ui/lab/Alert').AlertProps} AlertProps */ // TODO: Change showNotification to accept a configuration object instead of so @@ -10,6 +11,7 @@ * @param {string} message * @param {alertSeverity} [severity] Corresponds to the `severity` prop here: * https://material-ui.com/api/alert/ + * @param {AlertProps['onClick']} onClick * @returns {state} * @see https://material-ui.com/api/alert/ */ diff --git a/src/game-logic/reducers/updatePeer.js b/src/game-logic/reducers/updatePeer.js index 06ad49df7..a9b0f8e0d 100644 --- a/src/game-logic/reducers/updatePeer.js +++ b/src/game-logic/reducers/updatePeer.js @@ -1,18 +1,47 @@ +/** + * @typedef {import('../../components/Farmhand/Farmhand').default} Farmhand + * @typedef {import('../../components/Farmhand/Farmhand').farmhand.state} farmhand.state + * @typedef {import('../../index').farmhand.peerMetadata} farmhand.peerMetadata + */ import { MAX_LATEST_PEER_MESSAGES } from '../../constants' +import { NEW_COW_OFFERED_FOR_TRADE } from '../../templates' +import { dialogView } from '../../enums' + +import { showNotification } from './showNotification' /** * @param {farmhand.state} state + * @param {Farmhand} farmhand + * @param {farmhand.peerMetadata} peerMetadata * @param {string} peerId The peer to update - * @param {Object} state * @returns {farmhand.state} */ -export const updatePeer = (state, peerId, peerState) => { +export const updatePeer = (state, farmhand, peerMetadata, peerId) => { const peers = { ...state.peers } - peers[peerId] = peerState + + const previousPeerMetadata = peers[peerId] + + const previousCowOfferedId = previousPeerMetadata?.cowOfferedForTrade?.id + const newCowOfferedId = peerMetadata.cowOfferedForTrade?.id + + const isNewTrade = newCowOfferedId && previousCowOfferedId !== newCowOfferedId + + peers[peerId] = peerMetadata // Out of date peer clients may not provide pendingPeerMessages, so default // it here. - const { pendingPeerMessages = [] } = peerState + const { pendingPeerMessages = [] } = peerMetadata + + if (isNewTrade) { + state = showNotification( + state, + NEW_COW_OFFERED_FOR_TRADE`${peerMetadata}`, + 'info', + () => { + farmhand.openDialogView(dialogView.ONLINE_PEERS) + } + ) + } return { ...state, diff --git a/src/game-logic/reducers/updatePeer.test.js b/src/game-logic/reducers/updatePeer.test.js index 2655a5fdf..ea2c518c0 100644 --- a/src/game-logic/reducers/updatePeer.test.js +++ b/src/game-logic/reducers/updatePeer.test.js @@ -1,16 +1,26 @@ +/** + * @typedef {import('../../index').farmhand.peerMetadata} farmhand.peerMetadata + */ +import Farmhand from '../../components/Farmhand' import { MAX_LATEST_PEER_MESSAGES } from '../../constants' +import { NEW_COW_OFFERED_FOR_TRADE } from '../../templates' +import { getCowStub } from '../../test-utils/stubs/cowStub' +import { getPeerMetadataStub } from '../../test-utils/stubs/peerMetadataStub' import { updatePeer } from './updatePeer' +const stubPeerMetadata = getPeerMetadataStub() + describe('updatePeer', () => { test('updates peer data', () => { const { latestPeerMessages, peers } = updatePeer( { latestPeerMessages: [], - peers: { abc123: { foo: true } }, + peers: { abc123: stubPeerMetadata }, }, - 'abc123', - { foo: false } + Farmhand, + { foo: false }, + 'abc123' ) expect(latestPeerMessages).toEqual([]) @@ -21,12 +31,51 @@ describe('updatePeer', () => { const { latestPeerMessages } = updatePeer( { latestPeerMessages: new Array(50).fill('message'), - peers: { abc123: { foo: true } }, + peers: { abc123: stubPeerMetadata }, }, - 'abc123', - { foo: false } + Farmhand, + { foo: false }, + 'abc123' ) expect(latestPeerMessages).toHaveLength(MAX_LATEST_PEER_MESSAGES) }) + + test('shows a notification when a new cow is offered', () => { + const { todaysNotifications } = updatePeer( + { + latestPeerMessages: [], + todaysNotifications: [], + peers: { + abc123: stubPeerMetadata, + }, + }, + Farmhand, + { ...stubPeerMetadata, cowOfferedForTrade: getCowStub() }, + 'abc123' + ) + + expect(todaysNotifications[0]).toEqual( + expect.objectContaining({ + message: NEW_COW_OFFERED_FOR_TRADE`${stubPeerMetadata}`, + }) + ) + }) + + test('does not show a notification when a cow is rescinded', () => { + const { todaysNotifications } = updatePeer( + { + latestPeerMessages: [], + todaysNotifications: [], + peers: { + abc123: { ...stubPeerMetadata, cowOfferedForTrade: getCowStub() }, + }, + }, + Farmhand, + stubPeerMetadata, + 'abc123' + ) + + expect(todaysNotifications).toEqual([]) + }) }) diff --git a/src/handlers/peer-events.js b/src/handlers/peer-events.js index 33db39ab2..5d20068bb 100644 --- a/src/handlers/peer-events.js +++ b/src/handlers/peer-events.js @@ -1,3 +1,5 @@ +/** @typedef {import('../components/Farmhand/Farmhand').default} Farmhand */ +/** @typedef {import('../index').farmhand.peerMetadata} farmhand.peerMetadata */ import { cowTradeRejectionReason } from '../enums' import { COW_TRADED_NOTIFICATION } from '../templates' import { @@ -16,11 +18,11 @@ import { /** * @param {Farmhand} farmhand - * @param {Object} peerState + * @param {farmhand.peerMetadata} peerMetadata * @param {string} peerId */ -export const handlePeerMetadataRequest = (farmhand, peerState, peerId) => { - farmhand.updatePeer(peerId, peerState) +export const handlePeerMetadataRequest = (farmhand, peerMetadata, peerId) => { + farmhand.updatePeer(farmhand, peerMetadata, peerId) } /** diff --git a/src/index.js b/src/index.js index 9d27ca489..ad2a41788 100644 --- a/src/index.js +++ b/src/index.js @@ -2,6 +2,10 @@ * @namespace farmhand */ +/** + * @typedef {import("./components/Farmhand/Farmhand").farmhand.state} farmhand.state + */ + /** * @typedef {import("./enums").cropType} cropType * @typedef {import("./enums").cowColors} cowColors @@ -194,6 +198,17 @@ * @property {string} message */ +/** + * @typedef farmhand.offeredCow + * @type {farmhand.cow} + * @property {string} ownerId + */ + +/** + * @typedef farmhand.peerMetadata + * @type {Pick & { cowOfferedForTrade?: farmhand.offeredCow }} + */ + /** * @typedef farmhand.upgradesMetadatum * @type {Object} diff --git a/src/templates.js b/src/templates.js index 02fbefed1..9e8ea5f7d 100644 --- a/src/templates.js +++ b/src/templates.js @@ -1,6 +1,8 @@ /** * @typedef {import("./index").farmhand.item} farmhand.item + * @typedef {import("./index").farmhand.crop} farmhand.crop * @typedef {import("./index").farmhand.keg} keg + * @typedef {import("./index").farmhand.peerMetadata} farmhand.peerMetadata */ /** @@ -13,6 +15,7 @@ import { itemsMap } from './data/maps' import { moneyString } from './utils/moneyString' import { getCowDisplayName, + getPlayerName, getRandomLevelUpRewardQuantity, integerString, } from './utils' @@ -355,3 +358,11 @@ export const FERMENTED_CROP_NAME = (_, item) => `Fermented ${item.name}` */ export const KEG_SPOILED_MESSAGE = (_, keg) => `Oh no! Your ${FERMENTED_CROP_NAME`${itemsMap[keg.itemId]}`} has spoiled!` + +/** + * @param {TemplateStringsArray} _ + * @param {farmhand.peerMetadata} peerMetadata + * @returns {string} + */ +export const NEW_COW_OFFERED_FOR_TRADE = (_, peerMetadata) => + `A new cow is being offered for trade by ${getPlayerName(peerMetadata.id)}!` diff --git a/src/test-utils/stubs/cowStub.js b/src/test-utils/stubs/cowStub.js new file mode 100644 index 000000000..c64bb3b1b --- /dev/null +++ b/src/test-utils/stubs/cowStub.js @@ -0,0 +1,30 @@ +/** @typedef {import('../../index').farmhand.cow} farmhand.cow */ + +import { v4 as uuid } from 'uuid' + +import { cowColors, genders } from '../../enums' + +export const getCowStub = () => { + /** @type farmhand.cow */ + const cow = { + baseWeight: 1000, + color: cowColors.BLUE, + colorsInBloodline: {}, + daysOld: 1, + daysSinceMilking: 0, + daysSinceProducingFertilizer: 0, + gender: genders.FEMALE, + happiness: 0, + happinessBoostsToday: 0, + id: uuid(), + isBred: false, + isUsingHuggingMachine: false, + name: '', + originalOwnerId: uuid(), + ownerId: uuid(), + timesTraded: 0, + weightMultiplier: 1, + } + + return cow +} diff --git a/src/test-utils/stubs/peerMetadataStub.js b/src/test-utils/stubs/peerMetadataStub.js new file mode 100644 index 000000000..d2ca7bc22 --- /dev/null +++ b/src/test-utils/stubs/peerMetadataStub.js @@ -0,0 +1,19 @@ +/** @typedef {import('../../index').farmhand.peerMetadata} farmhand.peerMetadata */ + +import { v4 as uuid } from 'uuid' + +export const getPeerMetadataStub = () => { + /** @type farmhand.peerMetadata */ + const peerMetadata = { + cowsSold: {}, + cropsHarvested: 0, + dayCount: 0, + id: uuid(), + itemsSold: {}, + money: 0, + pendingPeerMessages: [], + version: '0.0.0', + } + + return peerMetadata +} diff --git a/src/utils/index.js b/src/utils/index.js index ed7d3e570..966db1c07 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -989,13 +989,15 @@ export const transformStateDataForImport = state => { return sanitizedState } -/** - * @param {string} playerId - * @returns {string} - */ -export const getPlayerName = memoize(playerId => { - return funAnimalName(playerId) -}) +export const getPlayerName = memoize( + /** + * @param {string} playerId + * @returns {string} + */ + playerId => { + return funAnimalName(playerId) + } +) /** * @param {number} currentInventoryLimit