Skip to content

Instantly share code, notes, and snippets.

@Domiii
Last active October 19, 2017 15:41
Show Gist options
  • Save Domiii/73836e54ce2bdbc136a6560dd1941a15 to your computer and use it in GitHub Desktop.
Save Domiii/73836e54ce2bdbc136a6560dd1941a15 to your computer and use it in GitHub Desktop.
Realm of Empiires - Assist
/**
########################################################################
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">&nbsp;&nbsp;&nbsp;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