· 6 years ago · Jun 22, 2019, 12:12 AM
1(() => {
2
3 /**
4 * Initialization
5 */
6
7 var settings = $.extend(window.playerFarmSettings || {}, {
8 // When calculating how many resources are available for farming
9 // from a villa, determines whether to consider existing commands
10 // that will land after the LT from the current village
11 //
12 // ('true' prevents wasted runs, 'false' allows closer villages
13 // to get more resources.)
14 prioritizeExistingCommands: true,
15
16 // Whether or not to always include 1 scout in each attack, if
17 // available
18 addScout: true,
19
20 // Allowed troop compositions to use when farming; each array
21 // is a different composition. By default can either send at
22 // least 5 lc, or at least 25 spears *and* 10 swords.
23 //
24 // Ordered by priority
25 allowedTroops: [
26 [
27 { name: 'light', min: 5, max: 75 }
28 ],
29 [
30 { name: 'spear', min: 25, max: 75 },
31 { name: 'sword', min: 10, max: 10 }
32 ]
33 ],
34
35 cookieName: 'pf-history'
36 });
37
38 console.log('Using settings: ', settings);
39
40 if (!window.playerVillageCoords) {
41 alert('Assign playerVillageCoords to use the script');
42 return;
43 }
44
45 function parseCoord(coord) {
46 coord = coord.trim();
47 let split = coord.split('|');
48 return {
49 x: parseInt(split[0]),
50 y: parseInt(split[1]),
51 value: coord
52 };
53 }
54
55 var targetCoords = window.playerVillageCoords.trim();
56 if (typeof targetCoords == 'string') {
57 targetCoords = targetCoords
58 .split(/[,\s]/)
59 .map(parseCoord);
60 }
61
62 // targetCoords is array of:
63 // [ { x: 500, y: 500, value: '500|500' }, ... ]
64
65 console.log('Got target coords: ', targetCoords);
66
67 // remove data for villages that are no longer targeted
68 updateSavedData(getSavedData().filter(village => {
69 var matchingCoord = targetCoords.filter(c => c.value == village.coords);
70 return matchingCoord.length > 0;
71 }));
72
73
74 $.getAll = function (
75 urls, // array of URLs
76 onLoad, // called when any URL is loaded, params (index, data)
77 onDone, // called when all URLs successfully loaded, no params
78 onError // called when a URL load fails or if onLoad throws an exception, params (error)
79 ) {
80 var numDone = 0;
81 var lastRequestTime = 0;
82 var minWaitTime = 200; // ms between requests
83 loadNext();
84 function loadNext() {
85 if (numDone == urls.length) {
86 onDone();
87 return;
88 }
89
90 let now = Date.now();
91 let timeElapsed = now - lastRequestTime;
92 if (timeElapsed < minWaitTime) {
93 let timeRemaining = minWaitTime - timeElapsed;
94 setTimeout(loadNext, timeRemaining);
95 return;
96 }
97
98 console.log('Getting ', urls[numDone]);
99 lastRequestTime = now;
100 $.get(urls[numDone])
101 .done((data) => {
102 try {
103 onLoad(numDone, data);
104 ++numDone;
105 loadNext();
106 } catch (e) {
107 onError(e);
108 }
109 })
110 .fail((xhr) => {
111 onError(xhr);
112 })
113 }
114 };
115
116 function getSavedData() {
117 let result = JSON.parse(localStorage.getItem('pf-data') || '[]');
118 // Dates get stored as strings, parse them back to their original Date objects
119 result.forEach((village) => {
120 village.reports.forEach((report) => {
121 if (report.occurredAt)
122 report.occurredAt = new Date(report.occurredAt);
123 });
124
125 village.currentCommands.forEach((cmd) => {
126 if (cmd.landsAt)
127 cmd.landsAt = new Date(cmd.landsAt);
128 });
129 });
130 return result;
131 }
132
133 function updateSavedData(villageData) {
134 localStorage.setItem('pf-data', JSON.stringify(villageData));
135 }
136
137 function clearSavedData() {
138 localStorage.removeItem('pf-data');
139 }
140
141 function makeProgressLabel(action, numDone, numTotal) {
142 return `${action} (${numDone}/${numTotal} done)`;
143 }
144
145 /**
146 * Common/shared data
147 */
148
149 // Contains functions so that we can modify the results without changing the global values
150 let Data = {
151 travelSpeeds: () => ({
152 spear: 18, sword: 22, axe: 18, archer: 18, spy: 9,
153 light: 10, marcher: 10, heavy: 11, ram: 30, catapult: 30,
154 paladin: 10
155 }),
156
157 unitHauls: () => ({
158 spear: 25, sword: 15, axe: 10,
159 archer: 10, spy: 0, light: 80,
160 marcher: 50, heavy: 50, ram: 0,
161 catapult: 0, knight: 100, snob: 0
162 })
163 };
164
165 /**
166 * App start
167 */
168
169 if (redirectToRallyPoint())
170 return;
171
172 // TODO - should show data download/management UI if no data is stored yet
173 addManagementInterfaceLink();
174 fillRallyPoint(() => {
175 console.log('Filled rally point')
176 }, (err) => {
177 console.error(err);
178 alert('An error occurred');
179 });
180
181 //updateTargetVillageData(console.log, console.error, console.log);
182
183
184
185 /**
186 * Management UI
187 */
188 function redirectToRallyPoint() {
189 function contains(str, substr) {
190 return str.indexOf(substr) >= 0;
191 }
192 let href = window.location.href;
193 let isRallyPoint = contains(href, 'screen=place') && (contains(href, 'mode=command') || !contains(href, 'mode='));
194 if (isRallyPoint) {
195 return false;
196 } else {
197 if (confirm('Redirecting to rally point.')) {
198 let targetUrl = `/game.php?village=${game_data.village.id}&screen=place&mode=command`
199 window.location.href = targetUrl;
200 }
201 return true;
202 }
203 }
204
205 function displayManagementUI() {
206 // TODO
207 // Should make a popup using TW's Dialog.show(...)
208
209 var html = `
210 <div id="buttonRow" style="width:500px">
211 <p>
212 ${makeTwButton('pf-refresh-data', 'Refresh Village Data')}
213 ${makeTwButton('pf-display-data', 'Show Village Data')}
214 </p>
215 <p id="pf-update-progress">
216 </p>
217 </div>
218 <div id="details" style="height:0px;overflow-y: auto">
219 <table id="detailsVillages" class="vis">
220 </table>
221 </div>
222 `;
223
224
225 function onDialogClosed() {
226 console.log('closed');
227 }
228
229 Dialog.show('player-farm', html.trim(), onDialogClosed, {});
230
231 //$('#pf-display-data').hide();
232 $('#popup_box_player-farm').width("700px");
233
234 $('#pf-display-data').click(() => {
235 $('#details').height("500px");
236 displayTargetInfo(targetCoords);
237 });
238
239 var $updateProgressContainer = $('#pf-update-progress');
240
241 function onUpdateProgress(msg) {
242 $updateProgressContainer.text(msg);
243 }
244
245 function onUpdateError(err) {
246 $updateProgressContainer.text(err);
247 }
248
249 $('#pf-refresh-data').click(() => {
250 updateTargetVillageData((result) => {
251 updateSavedData(result);
252 console.log('Stored result data: ', JSON.stringify(result));
253 alert('Done');
254 $updateProgressContainer.text('');
255 $('#pf-display-data').show();
256 }, onUpdateError, onUpdateProgress);
257 });
258
259 }
260
261 function makeTwButton(id, text) {
262 return `<input type="button" id="${id}" class="btn evt-confirm-btn btn-confirm-yes" value="${text}">`;
263 }
264
265 function displayTargetInfo(coords) {
266 $('#detailsVillages').empty();
267 $('#detailsVillages').append(`<tbody>
268 <tr class="title">
269 <td style="background-color: rgb(193, 162, 100); background-image: url('https://dsen.innogamescdn.com/asset/fa6f0423/graphic/screen/tableheader_bg3.png'); background-repeat: repeat-x; font-weight: bold;"); background-repeat: repeat-x; font-weight: bold;">Nr.</td>
270 <td style="background-color: rgb(193, 162, 100); background-image: url('https://dsen.innogamescdn.com/asset/fa6f0423/graphic/screen/tableheader_bg3.png'); background-repeat: repeat-x; font-weight: bold;"); background-repeat: repeat-x; font-weight: bold;">Player</td>
271 <td style="background-color: rgb(193, 162, 100); background-image: url('https://dsen.innogamescdn.com/asset/fa6f0423/graphic/screen/tableheader_bg3.png'); background-repeat: repeat-x; font-weight: bold;"); background-repeat: repeat-x; font-weight: bold;">Village name</td>
272 <td style="background-color: rgb(193, 162, 100); background-image: url('https://dsen.innogamescdn.com/asset/fa6f0423/graphic/screen/tableheader_bg3.png'); background-repeat: repeat-x; font-weight: bold;"); background-repeat: repeat-x; font-weight: bold;">Points</td>
273 <td style="background-color: rgb(193, 162, 100); background-image: url('https://dsen.innogamescdn.com/asset/fa6f0423/graphic/screen/tableheader_bg3.png'); background-repeat: repeat-x; font-weight: bold;"); background-repeat: repeat-x; font-weight: bold;">Distance</td>
274 </tr>
275 </tbody>`);
276 //create header
277 for (var current = 0; current < coords.length; current++) {
278 //create content
279 createCurrentVillageTable("detailsVillages", current, [[current + 1, coords[current].value, "", "", ""]]);
280 }
281 }
282
283 function createCurrentVillageTable(divID, villageNumber, tableData) {
284 //make object from coord
285 villageCoord = { value: tableData[0][1] };
286 let villageData = findVillageData(villageCoord);
287
288 var tableBody = document.getElementById(divID);
289 $(tableBody).append(`<tr id="testVillage${villageNumber + 1}"><th>${villageNumber + 1}</th><th id="playerName${villageNumber + 1}"></th><th id="villageName${villageNumber + 1}"></th><th id="villagePoints${villageNumber + 1}"></th><th id="villageDistance${villageNumber + 1}"></th><th></th></tr>`);
290
291 if (villageData.reports != null) {
292 console.log(villageData.reports[0].occurredAt.toLocaleString());
293 lastHaul = "Last haul: " + villageData.reports[0].occurredAt.toLocaleString();
294 }
295 else {
296 lastHaul = "Last haul: Unknown";
297 }
298
299
300 var currentReport = [];
301
302 $.get(window.location.origin + `/game.php?&screen=api&ajax&screen=report&view=${villageData.reports[0].id}`, function (data) {
303 thisReport = $(data).find(".report_ReportAttack")[0];
304 currentReport.push(thisReport);
305 console.log(currentReport);
306 $(`#reportSpoiler${villageNumber + 1}`).append(`<div style="margin:20px; margin-top:5px; max-width:100%"><div class="quotetitle"><b>Report:</b> <input type="button" value="Show" style="width:45px;font-size:10px;margin:0px;padding:0px;" onclick="if (this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display != '') { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = ''; this.innerText = ''; this.value = 'Hide'; } else { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = 'none'; this.innerText = ''; this.value = 'Show'; }" /></div><div class="quotecontent" ><div style="border:1px solid black;display: none;">${currentReport[0].innerHTML}</div></div></div>`);
307 });
308
309 // add collapse image
310 $(`#testVillage${villageNumber + 1}`)[0].children[$(`#testVillage${villageNumber + 1}`)[0].children.length - 1].innerHTML = `<img data-hidden="false" class="toggle" style="display:block; margin-left: auto; margin-right: auto;cursor:pointer;" src="graphic/plus.png")></img>`;
311
312
313 // add data from village to next row
314 $(`#testVillage${villageNumber + 1}`).eq(0).after(`
315 <tr id="details${villageNumber + 1}" style="display:none">
316 <td></td>
317 <td>Coordinate: ${villageData.coords}</td>
318 <td>Village id: ${villageData.id}</td>
319 <td>${lastHaul}</td>
320 <td></td>
321 </tr>
322 <tr style="display:none">
323 <td><span class="icon header wood"></span></td>
324 <td id="woodHaul${villageNumber + 1}"></td>
325 <td id="woodScout${villageNumber + 1}"></td>
326 <td id="woodBuilding${villageNumber + 1}"></td>
327 <td></td>
328 </tr>
329 <tr style="display:none">
330 <td><span class="icon header stone"></span></td>
331 <td id="clayHaul${villageNumber + 1}"></td>
332 <td id="clayScout${villageNumber + 1}"></td>
333 <td id="clayBuilding${villageNumber + 1}"></td>
334 <td></td>
335 </tr>
336 <tr style="display:none">
337 <td><span class="icon header iron"></span></td>
338 <td id="ironHaul${villageNumber + 1}"></td>
339 <td id="ironScout${villageNumber + 1}"></td>
340 <td id="ironBuilding${villageNumber + 1}"></td>
341 <td></td>
342 </tr>
343 <tr style="display:none"><td id="reportSpoiler${villageNumber + 1}" colspan=5><div></div></td></tr>`);
344
345 //do the haul stuff
346 if (villageData.reports[0].res.haul != null) {
347 if (villageData.reports[0].res.haul.wood != null) {
348 $(`#woodHaul${villageNumber + 1}`).text(`Last haul: ${villageData.reports[0].res.haul.wood}`);
349 }
350 else {
351 $(`#woodHaul${villageNumber + 1}`).text(`Last haul: Looted all`);
352 }
353 if (villageData.reports[0].res.haul.clay != null) {
354 $(`#clayHaul${villageNumber + 1}`).text(`Last haul: ${villageData.reports[0].res.haul.clay}`);
355 }
356 else {
357 $(`#clayHaul${villageNumber + 1}`).text(`Last haul: Looted all`);
358 }
359 if (villageData.reports[0].res.haul.iron != null) {
360 $(`#ironHaul${villageNumber + 1}`).text(`Last haul: ${villageData.reports[0].res.haul.iron}`);
361 }
362 else {
363 $(`#ironHaul${villageNumber + 1}`).text(`Last haul: Looted all`);
364 }
365 }
366 else {
367 $(`#woodHaul${villageNumber + 1}`).text(`Last haul: No data`);
368 $(`#clayHaul${villageNumber + 1}`).text(`Last haul: No data`);
369 $(`#ironHaul${villageNumber + 1}`).text(`Last haul: No data`);
370 }
371
372 //do the scouty stuff
373 if (villageData.reports[0].res.scouted != null) {
374 if (villageData.reports[0].res.scouted.wood != null) {
375 $(`#woodScout${villageNumber + 1}`).text(`Last scout: ${villageData.reports[0].res.scouted.wood}`);
376 }
377 else {
378 $(`#woodScout${villageNumber + 1}`).text(`Last scout: No res left`);
379 }
380 if (villageData.reports[0].res.scouted.clay != null) {
381 $(`#clayScout${villageNumber + 1}`).text(`Last scout: ${villageData.reports[0].res.scouted.clay}`);
382 }
383 else {
384 $(`#clayScout${villageNumber + 1}`).text(`Last scout: No res left`);
385 }
386 if (villageData.reports[0].res.scouted.iron != null) {
387 $(`#ironScout${villageNumber + 1}`).text(`Last scout: ${villageData.reports[0].res.scouted.iron}`);
388 }
389 else {
390 $(`#ironScout${villageNumber + 1}`).text(`Last scout: No res left`);
391 }
392 }
393 else {
394 $(`#woodScout${villageNumber + 1}`).text(`Last scout: No data`);
395 $(`#clayScout${villageNumber + 1}`).text(`Last scout: No data`);
396 $(`#ironScout${villageNumber + 1}`).text(`Last scout: No data`);
397 }
398
399 //do the building stuff
400 //grab estimates of pit levels
401
402 estimatePitLevels([villageData], () => { });
403
404 if (villageData.reports[0].buildings != null) {
405 console.log("found buildings");
406 $(`#woodBuilding${villageNumber + 1}`).text(`Building level: ${villageData.reports[0].buildings.wood}`);
407 $(`#clayBuilding${villageNumber + 1}`).text(`Building level: ${villageData.reports[0].buildings.clay}`);
408 $(`#ironBuilding${villageNumber + 1}`).text(`Building level: ${villageData.reports[0].buildings.iron}`);
409 }
410 else {
411 console.log("didnt find buildings");
412 $(`#woodBuilding${villageNumber + 1}`).text(`Building level estimate: ${villageData.estimates.woodLevel}`);
413 $(`#clayBuilding${villageNumber + 1}`).text(`Building level estimate: ${villageData.estimates.clayLevel}`);
414 $(`#ironBuilding${villageNumber + 1}`).text(`Building level estimate: ${villageData.estimates.ironLevel}`);
415 }
416
417
418 //add listener
419 $(`#testVillage${villageNumber + 1}`).click(() => {
420 console.log("Clicked");
421 if ($(`#testVillage${villageNumber + 1}`)[0].children[$(`#testVillage${villageNumber + 1}`)[0].children.length - 1].children[0].src == window.location.origin + "/graphic/plus.png") {
422 console.log('should go to minus');
423 $(`#testVillage${villageNumber + 1}`)[0].children[$(`#testVillage${villageNumber + 1}`)[0].children.length - 1].children[0].src = "/graphic/minus.png";
424 //change this to the rows after the header
425 $(`#testVillage${villageNumber + 1}`)[0].nextElementSibling.style = "display:table-row";
426 $(`#testVillage${villageNumber + 1}`)[0].nextElementSibling.nextElementSibling.style = "display:table-row";
427 $(`#testVillage${villageNumber + 1}`)[0].nextElementSibling.nextElementSibling.nextElementSibling.style = "display:table-row";
428 $(`#testVillage${villageNumber + 1}`)[0].nextElementSibling.nextElementSibling.nextElementSibling.nextElementSibling.style = "display:table-row";
429 $(`#testVillage${villageNumber + 1}`)[0].nextElementSibling.nextElementSibling.nextElementSibling.nextElementSibling.nextElementSibling.style = "display:table-row";
430 }
431 else {
432 console.log('should go to plus');
433 $(`#testVillage${villageNumber + 1}`)[0].children[$(`#testVillage${villageNumber + 1}`)[0].children.length - 1].children[0].src = "/graphic/plus.png";
434 //change this to the rows after the header
435 $(`#testVillage${villageNumber + 1}`)[0].nextElementSibling.style = "display:none";
436 $(`#testVillage${villageNumber + 1}`)[0].nextElementSibling.nextElementSibling.style = "display:none";
437 $(`#testVillage${villageNumber + 1}`)[0].nextElementSibling.nextElementSibling.nextElementSibling.style = "display:none";
438 $(`#testVillage${villageNumber + 1}`)[0].nextElementSibling.nextElementSibling.nextElementSibling.nextElementSibling.style = "display:none";
439 $(`#testVillage${villageNumber + 1}`)[0].nextElementSibling.nextElementSibling.nextElementSibling.nextElementSibling.nextElementSibling.style = "display:none";
440 }
441 });
442
443 // display village info
444 $.get('/game.php?&screen=api&ajax=target_selection&input=' + villageData.coords + '+&type=coord', function (json) {
445 data = JSON.parse(json);
446 if (data.villages[0].player_name != null) {
447 $(`#playerName${villageNumber + 1}`).text(data.villages[0].player_name);
448 }
449 else {
450 $(`#playerName${villageNumber + 1}`).text("");
451 }
452 console.log(data.villages[0].points);
453 $(`#villageName${villageNumber + 1}`).text(data.villages[0].name);
454 $(`#villagePoints${villageNumber + 1}`)[0].innerHTML=data.villages[0].points;
455 $(`#villageDistance${villageNumber + 1}`).text(data.villages[0].distance);
456 });
457
458
459 }
460
461 /**
462 * Rally point logic
463 */
464
465 function fillRallyPoint(onDone, onError) {
466 // TODO
467
468 let availableTroops = getAvailableTroops();
469 console.log('Got troops: ', availableTroops);
470
471 let selectedComposition = getBestComposition(availableTroops);
472 if (!selectedComposition) {
473 alert('Not enough troops!');
474 return;
475 }
476
477 let nextVillage = loadNextVillage();
478 let villageData = findVillageData(nextVillage);
479
480 if (villageData) {
481 calculateTroopsFromData(villageData, selectedComposition, (troopCounts) => applyTroopCounts(troopCounts));
482 } else {
483 let troopCounts = selectedComposition.map(unit => ({ name: unit.name, count: unit.min }));
484 console.log('No village data available, using minimum for current composition');
485 applyTroopCounts(troopCounts);
486 }
487
488 // Applies the expected number of troops and applies scout option
489 function applyTroopCounts(troopCounts) {
490 troopCounts = troopCounts.filter(t => t.count > 0);
491
492 if (!containsScouts(troopCounts) && availableTroops['spy'] && settings.addScout)
493 troopCounts.push({ name: 'spy', count: 1 });
494
495 troopCounts = troopCounts.map((unit) => ({ name: unit.name, count: Math.min(unit.count, availableTroops[unit.name]) }));
496 console.log('Fill counts after considering available troops: ', troopCounts);
497
498 fillTroopCounts(troopCounts);
499 onDone();
500 }
501
502 /**
503 * Helper functions
504 */
505
506 function loadHistory() {
507 return JSON.parse(localStorage.getItem(settings.cookieName) || '[]');
508 }
509
510 function saveHistory(history) {
511 // should be array of coord strings, not parsed coord objects
512 let coords = history.map(c => {
513 if (typeof c == 'object') {
514 return c.value;
515 } else {
516 return c;
517 }
518 });
519
520 localStorage.setItem(settings.cookieName, JSON.stringify(coords));
521 }
522
523 function containsScouts(troops) {
524 let scouts = troops.filter(t => t.name == 'spy');
525 if (scouts.length) return scouts[0].count > 0;
526 else return false;
527 }
528
529 function getAvailableTroops() {
530 let availableTroops = {};
531 game_data.units
532 // Get counts for supported units
533 .map(name => ({
534 name: name,
535 match: $(`#units_entry_all_${name}`).text().match(/(\d+)/)
536 }))
537 // Ignore units without any data (aka militia)
538 .filter(d => d.match != null)
539 // Process
540 .map(d => ({
541 name: d.name,
542 count: parseInt(d.match[1])
543 }))
544 // Store in object
545 .forEach((d) => {
546 availableTroops[d.name] = d.count;
547 });
548
549 return availableTroops;
550 }
551
552 function getBestComposition(availableTroops) {
553 let availableCompositions = settings.allowedTroops
554 .filter(comp => {
555 let unsatisfiedReqs = comp.filter(unit => availableTroops[unit.name] < unit.min);
556 return unsatisfiedReqs.length == 0;
557 });
558
559 if (availableCompositions.length)
560 return availableCompositions[0];
561 else
562 return null;
563 }
564
565 function loadNextVillage() {
566 let history = loadHistory();
567 let availableVillages = targetCoords.filter(c => history.indexOf(c.value) < 0);
568 if (availableVillages.length == 0) {
569 alert('Gone through all villages, starting from the beginning');
570 history = [];
571 availableVillages = targetCoords;
572 saveHistory(history);
573 }
574
575 let nextVillage = availableVillages[0];
576 console.log('Filling rally point for village: ', nextVillage);
577
578 history.push(nextVillage.value);
579 saveHistory(history);
580
581 let $coordsInput = $('input[data-type=player]');
582 $coordsInput.val(nextVillage.value);
583 $coordsInput.submit();
584 return nextVillage;
585 }
586
587 function calculateTroopsFromData(villageData, composition, onDone) {
588 console.log('Using stored data: ', villageData);
589 let slowestUnit = getSlowestUnit(composition);
590 console.log('Slowest unit is: ', slowestUnit);
591
592 // These functions were originally supposed to process all village data, so we could skip
593 // targets that might not have resources by the time the current attack lands. Just
594 // a TODO for now
595 villageData = [villageData];
596
597 estimatePitLevels(villageData, () => {
598 calculateResourceEstimates(villageData, (estimates) => {
599 modifyEstimatesForExistingCommands(villageData, estimates);
600 let estimate = estimates[0];
601 let currentResources = estimate.current;
602 let laterResources = estimate.afterTravel[slowestUnit];
603
604 console.log('Current resource estimate: ', currentResources);
605 console.log('Resource estimate after travel by ' + slowestUnit, laterResources);
606
607 let targetHaul = laterResources.total;
608 let bestTroops = getFarmTroopsFromComposition(composition, targetHaul);
609 console.log('Generated best troops: ', bestTroops);
610 let bestHaul = calculateMaxHaul(bestTroops);
611 console.log('Best troops will haul ' + bestHaul + ' for a target haul of ' + targetHaul);
612
613 let troopsArray = Object.keys(bestTroops).map(name => ({ name: name, count: bestTroops[name] }));
614 onDone(troopsArray);
615 }, onError);
616 }, onError);
617 }
618
619 function getFarmTroopsFromComposition(composition, targetHaul) {
620 let troopsMin = {}, troopsMax = {};
621 composition.forEach((unit) => {
622 troopsMin[unit.name] = unit.min;
623 troopsMax[unit.name] = unit.max;
624 });
625
626 let haulMin = calculateMaxHaul(troopsMin);
627 let haulMax = calculateMaxHaul(troopsMax);
628
629 // Ratio of troops required to reach the target haul
630 let troopsRatio = (targetHaul - haulMin) / (haulMax - haulMin);
631 troopsRatio = Math.min(1, troopsRatio);
632 troopsRatio = Math.max(0, troopsRatio);
633
634 console.log('Optimal haul ratio at: ', troopsRatio);
635 console.log(`(based on min haul of ${haulMin} and max haul of ${haulMax} for target haul ${targetHaul})`);
636
637 let result = {};
638 composition.forEach((unit) => {
639 let targetCount = unit.min + troopsRatio * (unit.max - unit.min);
640 result[unit.name] = Math.ceil(targetCount);
641 });
642
643 return result;
644 }
645
646 function getSlowestUnit(troops) {
647 // Make a copy first
648 troops = troops.slice(0);
649 let allSpeeds = Data.travelSpeeds();
650 let troopSpeeds = troops.map(unit => ({ name: unit.name, speed: allSpeeds[unit.name] }));
651 troopSpeeds.sort((a, b) => b.speed - a.speed);
652 return troopSpeeds[0].name;
653 }
654
655 function fillTroopCounts(troops) {
656 // convert from object to array if necessary
657 if (!(troops instanceof Array)) {
658 troops = Object.keys(troops).map((unit) => ({ name: unit, count: troops[unit] }));
659 }
660
661 $('input[id^=unit_input_]').val('');
662 troops.forEach((unit) => $(`#unit_input_${unit.name}`).val(unit.count));
663 }
664
665
666 }
667
668 function findVillageData(coord) {
669 let matches = getSavedData().filter(v => v.coords == coord.value);
670 if (matches.length > 0)
671 return matches[0];
672 else
673 return null;
674 }
675 function addManagementInterfaceLink() {
676 // TODO
677 // Adds a link somewhere on rally point that opens main data management UI
678 $('#playerFarmUI').remove();
679 var openUI = `
680 <div id="playerFarmUI" class="target-select clearfix vis" style="display:inline-block">
681 <h4>Script tools:</h4>
682 <table class="vis" style="width: 100%">
683 <tbody>
684 <tr>
685 <td>
686 ${makeTwButton('openUI', 'Open UI')}</td>
687 </tr>
688 </tbody></table>
689 </div>`;
690 $("#command_actions").after(openUI);
691 $("#playerFarmUI").draggable();
692 $('#openUI').click(displayManagementUI);
693 }
694
695
696
697 /**
698 * Data loading
699 */
700
701 function parseDate(text) {
702 // Regular date parsing assumes text is in user's local time,
703 // need to manually convert it to a UTC time
704 let parsed = new Date(text);
705
706 return new Date(
707 Date.UTC(
708 parsed.getFullYear(), parsed.getMonth(), parsed.getDate(), parsed.getHours(), parsed.getMinutes(), parsed.getSeconds(), parsed.getMilliseconds()
709 )
710 );
711 }
712
713 function flattenArray(array) {
714 let result = [];
715 array.forEach((subarray) => result.push(...subarray));
716 return result;
717 }
718
719 function checkContainsCaptcha(html) {
720 return html.indexOf('data-bot-protect=') >= 0;
721 }
722
723 // requests for pages/etc, store results when done
724 function updateTargetVillageData(
725 onDone, // invoke when all tasks are complete and data is saved, onDone(loadedData)
726 onError, // invoke when any error occurs, onError(errorInfo)
727 onProgress // report progress of data loading, ie onProgress("loading village 5/22")
728 ) {
729 // will be ie { '500|500': 123, ... }
730 var villageIds = {};
731 // will be ie [ { targetCoord: '500|500', reportId: 456, reportUrl: reportLink1 }, ... ]
732 var reportLinks = [];
733 // will be ie { '500|500': [ report1, report2, etc. ]}, same format as villageData.reports
734 var reportData = {};
735
736 // Prefill with existing data
737 getSavedData().forEach((village) => {
738 reportData[village.coords] = village.reports;
739 villageIds[village.coords] = village.id;
740 });
741
742 var existingReports = getSavedData().map(v => v.reports.map(r => r.id));
743 existingReports = flattenArray(existingReports);
744
745
746 // Get village IDs
747 getVillageIds(() => {
748 // Get report links after we get village IDs
749 getVillageReportLinks(() => {
750 // Get reports after we get all the links
751 getReports(() => {
752 // Build the final data
753 var allVillageData = buildVillageData();
754 // Pass data to onDone
755 onDone(allVillageData);
756 });
757 });
758 });
759
760 // Gets IDs for all villas and stores in villageIds object
761 function getVillageIds(onDone) {
762 var villageInfoUrls = targetCoords
763 .filter(c => !villageIds[c.value])
764 .map(coord => ({ coord: coord, url: `game.php?&screen=api&ajax=target_selection&input=${coord.value}&type=coord` }));
765
766 console.log('Made villa info URLs: ', villageInfoUrls);
767 $.getAll(villageInfoUrls.map(u => u.url),
768 (i, data) => {
769 onProgress && onProgress(makeProgressLabel('Getting village IDs', i, villageInfoUrls.length))
770
771 var coord = villageInfoUrls[i].coord;
772 var village = JSON.parse(data).villages[0];
773 if (village)
774 villageIds[coord.value] = village.id;
775 else
776 villageIds[coord.value] = null;
777 },
778 () => {
779 onDone()
780 },
781 onError
782 );
783 }
784
785 // Gets report links for all villas and stores in reportLinks object
786 function getVillageReportLinks(onDone) {
787 var villagePageUrls = targetCoords.map(coord => {
788 let id = villageIds[coord.value];
789 return `/game.php?&screen=info_village&id=${id}`;
790 });
791
792 $.getAll(villagePageUrls,
793 (i, villagePage) => {
794
795 if (checkContainsCaptcha(villagePage)) {
796 throw "Captcha was triggered, refresh and try again";
797 }
798
799 // Pulling report links from villa page
800 var data = $(villagePage).find("#report_table")[0];
801
802 //find all URLs
803 var reportTag = $(data).find('input[type="checkbox"]').siblings("a");
804
805 //get report IDs and urls
806 var coord = targetCoords[i];
807 for (let i = 0; i < reportTag.length; i++) {
808 var currentReportID = reportTag[i].href.match(/(view\=)(\d*)/)[2];
809 // check that we haven't loaded this report before
810 if (existingReports.indexOf(currentReportID) < 0) {
811 reportLinks.push({
812 reportId: currentReportID,
813 reportUrl: reportTag[i].href,
814 targetCoord: coord.value
815 });
816 }
817 }
818
819 onProgress && onProgress(makeProgressLabel('Collecting report links', i, villagePageUrls.length));
820 },
821 onDone,
822 onError
823 )
824 }
825
826 //get report information, stores in reportData object
827 function getReports(onDone) {
828 $.getAll(reportLinks.map(l => l.reportUrl), // map from array of objects to array of urls
829 (i, data) => {
830 if (checkContainsCaptcha(data)) {
831 throw "Captcha was triggered, refresh and try again";
832 }
833
834 var reportUrlInfo = reportLinks[i];
835 var currentReportData = $(data).find(".report_ReportAttack");
836
837 //check if scout data exists
838 var buildingDataCurrentReport = null;
839 if (($(currentReportData).find("#attack_spy_building_data")[0] == undefined) == false) {
840 //grab building data for current report
841 var buildingDataCurrentReport = JSON.parse($(currentReportData).find("#attack_spy_building_data")[0].value);
842 let woodData = buildingDataCurrentReport.filter(function (pit) { return pit.id == "wood" })[0];
843 let clayData = buildingDataCurrentReport.filter(function (pit) { return pit.id == "stone" })[0];
844 var ironData = buildingDataCurrentReport.filter(function (pit) { return pit.id == "iron" })[0];
845
846 // check that there's actually pit data in the report (happens if all pits lv 0)
847 if (woodData || clayData || ironData) {
848 buildingDataCurrentReport = {
849 wood: woodData ? parseInt(woodData.level) : 1,
850 clay: clayData ? parseInt(clayData.level) : 1,
851 iron: ironData ? parseInt(ironData.level) : 1
852 };
853 } else {
854 buildingDataCurrentReport = null;
855 }
856 }
857 console.log("Building data: ", buildingDataCurrentReport);
858 //check if haul data exists
859 if (($(currentReportData).find("#attack_results")[0] == undefined) == false) {
860 //collect haul data for current report
861 var woodHaul = parseInt($($(currentReportData).find('#attack_results .nowrap')[0]).text().replace(/[^\d\s]/g, ''));
862 var clayHaul = parseInt($($(currentReportData).find('#attack_results .nowrap')[1]).text().replace(/[^\d\s]/g, ''));
863 var ironHaul = parseInt($($(currentReportData).find('#attack_results .nowrap')[2]).text().replace(/[^\d\s]/g, ''));
864 var haulTotal = $(currentReportData).find("#attack_results tr td")[1].innerText;
865 console.log(haulTotal);
866 haulTotal = haulTotal.split("/");
867 }
868 else {
869 var woodHaul = null;
870 }
871
872 // if this is none, there is no info
873 if (($(currentReportData).find("#attack_spy_resources")[0] == undefined) == false) {
874 var scoutInfoExists = $($(currentReportData).find("#attack_spy_resources")[0]).find('td')[0].innerText;
875 }
876
877 //collect spy data
878 if (reportData[reportUrlInfo.targetCoord]) {
879 // if coord already has report info keep adding to it.
880 }
881 else {
882 // create coord information
883 reportData[reportUrlInfo.targetCoord] = [];
884 }
885
886 //get battle time
887 var timeOfBattle = parseDate(currentReportData[0].parentElement.parentElement.children[1].children[1].innerText);
888
889 var currentReport = {
890 "id": reportUrlInfo.reportId,
891 "res": {
892 "haul": {},
893 "scouted": {}
894 },
895 "buildings": {},
896 "wasMaxHaul": {},
897 "occurredAt": timeOfBattle
898 };
899
900 reportData[reportUrlInfo.targetCoord].push(currentReport);
901
902 // push scout info
903 if (scoutInfoExists == "none" || scoutInfoExists == null) {
904 currentReport.res.scouted = null;
905 }
906 else {
907 var woodScouted = parseInt($($(currentReportData).find('#attack_spy_resources span.nowrap')[0]).text().replace(/[^\d\s]/g, ''));
908 var clayScouted = parseInt($($(currentReportData).find('#attack_spy_resources span.nowrap')[1]).text().replace(/[^\d\s]/g, ''));
909 var ironScouted = parseInt($($(currentReportData).find('#attack_spy_resources span.nowrap')[2]).text().replace(/[^\d\s]/g, ''));
910 currentReport.res.scouted =
911 {
912 "wood": woodScouted,
913 "clay": clayScouted,
914 "iron": ironScouted
915 };
916 }
917
918 // push haul info
919 if (woodHaul != null) {
920 currentReport.res.haul =
921 {
922 "wood": woodHaul,
923 "clay": clayHaul,
924 "iron": ironHaul
925 };
926 if (haulTotal[0] == haulTotal[1]) {
927 currentReport.wasMaxHaul = true;
928 }
929 else {
930 currentReport.wasMaxHaul = false;
931 }
932 }
933 else {
934 currentReport.wasMaxHaul = null;
935 currentReport.res.haul = null;
936 }
937
938 //push building info
939 if (buildingDataCurrentReport != null) {
940 currentReport.buildings = buildingDataCurrentReport;
941 }
942 else {
943 currentReport.buildings = null;
944 }
945 console.table(reportData[reportUrlInfo.targetCoord]);
946
947 onProgress(makeProgressLabel('Loading reports', i, reportLinks.length));
948 },
949 onDone,
950 onError
951 );
952 }
953
954 // Takes all the downloaded info and returns the final village objects
955 function buildVillageData() {
956 return targetCoords.map((coord) => {
957 // set up the data
958 /*
959 var villageData = { "coords": coord.value, "id": villageID, "reports": [], "estimates": {}, "currentCommands": [] };
960 var villageReports = reportData[coord.value];*/
961
962 return {
963 coords: coord.value,
964 id: villageIds[coord.value],
965 reports: reportData[coord.value] || [],
966 estimates: null,
967 currentCommands: []
968 };
969 });
970 }
971 }
972
973
974
975 /**
976 * Data processing
977 */
978
979 function getWorldSettings(onDone, onError) {
980 const lsKey = 'pf-worldsettings';
981 if (localStorage.getItem(lsKey)) {
982 const worldSettingsJson = localStorage.getItem(lsKey);
983 console.log('Loaded world settings from local storage: ', worldSettingsJson)
984 let worldSettings = JSON.parse(worldSettingsJson);
985 onDone(worldSettings.worldSpeed, worldSettings.unitSpeed);
986 } else {
987 const settingsUrl = `${window.location.origin}/interface.php?func=get_config`;
988 console.log('Loading world settings from URL: ', settingsUrl);
989 $.get(settingsUrl)
990 .done((data) => {
991 let $xml = $(data);
992 console.log('Got XML', data, $xml);
993 let worldSettings = {
994 worldSpeed: parseFloat($xml.find('speed').text()),
995 unitSpeed: parseFloat($xml.find('unit_speed').text())
996 };
997 console.log('Found world settings: ', worldSettings);
998 localStorage.setItem(lsKey, JSON.stringify(worldSettings));
999 onDone(worldSettings.worldSpeed, worldSettings.unitSpeed);
1000 })
1001 .fail((xhr) => {
1002 onError(`Failed to load URL ${settingsUrl}: code ${xhr.status} / ${xhr.statusText}`);
1003 })
1004 }
1005 }
1006
1007 function getResourceRates(onDone, onError) {
1008 getWorldSettings((worldSpeed) => {
1009 let pitResourceRates = [
1010 30, 35, 41, 47, 55, 64, 74, 86, 100, 117,
1011 136, 158, 184, 214, 249, 289, 337, 391, 455, 530,
1012 616, 717, 833, 969, 1127, 1311, 1525, 1774, 2063, 2400
1013 ];
1014 pitResourceRates.forEach((res, i) => pitResourceRates[i] = Math.ceil(res * worldSpeed));
1015 onDone(pitResourceRates);
1016 }, onError);
1017 }
1018
1019 // sorts the given array based on the given dateProp in descending order (most recent
1020 // is first)
1021 function sortByDate(array, dateProp) {
1022 array.sort((a, b) => b[dateProp].valueOf() - a[dateProp].valueOf());
1023 }
1024
1025 function reportHasData(report) {
1026 return report.res.haul || report.res.scouted;
1027 }
1028
1029 function roundResources(resources) {
1030 return {
1031 wood: Math.round(resources.wood),
1032 clay: Math.round(resources.clay),
1033 iron: Math.round(resources.iron)
1034 };
1035 }
1036
1037 // Get amount of res left over after looting
1038 function reportReminaingRes(report, resType) {
1039 if (report.res.scouted) {
1040 return report.res.scouted[resType];
1041 } else {
1042 return 0;
1043 }
1044 }
1045
1046 // Get total amount of res shown in report
1047 function reportTotalRes(report, resType) {
1048 let total = 0;
1049 if (report.res.haul) total += report.res.haul[resType];
1050 if (report.res.scouted) total += report.res.scouted[resType];
1051 return total;
1052 }
1053
1054 function calculateMaxHaul(troops) {
1055 const unitHauls = Data.unitHauls();
1056
1057 let total = 0;
1058 Object.keys(troops).forEach((unit) => {
1059 let count = troops[unit];
1060 total += count * unitHauls[unit];
1061 });
1062 return total;
1063 }
1064
1065 // Modifies villageData[].estimates, onDone() takes no params
1066 function estimatePitLevels(villageData, onDone, onError) {
1067 function estimatePitByResDifference(pitRates, resDiff, timeDiff) {
1068 let timeDiffHours = timeDiff / (1000 * 60 * 60);
1069 let resPerHour = resDiff / timeDiffHours;
1070 if (resPerHour <= 0)
1071 return 1;
1072
1073 let pitLevel = 0;
1074 for (let i = 0; i < pitRates.length; i++) {
1075 // Keep updating pitLevel to the current level until
1076 // we hit a point where the given 'resPerHour' can't
1077 // meet the expected 'rate'
1078 let rate = pitRates[i];
1079
1080 // arbitrary +3 to correct any off-by-one errors, ie lv5 @ 55/hr, but we've detected 54/hr - should
1081 // be detected as lv5 but would use lv4 instead because of that small difference..
1082 if (resPerHour + 3 >= rate)
1083 pitLevel = i + 1;
1084 else
1085 break;
1086 }
1087 return pitLevel;
1088 }
1089
1090 // pitName needs to match the name of reports[].buildings, ie 'wood'/'clay'/'iron'
1091 function estimatePit(pitRates, village, pitName) {
1092 let highestPitLevel = 1;
1093 // Pull from latest report first, if available
1094 for (let i = 0; i < village.reports.length; i++) {
1095 let report = village.reports[i];
1096 if (report.buildings) {
1097 highestPitLevel = Math.max(highestPitLevel, report.buildings[pitName]);
1098 break;
1099 }
1100 }
1101
1102 let usefulReports = village.reports
1103 // Skip reports that had max haul and didn't have any scout on resources
1104 .filter(r => r.res.scouted != null || !r.wasMaxHaul)
1105 // Skip reports without any useful data
1106 .filter(r => reportHasData(r));
1107
1108 sortByDate(usefulReports, 'occurredAt');
1109
1110 // Compare time and res between reports
1111 for (let i = 1; i < usefulReports.length; i++) {
1112 // first is earlier than second
1113 let first = usefulReports[i - 1];
1114 let second = usefulReports[i];
1115
1116 // time between reports in ms
1117 let timeDifference = first.occurredAt.valueOf() - second.occurredAt.valueOf();
1118 let previousLeftoverRes = reportReminaingRes(second, pitName);
1119 let currentTotalRes = reportTotalRes(first, pitName);
1120
1121 // detect possible pit level
1122 let resGainedBetweenReports = currentTotalRes - previousLeftoverRes;
1123 let expectedPitLevel = estimatePitByResDifference(pitRates, resGainedBetweenReports, timeDifference);
1124
1125 // take the highest level seen
1126 highestPitLevel = Math.max(expectedPitLevel, highestPitLevel);
1127 }
1128 return highestPitLevel;
1129 }
1130
1131 getResourceRates((pitResourceRates) => {
1132 villageData.forEach((village) => {
1133 village.estimates = {
1134 woodLevel: estimatePit(pitResourceRates, village, 'wood'),
1135 clayLevel: estimatePit(pitResourceRates, village, 'clay'),
1136 ironLevel: estimatePit(pitResourceRates, village, 'iron')
1137 };
1138 });
1139
1140 console.log('Generated estimates');
1141 console.table(villageData.map(v => ({ coords: v.coords, ...v.estimates })));
1142 onDone();
1143 });
1144 }
1145
1146 // Modifies given resource estimates based on max hauls from existing commands
1147 // that are still traveling
1148 function modifyEstimatesForExistingCommands(villageData, estimates) {
1149 // TODO: Use settings.prioritizeExistingCommands option here
1150 // (Need to know what troop type is being used in the pending attack though)
1151 estimates.forEach((estimate, i) => {
1152 let village = villageData[i];
1153 let relevantCommands = village.currentCommands || [];
1154 let totalHaul = 0;
1155 relevantCommands.forEach((cmd) => totalHaul += calculateMaxHaul(cmd.troops));
1156 console.log('Total haul: ', totalHaul);
1157
1158 estimate.current = resourcesAfterHaul(estimate.current, totalHaul);
1159 Object.keys(estimate.afterTravel).forEach((unit) => {
1160 estimate.afterTravel[unit] = resourcesAfterHaul(estimate.afterTravel[unit], totalHaul);
1161 });
1162 });
1163
1164 function resourcesAfterHaul(resources, maxHaul) {
1165 let proportionTaken = resources.total > 0
1166 ? Math.min(maxHaul / resources.total, 1)
1167 : 1;
1168 //console.log('Proportion taken: ', proportionTaken);
1169 let proportionRemaining = 1 - proportionTaken;
1170
1171 let result = roundResources({
1172 wood: resources.wood * proportionRemaining,
1173 clay: resources.clay * proportionRemaining,
1174 iron: resources.iron * proportionRemaining
1175 });
1176
1177 result.total = result.wood + result.clay + result.iron;
1178 return result;
1179 }
1180 }
1181
1182 // Returns array of estimated resources for the given villages, both current and after travel
1183 // by different unit speeds (not considering existing commands)
1184 function calculateResourceEstimates(villageData, onDone, onError) {
1185 var currentCoords = {
1186 x: game_data.village.x,
1187 y: game_data.village.y,
1188 value: game_data.village.coord
1189 };
1190
1191 getResourceRates((pitRates) => {
1192 let currentResources = estimateCurrentResources(pitRates);
1193 console.log("Current resource estimates: ", currentResources);
1194 getWorldSettings((gameSpeed, unitSpeed) => {
1195 let travelModifier = gameSpeed * unitSpeed;
1196 let resourceEstimates = calculateResourcesAfterTravel(currentResources, travelModifier, pitRates)
1197 onDone(resourceEstimates);
1198 }, onError);
1199 });
1200
1201 function estimateCurrentResources(pitRates) {
1202 return villageData.map((village) => {
1203 let validReports = village.reports.filter(reportHasData);
1204 sortByDate(validReports, 'occurredAt');
1205 let latestReport = validReports.length
1206 ? validReports[0]
1207 : null;
1208
1209 let lastSeenResources = latestReport
1210 ? latestReport.res.scouted || { wood: 0, clay: 0, iron: 0 }
1211 : { wood: 0, clay: 0, iron: 0 };
1212
1213 let now = Timing.getCurrentServerTime();
1214 // time in ms
1215 let timeSinceReport = latestReport
1216 ? now - latestReport.occurredAt.valueOf()
1217 : 0;
1218
1219 let hoursSinceReport = timeSinceReport / (60 * 60 * 1000);
1220 console.log('Estimating current resources using report from ' + hoursSinceReport + ' hours ago');
1221
1222 let currentResources = roundResources({
1223 wood: lastSeenResources.wood + hoursSinceReport * pitRates[village.estimates.woodLevel - 1],
1224 clay: lastSeenResources.clay + hoursSinceReport * pitRates[village.estimates.clayLevel - 1],
1225 iron: lastSeenResources.iron + hoursSinceReport * pitRates[village.estimates.ironLevel - 1]
1226 });
1227
1228 currentResources.total = currentResources.wood + currentResources.clay + currentResources.iron;
1229
1230 return currentResources;
1231 });
1232 }
1233
1234 function distance(a, b) {
1235 return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
1236 }
1237
1238 function calculateResourcesAfterTravel(currentEstimates, travelModifier, pitRates) {
1239 let travelSpeeds = Data.travelSpeeds()
1240 let validUnits = game_data.units.filter(u => !!travelSpeeds[u]);
1241 validUnits.forEach((name) => travelSpeeds[name] /= travelModifier);
1242 console.log('Made effective travel speeds: ', travelSpeeds);
1243
1244 let resourceEstimates = [];
1245 villageData.forEach((village, i) => {
1246 let dist = distance(currentCoords, parseCoord(village.coords));
1247 let travelTimes = validUnits.map(unit => ({
1248 unit: unit,
1249 seconds: Math.round(travelSpeeds[unit] * 60 * dist)
1250 }));
1251 console.log('Made travel times for village ' + village.coords)
1252 //console.table(
1253 console.log(
1254 travelTimes.map(t => ({ ...t, speed: travelSpeeds[t.unit] }))
1255 );
1256
1257 let villageCurrentEstimate = currentEstimates[i];
1258 let travelResourceEstimates = {};
1259 travelTimes.forEach((t) => {
1260 let travelHours = t.seconds / (60 * 60);
1261 let resourcesCreated = {
1262 wood: Math.ceil(pitRates[village.estimates.woodLevel - 1] * travelHours),
1263 clay: Math.ceil(pitRates[village.estimates.clayLevel - 1] * travelHours),
1264 iron: Math.ceil(pitRates[village.estimates.ironLevel - 1] * travelHours)
1265 };
1266
1267 let totalResources = roundResources({
1268 wood: resourcesCreated.wood + villageCurrentEstimate.wood,
1269 clay: resourcesCreated.clay + villageCurrentEstimate.clay,
1270 iron: resourcesCreated.iron + villageCurrentEstimate.iron
1271 });
1272 totalResources.total = totalResources.wood + totalResources.clay + totalResources.iron;
1273 travelResourceEstimates[t.unit] = totalResources;
1274 });
1275
1276 resourceEstimates.push({
1277 villageCoords: village.coords,
1278 villageId: village.id,
1279 current: villageCurrentEstimate,
1280 afterTravel: travelResourceEstimates
1281 })
1282 });
1283
1284 return resourceEstimates;
1285 }
1286 }
1287
1288})();
1289//# sourceURL=https://tylercamp.me/tw/playerfarm.js