diff --git a/.gitignore b/.gitignore index 106b6bfe..5ea91171 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,5 @@ testing/ .circleci/ changelog.md master.json -.env \ No newline at end of file +.env +.vscode/settings.json \ No newline at end of file diff --git a/src/API/skyblock/getSkyblockBingo.js b/src/API/skyblock/getSkyblockBingo.js new file mode 100644 index 00000000..9963dad8 --- /dev/null +++ b/src/API/skyblock/getSkyblockBingo.js @@ -0,0 +1,8 @@ +module.exports = async function () { + const BingoData = require('../../structures/Skyblock/Static/BingoData.js'); + + const res = await this._makeRequest('/resources/skyblock/election'); + if (res.raw) return res; + + return new BingoData(res); +}; diff --git a/src/API/skyblock/getSkyblockBingoByPlayer.js b/src/API/skyblock/getSkyblockBingoByPlayer.js new file mode 100644 index 00000000..9a15f314 --- /dev/null +++ b/src/API/skyblock/getSkyblockBingoByPlayer.js @@ -0,0 +1,14 @@ +const getSkyblockBingo = require('./getSkyblockBingo'); +const toUuid = require('../../utils/toUuid'); +const Errors = require('../../Errors'); +module.exports = async function (query, { fetchBingoData = false }) { + if (!query) throw new Error(Errors.NO_NICKNAME_UUID); + const PlayerBingo = require('../../structures/SkyBlock/PlayerBingo'); + query = await toUuid(query); + const res = await this._makeRequest(`/skyblock/uuid?player=${query}`); + if (res.raw) return res; + let bingoData = null; + if (fetchBingoData) bingoData = await getSkyblockBingo.call(this); + + return new PlayerBingo(data, bingoData); +}; diff --git a/src/Client.js b/src/Client.js index 60475ac3..2a30c0cf 100644 --- a/src/Client.js +++ b/src/Client.js @@ -312,6 +312,20 @@ class Client extends EventEmitter { * console.log(products[0].productId); // INK_SACK:3 * }) * .catch(console.log); + */ /** + * Allows you to get bingo data + * @method + * @name Client#getSkyblockBingo + * @param {MethodOptions} [options={}] Options + * @return {Promise} + */ + /** + * Allows you to get bingo data of a player + * @method + * @name Client#getSkyblockBingoByPlayer + * @param {string} query UUID / IGN of player + * @param {PlayerBingoOptions} [options={}] Options + * @return {Promise} */ /** * Allows you to get SB government @@ -411,4 +425,12 @@ const defaultCache = require('./Private/defaultCache.js'); * @property {boolean} [includeItemBytes=false] Whether to include item bytes in the result * @prop {object} [headers={}] Extra Headers ( like User-Agent ) to add to request. Overrides the headers globally provided. */ +/** + * @typedef {object} PlayerBingoOptions + * @property {boolean} [raw=false] Raw data + * @property {boolean} [noCacheCheck=false] Disable/Enable cache checking + * @property {boolean} [noCaching=false] Disable/Enable writing to cache + * @property {boolean} [fetchBingoData=false] Fetches bingo data to give more information + * @prop {object} [headers={}] Extra Headers ( like User-Agent ) to add to request. Overrides the headers globally provided. + */ module.exports = Client; diff --git a/src/index.js b/src/index.js index 6f2309f1..f0708e59 100644 --- a/src/index.js +++ b/src/index.js @@ -29,6 +29,8 @@ module.exports = { SkyblockPet: require('./structures/SkyBlock/SkyblockPet'), SkyblockGovernment: require('./structures/Skyblock/Static/Government.js'), SkyblockCandidate: require('./structures/Skyblock/Static/Candidate.js'), + SkyblockBingoData: require('./structures/Skyblock/Static/BingoData.js'), + SkyblockBingo: require('./structures/Skyblock/Static/Bingo.js'), /* Skyblock Auctions */ BaseAuction: require('./structures/SkyBlock/Auctions/BaseAuction.js'), diff --git a/src/structures/SkyBlock/PlayerBingo.js b/src/structures/SkyBlock/PlayerBingo.js new file mode 100644 index 00000000..eb7930ab --- /dev/null +++ b/src/structures/SkyBlock/PlayerBingo.js @@ -0,0 +1,65 @@ +/** + * @typedef {import('./Static/BingoData.js')} BingoData + * @typedef {import('./Static/Bingo.js')} Bingo + */ + +/** + * Player Bingo Class + */ +class PlayerBingo { + /** + * Constructor + * @param {Object} data data + * @param {BingoData|null} bingoData bingo data + */ + constructor(data, bingoData) { + const events = data.success && Array.isArray(data.events) ? data.events : []; + /** + * Data per event + * @type {PlayerBingoDataPerEvent} + */ + this.dataPerEvent = events.map((eventData) => { + let doneGoals = eventData.completed_goals; + if (!Array.isArray(doneGoals)) doneGoals = []; + const enrichable = parseInt(eventData.key, 10) === bingoData?.id; + if (enrichable) doneGoals = populateGoals(doneGoals, bingoData.goals); + return { + eventId: parseInt(eventData.key, 10) || null, + points: parseInt(eventData.points, 10) || 0, + goalsCompleted: doneGoals, + enrichedGoals: enrichable + }; + }); + } +} + +/** + * Populate goals + * For compatibility and lazy handling, uncompleted goals will be hidden in a property + * @param {string[]} achieved achieved goals + * @param {Bingo[]} all All goals + * @returns {SpecialBingoArray} + */ +function populateGoals(achieved, all) { + const populatedAchieved = []; + const unachieved = []; + for (const goal of all) { + if (achieved.find((str) => str === goal.name)) populatedAchieved.push(goal); + else unachieved.push(goal); + } + populatedAchieved.unachievedGoals = unachieved; + return populatedAchieved; +} + +/** + * @typedef {Bingo[] & {'unachievedGoals': Bingo[]}} SpecialBingoArray + */ +/** + * @typedef {Object} PlayerBingoDataPerEvent + * @property {number} eventId ID of event + * @property {number} points Points acquired + * @property {boolean} enrichedGoals Whether the goals are enriched (populated with data from static skyblock bingp data) + * @property {SpecialBingoArray|string[]} goalsCompleted Special Bingo Array if enrichedGoals is true. You can however always treat SpecialBingoArray as string[] + */ + +module.exports = PlayerBingo; diff --git a/src/structures/SkyBlock/Static/Bingo.js b/src/structures/SkyBlock/Static/Bingo.js new file mode 100644 index 00000000..1c4c429e --- /dev/null +++ b/src/structures/SkyBlock/Static/Bingo.js @@ -0,0 +1,106 @@ +/** + * Bingo class + */ +class Bingo { + /** + * Constructor + * @param {Object} data data + * @param {number} position Position + */ + constructor(data, position = 0) { + /** + * Name of this bingo goal + * @type {string} + */ + this.name = data.name; + /** + * string ID (code name) + * @type {string} + */ + this.id = data.id; + const [row, column] = parsePosition(position); + /** + * 1-indexed row + * @type {number|null} + */ + this.row = row; + /** + * 1-indexed colmun + * @type {number|null} + */ + this.column = column; + /** + * Bingo lore, with color codes + * @type {string} + */ + this.rawLore = data.lore; + /** + * Bingo lore in plain text + * @type {string} + */ + this.lore = data.lore?.replace?.(/§([1-9]|[a-l])|§/gm, '') || null; + /** + * Only available for TIERED bingos + * Shows you the requirement for each tier of this achievement + * @type {number[]} + */ + this.tiers = Array.isArray(data.tiers) ? data.tiers.map((x) => parseInt(x, 10) || 0) : null; + /** + * Only available for TIERED bingos + * Difference between each tier requirement, if it is constant + * @type {number|null} + */ + this.tierStep = this.#getTierStep(); + /** + * Only available for ONE_TIERED bingos + * @type {number|null} + */ + this.requiredAmount = parseInt(data.requiredAmount, 10) ?? null; + /** + * Type of Bingo + * ONE_TIME means the goal doesn't have a specific amount + * ONE_TIER means the goal specifies 1 amount to achieve + * TIERED means the goal specifies more than 1 amount to achieve + * @type {'ONE_TIME'|'ONE_TIER'|'TIERED'} + */ + this.type = this.tiers ? 'TIERED' : this.requiredAmount ? 'ONE_TIER' : 'ONE_TIME'; + } + /** + * As string + * BEWARE this returns ID to assure compatibility with PlayerBingo + * @return {string} + */ + toString() { + return this.id; + } + /** + * Gets tier step, if constant + * @private + * @returns {number|null} + */ + #getTierStep() { + if (this.type !== 'TIERED') return null; + // No step possible + if (this.tiers.length < 2) return null; + const hypotheticStep = this.tiers[1] - this.tiers[0]; + // Check if every 2 elements have the same step + const isConstant = this.tiers.slice(1).every((el, index) => { + return hypotheticStep === this.tiers[index - 1] - el; + }); + if (!isConstant) return null; + return hypotheticStep; + } +} + +/** + * Parses row and col from position, assuming bingo table is 5x5 + * @param {number} position Position + * @returns {number[]} + */ +function parsePosition(position) { + const x = (position % 5) + 1; + const y = Math.floor(position / 5) + 1; + return [x, y]; +} + +module.exports = Bingo; diff --git a/src/structures/SkyBlock/Static/BingoData.js b/src/structures/SkyBlock/Static/BingoData.js new file mode 100644 index 00000000..e82c2b0d --- /dev/null +++ b/src/structures/SkyBlock/Static/BingoData.js @@ -0,0 +1,45 @@ +const Bingo = require('./Bingo.js'); + +/** + * SB Bingo Class + */ +class BingoData { + /** + * constructor + * @param {Object} data + */ + constructor(data) { + /** + * Last time this resource was updated + * @type {number} + */ + this.lastUpdatedTimestamp = parseInt(data.lastUpdated, 10); + /** + * Last time this resource was updated, as Date + * @type {Date|null} + */ + this.lastUpdatedAt = new Date(this.lastUpdatedTimestamp); + /** + * Bingo ID + * @type {number|null} + */ + this.id = parseInt(data.id, 10) || null; + /** + * Goals + * @type {Bingo[]|null} + */ + this.goals = Array.isArray(data.goals) ? data.goals.map((goal, index) => new Bingo(goal, index)) : null; + } + /** + * Gets a goal on the bingo table by row and column + * @param {number} column Column number (starts at 1) + * @param {number} row Row number (starts at 1) + * @returns {Bingo|undefined} + */ + getGoal(column, row) { + if (!this.goals || this.goals.length < 1) return; + return this.goals.find((goal) => goal.row === row && goal.column === column); + } +} + +module.exports = BingoData; diff --git a/src/structures/SkyBlock/Static/Government.js b/src/structures/SkyBlock/Static/Government.js index 557e1d9e..d46b4d06 100644 --- a/src/structures/SkyBlock/Static/Government.js +++ b/src/structures/SkyBlock/Static/Government.js @@ -50,6 +50,13 @@ class GovernmentData { */ this.currentElectionFor = parseInt(data.current.year, 10) || null; } + /** + * As string + * @return {string} + */ + toString() { + return this.name; + } } module.exports = GovernmentData; diff --git a/tests/Client#getSkyblockBingo.js b/tests/Client#getSkyblockBingo.js new file mode 100644 index 00000000..8edb7f60 --- /dev/null +++ b/tests/Client#getSkyblockBingo.js @@ -0,0 +1,17 @@ +/* eslint-disable no-undef */ +const { SkyblockBingoData } = require('../src'); +const { client } = require('./Client.js'); +const { expect } = require('chai'); + +describe('Client#getSkyblockBingo', async () => { + let bingo; + it('expect not to throw', async () => { + bingo = await client.getSkyblockBingo(); + }); + it('should be an objecct', () => { + expect(bingo).to.be.an('object'); + }); + it('required keys should exist', () => { + expect(bingo).instanceOf(SkyblockBingoData); + }); +}); diff --git a/tests/Client#getSkyblockGoverment.js b/tests/Client#getSkyblockGoverment.js index 65987d69..3b6e97e5 100644 --- a/tests/Client#getSkyblockGoverment.js +++ b/tests/Client#getSkyblockGoverment.js @@ -1,19 +1,17 @@ /* eslint-disable no-undef */ -const { SkyblockGovernment } = require('../src/'); +const { SkyblockGovernment } = require('../src'); const { client } = require('./Client.js'); const { expect } = require('chai'); -describe('Client#getSkyblockMember', async () => { +describe('Client#getSkyblockGoverment', async () => { let goverment; - describe('Random (1)', async () => { - it('expect not to throw', async () => { - goverment = await client.getSkyblockGoverment(); - }); - it('should be an objecct', () => { - expect(goverment).to.be.an('object'); - }); - it('required keys should exist', () => { - expect(goverment).instanceOf(SkyblockGovernment); - }); + it('expect not to throw', async () => { + goverment = await client.getSkyblockGoverment(); + }); + it('should be an objecct', () => { + expect(goverment).to.be.an('object'); + }); + it('required keys should exist', () => { + expect(goverment).instanceOf(SkyblockGovernment); }); });