import { languageString } from 'warframe-worldstate-data/utilities'; import WorldstateObject from './WorldstateObject.js'; const apiBase = process.env.API_BASE_URL || 'https://api.warframestat.us'; const bountyRewardRegex = /(?:Tier([ABCDE])|Narmer)Table([ABC])Rewards/i; const ghoulRewardRegex = /GhoulBountyTable([AB])Rewards/i; /** * Determine the level string for the bounty * @param {object} job Original raw job data * @returns {string} level range string */ const getLevelString = (job) => `${job.minEnemyLevel} - ${job.maxEnemyLevel}`; const determineLocation = (i18n, isVault, raw) => { const last = String(i18n).split('/').slice(-1)[0]; const bountyMatches = last.match(bountyRewardRegex); const ghoulMatches = last.match(ghoulRewardRegex); const isBounty = bountyMatches && bountyMatches.length; const isGhoul = ghoulMatches && ghoulMatches.length; const isCetus = /eidolonjob/i.test(i18n); const isVallis = /venusjob/i.test(i18n); const isDeimos = /deimosmissionrewards/i.test(i18n); const rotation = isBounty ? bountyMatches[2] : ''; const levelString = getLevelString(raw); let location; let levelClause; if (isCetus) { location = 'Earth/Cetus '; if (isGhoul) { levelClause = `(Level ${levelString} Ghoul Bounty)`; } else { levelClause = `(Level ${levelString} Cetus Bounty)`; } } if (isVallis) { location = 'Venus/Orb Vallis '; levelClause = `(Level ${levelString} Orb Vallis Bounty)`; } if (isDeimos) { location = 'Deimos/Cambion Drift '; // this will need to be updated when the actual drops are released const variant = isVault ? 'Isolation Vault' : 'Cambion Drift Bounty'; levelClause = `(Level ${levelString} ${variant})`; } const locationWRot = `${location}${levelClause}, Rot ${rotation.length ? rotation : 'A'}`; return { location, locationWRot }; }; const getBountyRewards = async (i18n, isVault, raw) => { let location; let locationWRot; if (i18n.endsWith('PlagueStarTableRewards')) { location = 'plague star'; locationWRot = 'Earth/Cetus (Level 15 - 25 Plague Star), Rot A'; } if (!location || !locationWRot) { ({ location, locationWRot } = determineLocation(i18n, isVault, raw)); } const url = `${apiBase}/drops/search/${encodeURIComponent(location)}?grouped_by=location`; const reply = await fetch(url) .then((res) => res.json()) .catch(() => {}); // swallow errors const pool = (reply || {})[locationWRot]; if (!pool) { return ['Pattern Mismatch. Results inaccurate.']; } const results = pool.rewards; if (results) { return Array.from(new Set(results.map((result) => result.item))); } /* istanbul ignore next */ return []; }; const FIFTY_MINUTES = 3000000; /** * Represents a syndicate daily mission * @augments {WorldstateObject} */ export default class SyndicateJob extends WorldstateObject { /** * Generate a job with async data (reward pool) * @param {object} data The syndicate mission data * @param {Date} expiry The syndicate job expiration * @param {object} deps The dependencies object * @param {string} deps.locale Locale to use for translations * @returns {Promise<SyndicateJob>} The created SyndicateJob object with rewardPool */ static async build(data, expiry, deps) { const job = new SyndicateJob(data, expiry, deps); job.rewardPool = await getBountyRewards(data.rewards, data.isVault, data); return job; } /** * Construct a job without async data (reward pool) * @param {object} data The syndicate mission data * @param {Date} expiry The syndicate job expiration * @param {object} deps The dependencies object * @param {string} deps.locale Locale to use for translations * * This DOES NOT populate the reward pool */ constructor(data, expiry, { locale = 'en' }) { super({ _id: { $oid: data.JobCurrentVersion ? data.JobCurrentVersion.$oid : `${(data.jobType || '').split('/').slice(-1)[0]}${expiry.getTime()}`, }, }); /** * Array of strings describing rewards * @type {Array.<string>} */ this.rewardPool = []; const chamber = ((data.locationTag || '').match(/[A-Z]+(?![a-z])|[A-Z]?[a-z]+|\d+/g) || []).join(' '); /** * The type of job this is * @type {string} */ this.type = languageString(data.jobType, locale) || `${data.isVault ? 'Isolation Vault' : ''} ${chamber}`; /** * Array of enemy levels * @type {Array.<number>} */ this.enemyLevels = [data.minEnemyLevel, data.maxEnemyLevel]; /** * Array of standing gains per stage of job * @type {Array.<number>} */ this.standingStages = data.xpAmounts; /** * Minimum mastery required to participate * @type {number} */ this.minMR = data.masteryReq || 0; /** * Whether or not this is a Vault job. * No indication for difference of normal vs arcana vaults. * @type {boolean} */ this.isVault = data.isVault; /** * Corresponding chamber. Nullable * @type {string|null} */ this.locationTag = data.locationTag; /** * End time for the syndicate mission. * Should be inherited from the Syndicate, but some are timebound. * @type {Date} */ this.expiry = expiry; /** * What time phase this bounty is bound to * @type {string} */ this.timeBound = undefined; if (data.jobType && data.jobType.toLowerCase().includes('narmer')) { if (data.jobType.toLowerCase().includes('eidolon')) { this.timeBound = 'day'; this.expiry = new Date(this.expiry.getTime() - FIFTY_MINUTES); } else { this.timeBoound = 'night'; } } } }