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