diff --git a/plugins/missions.user.js b/plugins/missions.user.js index 97b94a4b..aa76a29a 100644 --- a/plugins/missions.user.js +++ b/plugins/missions.user.js @@ -2,11 +2,11 @@ // @id iitc-plugin-missions@jonatkins // @name IITC plugin: Missions // @category Info -// @version 0.0.1.@@DATETIMEVERSION@@ +// @version 0.0.2.@@DATETIMEVERSION@@ // @namespace https://github.com/jonatkins/ingress-intel-total-conversion // @updateURL @@UPDATEURL@@ // @downloadURL @@DOWNLOADURL@@ -// @description [@@BUILDNAME@@-@@BUILDDATE@@] WORK IN PROGRESS: view missions. Currently, only adds a mission start portal highlighter +// @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* @@ -19,33 +19,653 @@ // PLUGIN START //////////////////////////////////////////////////////// -// use own namespace for plugin -window.plugin.missions = function() {}; +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('Open mission(s)'); + }, 0); + }, -window.plugin.missions.highlight = function(data) { - var opacity = 0.7; - var color = undefined; + openTopMissions: function(bounds) { + bounds = bounds || window.map.getBounds(); + this.loadMissionsInBounds(bounds, this.showMissionListDialog.bind(this)); + }, - if (data.portal.options.data.mission50plus) { - color='red'; - } else if (data.portal.options.data.mission) { - color='darkorange'; - } + openPortalMissions: function() { + var me = this, + portal = window.portals[window.selectedPortal]; + if (!portal) { + return; + } + this.loadPortalMissions(window.selectedPortal, function(missions) { + if (!missions.length) { + return; + } - if (color) { - data.portal.setStyle({fillColor: color, fillOpacity: opacity}); - } -} + if (missions.length === 1) { + me.loadMission(missions[0].guid, me.showMissionDialog.bind(me)); + } else { + me.showMissionListDialog(missions); + } + }); + }, -window.plugin.missions.setup = function() { + openMission: function(guid) { + this.loadMission(guid, this.showMissionDialog.bind(this)); + }, - window.addPortalHighlighter('Mission start point', window.plugin.missions.highlight); + 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.collapsFix, + expandCallback: this.collapsFix + }); + }, + showMissionListDialog: function(missions) { + dialog({ + html: this.renderMissionList(missions), + height: 'auto', + width: '400px', + collapseCallback: this.collapsFix, + expandCallback: this.collapsFix + }); + }, + + collapsFix: 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('Open mission(s) in window'); + + // 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; +var setup = window.plugin.missions.setup.bind(window.plugin.missions); // PLUGIN END ////////////////////////////////////////////////////////// -@@PLUGINEND@@ +@@PLUGINEND@@ \ No newline at end of file