= IITC Passcode Redemption overhaul, part 2

* Display random words of encouragement for successfully redeemed passcodes
* Refactor redeeming into smaller functions, using more jQuery logic
* Add redeem "handlers" - combinations of "decoders" and "formatters" for each taxonomy of item
* Add heuristic item guessing. If IITC can't figure out directly what an item is, try to guess what taxonomy of item it would be closest to. (For instance, if an item is truly unknown, but has a "rarity" attribute, it's going to be close to a modResource. Likewise, if an unknown item has a "level" attribute, it's going to be close to a resourceWithLevels.
** Tailor formatting and decoding based upon that guess. Worst case: the item is truly a mystery. Don't discard it: display it in a default format.
** Let the user know if we tried to heuristically guess an item. This won't happen unless something changes on the Ingress backend.
This commit is contained in:
Morgan Jones 2013-04-25 01:09:22 -05:00
parent a80617e6e7
commit 2d662f9e79

View File

@ -1,101 +1,231 @@
// REDEEMING /////////////////////////////////////////////////////////
window.REDEEM_RES_LONG = {'RES_SHIELD' : 'Portal Shield',
'EMITTER_A' : 'Resonator',
'EMP_BURSTER' : 'XMP Burster',
'POWER_CUBE' : 'Power Cube'};
/* Resource type names mapped to actual names and abbreviations.
* Add more here if necessary.
*/
window.REDEEM_RESOURCES = {
RES_SHIELD: {long: 'Portal Shield', short: 'SH'},
EMITTER_A: {long: 'Resonator', short: 'R'},
EMP_BURSTER: {long: 'XMP Burster', short: 'X'},
POWER_CUBE: {long: 'Power Cube', short: 'C'}
};
window.REDEEM_RES_SHORT = {'RES_SHIELD' : 'S',
'EMITTER_A' : 'R',
'EMP_BURSTER' : 'X',
'POWER_CUBE' : 'C'};
/* Redemption errors. Very self-explanatory.
*/
window.REDEEM_ERRORS = {
ALREADY_REDEEMED: 'The passcode has already been redeemed.',
ALREADY_REDEEMED_BY_PLAYER : 'You have already redeemed this passcode.',
INVALID_PASSCODE: 'This passcode is invalid.'
};
window.REDEEM_ERRORS = {'ALREADY_REDEEMED' : 'The passcode has already been redeemed.',
'ALREADY_REDEEMED_BY_PLAYER' : 'You have already redeemed this passcode.',
'INVALID_PASSCODE' : 'This passcode is invalid.'};
/* These are HTTP status codes returned by the redemption API.
* TODO: Move to another file? Use more generally across IITC?
*/
window.REDEEM_STATUSES = {
429: 'You have been rate-limited by the server. Wait a bit and try again.',
500: 'Internal server error'
};
window.REDEEM_STATUSES = {429 : 'You have been rate-limited by the server. Wait a bit and try again.'};
/* Encouragement for people who got it in.
* Just for fun.
*/
window.REDEEM_ENCOURAGEMENT = [
"Passcode accepted!",
"Access granted.",
"Asset transfer in progress.",
"Well done, Agent.",
"Make the " + {'RESISTANCE' : 'Resistance', 'ALIENS' : 'Enlightened'}[PLAYER.team] + " proud!"
];
/* Redemption "handlers" handle decoding and formatting for rewards.
*
* Redemption "decoders" are used for returning the primary attribute (key) from
* different types of items. Pretty self-explanatory.
*
* Redemption "formatters" are used for formatting specific types of password rewards.
* Right now, Ingress has resourceWithLevels (leveled resources) and modResource (mods).
* Resources with levels have levels, and mods have rarity. Format them appropriately.
*/
window.REDEEM_HANDLERS = {
'resourceWithLevels' : {
decode: function(type, resource) {return resource.level;},
format: function(acquired, level) {
var prefix = '<span style="color: ' + (window.COLORS_LVL[level] || 'white') + ';">';
var suffix = '</span>';
return {
table: '<td>' + prefix + 'L' + level + suffix + '</td><td>' + acquired.name.long + ' [' + acquired.count + ']</td>',
html: acquired.count + '@' + acquired.name.short + prefix + level + suffix,
plain: acquired.count + '@' + acquired.name.short + level
};
}
},
'modResource' : {
decode: function(type, resource) {return resource.rarity;},
format: function(acquired, rarity) {
var prefix = '<span style="color: ' + (window.COLORS_MOD[rarity] || 'white') + ';">';
var suffix = '</span>';
var abbreviation = rarity.split('_').map(function (i) {return i[0];}).join('');
return {
table: '<td>' + prefix + abbreviation + suffix + '</td><td>' + acquired.name.long + ' [' + acquired.count + ']</td>',
html: acquired.count + '@' + prefix + abbreviation + suffix,
plain: acquired.count + '@' + abbreviation
};
}
},
'default' : {
decode: function(type, resource) {return 'UNKNOWN';},
format: function(acquired, group) {
return {
table: '<td>+</td><td>' + acquired.name.long + ' [' + acquired.count + ']</td>',
html: acquired.count + '@' + acquired.name.short,
plain: acquired.count + '@' + acquired.name.short
};
}
}
};
/* Redemption "hints" hint at what an unknown resource might be from its object properties.
*/
window.REDEEM_HINTS = {
level: 'resourceWithLevels',
rarity: 'modResource'
};
window.handleRedeemResponse = function(data, textStatus, jqXHR) {
var passcode = this.passcode, to_alert, to_log;
if(data.error) {
// Errors are now in window.REDEEM_ERRORS.
var error = window.REDEEM_ERRORS[data.error] || 'There was a problem redeeming the passcode. Try again?';
// Show an alert and add a console log
alert('<strong>' + data.error + '</strong>\n' + error);
console.log(this.passcode + ' => [ERROR] ' + data.error);
to_alert = '<strong>' + data.error + '</strong><br />' + (window.REDEEM_ERRORS[data.error] || 'There was a problem redeeming the passcode. Try again?');
to_log = '[ERROR] ' + data.error;
} else if(data.result) {
// Successful redemption
var payload = {};
var table_result = ['<th colspan="2"><strong>Passcode accepted!</strong></th>'], plain_result = [];
var table = '', plain = '';
// Get AP, XM, and other static quantities
var scores = [[parseInt(data.result.apAward), 'AP'], [parseInt(data.result.xmAward), 'XM']];
for(var i in scores) {
if(scores[i][0] > 0) {
table_result.push('<td>+</td><td>' + scores[i][0] + ' ' + scores[i][1] + '</td>');
plain_result.push(scores[i][0] + ' ' + scores[i][1]);
}
}
var payload = {};
var encouragement = window.REDEEM_ENCOURAGEMENT[Math.floor(Math.random() * window.REDEEM_ENCOURAGEMENT.length)];
var inferred = [];
var results = {
'table' : ['<th colspan="2" style="text-align: left;"><strong>' + encouragement + '</strong></th>'],
'html' : [],
'plain' : []
};
// Track frequencies and levels of items
for(var i in data.result.inventoryAward) {
var acquired = data.result.inventoryAward[i][2], primary, secondary, type;
if(acquired.modResource) {
primary = acquired.modResource.resourceType;
secondary = acquired.modResource.rarity;
type = 'mod';
} else if(acquired.resourceWithLevels) {
primary = acquired.resourceWithLevels.resourceType;
secondary = parseInt(acquired.resourceWithLevels.level);
type = 'leveled';
}
$.each(data.result.inventoryAward, function (award_idx, award) {
var acquired = award[2], handler, type, key, name;
payload[primary] = payload[primary] || {};
payload[primary][secondary] = payload[primary][secondary] || {};
payload[primary][secondary].type = payload[primary][secondary].type || type;
payload[primary][secondary].count = payload[primary][secondary].count || 0;
payload[primary][secondary].count += 1;
// The "what the heck is this item" heuristic
$.each(acquired, function (taxonomy, resource) {
if('resourceType' in resource) {
if(taxonomy in window.REDEEM_HANDLERS) {
// Cool. We know how to directly handle this item.
handler = {
functions: window.REDEEM_HANDLERS[taxonomy],
taxonomy: taxonomy,
processed_as: taxonomy
};
} else {
// Let's see if we can get a hint for how we should handle this.
$.each(resource, function (resource_key, resource_value) {
if(resource_key in window.REDEEM_HINTS) {
// We're not sure what this item is, but we can process it like another item
handler = {
functions: (window.REDEEM_HANDLERS[window.REDEEM_HINTS[resource_key]] || window.REDEEM_HANDLERS['default']),
taxonomy: taxonomy,
processed_as: window.REDEEM_HINTS[resource_key]
};
return false;
}
return true;
});
// Fall back to the default handler if necessary
handler = handler || {
functions: window.REDEEM_HANDLERS['default'],
taxonomy: taxonomy,
processed_as: 'default'
};
}
// Collect the data that we know
type = resource.resourceType;
key = handler.functions.decode(type, resource);
name = window.REDEEM_RESOURCES[type] || {long: type + '*', short: type[0] + '*'};
// Decide if we inferred this resource
if(!(type in window.REDEEM_RESOURCES) || handler.taxonomy !== handler.processed_as) {
inferred.push({type: type, key: key, handler: handler});
}
return false;
}
return true;
});
// Update frequencies
payload[type] = payload[type] || {};
payload[type][key] = payload[type][key] || {};
payload[type][key].handler = payload[type][key].handler || handler;
payload[type][key].type = payload[type][key].type || type;
payload[type][key].name = payload[type][key].name || name;
payload[type][key].count = payload[type][key].count || 0;
payload[type][key].count += 1;
});
// Get AP, XM, and other static quantities
$.each([{label: 'AP', award: parseInt(data.result.apAward)}, {label: 'XM', award: parseInt(data.result.xmAward)}], function(idx, val) {
if(val.award > 0) {
var formatted = val.award + ' ' + val.label;
results.table.push('<td>+</td><td>' + formatted + '</td>');
results.html.push(formatted);
results.plain.push(formatted);
}
return true;
});
// Build the formatted results alphabetically
$.each(Object.keys(payload).sort(), function(type_idx, type) {
$.each(Object.keys(payload[type]).sort(), function(key_idx, key) {
var acquired = payload[type][key];
$.each(acquired.handler.functions.format(acquired, key), function(format, string) {
results[format].push(string);
return true;
});
return true;
});
return true;
});
if (inferred.length > 0) {
results.table.push('<td style="font-family: monospace;">**<td style="font-family: monospace;"><strong>IITC had to guess!</strong></td>');
results.table.push('<td style="font-family: monospace;">**<td style="font-family: monospace;"><strong>Submit a log including:</strong></td>');
$.each(inferred, function (idx, val) {
var type = val.type + ':' + val.key, taxonomy = val.handler.taxonomy + ' =~ ' + val.handler.processed_as;
results.table.push('<td style="font-family: monospace;">!</td><td style="font-family: monospace;"><em>' + type + '</em></td>');
results.table.push('<td style="font-family: monospace;">!</td><td style="font-family: monospace;">' + taxonomy + '</td>');
console.log(passcode + ' => [INFERRED] ' + type + ' :: ' + taxonomy);
});
}
// Build the table and plaintext arrays
var keys = Object.keys(payload).sort();
for(var k in keys) {
var primary = payload[keys[k]], long_name = window.REDEEM_RES_LONG[keys[k]] || keys[k], short_name = window.REDEEM_RES_SHORT[keys[k]] || '?';
var table_array = [], plain_array = [];
for(var secondary in primary) {
var acquired = primary[secondary];
var span_prefix = acquired.type === 'leveled' ? '<span style="color: ' + window.COLORS_LVL[secondary] + ';">' : '<span style="color: ' + window.COLORS_MOD[secondary] + ';">';
var span_infix = acquired.type === 'leveled' ? secondary : secondary.split('_').map(function (i) {return i[0];}).join('');
var span_suffix = '</span>'
table_array.push('<td>' + span_prefix + (acquired.type === 'leveled' ? 'L' : '') + span_infix + span_suffix + '</td><td>' + long_name + ' [' + primary[secondary].count + ']</td>');
plain_array.push(primary[secondary].count + '@' + (acquired.type === 'leveled' ? short_name : '') + span_prefix + span_infix + span_suffix);
}
table_result.push(table_array.join(''));
plain_result.push(plain_array.join('/'));
}
// Add more HTML tags
plain = '<span style="font-family: monospace;">' + plain_result.join('/') + '</span>';
table_result.push('<td style="font-family: monospace;">&gt;&gt;</td><td><a href="javascript:alert(\'' + escape(plain) + '\', true);" style="font-family: monospace;">[plaintext]</a>');
table = '<table class="redeem-result">' + table_result.map(function(a) {return '<tr>' + a + '</tr>';}).join("\n") + '</table>';
// Add table footers
results.table.push('<td style="font-family: monospace;">&gt;&gt;</td><td><a href="javascript:alert(\'' +
escape('<span style="font-family: monospace;"><strong>' + encouragement + '</strong><br />' + results.html.join('/') + '</span>') +
'\', true);" style="font-family: monospace;">[plaintext]</a>');
// Display formatted versions in a table, plaintext, and the console log
alert(table, true);
console.log(this.passcode + ' => ' + $(plain).text());
to_alert = '<table class="redeem-result">' + results.table.map(function(a) {return '<tr>' + a + '</tr>';}).join("\n") + '</table>';
to_log = results.plain.join('/');
}
alert(to_alert, true);
console.log(passcode + ' => ' + to_log);
}
window.setupRedeem = function() {
$("#redeem").keypress(function(e) {
if((e.keyCode ? e.keyCode : e.which) != 13) return;
if((e.keyCode ? e.keyCode : e.which) !== 13) return;
var data = {passcode: $(this).val()};
window.postAjax('redeemReward', data, window.handleRedeemResponse,
function(response) {
var extra = ''
var extra = '';
if(response.status) {
extra = (window.REDEEM_STATUSES[response.status] || 'The server indicated an error.') + ' (HTTP ' + response.status + ')';
} else {