-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbot.js
More file actions
3192 lines (3151 loc) · 141 KB
/
bot.js
File metadata and controls
3192 lines (3151 loc) · 141 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//Paste the contents of this file directly into the developer console
$("#botHelp").remove();
$('#helpDiv').prepend($(`<div id="botHelp">
<h2>Simba by Griffin Stone</h2>
<p>This program adds queueing buttons to the left of tasks you can automate, settings in the upper right, and queue information in the lower right.</p>
<p>Left click to increase a value, right click to decrease a value.</p>
<p>Queueing buttons are displayed as a 0, 1, or ∞ to the left of a button. <ul>
<li>0: Don't buy this item.</li>
<li>1: Put this item in the queue. When you have enough resources to buy it, and nothing higher in the queue needs those resources, buy it and put it at the bottom of the queue</li>
<li>∞: As 1, but always move this item to the top of the queue (buy as many as possible).</li>
</ul></p>
<p>Settings: <ul>
<li>Bot: Turns all bot functions on/off</li>
<li>Game Speed: Can increase the speed of all game progress (even when the bot is off)</li>
<li>Bot Speed: Controls how frequently the bot runs (per game tick)</li>
<li>Bot Timer: <ul>
<li>game: Run the bot in the game loop. Guarantees the bot will not lag behind the game, but may cause the game to lag. Very useful if using Web Worker.</li>
<li>independent: Run the bot on a timer independent of the game loop.</li>
</ul></li>
<li>Timeskip bug: There is a bug in the game that causes time, but not the calendar, to move faster whenever you click a building. This causes the calendar to not be an accurate indicator of how much time has passed, and the bot to fall behind the game. Change to "fixed" to prevent time skipping.</li>
<li>API: Increase to use more of the game API to improve performance and add features like trade log combining. Decrease to instead use the actual buttons in the game interface.</li>
<li>Up Next: Controls the amount of information displayed in the Up Next panel.
<br />In Verbose mode, enqueued items you have enough storage to buy are displayed, followed by the estimated time until it is bought and the resources you need more of to buy them. In Concise, the time and resources are not displayed.
<br />If one of the needed resources is reserved by an item higher in the queue, this item is grayed out. In Concise, it is hidden instead.
</li>
<li>Master Plan: Try to move upwards through the tech tree automatically.<ul>
<li>off: Don't queue anything you haven't asked for. Recommended to still have an idle game experience, but with less management.</li>
<li>naive: Automatically enqueue technologies and upgrades that aren't useless or dangerous once the time to produce them is less than 15 minutes. Not recommended if you're actively managing the bot (and can make better decisions).</li>
</ul></li>
<li>Smart Storage: Don't buy storage (barn, warehouse, harbour) you don't need.<ul>
<li>off: Buy storage as normal. The bot may buy too much storage, especially warehouses.</li>
<li>aggressive: Only buy storage if you don't have enough to buy a queued upgrade or normal building (aqueduct, log house, library, mine, smelter, or workshop). Recommended in the early game.</li>
<li>conservative: Only buy storage if you don't have enough to buy any of your queued normal buildings. Recommended in the mid and late game.</li>
</ul></li>
<li>Auto Craft: Automatically craft resouces that are near their max storage capacity. The bot won't reserve resources for buildings whose cost is so near your storage capacity that a needed resouce would be auto crafted, but may buy them anyway if it can.<ul>
<li>off: Don't auto craft.</li>
<li>normal: Craft resources that would be full by the time the bot plans to run again.</li>
<li>safe: As normal, but assume twice as much time will pass before the bot runs. Useful at high game speeds, high bot speeds, or if the game is laggy. Consider using this mode if you see "Warning: [resource] full" messages in the console.</li>
</ul></li>
<li>Auto Farmer: Calculates the amount of catnip you need stockpiled today to survive a regular winter. Unlike the food advisor, this factors in the amount of catnip you're expected to produce before winter, making it more accurate and less overcautious.<ul>
<li>off: Don't automatically switch kittens to farmers. The bot won't buy buildings or housing that would reduce your catnip below the needed catnip stockpile for a cold winter. You can starve if you have two cold seasons in the same year.</li>
<li>on: Automatically switch a kitten to Farmer (from the most common job) if your catnip stockpile is not enough to survive winter. The bot will buy housing, but not buildings, that would reduce your catnip below the needed catnip stockpile for a cold winter. Also, gather the first 10 catnip of the game.</li>
</ul></li>
<li>Farmer ratio: Calculates the ratio of wood per farmer to wood per woodcutter. If this ratio is greater than one, you should switch all your woodcutters to farmers.</li>
<li>Auto Converters: When on, automatically turns off any conversion buildings (eg. smelter) if all of the conversion products are full and nothing in the queue needs the resources you can craft from them. The disabled buildings will be listed below, and won't be purchased.</li>
<li>Auto SETI: When on, automatically observe the sky. Doesn't work during redshift; if you have offline progression enabled, consider buying SETI to get more starcharts.</li>
</ul>If one setting is being overridden by another, its effective value will be displayed in parentheses. For example, Bot Speed currently cannot be faster than 1/Game Speed, unless Bot Timer is set to game.</p>
<p>Special buttons: These queueing buttons look normal but have a special effect when enabled, and may not actually use the queue.<ul>
<li>Send Hunters: Send hunters whenever your catpower is full or you have nothing else in the queue which needs catpower</li>
<li>Steel (in Workshop): Always make as much steel as possible (to prevent wasting coal, even if your queue needs more iron than coal)</li>
<li>Transcend: Wait for faith to be full, then click Transcend once, then click Faith Reset, then Praise the Sun.</li>
<li>Jobs: In the Jobs panel, left click a job to add it to the job queue; right click to remove. Whenever the bot buys housing, it will assign the new kitten to next job in the queue, then send that job to the end of the queue. The bot will not assign new kittens when you buy housing manually.</li>
<li>Combust TC: Combusts a time crystal whenever your chronoheat is under half of max, but only during the first 50 days of the year.</li>
<li>Buy bcoin: Sells bcoin whenever the market is about to crash or the elders are about to leave, and buys bcoin otherwise.</li>
</ul></p>
<p>Leaders: It is recommended that you make an Artisan your leader. For all tasks except crafting, if you already have a leader, the bot will automatically switch to a leader of the appropriate type. If API is set to none, a leader of the appropriate type must appear on the first page of kittens (you may need to promote one).</p>
<p>Trade: Sending trade caravans is queued like buildings. <ul>
<li>When a trade is bought from the queue, it sends as many caravans as possible without spending a resource reserved for something higher in the queue.</li>
<li>If your gold is near full, the bot will prioritize trades over other queued items (but not priority infinity items).</li>
<li>The bot will not make trades for resources that you have a lot of already, unless you enable ignoreNeeds.</li>
<li>It will only trade during optimal seasons, unless you enable ignoreSeasons.</li>
</ul>Next to tradeable resources, the bot will display the amount of time it would take one no-skill kitten to produce that much resource (in cats*ticks), or if kittens can't produce it, the amount of time it would take all your production to produce it (in ticks).</p>
<p>Allowed Crafts: In the crafting panel, you can restrict the crafting of any recipe which costs both crafted and noncrafted resources, or any unicorn conversion. In "slow" mode, the bot will only craft the recipe if you own more of the components than the result.</p>
<p>Auto Faith Reset: In the Order of the Sun panel, you can set Auto Reset to a percentage. When your base faith bonus (before Black Obelisk and Atheism bonuses) reaches this percentage, Simba will automatically store maximum faith, then reset praised faith (with apocrypha), then praise the sun.</p>
<p>Compatibility: Simba requires a modern browser (with HTML5 and ES6) and is currently only compatible with the English version of the game.</p>
<hr />
<h3>Kittens Game Official "Help"</h3>
</div>`));
$('#helpDiv').css({"margin-top": "0", top: "10%", overflow: "auto", "max-height": "75%"});
$("#botInfo").remove();
$('#gamePageContainer').append($('<div id="botInfo" style="position: absolute; bottom: 50px; right: 10px;">'));
updateBotInfoWidth = () => $("#botInfo").css("max-width", Math.max(225, $("#game").width() - $("#rightColumn")[0].getBoundingClientRect().right - 20));
updateBotInfoWidth();
$(window).resize(updateBotInfoWidth);
if (window.stopLoop) stopLoop();
/**************
* Utilities
**************/
flattenArr = arr => arr.reduce((acc, val) => acc.concat(val), []); //from Mozilla docs
getOwnText = el => { //https://stackoverflow.com/a/21249659/1914005
return $(el).contents().map(function () {
return this.nodeType == 3 && $.trim(this.nodeValue) ? $.trim(this.nodeValue) : undefined;
}).get().join('')
}
arrayToObject = (array, keyField) => {//https://medium.com/dailyjs/rewriting-javascript-converting-an-array-of-objects-to-an-object-ec579cafbfc7
return array.reduce((obj, item) => {
obj[item[keyField]] = item
return obj
}, {})
}
maxBy = (arr, fun) => {
var maxValue = Math.max(...arr.map(fun));
//we could reduce the number of iterations by one but it doesn't matter
return arr.find(item => fun(item) === maxValue);
}
Object.fromEntries = Object.fromEntries || function(arr) {
var result = {};
arr.forEach(entry => {
result[entry[0]] = entry[1];
})
return result;
}
removeOne = (arr, match) => {
var found = false;
return arr.filter(item => {
if (item === match && !found) {
found = true;
return false;
} else {
return true;
}
});
}
setAddAll = (dest, src) => src.forEach(dest.add.bind(dest));
/**************
* Main Loop
**************/
mainLoop = () => {
doHarvestingChores();
manageJobs();
state.queue = buyPrioritiesQueue(state.queue);
if (state.autoConverters) manageConverters();
doSpendingChores();
if (state.masterPlanMode) queueNewTechs();
if (!state.geneticAlgorithm) updateUi();
countTicks();
}
doHarvestingChores = () => {
if (state.autoFarmer) gatherIntialCatnip();
if (state.autoSteel) craftAll("steel");
if (state.autoSeti) observeSky();
}
doSpendingChores = () => {
loadUnicornRecipes();
if (state.autoHunt) autoHunting();
if (state.autoBcoin) autoTradeBcoin();
if (state.autoReset && getResetThreshold(state.autoResetThreshold) < getBaseFaithProductionBonus(game.religion.faith)) {
if (!findQueue("Transcend") && !findQueue("Faith Reset")) {
enable("Faith Reset", "Religion", "Order of the Sun");
}
} else {
disable("Faith Reset");
}
doAutoCraft();
}
observeSky = () => {
if (state.api >= 1) {
if (game.calendar.observeRemainingTime) {
game.calendar.observeHandler();
}
} else {
$('#observeBtn').click();
}
}
autoHunting = () => {
if ((isResourceFull("manpower") || getTotalDemand("manpower") === 0) && getResourceOwned("manpower") >= 100) {
withLeader("Manager", hunt);
}
}
hunt = () => {
if (state.api >= 1) {
game.village.huntAll();
} else {
$('a:contains("Send hunters")')[0].click();
}
}
manageJobs = () => {
//assigning first leader is one of the only things the bot will do with no way to disable it; maybe add an option?
assignFirstLeader();
if (getUnlockedJobs() > state.jobsUnlocked && state.geneticAlgorithm) {
state.jobQueue = state.originalJobQueue;
reassignAllJobs();
state.jobsUnlocked = getUnlockedJobs();
}
if (state.autoFarmer) {
preventStarvation();
}
if (game.village.isFreeKittens() && (state.geneticAlgorithm || state.populationIncrease > 0 && game.village.sim.getKittens() > state.kittensAssigned)) {
assignNewKittenJob();
}
}
getUnlockedJobs = () => getJobCounts().filter(job => isJobEnabled(job.name)).length
getNextQueuedJob = () => {
var job = state.jobQueue.find(job => isJobEnabled(job))
if (job) {
state.jobQueue = removeOne(state.jobQueue, job);
state.jobQueue.push(job);
} else {
//job that's always enabled
job = "woodcutter";
}
return job;
}
assignNewKittenJob = () => {
var job = getNextQueuedJob();
increaseJob(job);
log("Assigned new kitten to " + job);
recountKittens(job);
}
reassignAllJobs = () => {
var goalJobs = { farmer: state.temporaryFarmers }
for (var k = 0; k < game.village.sim.getKittens() - state.temporaryFarmers; k++) {
var job = getNextQueuedJob();
goalJobs[job] = (goalJobs[job] || 0) + 1;
}
console.log(goalJobs);
doReassignment = () => {
Object.keys(goalJobs).forEach(jobName => {
var toReduce = getJobCounts().find(job => job.name == jobName).val - goalJobs[jobName];
for (var i = 0; i < toReduce; i++) {
log("Unassigning " + jobName)
decreaseJob(jobName);
}
});
Object.keys(goalJobs).forEach(jobName => {
var toIncrease = goalJobs[jobName] - getJobCounts().find(job => job.name == jobName).val;
for (var i = 0; i < toIncrease; i++) {
log("Assigning " + jobName)
increaseJob(jobName);
}
});
}
if (state.api) {
doReassignment();
} else {
withTab("Village", doReassignment);
}
}
countTicks = () => {
var ticksPassed = game.ticks - state.ticks;
if (ticksPassed !== state.ticksPerLoop) console.log(ticksPassed + " ticks passed (expected " + state.ticksPerLoop + ")")
state.ticks = game.ticks;
state.blackcoinTimer++;
state.tradeTimer++;
if (state.geneticAlgorithm) {
reportFitness();
if (shouldStopRun()) {
if ($("#runFinished").length === 0) {
$("html").append('<div id="runFinished">');
}
if (!game.isPaused) {
game.togglePause();
}
setRunning(false);
}
}
}
/**************
* Queue Logic
**************/
Reservations = class {
constructor(initialReserved) {
this.reserved = Object.fromEntries(Object.entries(initialReserved).map(entry => [entry[0], Object.assign({}, entry[1])]));
}
//reserve current now, plus once you own current, reserve production every tick, until you have total
get(res) { if (!this.reserved[res]) { this.reserved[res] = { current: 0, production: 0, total: 0 }; } return this.reserved[res]; }
add(res, current, production, total) {
if ([current, production, total].some(isNaN)) throw new Error("Reserving NaN!")
var reservation = this.get(res);
reservation.current += current;
reservation.production += production;
reservation.total += total;
}
addCurrent(res, val) { this.add(res, val, 0, val) }
addOverTime(res, val, ticks, productionAvailable) {
//todo maybe it would be correct to just not include this, and if production is negative, reserve a negative amount of production if appropriate
if (productionAvailable < 0) productionAvailable = 0;
var maxOverTime = ticks * productionAvailable;
var currentNeeded;
var productionNeeded;
if (maxOverTime <= 0 || isNaN(maxOverTime)) {
productionNeeded = 0;
currentNeeded = val;
} else if (maxOverTime >= val) {
productionNeeded = val / ticks;
currentNeeded = 0;
} else {
productionNeeded = productionAvailable;
currentNeeded = val - maxOverTime;
}
this.add(res, currentNeeded, productionNeeded, val);
}
clone() { return new Reservations(this.reserved) }
};
getTimeToChange = (resource, reservations) => {
var ingredients = ingredientsMap[resource] || new Set();
return Math.ceil(Math.min(
...Object.entries(reservations.reserved)
.filter(entry => {
var reservedResource = entry[0];
return reservedResource == resource || ingredients.has(reservedResource);
})
.map(entry => {
var reservation = entry[1];
if (reservation.total <= reservation.current) return Infinity;
return (reservation.total - reservation.current) / reservation.production;
})
))
}
getFutureReservations = (reservations, ticksPassed) => {
return new Reservations(Object.fromEntries(Object.entries(reservations.reserved).map(entry => {
var resource = entry[0];
var reservation = entry[1];
var totalProductionPerTick = reservation.production;
var totalProduction = totalProductionPerTick * ticksPassed;
var total = Math.max(0, reservation.total - totalProduction);
var current = Math.min(reservation.current, total);
return [resource, { current: current, production: total > current ? reservation.production : 0, total: total }]
})))
}
//get the sum of the prices and the prices to craft any missing resources
getEffectivePrices = (prices, reserved) => {
var totalPricesMap = {};
var shortages = prices;
var maxDepth = 10;
var getShortage = price => ({ name: price.name,
val: Math.max(0, price.val - Math.max(0, getResourceOwned(price.name) - reserved.get(price.name).current - (totalPricesMap[price.name] || 0)))
})
while (shortages.length) {
if (!maxDepth--) {
//if the game ever lets you craft scaffold back into catnip...
console.error("Infinite loop finding shortages:")
console.error(shortages)
break;
}
var nextShortages = flattenArr(shortages.map(getShortage).map(getIngredientsNeeded));
shortages.forEach(price => totalPricesMap[price.name] = (totalPricesMap[price.name] || 0) + price.val)
shortages = nextShortages;
}
return Object.entries(totalPricesMap).map(price => ({name: price[0], val: price[1]}))
}
getTicksNeededPerPrice = (effectivePrices, reserved) => {
var pricesToCalculate = Array.from(effectivePrices);
var ticksNeededPerPrice = {};
while (pricesToCalculate.length) {
var priceIdx = pricesToCalculate.findIndex(price =>
!canCraft(price.name)
|| getCraftPrices(price.name).every(subPrice => ticksNeededPerPrice[subPrice.name] !== undefined)
);
if (priceIdx === -1) {
console.error(pricesToCalculate);
console.error(ticksNeededPerPrice);
throw new Error("Loop in crafting recipes, or missing effective price for ingredient");
}
var price = pricesToCalculate.splice(priceIdx, 1)[0];
if (canAffordOne(price, reserved)) {
ticksNeededPerPrice[price.name] = 0;
} else if (getEffectiveResourcePerTick(price.name) === 0) {
if (canCraft(price.name)) {
var worstIngredientTicks = Math.max(
...getCraftPrices(price.name).map(subPrice => ticksNeededPerPrice[subPrice.name])
);
if (isNaN(worstIngredientTicks)) {
console.error([effectivePrices, pricesToCalculate, price]);
throw new Error("Got NaN ticks needed");
}
ticksNeededPerPrice[price.name] = worstIngredientTicks;
} else {
ticksNeededPerPrice[price.name] = Infinity;
}
} else {
var forSteel = price.name === "coal" || price.name === "iron" && effectivePrices.some(price => price.name === "steel");
ticksNeededPerPrice[price.name] = getTicksToEnough(price, reserved, undefined, forSteel);
}
}
return ticksNeededPerPrice;
}
getTicksNeeded = (ticksPerPrice, originalPrices) => {
return Math.max(0, ...originalPrices.map(price => ticksPerPrice[price.name]));
}
reserveBufferTime = game.rate * 60 * 5; //5 minutes: reserve enough of non-limiting resources to be this far ahead of the limiting resource
bufferTicksNeeded = ticksNeeded => Math.max(0, ticksNeeded - reserveBufferTime);
canAffordOne = (price, reserved) => price.val <= 0 || getResourceOwned(price.name) - (reserved ? reserved.get(price.name).current : 0) >= price.val;
canAfford = (prices, reserved) => prices.every(price => canAffordOne(price, reserved));
//ticks until you have enough. May be infinite.
getTicksToEnough = (price, reserved, owned, forSteel, timeLimit) => {
if (timeLimit === undefined) timeLimit = Infinity;
if (owned === undefined) {
if (price.name === "iron" && state.autoSteel && !forSteel) {
//this might be negative; that's ok
owned = getResourceOwned(price.name) - getResourceOwned("coal");
} else {
owned = getResourceOwned(price.name);
}
}
if (owned - reserved.get(price.name).current >= price.val || price.val <= 0) {
return 0;
}
if (getEffectiveResourcePerTick(price.name) === 0) {
if (canCraft(price.name)) {
var ingredients = getIngredientsNeeded({name: price.name, val: price.val + reserved.get(price.name).current - owned});
var ingredientForSteel = state.autoSteel && price.name === "steel";
if (ingredientForSteel) {
//autoSteel ignores reservations
//ugh way too much special case logic for this
reserved = new Reservations({});
}
//more accurate calculation for crafted resources with no production
//still inaccurate for manuscript once you have printing press, but at least this special case is the common case for crafts
return Math.max(...ingredients.map(ingredientPrice => getTicksToEnough(ingredientPrice, reserved, undefined, ingredientForSteel, timeLimit)))
} else {
//just a performance improvement
return Infinity;
}
}
//maybe optimize this to reduce the total number of calls to getFutureReservations (easily cached)
var timeToChange = getTimeToChange(price.name, reserved);
var baseProduction = getCraftingResourcePerTick(price.name, reserved, forSteel);
var freeProduction; //production not reserved at all
var reservedForShortageProduction; //production reserved for a "current" reservation
//this seems too complicated, maybe if Reservations was redesigned this would be simpler
if (reserved.get(price.name).current > owned) {
//just ignore negative production, it won't last forever. needed to prevent infinite loop
reservedForShortageProduction = Math.max(0, baseProduction);
freeProduction = 0;
timeToChange = Math.min(timeToChange, Math.ceil((reserved.get(price.name).current - owned) / reservedForShortageProduction));
} else {
reservedForShortageProduction = 0;
freeProduction = baseProduction - reserved.get(price.name).production;
}
if (freeProduction <= 0) {
//all production is reserved
if (timeToChange >= timeLimit) return Infinity;
return timeToChange + getTicksToEnough(price, getFutureReservations(reserved, timeToChange), owned + reservedForShortageProduction * timeToChange, forSteel, timeLimit - timeToChange);
} else if (freeProduction * timeToChange + owned - reserved.get(price.name).current >= price.val) {
return Math.ceil((price.val + reserved.get(price.name).current - owned) / freeProduction);
} else {
if (timeToChange === Infinity) throw new Error("Infinite price???");
if (timeToChange >= timeLimit) return Infinity;
return timeToChange + getTicksToEnough(price, getFutureReservations(reserved, timeToChange), owned + freeProduction * timeToChange, forSteel, timeLimit - timeToChange)
}
}
getResourcesToReserve = (effectivePrices, ticksPerPrice, ticksNeeded, reserved) => {
var isLimitingResource = price => ticksPerPrice[price.name] >= ticksNeeded;
//this is probably wrong in the case of items costing steel + iron/plate, but atm this is just some upgrades so don't bother
var needSteel = effectivePrices.some(price => price.name === "steel");
var newReserved = reserved.clone();
var limitingResources = {};
var reserveNonLimiting = price =>
newReserved.addOverTime(price.name, price.val, ticksNeeded, getCraftingResourcePerTick(price.name, reserved, needSteel));
var reserveLimiting = price => {
newReserved.addCurrent(price.name, price.val);
limitingResources[price.name] = true;
}
effectivePrices.forEach(price => {
if (isLimitingResource(price)) {
reserveLimiting(price);
} else {
reserveNonLimiting(price);
}
})
return {newReserved: newReserved, limitingResources: Object.keys(limitingResources)};
}
findPriorities = (queue, reserved) => {
var priorities = [];
var seen = {}
queue.forEach(found => {
var prices = found.getPrices();
if (!seen[found.name] && found.isEnabled() && haveEnoughStorage(prices, reserved)) {
seen[found.name] = !canAfford(prices, reserved); //allow buying more than one copy in a single loop
var unavailableResources = prices.map(price => price.name).filter(res => reserved.get(res).current > getResourceOwned(res));
var viable = unavailableResources.length ? false : true;
var effectivePrices = getEffectivePrices(prices, reserved);
var ticksPerPrice = getTicksNeededPerPrice(effectivePrices, reserved);
var realTicksNeeded = getTicksNeeded(ticksPerPrice, prices);
var ticksNeeded = bufferTicksNeeded(realTicksNeeded);
var toReserve = getResourcesToReserve(effectivePrices, ticksPerPrice, ticksNeeded, reserved);
priorities.push({bld: found, reserved, viable, unavailable: unavailableResources, limiting: toReserve.limitingResources, ticksNeeded: realTicksNeeded, ticksPerPrice});
reserved = toReserve.newReserved;
}
});
return priorities;
}
subtractUnreserved = (reserved, bought) => {
var newReserved = reserved.clone();
//this shouldn't cause a negative reservation
Object.keys(bought).forEach(res => newReserved.addCurrent(res, -bought[res]))
return newReserved;
}
tryBuy = (priorities) => {
var bought = [];
var reservationsBought = {};
var realRender = null;
if (state.api) {
realRender = game.render;
game.render = () => undefined;
}
try {
priorities.forEach(plan => {
var bld = plan.bld;
var reserved = subtractUnreserved(plan.reserved, reservationsBought);
var prices = bld.getPrices();
craftAdditionalNeeded(prices, reserved);
if (canAfford(prices, reserved)) {
var numBought = bld.buy(reserved);
if (numBought === undefined) numBought = 1;
if (numBought) {
if (!bld.silent) log("Buying " + bld.name, bld.quiet);
if (!bld.noLog) logBuy(bld, numBought);
bought = bought.concat([bld]);
//unreserve resources -- makes trading not have as many log entries
prices.forEach(price => reservationsBought[price.name] = (reservationsBought[price.name] || 0) + price.val);
}
}
});
return bought;
} finally {
if (realRender) {
game.render = realRender;
if (state.renderNeeded) {
state.renderNeeded = false;
game.render();
}
}
}
}
updateQueue = (queue, bought) => {
return queue.filter(bld => !bought.includes(bld)).concat(bought.filter(bld => !bld.once));
}
promoteHighPriorities = queue => {
if (isResourceFull("gold")) {
queue = promoteMulti(queue, bld => bld.constructor.name === "Trade" && bld.isEnabled());
}
queue = promoteMulti(queue, bld => bld.maxPriority);
return queue;
}
getInitialReservations = () => {
var reserved = new Reservations({});
reserved.addCurrent("catnip", getWinterCatnipStockNeeded(canHaveColdSeason()));
reserved.addCurrent("furs", getFursStockNeeded());
return reserved;
}
buyPrioritiesQueue = (queue) => {
queue = promoteHighPriorities(queue);
var reserved = getInitialReservations();
var priorities = findPriorities(queue, reserved);
botDebug.priorities = priorities;
updateUpNext(priorities);
var bought = tryBuy(priorities);
return updateQueue(queue, bought);
}
/**************
* Resources
**************/
getResourceOwned = resName => game.resPool.get(resName).value
getResourceMax = resName => game.resPool.get(resName).maxValue || Infinity
getCraftInternalName = longName => game.workshop.crafts.find(row => row.label === longName).name;
getResourceLongTitle = resInternalName => game.workshop.crafts.find(row => row.name === resInternalName).label;
resourceTitleCache = arrayToObject(game.resPool.resources, "name");
resourceNameCache = arrayToObject(game.resPool.resources, "title");
getResourceTitle = resInternalName => resourceTitleCache[resInternalName].title;
getResourceInternalName = resTitle => resourceNameCache[resTitle].name;
fixPriceTitle = price => ({ val: price.val, name: getResourceTitle(price.name) });
getPrice = (prices, res) => (prices.filter(price => price.name === res)[0] || {val: 0}).val
//TODO calculate if resource production is zero (getEffectiveProduction -- make sure all events are ok)
getTotalDemand = (res, craftOnly) => {
//this could be optimized a lot...
var prices = flattenArr(state.queue.filter(bld => bld.isEnabled()).filter((x, i, a) => a.findIndex(o => o.name == x.name) == i).map(bld => bld.getPrices()).filter(prices => haveEnoughStorage(prices)));
var allPrices = [];
var depth = 0;
while (prices.length && depth < 10) {
if (depth == 9) console.error("Infinite loop for " + res)
if (depth || !craftOnly) allPrices = allPrices.concat(prices);
prices = flattenArr(prices
.filter(price => canCraft(price.name) && getResourceOwned(price.name) < price.val)
.map(price => multiplyPrices(getCraftPrices(price.name), Math.ceil(price.val / getCraftRatio(price.name)))))
}
return allPrices
.filter(price => price.name === res)
.map(price => price.val)
.reduce((acc, item) => acc + item, 0)
}
getSafeStorage = (res, autoCraftLevel, additionalProduction, forPurchase) => {
if (autoCraftLevel === undefined) autoCraftLevel = state.autoCraftLevel;
if (!additionalProduction) additionalProduction = 0;
var max = getResourceMax(res);
//missing: expected unused storage when resource is about to be crafted away
//only relevant for purchases
var expectedMissing = forPurchase && reverseCraftMap[res] ? reverseCraftMap[res] * 2/3 : Infinity;
//production: amount of spare storage needed after resources is crafted away
var expectedProduction = autoCraftLevel * state.ticksPerLoop * (getEffectiveResourcePerTick(res, state.ticksPerLoop) + additionalProduction);
return max === Infinity ? max : max - Math.min(expectedProduction, expectedMissing);
}
haveEnoughStorage = (prices, reserved) => {
return prices.every(price => getSafeStorage(price.name, state.autoCraftLevel * 2/3, undefined, true) >= price.val + (reserved ? reserved.get(price.name).current : 0))
|| canAfford(prices, reserved)
}
isResourceFull = (res, additionalProduction) => getResourceOwned(res) >= getSafeStorage(res, Math.max(state.autoCraftLevel, 1), additionalProduction);
getCraftingResourcePerTick = (res, reserved, forSteel) => {
var resourcePerTick = getEffectiveResourcePerTick(res);
if (canCraft(res)) {
//special case steel: we always craft it
var ignoreReservations = res === "steel" && state.autoSteel || !reserved;
var prices = getCraftPrices(res);
//for catnip, this will be wrong due to seasons
//don't worry about it for now
resourcePerTick += Math.max(0, getCraftRatio(res) * Math.min(...prices.map(price =>
(getCraftingResourcePerTick(price.name, reserved, res === "steel" || forSteel) - (ignoreReservations ? 0 : reserved.get(price.name).production)) / price.val
)));
}
if (res === "iron" && state.autoSteel && !forSteel) {
resourcePerTick = Math.max(0, resourcePerTick - getCraftingResourcePerTick("coal", reserved));
}
//todo production from trade???? maybe just blueprints based on gold income?? needs more consistent trading
return resourcePerTick;
}
/**
* bestCaseTicks: if nonzero, assume you will have this many ticks of the maximum possible astronomical events (for save storage calculation)
*/
getEffectiveResourcePerTick = (res, bestCaseTicks) => {
if (!bestCaseTicks) bestCaseTicks = 0;
var resourcePerTick = game.getResourcePerTick(res, true);
var bestCaseDays = Math.ceil(bestCaseTicks * game.calendar.dayPerTick);
var effectiveDaysPerTick = bestCaseTicks ? bestCaseDays / bestCaseTicks : game.calendar.dayPerTick;
resourcePerTick += game.getEffect(res + "PerDay") * effectiveDaysPerTick;
var meteorRatio = game.prestige.getPerk("chronomancy").researched ? 1.1 : 1
var starRatio = meteorRatio * (game.prestige.getPerk("astromancy").researched ? 2 : 1)
if (res === "science" || res === "starchart" && game.science.get("astronomy").researched) {
var astronomicalEventChance = bestCaseTicks ? 1 : Math.min(((25 / 10000) + game.getEffect("starEventChance")) * starRatio, 1);
var eventsPerTick = astronomicalEventChance * effectiveDaysPerTick;
var valuePerEvent;
if (res === "science") {
var celestialBonus = game.workshop.get("celestialMechanics").researched ? 5 : 0;
valuePerEvent = (25 + celestialBonus) * ( 1 + game.getEffect("scienceRatio"));
} else {
valuePerEvent = 1;
}
resourcePerTick += eventsPerTick * valuePerEvent;
}
if (res === "minerals" || res === "science" && game.workshop.get("celestialMechanics").researched) {
var meteorChance = bestCaseTicks ? 1 : 10 / 10000 * meteorRatio;
var eventsPerTick = meteorChance * effectiveDaysPerTick;
var valuePerEvent;
if (res === "minerals") {
valuePerEvent = 50 + 25 * game.getEffect("mineralsRatio");
} else {
valuePerEvent = 15 * ( 1 + game.getEffect("scienceRatio"));
}
resourcePerTick += eventsPerTick * valuePerEvent;
}
var unicornRatio = game.prestige.getPerk("unicornmancy").researched ? 1.1 : 1;
if (res === "ivory") {
var ivoryMeteorChance = bestCaseTicks ? 1 : Math.min(1, game.getEffect("ivoryMeteorChance") / 10000 * unicornRatio);
var eventsPerTick = ivoryMeteorChance * effectiveDaysPerTick;
var valuePerEvent = (250 + 1500 / 2) * (1 + game.getEffect("ivoryMeteorRatio"));
resourcePerTick += eventsPerTick * valuePerEvent;
}
if ((res === "furs" || res === "ivory") && state.autoHunt) {
var effectiveCatpowerPerTick = Math.max(0,
getEffectiveResourcePerTick("manpower", bestCaseTicks)
- getEffectiveResourcePerTick("gold", bestCaseTicks) * 50 / 15
)
resourcePerTick += (res === "furs" ? getFursPerHunt() : getIvoryPerHunt()) * effectiveCatpowerPerTick / 100;
}
if (res === "antimatter") {
resourcePerTick += game.getEffect("antimatterProduction") / 400 * game.calendar.dayPerTick;
}
//don't bother with unicorn rifts and other events for now
return resourcePerTick;
}
var getHuntRatio = () => {
var managerBonus = .05 * (1 + game.prestige.getBurnedParagonRatio())
var huntRatio = game.getEffect("hunterRatio") + managerBonus;
return huntRatio;
}
var getFursPerHunt = () => {
var maxResult = 80 + 65 * getHuntRatio();
return maxResult / 2;
}
var getIvoryPerHunt = () => {
var maxResult = 50 + 40 * getHuntRatio();
return maxResult / 2;
}
/**************
* Conversion
**************/
converters = ["smelter", "calciner", "biolab", "accelerator"]
getEffectsByNamePattern = (effects, pattern) => {
return Object.keys(effects)
.filter(name => name.match(pattern))
.map(key => ({name: key.replace(pattern, ""), val: effects[key]}))
}
getConsumptionsPerTick = effects => getEffectsByNamePattern(effects, /PerTickCon$/)
getProductionsPerTick = effects => getEffectsByNamePattern(effects, /PerTickAutoprod$/)
//if adding is true, reject productions that, if added, would make the resource become full
//otherwise, only reject productions of resources that are already full
//either way, only reject productions if they cannot be crafted into something useful
isUselessProduction = (production, adding) => {
return production.val === 0 || (
isResourceFull(production.name, adding ? production.val * getResourceConversionRatio(production.name) : 0)
&& !autoCrafts.some(craft =>
canCraft(craft.name) && craft.prices.some(price => price.name === production.name) && getTotalDemand(craft.name, true) > 0
)
)
}
manageConverters = () => {
converters.map(name => game.bld.get(name)).forEach(converter => {
var productions = getProductionsPerTick(converter.effects);
if (converter.on && productions.length && productions.every(isUselessProduction)) {
console.log("Disabling " + converter.name);
while (converter.on && productions.every(isUselessProduction)) {
enableConverter(converter, -1);
state.disabledConverters[converter.name] = Math.min(converter.val, 1 + (state.disabledConverters[converter.name] || 0));
}
} else if (state.disabledConverters[converter.name] && productions.some(production => !isUselessProduction(production, true))) {
console.log("Re-enabling " + converter.name);
enableConverter(converter, 1);
state.disabledConverters[converter.name]--;
}
})
}
getResourceConversionRatio = res => {
return getResourceGenericRatio(res) * (1 + game.prestige.getParagonProductionRatio() * 0.05);
}
enableConverter = (converter, amount) => {
if (state.api >= 1) {
converter.on = Math.max(0, Math.min(converter.val, converter.on + amount));
game.upgrade(converter.upgrades);
} else {
withTab("Bonfire", () => {
var outerButton = findButton(converter.label);
var button = amount > 0 ? getBuildingIncreaseButton(outerButton) : getBuildingDecreaseButton(outerButton);
for (var i = 0; i < Math.abs(amount); i++) {
button.click();
}
})
}
}
resetConverters = () => {
Object.entries(state.disabledConverters).forEach(disabled => enableConverter(game.bld.get(disabled[0]), disabled[1]))
state.disabledConverters = {};
}
/**************
* Crafting
**************/
Recipe = class {
constructor(data) {
//assumes that changes in game state are automatically reflected in data object
//if this changes, we'll need to instead save the name and make the data a getter property
this.data = data;
this.shouldCraftAll = data.prices.some(price => getResourceMax(price.name) < Infinity);
}
get name() {
return this.data.name;
}
//sidebar
get title() {
return this.data.label;
}
//Workshop tab craft button
get longTitle() {
return getResourceLongTitle(this.name);
}
get prices() {
//data.prices doesn't account for starchart discount on ships
return game.workshop.getCraftPrice(this.data);
}
get canCraft() {
return this.data.unlocked
//construction required for first craft, though it is possible to cheat and click the buttons before they're shown
&& ((game.bld.get("workshop").val && game.science.get("construction").researched) || this.name === "wood")
&& this.prices.every(price => getResourceMax(price.name) >= price.val);
}
get allowRestriction() {
return this.prices.some(price => canCraft(price.name)) && this.prices.some(price => !canCraft(price.name));
}
get mayCraft() {
return this.canCraft && (!state.restrictedRecipes[this.name]
|| state.restrictedRecipes[this.name] == 1 && this.prices.every(price => getResourceMax(price.name) < Infinity || getResourceOwned(price.name) >= getResourceOwned(this.name)))
}
get craftRatio() {
return getCraftRatio(this.name);
}
craftAll() {
if (this.shouldCraftAll) {
craftAll(this.name);
}
}
craftMultiple(amount) {
craftMultiple(this.data, amount);
}
}
UnicornRecipe = class extends Recipe {
constructor(name, button, getRatio, apiCraftMultiple) {
super({name, label: getResourceTitle(name), prices: button.model.prices});
this.button = button;
this.getRatio = getRatio;
this.apiCraftMultiple = apiCraftMultiple;
}
get longTitle() {
return this.button.model.name;
}
get prices() {
return this.data.prices;
}
get canCraft() {
return game.bld.get("ziggurat").val && game.science.get("theology").researched;
}
get allowRestriction() {
return true;
}
get craftRatio() {
return this.getRatio();
}
craftAll() {
//note: all 4 current unicorn recipes have shouldCraftAll==false
if (this.shouldCraftAll) {
this.craftMultiple(getAffordableAmount(this.prices))
}
}
craftMultiple(amount) {
if (amount > 0) {
if (state.api >= 1) {
this.apiCraftMultiple(this.button, amount)
} else {
withTab("Religion", () => {
for (var i = 0; i < amount; i++) {
buyButton(this.longTitle);
}
});
}
}
}
}
buyItemMultiple = (button, amount) => {
for (var i = 0; i < amount; i++) {
button.controller.buyItem(button.model, null, () => {});
}
}
sacrificeMultiple = (button, amount) => button.controller.sacrifice(button.model, amount)
refineMultiple = (button, amount) => button.controller._refine(button.model, amount)
recipeMap = arrayToObject(game.workshop.crafts.map(data => new Recipe(data)), "name");
getIngredientsMap = (recipeMap) => {
var ingredientsMap = {};
Object.values(recipeMap).forEach(recipe => {
ingredientsMap[recipe.name] = ingredientsMap[recipe.name] || new Set();
//add child ingredients
recipe.prices.forEach(price => {
var ingredient = price.name;
ingredientsMap[recipe.name].add(ingredient);
if (ingredientsMap[ingredient]) {
setAddAll(ingredientsMap[recipe.name], ingredientsMap[ingredient])
}
})
//add ingredients to parents
Object.values(ingredientsMap).forEach(ingredients => {
if (ingredients.has(recipe.name)) {
setAddAll(ingredients, ingredientsMap[recipe.name]);
}
})
})
return ingredientsMap;
}
//maps resources to all the resources needed to craft them
//could extend this to include the total price of those ingredients, but would need to be updated if prices change
ingredientsMap = getIngredientsMap(recipeMap);
loadUnicornRecipes = () => {
if (!recipeMap["tears"] && game.bld.get("ziggurat").val) {
withTab("Religion", () => {
//need to open the religion tab once for the game to load these buttons
var unicornRecipes = arrayToObject([
new UnicornRecipe("tears", game.religionTab.sacrificeBtn,
() => game.bld.get("ziggurat").val, sacrificeMultiple),
new UnicornRecipe("timeCrystal", game.religionTab.sacrificeAlicornsBtn,
() => 1 + game.getEffect("tcRefineRatio"), sacrificeMultiple),
new UnicornRecipe("sorrow", game.religionTab.refineBtn,
() => 1, buyItemMultiple),
new UnicornRecipe("relic", game.religionTab.refineTCBtn,
() => 1 + game.getEffect("relicRefineRatio") * game.religion.getZU("blackPyramid").val, refineMultiple),
], "name");
Object.assign(recipeMap, unicornRecipes);
ingredientsMap = getIngredientsMap(recipeMap);
});
}
}
getCraftPrices = craft => { return recipeMap[craft].prices }
multiplyPrices = (prices, quantity) => prices.map(price => ({ name: price.name, val: price.val * quantity }))
getIngredientsNeeded = price =>
(canCraft(price.name) ? multiplyPrices(getCraftPrices(price.name), Math.ceil(price.val / getCraftRatio(price.name))) : [])
craftTableElem = $('.craftTable');
getCraftTableElem = () => {
if (!craftTableElem.length) {
craftTableElem = $('.craftTable');
}
return craftTableElem;
}
getAffordableAmount = prices => Math.min(...prices.map(price => Math.floor(getResourceOwned(price.name) / price.val)))
findCraftAllButton = (name) => getCraftTableElem().children('div.res-row:contains("' + getResourceTitle(name) + '")').find('div.craft-link:contains("all")')[0]
findCraftButtons = (name) => getCraftTableElem().children('div.res-row:contains("' + getResourceTitle(name) + '")').find('div.craft-link:contains("+")');
craftFirstTime = name => {
if (canAfford(getCraftPrices(name))) {
withTab("Workshop", () => {
var longTitle = getResourceLongTitle(name);
log("First time crafting " + longTitle, true);
findButton(longTitle).click();
})
}
}
craftAll = name => {
if (state.api >= 1) {
if (canCraft(name)) game.workshop.craftAll(name);
} else {
var button = findCraftButtonValues(name, 1).pop();
if (button) button.click();
}
}
var craftButtonRatios = [
//data from WCraftRow
{ min: 1, ratio: 0.01 },
{ min: 25, ratio: 0.05 },
{ min: 100, ratio: 0.1 },
//we could include the all as {ratio: 1} and treat it the same as the rest if we wanted
]
findCraftButtonValues = (craft, craftRatio) => {
if (!canCraft(craft)) {
return [];
} else if (craft === "wood" && game.bld.get("workshop").val === 0) {
return [{click: () => withTab("Bonfire", () => findButton("Refine catnip").click()), times: 1, amount: craftRatio}]
} else if (getResourceOwned(craft) === 0) {
return [{click: () => craftFirstTime(craft), times: 1, amount: craftRatio}]
} else {
var craftAllButton = findCraftAllButton(craft);
var craftAllTimes = getAffordableAmount(getCraftPrices(craft));
return craftButtonRatios.filter(ratio => craftAllTimes >= ratio.min).map((ratio, idx) => {
var craftTimes = Math.max(ratio.min, Math.floor(craftAllTimes * ratio.ratio))
var craftAmount = craftTimes * craftRatio
return {
click: () => { var button = findCraftButtons(craft)[idx]; if (button) button.click(); },
times: craftTimes,
amount: craftAmount
};
}).concat(craftAllButton ? {click: () => craftAllButton.click(), times: craftAllTimes, amount: craftAllTimes * craftRatio } : []);
}
}
craftOne = name => { var button = findCraftButtonValues(name, getCraftRatio(name))[0]; if (button) { button.click(); return button.amount; } else { return 0; } }
getEnoughResource = res => {
if (res === "furs") {
return getFursStockNeeded();
} else if (getResourceMax(res) === Infinity) {
return getEnoughCraft(res);
} else {
return getSafeStorage(res);
}
}
getEnoughCraft = res =>
state.queue
.filter((x, i, a) => x.isUnlocked() && a.findIndex(o => o.name == x.name) == i)
.map(bld => bld.getPrices())
.concat(game.science.techs.filter(tech => tech.unlocked && !tech.researched) //save some compendiums midgame
.map(tech => tech.prices))
.map(prices => getPrice(prices, res))
.reduce((sum, val) => sum + val, 0)
autoCrafts = game.workshop.crafts
//parchment is needed to spend culture and science autocrafting, and there's no other craft for furs
.filter(craft => craft.prices.some(price => getResourceMax(price.name) < Infinity || craft.name === "parchment"))
getReverseCraftMap = autoCrafts => {
var result = {
gold: 15
};
Object.values(autoCrafts).forEach(craft => {
craft.prices.forEach(price => {
result[price.name] = Math.min(price.val, result[price.name] || Infinity)
});
});
return result;
}
reverseCraftMap = getReverseCraftMap(autoCrafts);
preferSteelCrafting = () => {
//prefer steel over plate
var steelIndex = autoCrafts.findIndex(craft => craft.name === "steel")
var steelCraft = autoCrafts.splice(steelIndex, 1)[0];
autoCrafts.unshift(steelCraft);
}
preferSteelCrafting();
doAutoCraft = () => {
autoCrafts.forEach(craft => {
if (mayCraft(craft.name)) {
var getAmountToCraft = price => {
var safeCrafts = (getResourceOwned(price.name) - getEnoughResource(price.name)) / price.val
return getResourceMax(price.name) === Infinity ? Math.floor(safeCrafts) : Math.ceil(safeCrafts);
}
var timesToCraft = Math.min(...craft.prices.map(getAmountToCraft))
craftMultiple(craft, timesToCraft);
}
});
}
craftMultiple = (craft, timesToCraft) => {
if (timesToCraft <= 0) {
return;
}
if (state.api >= 1) {
game.craft(craft.name, timesToCraft);