/* global L -- eslint */
/**
* @file Hand any of these functions the details-hash of a portal, and they
* will return pretty, displayable HTML or parts thereof.
* @module portal_detail_display_tools
*/
/**
* Provides historical details about a portal including visitation, capture, and scout control status.
*
* @function getPortalHistoryDetails
* @param {Object} d - The portal detail object containing the history properties.
* @returns {string} HTML string representing the historical details of the portal.
*/
window.getPortalHistoryDetails = function (d) {
if (!d.history) {
return '<div id="historydetails" class="missing">History missing</div>';
}
var classParts = {};
['visited', 'captured', 'scoutControlled'].forEach(function (k) {
classParts[k] = d.history[k] ? 'class="completed"' : '';
});
return L.Util.template(
'<div id="historydetails">History: ' +
'<span id="visited" {visited}>visited</span> | ' +
'<span id="captured" {captured}>captured</span> | ' +
'<span id="scout-controlled" {scoutControlled}>scout controlled</span>' +
'</div>',
classParts
);
};
/**
* Returns displayable text and link about portal range including base range, link amp boost, and total range.
*
* @function getRangeText
* @param {Object} d - The portal detail object containing range information.
* @returns {Array} An array containing the range label, HTML content, and a tooltip title.
*/
window.getRangeText = function (d) {
var range = window.getPortalRange(d);
var title = `Base range:\t${window.digits(Math.floor(range.base))}m\nLink amp boost:\t×${range.boost}\nRange:\t${window.digits(Math.floor(range.range))}m`;
if (!range.isLinkable) title += '\nPortal is missing resonators,\nno new links can be made';
return [
'range',
'<a onclick="window.rangeLinkClick()"' +
(range.isLinkable ? '' : ' style="text-decoration:line-through;"') +
'>' +
window.formatDistance(range.range) +
'</a>',
title,
];
};
/**
* Given portal details, returns HTML code to display mod details.
*
* @function getModDetails
* @param {Object} d - The portal detail object containing mod information.
* @returns {string} HTML string representing the mod details of the portal.
*/
window.getModDetails = function (d) {
var mods = [];
var modsTitle = [];
var modsColor = [];
$.each(d.mods, function (ind, mod) {
var modName = '';
var modTooltip = '';
var modColor = '#000';
if (mod) {
// all mods seem to follow the same pattern for the data structure
// but let's try and make this robust enough to handle possible future differences
modName = mod.name || '(unknown mod)';
if (mod.rarity) {
modName = mod.rarity.capitalize().replace(/_/g, ' ') + ' ' + modName;
}
modTooltip = modName + '\n';
if (mod.owner) {
modTooltip += 'Installed by: ' + mod.owner + '\n';
}
if (mod.stats) {
modTooltip += 'Stats:';
for (var key in mod.stats) {
if (!Object.hasOwn(mod.stats, key)) continue;
var val = mod.stats[key];
// if (key === 'REMOVAL_STICKINESS' && val == 0) continue; // stat on all mods recently - unknown meaning, not displayed in stock client
// special formatting for known mod stats, where the display of the raw value is less useful
if (key === 'HACK_SPEED')
val = val / 10000 + '%'; // 500000 = 50%
else if (key === 'HIT_BONUS')
val = val / 10000 + '%'; // 300000 = 30%
else if (key === 'ATTACK_FREQUENCY')
val = val / 1000 + 'x'; // 2000 = 2x
else if (key === 'FORCE_AMPLIFIER')
val = val / 1000 + 'x'; // 2000 = 2x
else if (key === 'LINK_RANGE_MULTIPLIER')
val = val / 1000 + 'x'; // 2000 = 2x
else if (key === 'LINK_DEFENSE_BOOST')
val = val / 1000 + 'x'; // 1500 = 1.5x
else if (key === 'REMOVAL_STICKINESS' && val > 100) val = val / 10000 + '%'; // an educated guess
// else display unmodified. correct for shield mitigation and multihack - unknown for future/other mods
modTooltip += '\n+' + val + ' ' + key.capitalize().replace(/_/g, ' ');
}
}
if (mod.rarity) {
modColor = window.COLORS_MOD[mod.rarity];
} else {
modColor = '#fff';
}
}
mods.push(modName);
modsTitle.push(modTooltip);
modsColor.push(modColor);
});
var t = '';
for (let i = 0; i < mods.length; i++) {
t += '<span' + (modsTitle[i].length ? ' title="' + modsTitle[i] + '"' : '') + ' style="color:' + modsColor[i] + '">' + mods[i] + '</span>';
}
// and add blank entries if we have less than 4 mods (as the server no longer returns all mod slots, but just the filled ones)
for (let i = mods.length; i < 4; i++) {
t += '<span style="color:#000"></span>';
}
return t;
};
/**
* Generates text representing the current and total energy of a portal.
*
* @function getEnergyText
* @param {Object} d - The portal detail object containing energy information.
* @returns {Array} An array containing the energy label, formatted energy values, and a tooltip title.
*/
window.getEnergyText = function (d) {
var currentNrg = window.getCurrentPortalEnergy(d);
var totalNrg = window.getTotalPortalEnergy(d);
var title = currentNrg + ' / ' + totalNrg;
var fill = window.prettyEnergy(currentNrg) + ' / ' + window.prettyEnergy(totalNrg);
return ['energy', fill, title];
};
/**
* Generates HTML details for resonators deployed on a portal.
*
* @function getResonatorDetails
* @param {Object} d - The portal detail object containing resonator information.
* @returns {string} HTML string representing the resonator details of the portal.
*/
window.getResonatorDetails = function (d) {
var resoDetails = [];
// octant=slot: 0=E, 1=NE, 2=N, 3=NW, 4=W, 5=SW, 6=S, SE=7
// resos in the display should be ordered like this:
// N NE Since the view is displayed in rows, they
// NW E need to be ordered like this: N NE NW E W SE SW S
// W SE i.e. 2 1 3 0 4 7 5 6
// SW S
// note: as of 2014-05-23 update, this is not true for portals with empty slots!
var processResonatorSlot = function (reso, slot) {
var lvl = 0,
nrg = 0,
owner = null;
if (reso) {
lvl = parseInt(reso.level);
nrg = parseInt(reso.energy);
owner = reso.owner;
}
resoDetails.push(window.renderResonatorDetails(slot, lvl, nrg, owner));
};
// if all 8 resonators are deployed, we know which is in which slot
if (d.resonators.length === 8) {
// fully deployed - we can make assumptions about deployment slots
$.each([2, 1, 3, 0, 4, 7, 5, 6], function (ind, slot) {
processResonatorSlot(d.resonators[slot], slot);
});
} else {
// partially deployed portal - we can no longer find out which resonator is in which slot
for (var ind = 0; ind < 8; ind++) {
processResonatorSlot(ind < d.resonators.length ? d.resonators[ind] : null, null);
}
}
return '<table id="resodetails">' + window.genFourColumnTable(resoDetails) + '</table>';
};
/**
* Helper function that renders the HTML for a given resonator.
*
* @function renderResonatorDetails
* @param {number} slot - The slot number where the resonator is deployed. Starts with 0 (east) and rotates clockwise.
* So, last one is 7 (southeast).
* @param {number} level - The level of the resonator.
* @param {number} nrg - The energy of the resonator.
* @param {string|null} nick - The nickname of the owner of the resonator, or null if not applicable.
* @returns {Array} An array containing the HTML content of the resonator and the owner's nickname.
*/
window.renderResonatorDetails = function (slot, level, nrg, nick) {
const className = window.OCTANTS[slot] === 'N' ? 'meter north' : 'meter';
var max = window.RESO_NRG[level];
var fillGrade = level > 0 ? (nrg / max) * 100 : 0;
var inf =
(level > 0 ? 'energy:\t' + nrg + ' / ' + max + ' (' + Math.round(fillGrade) + '%)\n' + 'level:\t' + level + '\n' + 'owner:\t' + nick + '\n' : '') +
(slot !== null ? 'octant:\t' + window.OCTANTS[slot] + ' ' + window.OCTANTS_ARROW[slot] : '');
var style = fillGrade ? 'width:' + fillGrade + '%; background:' + window.COLORS_LVL[level] + ';' : '';
var color = level < 3 ? '#9900FF' : '#FFFFFF';
var lbar = level > 0 ? '<span class="meter-level" style="color: ' + color + ';"> L ' + level + ' </span>' : '';
var fill = '<span style="' + style + '"></span>';
var meter = '<span class="' + className + '" title="' + inf + '">' + fill + lbar + '</span>';
nick = nick ? '<span class="nickname">' + nick + '</span>' : null;
return [meter, nick || ''];
};
/**
* Calculates the AP gain from destroying and then capturing a portal by deploying resonators.
*
* @function getAttackApGainText
* @param {Object} d - The portal detail object containing portal information.
* @param {number} fieldCount - The number of fields linked to the portal.
* @param {number} linkCount - The number of links connected to the portal.
* @returns {Array} An array containing the label 'AP Gain', total AP gain, and a breakdown tooltip.
*/
window.getAttackApGainText = function (d, fieldCount, linkCount) {
var breakdown = window.getAttackApGain(d, fieldCount, linkCount);
var totalGain = breakdown.enemyAp;
var t = '';
if (window.teamStringToId(window.PLAYER.team) === window.teamStringToId(d.team)) {
totalGain = breakdown.friendlyAp;
t += 'Friendly AP:\t' + breakdown.friendlyAp + '\n';
t += ' Deploy ' + breakdown.deployCount + ', ';
t += 'Upgrade ' + breakdown.upgradeCount + '\n';
t += '\n';
}
t += 'Enemy AP:\t' + breakdown.enemyAp + '\n';
t += ' Destroy AP:\t' + breakdown.destroyAp + '\n';
t += ' Capture AP:\t' + breakdown.captureAp + '\n';
return ['AP Gain', window.digits(totalGain), t];
};
/**
* Provides details about the hack count and cooldown time of a portal.
*
* @function getHackDetailsText
* @param {Object} d - The portal detail object containing hack information.
* @returns {Array} An array containing the label 'hacks', short hack info, and a detailed tooltip.
*/
window.getHackDetailsText = function (d) {
var hackDetails = window.getPortalHackDetails(d);
var shortHackInfo = hackDetails.hacks + ' @ ' + window.formatInterval(hackDetails.cooldown);
var title =
`Hacks available every 4 hours\n` +
`Hack count:\t${hackDetails.hacks}\n` +
`Cooldown time:\t${window.formatInterval(hackDetails.cooldown)}\n` +
`Burnout time:\t${window.formatInterval(hackDetails.burnout)}`;
return ['hacks', shortHackInfo, title];
};
/**
* Generates text representing the total mitigation provided by shields and links on a portal.
*
* @function getMitigationText
* @param {Object} d - The portal detail object containing mitigation information.
* @param {number} linkCount - The number of links connected to the portal.
* @returns {Array} An array containing the label 'shielding', short mitigation info, and a detailed tooltip.
*/
window.getMitigationText = function (d, linkCount) {
var mitigationDetails = window.getPortalMitigationDetails(d, linkCount);
var mitigationShort = mitigationDetails.total;
if (mitigationDetails.excess) mitigationShort += ' (+' + mitigationDetails.excess + ')';
var title =
`Total shielding:\t${mitigationDetails.shields + mitigationDetails.links}\n` +
`- active:\t${mitigationDetails.total}\n` +
`- excess:\t${mitigationDetails.excess}\n` +
`From\n` +
`- shields:\t${mitigationDetails.shields}\n` +
`- links:\t${mitigationDetails.links} (${mitigationDetails.linkDefenseBoost}x)`;
return ['shielding', mitigationShort, title];
};
/**
* Displays a dialog with links to show the specified location on various map services.
*
* @function showPortalPosLinks
* @param {number} lat - Latitude of the location.
* @param {number} lng - Longitude of the location.
* @param {string} name - Name of the location.
*/
window.showPortalPosLinks = function (lat, lng, name) {
var encoded_name = encodeURIComponent(name);
var qrcode = '<div id="qrcode"></div>';
var script = "<script>$('#qrcode').qrcode({text:'GEO:" + lat + ',' + lng + "'});</script>";
var gmaps = '<a href="https://maps.google.com/maps?ll=' + lat + ',' + lng + '&q=' + lat + ',' + lng + '%20(' + encoded_name + ')">Google Maps</a>';
var bingmaps =
'<a href="https://www.bing.com/maps/?v=2&cp=' + lat + '~' + lng + '&lvl=16&sp=Point.' + lat + '_' + lng + '_' + encoded_name + '___">Bing Maps</a>';
var osm = '<a href="https://www.openstreetmap.org/?mlat=' + lat + '&mlon=' + lng + '&zoom=16">OpenStreetMap</a>';
var latLng = '<span>' + lat + ',' + lng + '</span>';
window.dialog({
html: '<div style="text-align: center;">' + qrcode + script + gmaps + '; ' + bingmaps + '; ' + osm + '<br />' + latLng + '</div>',
title: name,
id: 'poslinks',
});
};