Last active
October 19, 2017 15:41
-
-
Save Domiii/73836e54ce2bdbc136a6560dd1941a15 to your computer and use it in GitHub Desktop.
Realm of Empiires - Assist
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
######################################################################## | |
Intro | |
######################################################################## | |
Title: ROE Assist | |
Description: Loose collection of scripts that might make playing ROE a little bit more interesting. | |
Video of a very early version of the auto raid loop: http://i.imgur.com/tjjMOtg.gifv (30s video of 25 min. play-time) | |
######################################################################## | |
Instructions | |
######################################################################## | |
0) Make sure, you are running the game in a web browser, and in full-screen mode (important!) | |
1) Open your browser's console | |
2) Copy the following into your browser's console (then hit ENTER): $.getScript('https://goo.gl/KjDUxY') | |
4) DONE! You will now see new buttons showing up at the top. | |
NOTE 1: The script does not do anything unless you start pressing buttons | |
NOTE 2: The script does not store itself anywhere. Once you reload the page, it's gone, and you have to copy it over again! | |
NOTE 3: The script has an "Uninstall" button to allow you to easily get rid of it, even without a page refresh! :) | |
######################################################################## | |
Features | |
################################## | |
1) [Highlight] Show own villages' "raidable area" (red circle) | |
2) [Highlight] Show "raid-attractiveness" of rebel villages around village | |
3) [Raid Loop] Automatically keep sending out raids from the best suitable village to the best raid target, indefinitely | |
4) [Raid LC] Set raid LC count in GUI | |
5) [Caravans] Auto-loot caravans (picks "best" loot reliably); also displays current caravan level and progress in level | |
6) [Settings] Assist saves your settings in local browser cache so it remembers you when you come back to the same machine (does not interfere with anything) | |
######################################################################## | |
TODO | |
################################## | |
-> raid loop does not recognize returning troops for villages that are not selected | |
-> GUI: show the Assist log (listing everything assist did) | |
-> load all possible raid target villages from un-cached map tiles | |
*/ | |
(function (_oldAssist) { | |
const BuildingTypes = { | |
"Barracks": 1, | |
"Stable": 2, | |
"Headquarters": 3, | |
"Wall": 4, | |
"Silver Mine": 5, | |
"Treasury": 6, | |
"Defensive Towers": 7, | |
"Farm Land": 8, | |
"Palace": 9, | |
"Siege Workshop": 10, | |
"Trading Post": 11, | |
"Tavern": 12, | |
"Hiding Spot": 13 | |
}; | |
/** | |
* Draw a table from json array | |
* @param {array} json_data_array Data array as JSON multi dimension array | |
* @param {array} head_array Table Headings as an array (Array items must me correspond to JSON array) | |
* @param {array} item_array JSON array's sub element list as an array | |
* @param {string} destinaion_element '#id' or '.class': html output will be rendered to this element | |
* @returns {string} HTML output will be rendered to 'destinaion_element' | |
* @see https://stackoverflow.com/a/40457975/2228771 | |
*/ | |
function jsonToTable(json_data_array, head_array, item_array, destinaion_element) { | |
var table = '<table>'; | |
//TH Loop | |
table += '<tr>'; | |
$.each(head_array, function (head_array_key, head_array_value) { | |
table += '<th>' + head_array_value + '</th>'; | |
}); | |
table += '</tr>'; | |
//TR loop | |
$.each(json_data_array, function (key, value) { | |
table += '<tr>'; | |
//TD loop | |
$.each(item_array, function (item_key, item_value) { | |
table += '<td>' + value[item_value] + '</td>'; | |
}); | |
table += '</tr>'; | |
}); | |
table += '</table>'; | |
$(destinaion_element).append(table); | |
} | |
var Assist = { | |
version: '0.3.5', | |
timeouts: [], | |
intervals: [], | |
logData: [], | |
DefaultCfg: { | |
thisUrl: 'https://rawgit.com/Domiii/73836e54ce2bdbc136a6560dd1941a15/raw/c1801da6e7ccd0f494a26f7a880008c8afd22868/ROE_Assist.js', | |
targetMaxRebelDistance: 22, | |
raidTroops: { | |
5: 25 // cavalry | |
}, | |
minAutoVillageUpdateDelay: 12000, | |
minAutoRaidDelay: 6000, | |
maxAutoRaidDelay: 7000, | |
ui: { | |
} | |
}, | |
_buildConfigProxyHandler() { | |
return { | |
get: (target, name) => { | |
const cachedCfg = localStorage.getObject('__roe_assist_cfg') || {}; | |
if (cachedCfg.hasOwnProperty(name)) { | |
return cachedCfg[name]; | |
} | |
return target[name]; | |
}, | |
set: (target, name, value) => { | |
var { | |
merge, | |
isPlainObject | |
} = _; | |
let oldValue = Assist.Cfg[name]; | |
if (isPlainObject(value) && isPlainObject(oldValue)) { | |
// merge old and new | |
value = merge({}, oldValue, value); | |
} | |
const cachedCfg = localStorage.getObject('__roe_assist_cfg') || {}; | |
cachedCfg[name] = value; | |
localStorage.setObject('__roe_assist_cfg', cachedCfg); | |
} | |
}; | |
}, | |
setConfig(name, value) { | |
this.Cfg[name] = value; | |
}, | |
Data: { | |
Reports: { | |
xyMap: {}, | |
getLatestReport: function (xy) { | |
var reports = Assist.Data.Reports.xyMap[xy]; | |
if (reports) { | |
// reports are sorted in ascending order by date | |
return _.maxBy(reports, report => report.date.getTime()); | |
} | |
return null; | |
} | |
} | |
}, | |
setup() { | |
var promises = []; | |
// setup config | |
Assist.Cfg = new Proxy(Assist.DefaultCfg, Assist._buildConfigProxyHandler()); | |
// get lodash | |
if (typeof (_) === 'undefined') { | |
promises.push($.getScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.16.2/lodash.min.js')); | |
} | |
if (typeof (moment) === 'undefined') { | |
promises.push($.getScript('https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.15.1/moment.min.js')); | |
} | |
// add Math.clamp (https://gist.github.com/kujon/2781489) | |
(function () { Math.clamp = function (val, min, max) { return Math.min(Math.max(min, val), max); }; })(); | |
// make localStorage access easier | |
// see: https://stackoverflow.com/a/2010994/2228771 | |
Storage.prototype.setObject = function (key, value) { | |
this.setItem(key, JSON.stringify(value)); | |
} | |
Storage.prototype.getObject = function (key) { | |
return JSON.parse(this.getItem(key)); | |
} | |
return $.when.apply($, promises); | |
}, | |
setTimeout: function (___var_args) { | |
console.assert(arguments[0] instanceof Function); | |
var timer = setTimeout.apply(null, arguments); | |
Assist.timeouts.push(timer); | |
return timer; | |
}, | |
setInterval: function (___var_args) { | |
console.assert(arguments[0] instanceof Function); | |
var timer = setInterval.apply(null, arguments); | |
Assist.intervals.push(timer); | |
return timer; | |
}, | |
log(text) { | |
var time = moment(); | |
var ts = function toString() { | |
return '[' + this.time.format('LTS') + '] ' + this.text; | |
}; | |
const entry = { time, text, toString: ts }; | |
Assist.logData.push(entry); | |
console.log('[Assist] ' + entry); | |
}, | |
uninstall: function () { | |
window.Assist.timeouts.forEach((t) => clearTimeout(t)); | |
window.Assist.timeouts = null; | |
window.Assist.intervals.forEach((t) => clearInterval(t)); | |
window.Assist.intervals = null; | |
window.Assist.ui.resetMapHighlights(); | |
$('.assist-nav').remove(); | |
window.Assist = null; | |
Assist = null; | |
}, | |
playerId: function () { | |
return ROE.playerID; | |
}, | |
villages: function () { return ROE.Landmark.villages; }, | |
rebelVillages: function () { | |
return _.filter(ROE.Landmark.villages, { pid: ROE.CONST.specialPlayer_Rebel }); | |
}, | |
playerVillages: function () { | |
return _.filter(ROE.Landmark.villages_byid, { pid: ROE.playerID }); | |
}, | |
getAllReports: function () { | |
return new Promise((resolve, reject) => { | |
var done = function (list) { | |
Assist.Data.Reports.list = list; | |
resolve(list); | |
}; | |
//BDA.Database.SelectAll("Reports").done(done); | |
BDA.Database.FindRange("Reports", 50 * ROE.playersNumVillages, 0).done(done); | |
}); | |
}, | |
filterAttackReports: function (report) { | |
return report.type === "Attack"; | |
}, | |
getReportStats: function (report) { | |
// extract Date object | |
report.date = BDA.Utils.fromMsJsonDate(report.time); | |
// extract coordinates | |
var coordRegex = /\(([-\d]+)\,\s*([-\d]+)\)/g; | |
match = coordRegex.exec(report.subject); | |
if (!match) { | |
console.error("Could not extract coordinates from battle report"); | |
} | |
else { | |
report.x = parseInt(match[1]); | |
report.y = parseInt(match[2]); | |
} | |
report.xy = report.x + '_' + report.y; | |
var reports = Assist.Data.Reports.xyMap[report.xy]; | |
if (!reports) { | |
Assist.Data.Reports.xyMap[report.xy] = reports = []; | |
} | |
reports.push(report); | |
return report; | |
}, | |
getReportsStats: function (refresh) { | |
let first; | |
if (refresh) { | |
first = Assist.refreshReports(); | |
} | |
else { | |
first = Promise.resolve(); | |
} | |
return first | |
.then(() => Assist.getAllReports()) | |
.then((list) => _.chain(list) | |
.filter(Assist.filterAttackReports) | |
.map(Assist.getReportStats) | |
.value()); | |
}, | |
refreshReports: function () { | |
console.log("[Assist] refreshing reports..."); | |
// this is a bit tricky -> | |
// In an attempt to measure events accurately, | |
// we first delete the table, | |
// then we wait until there is stuff in the table again. | |
return new Promise((resolve, reject) => { | |
BDA.Database.Delete("Reports").done(function () { | |
var checkDelay = 300; | |
var totalTimeWaited = 0; | |
var checkCheck = function () { | |
// TODO: FindRange seems to be broken | |
BDA.Database.FindRange("Reports", 30, 0).done(function (r) { | |
totalTimeWaited += checkDelay; | |
if (r.length > 0 || totalTimeWaited > 5000) { | |
// we are done, or we waited long enough! | |
resolve(); | |
} | |
else { | |
console.warn('reports taking a while: waited ' + totalTimeWaited + 'ms'); | |
Assist.setTimeout(checkCheck, checkDelay); | |
} | |
}); | |
}; | |
ROE.Reports.showLatestReports(); | |
Assist.setTimeout(checkCheck, checkDelay); | |
}); | |
}); | |
}, | |
selectGovernment: function () { | |
ROE.UI.GovTypeSelect.init(); | |
}, | |
computeDist: function (a, b) { | |
var dx = (a.x - b.x); | |
var dy = (a.y - b.y); | |
return Math.sqrt((dx * dx) + (dy * dy)).toFixed(2); | |
}, | |
getVillages: function () { | |
const lastQueryTime = Assist._lastVillageQueryTime || 0; | |
const now = Date.now(); | |
const timePassed = now - lastQueryTime; | |
const needsUpdate = timePassed > Assist.Cfg.minAutoVillageUpdateDelay; | |
if (needsUpdate) { | |
Assist._lastVillageQueryTime = Date.now(); | |
} | |
return new Promise((resolve, reject) => { | |
ROE.Villages.getVillages(function (villages) { | |
//var unfreshVillages = _.filter(villages, v => !v._TroopsList); | |
var promises = villages.map(v => | |
new Promise((resolve, reject) => { | |
const fetchType = needsUpdate ? | |
ROE.Villages.Enum.ExtendedDataRetrieveOptions.ensureExistsAndTriggerGetLatest : | |
ROE.Villages.Enum.ExtendedDataRetrieveOptions.ensureExists; | |
ROE.Villages.getVillage(v.id, (vv) => { | |
vv._assist_lastCheckTime = new Date(); | |
resolve(vv); | |
}, fetchType); | |
}) | |
); | |
Promise.all(promises) | |
.then(resolve); | |
}); | |
}); | |
}, | |
getVillageQueueTime(v) { | |
return _.find(v.VOV.buildings, bldg => bldg.id === 3).recruitcount; | |
}, | |
getVillageQueueTimeFormat(v) { | |
const t = Assist.getVillageQueueTime(v); | |
return moment(new Date(parseInt(t))).fromNow(); | |
}, | |
getVillageQueueTimes() { | |
return Assist.getVillages().then(vs => { | |
return _.map(vs, Assist.getVillageQueueTime); | |
}); | |
}, | |
getVillageTreasuryFullTime(v) { | |
return _.find(v.upgrade.Buildings, { buildingID: BuildingTypes.Treasury }).EffectInfo.timeTillTreasuryFull; | |
}, | |
// TODO: recruitTimes! | |
// TODO: show in table | |
// TODO: add button to quickly zoom in on village | |
getAllVillageStats() { | |
return Assist.getVillages().then(vs => { | |
return _.map(vs, v => { | |
return { | |
queueTime: Assist.getVillageQueueTime(v), | |
treasuryFullTime: Assist.getVillageTreasuryFullTime(v), | |
recruitTimes: { | |
barracks, | |
stables, | |
tavern, | |
siegeWorkshop | |
} | |
}; | |
}); | |
}); | |
}, | |
// ################################################################################# | |
// ui scripts | |
// ################################################################################# | |
ui: { | |
setRaidStatus(statusText) { | |
Assist.raidStatus = statusText; | |
$('#assist-raid-status').val(statusText); | |
}, | |
showVillageStatsTable() { | |
// TODO: Make this work! | |
jsonToTable(); | |
}, | |
resetMapHighlights: function () { | |
$('.assist').remove(); | |
}, | |
getSelectionEl: function () { | |
return $('#surface #select'); | |
}, | |
drawArrowOnVillage: function (arrowCfg) { | |
var coords = Assist.ui.worldXYToView(arrowCfg); | |
var yOffset = Assist.ui.worldYToView(0.1); | |
var w = Assist.ui.worldSizeToView(1); | |
var h = Assist.ui.worldSizeToView(0.5); | |
var color = arrowCfg.color || 'red'; | |
var css = { | |
left: coords.left + 'px', | |
top: (coords.top + yOffset) + 'px', | |
position: 'absolute', | |
zIndex: 3001, | |
opacity: 1, | |
width: 0, | |
height: 0, | |
borderLeft: (w / 2) + 'px solid transparent', | |
borderRight: (w / 2) + 'px solid transparent', | |
borderTop: h + 'px solid ' + color | |
}; | |
var $el = $('<span class="assist assist-arrow"></span>').css(css); | |
$('#surface').append($el); | |
//console.log($el); | |
}, | |
drawCircle: function (centerXY, radius, width, color) { | |
width = width || 3; | |
color = color || 'red'; | |
var halfSize = Assist.ui.worldSizeToView(0.5); | |
var coords = Assist.ui.worldXYToView(centerXY); | |
var viewRadius = Assist.ui.worldSizeToView(radius); | |
var viewSize = 2 * viewRadius; | |
var css = { | |
left: (-viewRadius + coords.left + halfSize) + 'px', | |
top: (-viewRadius + coords.top + halfSize) + 'px', | |
position: 'absolute', | |
zIndex: 2500, | |
"border": width + "px solid " + color, | |
"borderRadius": "50%", | |
"width": viewSize + "px", | |
"height": viewSize + "px", | |
"MozBoxSizing": "border-box", | |
"boxSizing": "border-box" | |
}; | |
var $el = $('<span class="assist assist-circle"></span>').css(css); | |
$('#surface').append($el); | |
//console.log($el); | |
}, | |
addButton: function (title, cb) { | |
var but = '<input type="button" value="' + title + '"/>'; | |
return Assist.ui.addToNavbar(but, cb); | |
}, | |
addToNavbar: function (el, cb) { | |
var $el = $(el); | |
$('#assist-nav-cont').append($el); | |
$el.addClass('assist-nav'); | |
$el.css({ | |
'position': 'relative', | |
'float': 'left', | |
'backgroundImage': 'none' | |
}); | |
if (cb) { | |
$el.click(cb); | |
} | |
return $el; | |
}, | |
// convert village xy coordinates to pixel coordinates | |
worldXYToView: function ({ x, y }) { | |
return { | |
left: Assist.ui.worldXToView(x), | |
top: Assist.ui.worldYToView(y) | |
}; | |
}, | |
worldXToView: function (x) { | |
var lp = 84; | |
return x * (lp * ROE.Landmark.scale); | |
}, | |
worldYToView: function (y) { | |
var lp = 84; | |
return -y * (lp * ROE.Landmark.scale); | |
}, | |
worldSizeToView: function (n) { | |
var lp = 84; | |
return n * (lp * ROE.Landmark.scale); | |
}, | |
viewToWorld: function (pixels) { | |
var lp = 84; | |
return pixels / (lp * ROE.Landmark.scale); | |
}, | |
createCssClass: function createClass(name, rules) { | |
var style = document.createElement('style'); | |
style.type = 'text/css'; | |
document.getElementsByTagName('head')[0].appendChild(style); | |
if (!(style.sheet || {}).insertRule) | |
(style.styleSheet || style.sheet).addRule(name, rules); | |
else | |
style.sheet.insertRule(name + " {" + rules + "}", 0); | |
}, | |
dispatchKeyEvent: function (target, eventType) { | |
var e = document.createEvent("KeyboardEvent"); | |
var initMethod = typeof e.initKeyboardEvent !== 'undefined' ? "initKeyboardEvent" : "initKeyEvent"; | |
var args = [ | |
eventType || "keydown", // event type : keydown, keyup, keypress | |
true, // bubbles | |
true, // cancelable | |
window, // viewArg: should be window | |
false, // ctrlKeyArg | |
false, // altKeyArg | |
false, // shiftKeyArg | |
false, // metaKeyArg | |
40, // keyCodeArg : unsigned long the virtual key code, else 0 | |
0 // charCodeArgs : unsigned long the Unicode character associated with the depressed key, else 0 | |
]; | |
e[initMethod].apply(e, args); | |
target.dispatchEvent(e); | |
}, | |
dispatchMouseClick: function (target, args) { | |
Assist.ui.dispatchMouseEvent($(target)[0], 'click', args); | |
}, | |
dispatchMouseEvent: function (target, type, args) { | |
if (!target) { | |
throw new Error('mouse event target invalid'); | |
} | |
args = Object.assign({ "bubbles": true, "cancelable": false }, args); | |
const e = new MouseEvent(type, args); | |
target.dispatchEvent(e); | |
}, | |
closePopup: function (popup) { | |
$(popup).closest('.ui-dialog-content').dialog('close'); | |
}, | |
highlightRaidTargets: function () { | |
Assist.ui.resetMapHighlights(); | |
// Step #1: Draw circles around all own villages | |
var myVillages = Assist.playerVillages(); | |
myVillages.forEach(v => Assist.ui.drawCircle(v, Assist.Cfg.targetMaxRebelDistance, 3, 'red')); | |
// Step #2: Highlight raidable villages, based on their raidAttractiveness | |
return Assist.getReportsStats(true) | |
.then(function (reports) { | |
Assist.attacks.getRaidTargets() | |
.map(function (v) { | |
// go from green to red (hue from 0 to 140) | |
var hue = Math.clamp((v.raidAttractiveness || 0) * 140, 0, 140); | |
return { | |
x: v.x, | |
y: v.y, | |
color: 'hsla(' + hue + ', 100%, 50%, 0.4)' | |
}; | |
}) | |
.forEach(Assist.ui.drawArrowOnVillage); | |
}); | |
}, | |
refreshMapHighlights: function () { | |
if (Assist.Cfg.ui.mapHighlightsOn) { | |
Assist.ui.highlightRaidTargets(); | |
} | |
}, | |
// ################################################################################# | |
// add Assist UI | |
// ################################################################################# | |
installUI: function () { | |
// weird bugs: open reports at least once | |
Assist.ui.dispatchMouseClick($('#linkReports')); | |
setTimeout(() => Assist.ui.closePopup('#reports_popup'), 400); | |
var $el = $('<div id="assist-nav-cont" class="assist-nav"></div>'); | |
$('.ui-panel-main').append($el); | |
$el.css({ | |
'position': 'relative', | |
'float': 'left', | |
'background-color': 'black' | |
}); | |
Assist.ui.addButton('Uninstall', function ($event) { | |
Assist.uninstall(); | |
$event.preventDefault(); | |
}); | |
Assist.ui.addToNavbar('<label><input id="assist-caravan-loot" type="checkbox" />Caravans (<span id="assist-caravan-info"></span>)</label>') | |
.change(function () { | |
var checked = $(this).find(':checkbox:checked').length > 0; | |
Assist.Cfg.autoPickup = checked; | |
if (!checked) { | |
Assist.mapEvents.stopScan(); | |
} | |
else { | |
Assist.mapEvents.startScan(); | |
} | |
}); | |
if (ROE.Player.chooseGovType) { | |
Assist.ui.addButton('Choose Gov', function () { | |
Assist.selectGovernment(); | |
}); | |
} | |
Assist.ui.addToNavbar('<label><input type="checkbox" />Highlight</label>') | |
.change(function () { | |
var checked = $(this).find(':checkbox:checked').length > 0; | |
Assist.setConfig('ui', { mapHighlightsOn: checked }); | |
if (!checked) { | |
Assist.ui.resetMapHighlights(); | |
} | |
else { | |
Assist.ui.highlightRaidTargets(); | |
} | |
}); | |
// Assist.ui.addButton('Attack One', function() { | |
// //$('#assist-loop-attack').css('display', 'inline'); | |
// Assist.attacks.trySendNextRaid(false); | |
// }); | |
//Assist.ui.addToNavbar('<label id="assist-loop-attack" style="display:none;" ><input type="checkbox" />Loop Attack</label>') | |
Assist.ui.addToNavbar('<label id="assist-loop-attack" ><input type="checkbox" />Raid Loop</label>') | |
.change(function () { | |
var checked = $(this).find(':checkbox:checked').length > 0; | |
Assist.attacks.setRaidLoop(checked); | |
}); | |
Assist.ui.addToNavbar('<label for="assist-raid-lc"> Raid LC:</label>'); | |
var lcCountInput = Assist.ui.addToNavbar('<input id="assist-raid-lc" type="text" style="width: 4em;" />') | |
.change(function (t) { | |
var text = $(this).val(); | |
var lcCount = parseInt(text); | |
if (lcCount) { | |
Assist.setConfig('raidTroops', { | |
5: lcCount | |
}); | |
} | |
else { | |
$(this).val(Assist.Cfg.raidTroops['5']); | |
} | |
}); | |
// raid status | |
Assist.ui.addToNavbar('<input id="assist-raid-status" type="text" disabled />'); | |
$('#assist-raid-status').hide(); | |
// load config | |
Assist.ui.refreshMapHighlights(); | |
lcCountInput.val(Assist.Cfg.raidTroops['5']); | |
}, | |
/** | |
* This is copied as-is from the code | |
*/ | |
_creditFarmReturn(data) { | |
//_commonEventReturn(data); | |
//data.result will be a string in "x,y" format where X = players updated total credits, and Y = credits claimed in this call | |
var results = data.result.msg.split(','); | |
var creditsUpdated = parseInt(results[0]); | |
var creditsClaimed = parseInt(results[1]); | |
Assist.log(`got ${creditsClaimed} servants!`, data); | |
//in a credit farm call a claim of 0 was probably erroneous, or doesnt need any further action | |
if (creditsClaimed < 1) { return; } | |
//Animate the claim | |
var xCord = data.xCord; | |
var yCord = data.yCord; | |
var boxH = 25; | |
var boxW = 20; | |
var left = (ROE.Landmark.landpx * xCord + ROE.Landmark.rx) + (ROE.Landmark.landpx / 2) - (boxW / 2); | |
var top = ROE.Landmark.landpx * -yCord + ROE.Landmark.ry; | |
var credElementPos = $('.playerCredits').offset(); | |
var claimDiv = $('<div class="eventClaimDiv">').html('+' + creditsClaimed); | |
$('body').append(claimDiv); | |
claimDiv.css({ | |
left: left, | |
top: top, | |
height: boxH, | |
width: boxW | |
}).animate({ top: credElementPos.top, left: credElementPos.left, opacity: .5 }, 1000, "easeOutSine", function () { | |
$(this).remove(); | |
ROE.Frame.refreshPFHeader(creditsUpdated); | |
}); | |
} | |
}, | |
subscribeROEEvent: function (eventName, cb) { | |
// TODO! Not working... | |
BDA.Broadcast.subscribe($('#assist-nav-cont'), eventName, | |
function (event, data) { | |
cb(data); | |
} | |
); | |
}, | |
// ################################################################################# | |
// map event scripts | |
// ################################################################################# | |
mapEvents: { | |
nLootedCaravans: 0, | |
startScan: function () { | |
var delay = 900; | |
Assist.mapEvents.stopScan(); | |
Assist.mapEvents.scanTimer = Assist.setInterval(Assist.mapEvents.scanForMapEvents, delay); | |
}, | |
stopScan: function () { | |
Assist.mapEvents.unknownLoot = null; | |
if (Assist.mapEvents.scanTimer) { | |
clearInterval(Assist.mapEvents.scanTimer); | |
Assist.mapEvents.scanTimer = null; | |
} | |
}, | |
scanForMapEvents: function () { | |
if (Assist.mapEvents.unknownLoot) { | |
Assist.mapEvents.stopScan(); | |
$('assist-caravan-loot').val(false); | |
return; | |
} | |
ROE.Player.MapEvents.forEach((evt) => { | |
if (evt.typeID == 2) { | |
// caravan! | |
var x = evt.xCord; | |
var y = evt.yCord; | |
ROE.MapEvents.checkPlayerMapEvents(x, y); | |
} | |
else { | |
Assist.mapEvents.tryLootOtherMapEvents(evt); | |
} | |
}); | |
Assist.setTimeout(Assist.mapEvents.tryLootCaravan, 500); | |
}, | |
tryLootOtherMapEvents(evt) { | |
//ROE.Api.call_playermapevent_activate(evt.eventID, evt.typeID, evt.xCord, evt.yCord, Assist.ui._creditFarmReturn); | |
}, | |
tryLootCaravan: function () { | |
var rarities = ['common', 'uncommon', 'rare', 'epic', 'legendary']; | |
// value in silver per minute | |
var lootTypes = { | |
'unit_CM': { | |
name: 'cm', | |
value: 30 | |
}, | |
'unit_IN': { | |
name: 'inf', | |
value: 250, | |
}, | |
'unit_spy': { | |
name: 'spy', | |
value: 1200 | |
}, | |
'unit_LC': { | |
name: 'lc', | |
value: 1400 | |
}, | |
'unit_KN': { | |
name: 'knight', | |
value: 4000 | |
}, | |
'unit_ram': { | |
name: 'ram', | |
value: 2000 | |
}, | |
'unit_treb': { | |
name: 'treb', | |
value: 5000 | |
}, | |
'speed_research': { | |
name: 'research speedup', | |
value: 150, | |
}, | |
'speed_building': { | |
name: 'building speedup', | |
value: 300, | |
}, | |
'PF_silver': { | |
name: 'elven efficiency', | |
value: 80, | |
}, | |
'silverBag': { | |
name: 'silver', | |
value: 1000, | |
} | |
}; | |
var $unrevealed = $('.unrevealed'); | |
var $cards = $('.card .front'); | |
var lootIds = _.map($cards, m => $(m).attr('data-lootid')).filter(info => !!info); | |
if ($unrevealed.length) { | |
Assist.ui.dispatchMouseClick($unrevealed); | |
} | |
else if (lootIds.length === 3) { | |
var results = []; | |
// Determine which card to pick | |
lootIds.forEach((lootId, i) => { | |
var $el = $($cards[i]); | |
var rarity = _.find(rarities, r => $el.hasClass(r)); | |
// figure out the reward from the loot id string | |
var suffixes = rarities.map(r => '_' + r).join('|') + '|\\d+'; | |
var rxStr = `L([\\d]+)_(.*)(${suffixes})`; | |
var lootRegex = new RegExp(rxStr, 'g'); | |
var lootMatch = lootRegex.exec(lootId); | |
var countText = $el.text(); | |
var countRx = /(\d+)(?:\s*([mh]))?/g; | |
var countMatch = countRx.exec(countText); | |
const count = parseInt(countMatch[1]) * (countMatch[2] === 'h' ? 60 : 1); | |
var lootType, lootLevel; | |
if (lootMatch) { | |
lootLevel = parseInt(lootMatch[1]) || 1; | |
lootType = lootMatch[2]; | |
} | |
var lootInfo = lootTypes[lootType]; | |
if (!lootInfo) { | |
lootType = lootType || "<unknown>"; | |
lootLevel = lootLevel || 1; | |
Assist.mapEvents.unknownLoot = 'unknown lootType: ' + lootType + ' (' + lootId + ')'; | |
} | |
var lootName = lootInfo && lootInfo.name; | |
// var rarityPoints = _.indexOf(rarities, rarity); | |
// var typeFactor = (lootInfo.value || 0) + 2 * (lootLevel-1); | |
// var rarityFactor = Math.pow(2, rarityPoints); | |
// var points = typeFactor * rarityFactor; | |
var points = lootInfo && lootInfo.value * count; | |
var info = ['(' + (i + 1) + ')', | |
countText, 'x', lootName, | |
'[' + rarity + ']', | |
'-', points, 'points', | |
].join(' '); | |
results.push({ i, points, info }); | |
}); | |
// update player caravan status | |
var $caravanEl = $('.cards').parent(); | |
var caravanLevel = $caravanEl.find('.title').text().match(/(\d+)/g); | |
var caravanLevelStatus = $caravanEl.find('.collectedThisLevel span').text(); | |
$('#assist-caravan-info').text(caravanLevelStatus + ' @L' + caravanLevel); | |
// pick best decision (highest points) | |
var decision = _.maxBy(results, (r) => r.points); | |
// produce info | |
var nLootedCaravans = ++Assist.mapEvents.nLootedCaravans; | |
var infos = ' ' + _.map(results, 'info').join('\n '); | |
var msg = ' - choices:\n' + infos; | |
if (Assist.mapEvents.unknownLoot || !decision) { | |
msg = 'could not pick caravan loot: unknown loot present: ' + Assist.mapEvents.unknownLoot + msg; | |
} | |
else { | |
// select and click the card we want | |
const $card = $($cards[decision.i]); | |
Assist.ui.dispatchMouseClick($card); | |
msg = 'caravan looted #' + nLootedCaravans + ': ' + decision.info + msg; | |
} | |
// log! :) | |
Assist.log(msg); | |
} | |
} | |
}, | |
// ################################################################################# | |
// Attack + raid scripts | |
// ################################################################################# | |
attacks: { | |
areWeAttackingVillage: function (vid) { | |
var outGoingData = ROE.Troops.InOut2.getData(1); | |
var outGoingCommands = outGoingData.commands; | |
return _.some(outGoingCommands, { dvid: vid }); | |
}, | |
filterRaidableRebelVillages: function (myVillages, target) { | |
myVillages = myVillages || Assist.playerVillages(); | |
// check #1: no if out of range | |
if (!_.some(myVillages, myVillage => | |
Assist.computeDist(target, myVillage) < Assist.Cfg.targetMaxRebelDistance)) { | |
return false; | |
} | |
// check #2: weed out villages explicitly tagged as "bad" | |
if (target.note === 'bad') { | |
return false; | |
} | |
// check #3: no, if we are already attacking | |
if (Assist.attacks.areWeAttackingVillage(target.id)) { | |
return false; | |
} | |
return true; | |
}, | |
/** | |
* Sets a village's raidAttractiveness. | |
* 0 means no value at all. | |
* 1 is best. | |
*/ | |
getRaidAttractiveness: function (myVillages, target) { | |
myVillages = myVillages || Assist.playerVillages(); | |
var xy = target.x + '_' + target.y; | |
var report = Assist.Data.Reports.getLatestReport(xy); | |
// TODO: Need to factor morale into this as well | |
// Factor #1: time since last attack (the longer the better; max 24h (for servants)) | |
var timeFactor; | |
if (report) { | |
var maxTimeout = 12 * 60 * 60 * 1000; | |
var date = report.date; | |
var millisPassed = Date.now() - date.getTime(); | |
if (timeFactor < 1) { | |
// attacked within the last 12 hours | |
timeFactor /= 2; | |
} | |
timeFactor = Math.clamp(millisPassed / maxTimeout, 0, 1); | |
} | |
else { | |
timeFactor = 1; | |
} | |
// Factor #2: village distance (max 22) | |
var dist = _.min(myVillages.map(myVillage => Assist.computeDist(target, myVillage))); | |
var distFactor = Math.clamp(1 - dist / 22, 0, 1); | |
// Factor #3: village points (max 1000) | |
var pointsFactor = Math.clamp(0 + target.points / 1000, 0, 1); | |
// assign weights to each contributing factor | |
var featureVector = [distFactor, pointsFactor]; | |
var weights = [5, 1]; | |
var i = 0; | |
var valSq = _.sumBy(featureVector, (val) => val * val * weights[i++]) / _.sum(weights); | |
target.raidAttractiveness = timeFactor * Math.sqrt(valSq); | |
return target; | |
}, | |
getRaidTargets(sourceVillages) { | |
var rebelVillages = Assist.rebelVillages(); | |
return _.chain(rebelVillages) | |
// Step #1: Select all raidable rebel villages (in range and not already attacking) | |
.filter((target) => Assist.attacks.filterRaidableRebelVillages(sourceVillages, target)) | |
// Step #2: Use time since last attack and distance to compute desirability | |
.map((target) => Assist.attacks.getRaidAttractiveness(sourceVillages, target)) | |
//.orderBy((target) => target.raidAttractiveness, 'desc') | |
.value(); | |
}, | |
getBestRaidTarget(sourceVillages) { | |
// get all possible targets and compute their raidAttractiveness | |
var targets = Assist.attacks.getRaidTargets(sourceVillages); | |
// get target with highest value | |
return _.maxBy(targets, v => v.raidAttractiveness); | |
}, | |
setRaidLoop: function (loop) { | |
Assist.Cfg.raidLoop = loop; | |
if (loop) { | |
// continuesly try sending out raids | |
Assist.attacks.continueRaidLoop(true); | |
// show raid status | |
$('#assist-raid-status').show(200); | |
} | |
else if (Assist.attacks.loopTimer) { | |
// stop attacks | |
clearTimeout(Assist.attacks.loopTimer); | |
Assist.attacks.loopTimer = null; | |
// hide raid status | |
Assist.ui.setRaidStatus(''); | |
$('#assist-raid-status').hide(200); | |
} | |
}, | |
continueRaidLoop: function () { | |
if (!Assist.Cfg.raidLoop) return; | |
if (Assist.attacks.loopTimer) { | |
clearTimeout(Assist.attacks.loopTimer); | |
} | |
Assist.ui.setRaidStatus('starting raid loop...'); | |
Assist.attacks.trySendRaid(true) | |
.catch((err) => { | |
console.error('[Raid ERROR]', (err && err.stack || err)); | |
Assist.ui.setRaidStatus('internal ERROR while raiding... recovering...'); | |
return false; | |
}) | |
.then(() => { | |
var minDelay = Assist.Cfg.minAutoRaidDelay || 1000; | |
var maxDelay = Assist.Cfg.maxAutoRaidDelay || Assist.Cfg.minAutoRaidDelay; | |
delay = Math.round(Math.random() * (maxDelay - minDelay) + minDelay); | |
//console.log('[Assist] raid loop - waiting ' + (delay/1000) + 's'); | |
Assist.attacks.loopTimer = Assist.setTimeout(Assist.attacks.continueRaidLoop, delay); | |
}); | |
}, | |
getOwnVillagesWithEnoughTroops(troops) { | |
return Assist.getVillages() | |
.then((sourceVillages) => { | |
// get all own villages that have enough troops | |
return _.filter(sourceVillages, sourceVillage => { | |
var hasEnough = true; | |
for (var troopId in troops) { | |
var current = sourceVillage.TroopByID(troopId).YourUnitsCurrentlyInVillageCount; | |
var required = troops[troopId] || 0; | |
if (required > current) { | |
hasEnough = false; | |
break; | |
} | |
} | |
return hasEnough; | |
}); | |
}); | |
}, | |
/** | |
* Of the given of our villages, | |
* pick the one that would be best suitable at raiding the given target. | |
*/ | |
getBestSourceVillage: function (myVillages, target) { | |
// pick the village that's closest for now | |
return _.minBy(myVillages, (v) => Assist.computeDist(v, target)); | |
}, | |
trySendRaid(autoSend) { | |
return Assist.attacks.getOwnVillagesWithEnoughTroops(Assist.Cfg.raidTroops) | |
.then(function (sourceVillages) { | |
if (sourceVillages.length > 0) { | |
// make sure, we get the freshest reports before attacking! | |
Assist.ui.setRaidStatus('checking battle reports'); | |
return Assist.getReportsStats(true) | |
.then(function (reports) { | |
// get best target village | |
const targetVillage = Assist.attacks.getBestRaidTarget(sourceVillages); | |
// get best of my villages to attack it | |
var sourceVillage = Assist.attacks.getBestSourceVillage(sourceVillages, targetVillage); | |
// open attack popup and get this show on the road! | |
return Assist.attacks.attackTarget(sourceVillage, targetVillage, | |
Assist.Cfg.raidTroops, autoSend) | |
.then(() => { | |
Assist.ui.setRaidStatus('troops sent'); | |
}); | |
}); | |
} | |
else { | |
Assist.ui.setRaidStatus('not enough troops'); | |
if (!autoSend) { | |
Assist.log('not enough troops for raid'); | |
var warningSign = '<label style="background-color:red">NOT ENOUGH TROOPS :( :( :(</label>' | |
Assist.ui.addToNavbar(warningSign).fadeOut(2000); | |
} | |
return false; | |
} | |
}); | |
}, | |
attackTargetFromAny(targetVillage, troops) { | |
// TODO | |
}, | |
attackTarget(sourceVillage, targetVillage, troops, autoSend = true) { | |
console.assert(sourceVillage && targetVillage && troops, 'invalid attack parameters'); | |
return new Promise((resolve, reject) => { | |
var startedAttack = false; | |
const timeOutMs = 5000; | |
setTimeout(function () { | |
reject('Promise timed out after ' + timeOutMs + ' ms'); | |
}, timeOutMs); | |
var onStartAttack = function (data) { | |
if (startedAttack) return; | |
startedAttack = true; | |
var xy = targetVillage.x + '_' + targetVillage.y; | |
//Assist.log("planning attack on " + xy + "..."); | |
// fill in troops | |
for (var troopId in troops) { | |
var count = troops[troopId] || 0; | |
var input = $("#AttackUnit_" + troopId + " .attackButton input"); | |
input.val(count); | |
} | |
// Add START label | |
var $btn = $('#doAttackButton'); | |
$btn.append('<h1 style="position:absolute;display: inline;top: 15%;left: 10%;background-color: red;padding: 0;margin: 0;">START...</h1>'); | |
// Make things ready! | |
const $realBtn = $(".attackButton input")[0]; | |
if ($realBtn) { | |
Assist.ui.dispatchKeyEvent($realBtn, 'keyup'); | |
} | |
// else { | |
// // cannot figure out button | |
// Assist.ui.dispatchMouseClick($('[aria-describedby=CommandTroopsPopup] .ui-dialog-titlebar-close')[0]); | |
// reject(); | |
// return; | |
// } | |
if (autoSend) { | |
// GO! (delayed because button is not clickable right away) | |
Assist.setTimeout(() => { | |
if (!$("#attack_popup #doAttackButton").length) { | |
console.error('attack button not ready'); | |
reject(); | |
return; | |
} | |
// hit the attack button! | |
Assist.ui.dispatchMouseClick($("#attack_popup #doAttackButton")); | |
var xy = '(' + targetVillage.x + ', ' + targetVillage.y + ')'; | |
Assist.log("sent troops to " + xy + "..."); | |
// close popup right after | |
Assist.setTimeout(Assist.ui.closePopup.bind(null, "#attack_popup"), 300); | |
// refresh highlights shortly after | |
Assist.setTimeout(Assist.ui.refreshMapHighlights, 2000); | |
resolve(true); | |
}, 100); | |
} | |
}; | |
Assist.ui.setRaidStatus('getting ready to send out troops'); | |
ROE.Frame.popupCommandTroops(1, targetVillage.id, targetVillage.x, targetVillage.y, sourceVillage.id); | |
$("#attack_popup").bind("VillageExtendedInfoUpdated", | |
function (event, data) { | |
onStartAttack(data); | |
} | |
); | |
}); | |
} | |
} | |
}; | |
// set everything up | |
(function prep(_newAssist) { | |
// get rid of the old | |
if (_oldAssist) { | |
_oldAssist.uninstall(); | |
} | |
// bootstrap | |
window.Assist = _newAssist; | |
_newAssist.setup().done(_newAssist.ui.installUI()); | |
})(Assist); | |
})(window.Assist); | |
// ###################################################### | |
// add CSS styles | |
// Step #1: convert CSS to JSON (http://staxmanade.com/CssToReact/) | |
// Step #2: format resulting JSON (https://jsonformatter.curiousconcept.com/) | |
// ###################################################### | |
// Arrow CSS: http://www.cssarrowplease.com/ | |
//Assist.ui.createCssClass('.arrow_box', 'background-color: green;'); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment