// ==UserScript== // @id iitc-plugin-missions@jonatkins // @name IITC plugin: Missions // @category Info // @version 0.0.2.@@DATETIMEVERSION@@ // @namespace https://github.com/jonatkins/ingress-intel-total-conversion // @updateURL @@UPDATEURL@@ // @downloadURL @@DOWNLOADURL@@ // @description [@@BUILDNAME@@-@@BUILDDATE@@] View missions. Marking progress on waypoints/missions basis. Showing mission paths on the map. // @include https://www.ingress.com/intel* // @include http://www.ingress.com/intel* // @match https://www.ingress.com/intel* // @match http://www.ingress.com/intel* // @grant none // ==/UserScript== @@PLUGINSTART@@ // PLUGIN START //////////////////////////////////////////////////////// var decodeWaypoint = function(data) { var result = { hidden: data[0], guid: data[1], title: data[2], typeNum: data[3], type: [null, "Portal", "Field Trip"][data[3]], objectiveNum: data[4], objective: [null, "Hack this Portal", "Capture or Upgrade Portal", "Create Link from Portal", "Create Field from Portal", "Install a Mod on this Portal", "Take a Photo", "View this Field Trip Waypoint", "Enter the Passphrase"][data[4]], }; if (result.typeNum === 1) { result.portal = window.decodeArray.portalSummary(data[5]); // Portal waypoints have the same guid as the respective portal. result.portal.guid = result.guid; } return result; }; var decodeMission = function(data) { return { guid: data[0], title: data[1], description: data[2], authorNickname: data[3], authorTeam: data[4], // Notice: this format is weird(100%: 1.000.000) ratingE6: data[5], medianCompletionTimeMs: data[6], numUniqueCompletedPlayers: data[7], typeNum: data[8], type: [null, "Sequential", "Non Sequential", "Hidden"][data[8]], waypoints: data[9].map(decodeWaypoint), image: data[10] }; }; var decodeMissionSummary = function(data) { return { guid: data[0], title: data[1], image: data[2], ratingE6: data[3], medianCompletionTimeMs: data[4] }; }; var timeToRemaining = function(t) { var data = parseInt(t / 86400) + 'd ' + (new Date(t % 86400 * 1000)).toUTCString().replace(/.*(\d{2}):(\d{2}):(\d{2}).*/, "$1h $2m $3s"); data = data.replace('0d', ''); data = data.replace('00h', ''); data = data.replace('00m', ''); return data.trim(); }; window.plugin.missions = { // 3 days. missionCacheTime: 3 * 24 * 3600 * 1E3, // 3 weeks. portalMissionsCacheTime: 21 * 24 * 3600 * 1E3, onPortalSelected: function(event) { /*if(event.selectedPortalGuid === event.unselectedPortalGuid) { return; }*/ if (window.selectedPortal === null) { return; } var portal = window.portals[window.selectedPortal]; if (!portal || (!portal.options.data.mission && !portal.options.data.mission50plus)) { return; } // After select. setTimeout(function() { // #resodetails $('.linkdetails').append(''); }, 0); }, openTopMissions: function(bounds) { bounds = bounds || window.map.getBounds(); this.loadMissionsInBounds(bounds, this.showMissionListDialog.bind(this)); }, openPortalMissions: function() { var me = this, portal = window.portals[window.selectedPortal]; if (!portal) { return; } this.loadPortalMissions(window.selectedPortal, function(missions) { if (!missions.length) { return; } if (missions.length === 1) { me.loadMission(missions[0].guid, me.showMissionDialog.bind(me)); } else { me.showMissionListDialog(missions); } }); }, openMission: function(guid) { this.loadMission(guid, this.showMissionDialog.bind(this)); }, showMissionDialog: function(mission) { var me = this; var markers = this.highlightMissionPortals(mission); dialog({ // id: 'mission-' + mission.guid, title: mission.title, height: 'auto', html: this.renderMission(mission), width: '450px', closeCallback: function() { me.unhighlightMissionPortals(markers); }, collapseCallback: this.collapseFix, expandCallback: this.collapseFix }); }, showMissionListDialog: function(missions) { dialog({ html: this.renderMissionList(missions), height: 'auto', width: '400px', collapseCallback: this.collapseFix, expandCallback: this.collapseFix }); }, collapseFix: function() { if (this && this.parentNode) { this.parentNode.style.height = 'auto'; } }, loadMissionsInBounds: function(bounds, callback, errorcallback) { var me = this; window.postAjax('getTopMissionsInBounds', { northE6: ((bounds.getNorth() * 1000000) | 0), southE6: ((bounds.getSouth() * 1000000) | 0), westE6: ((bounds.getWest() * 1000000) | 0), eastE6: ((bounds.getEast() * 1000000) | 0) }, function(data) { var missions = data.result.map(decodeMissionSummary); if (!missions) { if (errorcallback) { errorcallback('Invalid data'); } return; } callback(missions); }, function(error) { console.log('Error loading missions in bounds', arguments); if (errorcallback) { errorcallback(error); } }); }, loadPortalMissions: function(guid, callback, errorcallback) { var me = this; // Mission summary rarely goes stale. if (me.cacheByPortalGuid[guid] && this.cacheByPortalGuid[guid].time > (Date.now() - this.portalMissionsCacheTime)) { callback(me.cacheByPortalGuid[guid].data); return; } window.postAjax('getTopMissionsForPortal', { guid: window.selectedPortal }, function(data) { var missions = data.result.map(decodeMissionSummary); if (!missions) { if (errorcallback) { errorcallback('Invalid data'); } return; } window.runHooks('portalMissionsLoaded', { missions: missions, portalguid: guid }); me.cacheByPortalGuid[guid] = { time: Date.now(), data: missions }; me.saveData(); callback(missions); }, function(error) { console.log('Error loading portal missions', arguments); if (errorcallback) { errorcallback(error); } // awww }); }, loadMission: function(guid, callback, errorcallback) { var me = this; // TODO: we need to refresh data often enough, portal data can quickly go stale if (this.cacheByMissionGuid[guid] && this.cacheByMissionGuid[guid].time > (Date.now() - this.missionCacheTime)) { callback(this.getMissionCache(guid, true)); return; } window.postAjax('getMissionDetails', { guid: guid }, function(data) { var mission = decodeMission(data.result); if (!mission) { if (errorcallback) { errorcallback('Invalid data'); } return; } window.runHooks('missionLoaded', { mission: mission }); me.cacheByMissionGuid[guid] = { time: Date.now(), data: mission }; me.saveData(); callback(mission); }, function() { console.log('Error loading mission data', guid, arguments); if (errorcallback) { errorcallback(error); } // awww }); }, renderMissionList: function(missions) { return missions.map(this.renderMissionSummary, this).join(' '); }, renderMissionSummary: function(mission) { var cachedMission = this.getMissionCache(mission.guid); var html = ''; var checked = this.settings.checkedMissions[mission.guid]; html += '
'; html += ''; html += '
' + mission.title + '
'; if (cachedMission) { html += '' + cachedMission.authorNickname + ''; html += '
'; } html += '' + timeToRemaining((mission.medianCompletionTimeMs / 1000) | 0); html += '' + (((mission.ratingE6 / 100) | 0) / 100) + '%'; if (cachedMission) { html += '' + cachedMission.numUniqueCompletedPlayers; html += '' + cachedMission.waypoints.length; } html += '
'; html += '
'; return html; }, renderMission: function(mission) { var me = this; var checked = this.settings.checkedMissions[mission.guid]; //commondatastorage.googleapis.com/ingress.com/img/map_icons/linkmodeicon.png var html = '
'; html += ''; html += ''; html += ''; html += ''; html += '
' + mission.title + '
'; html += '' + mission.authorNickname + ''; html += '
'; html += '
'; html += '' + timeToRemaining((mission.medianCompletionTimeMs / 1000) | 0); html += '' + (((mission.ratingE6 / 100) | 0) / 100) + '%'; html += '' + mission.numUniqueCompletedPlayers; html += '' + mission.waypoints.length; html += '
'; html += '

'; html += mission.description; html += '

'; html += '
'; html += mission.waypoints.map(function(waypoint, index) { return me.renderMissionWaypoint(waypoint, index, mission); }).join(' '); return html; }, renderMissionWaypoint: function(waypoint, index, mission) { var html = ''; html += '

'; html += '

'; if (waypoint.portal) { var color = 'white'; if (waypoint.portal.team === 'R') { // Yay color = '#00c2ff'; } else if (waypoint.portal.team === 'E') { // Booo color = '#28f428'; } var realHealth = (waypoint.portal.resCount / 8) * (waypoint.portal.health / 100); html += this.renderPortalCircle(color, waypoint.portal.level, realHealth); /* var radius = ((realHealth * 360) | 0); html += '
'; html += '
'; html += '
'; // 360 + 45 html += '
' + waypoint.portal.level + '
'; html += '
'; */ } if (waypoint.portal) { html += ''; } if (waypoint.title) { html += waypoint.title; } else { html += 'Unknown'; } if (waypoint.portal) { html += ''; } html += '
'; /* checkbox_grey checkbox_orange checkbox_cyan */ var img = 'cyan'; if (index === 0) { img = 'orange'; } else if (!waypoint.objective) { img = 'grey'; } // ✓ var mwpid = mission.guid + '-' + waypoint.guid; var checked = this.settings.checkedWaypoints[mwpid]; html += ''; html += ''; html += ''; html += ''; html += (waypoint.objective ? waypoint.objective : '?'); html += '

'; return html; }, renderPortalCircle: function(portalColor, portalLevel, portalHealth) { var s = 20, bg = '#999', c = portalColor, i = 14, ic = '#555', s2 = ((s / 2) | 0), si = (((s - i) / 2) | 0), d = (portalHealth * 180) | 0, num = portalLevel; var html = '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
' + num; html += '
'; html += '
'; return html; }, toggleWaypoint: function(mid, wpid, dontsave) { var mwpid = mid + '-' + wpid; var el = document.getElementsByClassName('wp-' + mwpid); if (!this.settings.checkedWaypoints[mwpid]) { this.settings.checkedWaypoints[mwpid] = true; window.runHooks('waypointFinished', { mission: this.getMissionCache(mid), waypointguid: wpid }); $(el).show(); } else { delete this.settings.checkedWaypoints[mwpid]; $(el).hide(); } if (!dontsave) { this.saveData(); } }, toggleMission: function(mid) { var mission = this.getMissionCache(mid); if (!mission) { return; } var el = document.getElementsByClassName('m-' + mid); var sumel = document.getElementsByClassName('mc-' + mid); if (!this.settings.checkedMissions[mid]) { this.settings.checkedMissions[mid] = true; mission.waypoints.forEach(function(waypoint) { if (!this.settings.checkedWaypoints[mid + '-' + waypoint.guid]) { this.toggleWaypoint(mid, waypoint.guid, true); } }, this); $(el).show(); $(sumel).css('background-color', 'rgba(255, 187, 0, 0.3)'); window.runHooks('missionFinished', { mission: mission }); } else { delete this.settings.checkedMissions[mid]; mission.waypoints.forEach(function(waypoint) { if (this.settings.checkedWaypoints[mid + '-' + waypoint.guid]) { this.toggleWaypoint(mid, waypoint.guid, true); } }, this); $(el).hide(); $(sumel).css('background-color', ''); } this.saveData(); }, getMissionCache: function(guid, updatePortals) { if (this.cacheByMissionGuid[guid]) { var cache = this.cacheByMissionGuid[guid]; // Update portal data from map if older then 2 minutes. if (updatePortals && cache.time < (Date.now() - (2 * 60 * 1000))) { cache.data.waypoints.map(function(waypoint) { if (!waypoint.portal) { return; } var wp = window.portals[waypoint.portal.guid]; if (!wp) { return; } $.extend(waypoint.portal, wp.options.data); }); } return cache.data; } return null; }, getPortalCache: function(guid) { if (this.cacheByPortalGuid[guid]) { return this.cacheByPortalGuid[guid].data; } return null; }, saveData: function() { this.checkCacheSize(); localStorage['plugins-missions-portalcache'] = JSON.stringify(this.cacheByPortalGuid); localStorage['plugins-missions-missioncache'] = JSON.stringify(this.cacheByMissionGuid); localStorage['plugins-missions-settings'] = JSON.stringify(this.settings); }, loadData: function() { this.cacheByPortalGuid = JSON.parse(localStorage['plugins-missions-portalcache'] || '{}'); this.cacheByMissionGuid = JSON.parse(localStorage['plugins-missions-missioncache'] || '{}'); this.settings = JSON.parse(localStorage['plugins-missions-settings'] || '{}'); }, checkCacheSize: function() { if (JSON.stringify(this.cacheByPortalGuid).length > 1e6) { // 1 MB not MiB ;) this.cleanupPortalCache(); } if (JSON.stringify(this.cacheByMissionGuid).length > 2e6) { // 2 MB not MiB ;) this.cleanupMissionCache(); } }, // Cleanup oldest half of the data. cleanupPortalCache: function() { var me = this; var cache = Object.keys(this.cacheByPortalGuid); cache.sort(function(a, b) { return me.cacheByPortalGuid[a].time - me.cacheByPortalGuid[b].time; }); var toDelete = (cache.length / 2) | 0; cache.splice(0, toDelete + 1).forEach(function(el) { delete me.cacheByPortalGuid[el]; }); }, // Cleanup oldest half of the data. cleanupMissionCache: function() { var me = this; var cache = Object.keys(this.cacheByMissionGuid); cache.sort(function(a, b) { return me.cacheByMissionGuid[a].time - me.cacheByMissionGuid[b].time; }); var toDelete = (cache.length / 2) | 0; cache.splice(0, toDelete + 1).forEach(function(el) { delete me.cacheByMissionGuid[el]; }); }, highlightMissionPortals: function(mission) { var markers = []; var prevPortal = null; mission.waypoints.forEach(function(waypoint) { if (!waypoint.portal) { return; } var portal = window.portals[waypoint.portal.guid]; if (!portal) { // not in view? return; } var marker = L.circleMarker( L.latLng(portal.options.data.latE6 / 1E6, portal.options.data.lngE6 / 1E6), { radius: portal.options.radius + Math.ceil(portal.options.radius / 2), weight: 3, opacity: 1, color: '#222', fill: false, dashArray: null, clickable: false } ); this.missionLayer.addLayer(marker); markers.push(marker); if (prevPortal) { var line = L.geodesicPolyline([ L.latLng(prevPortal.options.data.latE6 / 1E6, prevPortal.options.data.lngE6 / 1E6), L.latLng(portal.options.data.latE6 / 1E6, portal.options.data.lngE6 / 1E6) ], { color: '#222', opacity: 1, weight: 2, clickable: false }); this.missionLayer.addLayer(line); markers.push(line); } prevPortal = portal; }, this); return markers; }, unhighlightMissionPortals: function(markers) { markers.forEach(function(marker) { this.missionLayer.removeLayer(marker); }, this); }, onPortalChanged: function(type, guid, oldval) { var portal; if (type === 'add' || type === 'update') { // Compatibility portal = window.portals[guid] || oldval; if (!portal.options.data.mission && !portal.options.data.mission50plus) { return; } if (this.markedStarterPortals[guid]) { return; } this.markedStarterPortals[guid] = L.circleMarker( L.latLng(portal.options.data.latE6 / 1E6, portal.options.data.lngE6 / 1E6), { radius: portal.options.radius + Math.ceil(portal.options.radius / 2), weight: 3, opacity: 1, color: '#555', fill: false, dashArray: null, clickable: false } ); this.missionStartLayer.addLayer(this.markedStarterPortals[guid]); } else if (type === 'delete') { portal = oldval; if (!this.markedStarterPortals[guid]) { return; } this.missionStartLayer.removeLayer(this.markedStarterPortals[guid]); delete this.markedStarterPortals[guid]; } }, setup: function() { this.cacheByPortalGuid = {}; this.cacheByMissionGuid = {}; this.markedStarterPortals = {}; this.markedMissionPortals = {}; this.loadData(); if (!this.settings.checkedWaypoints) { this.settings.checkedWaypoints = {}; } if (!this.settings.checkedMissions) { this.settings.checkedMissions = {}; } $('#toolbox').append('Missions in view'); // window.addPortalHighlighter('Mission start point', this.highlight.bind(this)); window.addHook('portalSelected', this.onPortalSelected.bind(this)); /* I know iitc has portalAdded event but it is missing portalDeleted. So we have to resort to Object.observe */ var me = this; if (Object.observe) { // Chrome Object.observe(window.portals, function(changes) { changes.forEach(function(change) { me.onPortalChanged(change.type, change.name, change.oldValue); }); }); } else { // Firefox why no Object.observer ? :< window.addHook('portalAdded', function(data) { me.onPortalChanged('add', data.portal.options.guid, data.portal); }); // TODO: bug iitc dev for portalRemoved event var oldDeletePortal = window.Render.prototype.deletePortalEntity; window.Render.prototype.deletePortalEntity = function(guid) { if (guid in window.portals) { me.onPortalChanged('delete', guid, window.portals[guid]); } oldDeletePortal.apply(this, arguments); }; } this.missionStartLayer = new L.LayerGroup(); this.missionLayer = new L.LayerGroup(); window.addLayerGroup('Mission start portals', this.missionStartLayer, false); window.addLayerGroup('Mission portals', this.missionLayer, true); window.pluginCreateHook('missionLoaded'); window.pluginCreateHook('portalMissionsLoaded'); window.pluginCreateHook('missionFinished'); window.pluginCreateHook('waypointFinished'); } }; var setup = window.plugin.missions.setup.bind(window.plugin.missions); // PLUGIN END ////////////////////////////////////////////////////////// @@PLUGINEND@@