-
Notifications
You must be signed in to change notification settings - Fork 272
/
work-for-factions.js
1302 lines (1214 loc) · 94.4 KB
/
work-for-factions.js
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
import {
instanceCount, getConfiguration, getNsDataThroughFile, getFilePath, getActiveSourceFiles, tryGetBitNodeMultipliers,
formatDuration, formatMoney, formatNumberShort, disableLogs, log, getErrorInfo, tail
} from './helpers.js'
let options;
const argsSchema = [
['first', []], // Grind rep with these factions first. Also forces a join of this faction if we normally wouldn't (e.g. no desired augs or all augs owned)
['skip', []], // Don't work for these factions
['o', false], // Immediately grind company factions for rep after getting their invite, rather than first getting all company invites we can
['desired-stats', []], // Factions will be removed from our 'early-faction-order' once all augs with these stats have been bought out
['desired-augs', []], // The augmentations will keep a faction in our 'early-faction-order' regardless of whether they have any --desired-stats
['no-tail-windows', false], // Set to true to prevent the default behaviour of opening a tail window any time we initiate focused player work.
['no-focus', false], // Disable doing work that requires focusing (crime), and forces study/faction/company work to be non-focused (even if it means incurring a penalty)
['no-studying', false], // Disable studying.
['pay-for-studies-threshold', 200000], // Only be willing to pay for our studies if we have this much money
['training-stat-per-multi-threshold', 100], // Heuristic: Estimate that we can train this many levels for every mult / exp_mult we have in a reasonable amount of time.
['no-coding-contracts', false], // Disable purchasing coding contracts for reputation
['no-crime', false], // Disable doing crimes at all. (Also disabled with --no-focus)
['crime-focus', false], // Useful in crime-focused BNs when you want to focus on crime related factions
['fast-crimes-only', false], // Assasination and Heist are so slow, I can see people wanting to disable them just so they can interrupt at will.
['invites-only', false], // Just work to get invites, don't work for augmentations / faction rep
['prioritize-invites', false], // Prioritize working for as many invites as is practical before starting to grind for faction reputation
['get-invited-to-every-faction', false], // You want to be in every faction? You got it!
['karma-threshold-for-gang-invites', -40000], // Prioritize working for gang invites once we have this much negative Karma
['disable-treating-gang-as-sole-provider-of-its-augs', false], // Set to true if you still want to grind for rep with factions that only have augs your gang provides
['no-bladeburner-check', false], // By default, will avoid working if bladeburner is active and "The Blade's Simulacrum" isn't installed
];
// By default, consider these augs worth working towards regardless of whether they match one of the '--desired-stats'
const default_desired_augs = ["The Red Pill", "CashRoot Starter Kit", "The Blade's Simulacrum", "Neuroreceptor Management Implant"];
// Note: The way the game source encodes job requirements is: [1, 26, 49, 149] (for example), and all then all faction-related
// companies get a stat modifier of "+224", except a few which have "+249". Rather than replicate those gross numbers,
// I would just store those job stat requirements as [225, 250, 275, 375] below. I then keep track of the few companies
// which require +25 on all stat requirements. Don't worry, I make up for it by being convoluted in other ways...
const companySpecificConfigs = [
{ name: "NWO", statModifier: 25 },
{ name: "MegaCorp", statModifier: 25 },
{ name: "Blade Industries", statModifier: 25 },
{ name: "Fulcrum Secret Technologies", companyName: "Fulcrum Technologies" }, // Special snowflake
{ name: "Silhouette", companyName: "TBD", repRequiredForFaction: 1.0e7 } // Hack: 3.2e6 should be enough rep to get the CTO position, but once
// we hit this rep we might break out of the work loop before getting the final promotion, so we keep working until we get the faction invite.
]
const jobs = [ // Job stat requirements for a company with a base stat modifier of +224 (modifier of all megacorps except the ones above which are 25 higher)
{
name: "it",
reqRep: [0e0, 7e3, 35e3, 175e3],
reqHck: [225, 250, 275, 375], // [1, 26, 51, 151] + 224
reqCha: [0e0, 0e0, 275, 300], // [0, 0, 51, 76] + 224
repMult: [0.9, 1.1, 1.3, 1.4]
},
{
name: "software",
reqRep: [0e0, 8e3, 4e4, 2e5, 4e5, 8e5, 16e5, 32e5],
reqHck: [225, 275, 475, 625, 725, 725, 825, 975], // [1, 51, 251, 401, 501, 501, 601, 751] + 224
reqCha: [0e0, 0e0, 275, 375, 475, 475, 625, 725], // [0, 0, 51, 151, 251, 251, 401, 501] + 224
repMult: [0.9, 1.1, 1.3, 1.5, 1.6, 1.6, 1.75, 2.0]
},
]
const factions = ["Illuminati", "Daedalus", "The Covenant", "ECorp", "MegaCorp", "Bachman & Associates", "Blade Industries", "NWO", "Clarke Incorporated", "OmniTek Incorporated",
"Four Sigma", "KuaiGong International", "Fulcrum Secret Technologies", "BitRunners", "The Black Hand", "NiteSec", "Aevum", "Chongqing", "Ishima", "New Tokyo", "Sector-12",
"Volhaven", "Speakers for the Dead", "The Dark Army", "The Syndicate", "Silhouette", "Tetrads", "Slum Snakes", "Netburners", "Tian Di Hui", "CyberSec"];
const cannotWorkForFactions = ["Church of the Machine God", "Bladeburners", "Shadows of Anarchy"]
// These factions should ideally be completed in this order
const preferredEarlyFactionOrder = [
"Netburners", // Improve hash income, which is useful or critical for almost all BNs
"Tian Di Hui", "Aevum", // These give all the company_rep and faction_rep bonuses early game
"Daedalus", // Once we have all faction_rep boosting augs, there's no reason not to work towards Daedalus as soon as it's available/feasible so we can buy Red Pill
"CyberSec", /* Quick, and NightSec aug depends on an aug from here */ "NiteSec", "Tetrads", // Cha augs to speed up earning company promotions
"Bachman & Associates", // Boost company/faction rep for future augs
"BitRunners", // Fast source of some unique hack augs
"Fulcrum Secret Technologies", // Will be removed if hack level is too low to backdoor their server
"ECorp", // More cmp_rep augs, and some strong hack ones as well
"The Black Hand", // Fastest sources of hacking augs after the above companies
"The Dark Army", // Unique cmp_rep aug TODO: Can it sensibly be gotten before megacorps? Requires 300 all combat stats.
"Clarke Incorporated", "OmniTek Incorporated", "NWO", // More hack augs from companies
"Chongqing", // Unique Source of big 1.4x hack exp boost (Can only join if not in e.g. Aevum as well)
];
// This is an approximate order of most useful augmentations left to offer, assuming all early-game factions have been cleaned out
const preferredCompanyFactionOrder = [
"Bachman & Associates", // Augs boost company_rep by 1.65, faction_rep by 1.50. Lower rep-requirements than ECorp augs, so should be a priority to speed up future resets
"ECorp", // Offers 2.26 multi worth of company_rep and major hacking stat boosts (1.51 hack / 1.54 exp / 1.43 success / 3.0 grow / 2.8 money / 1.25 speed), but high rep reqs
"Clarke Incorporated", // Biggest boost to hacking after above factions (1.38)
"OmniTek Incorporated", // Next big boost to hacking after above factions (1.20) (NWO is bigger, but this has lower Cha reqs.)
"NWO", // Biggest boost to hacking after above factions (1.26)
"Blade Industries", // Mostly redundant after Ecorp - provides remaining hack-related augs (1.10 money, 1.03 speed)
"MegaCorp", // Offers 1 unique aug boosting all physical traits by 1.35
"KuaiGong International", // 1.40 to agility, defense, strength
"Fulcrum Secret Technologies", // Big boosts to company_rep and hacking, but requires high hack level to backdoor their server, so might have to be left until later
"Four Sigma", // No unique augs, but note that if accessible early on, Fulcrum + Four Sigma is a one-two punch to get all company rep boosting augs in just 2 factions
]
// Order in which to focus on crime factions. Start with the hardest-to-earn invites, assume we will skip to next best if not achievable.
const preferredCrimeFactionOrder = ["Slum Snakes", "Tetrads", "Speakers for the Dead", "The Syndicate", "The Dark Army", "The Covenant", "Daedalus", "Netburners", "NiteSec", "The Black Hand"];
// Gang factions in order of ease-of-invite. If gangs are available, as we near 54K Karma to unlock gangs (as per --karma-threshold-for-gang-invites), we will attempt to get into any/all of these.
const desiredGangFactions = ["Slum Snakes", "The Syndicate", "The Dark Army", "Speakers for the Dead"];
// Previously this was needed because you couldn't work for any gang factions once in a gang, but that was changed.
const allGangFactions = ["Speakers for the Dead", "The Dark Army", "The Syndicate", "Tetrads", "Slum Snakes", "The Black Hand", "NiteSec"];
const loopSleepInterval = 5000; // 5 seconds
const statusUpdateInterval = 60 * 1000; // 1 minute (outside of this, minor updates in e.g. stats aren't logged)
const checkForNewPrioritiesInterval = 10 * 60 * 1000; // 10 minutes. Interrupt whatever we're doing and check whether we could be doing something more useful.
const waitForFactionInviteTime = 30 * 1000; // The game will only issue one new invite every 25 seconds, so if you earned two by travelling to one city, might have to wait a while
let shouldFocus; // Whether we should focus on work or let it be backgrounded (based on whether "Neuroreceptor Management Implant" is owned, or "--no-focus" is specified)
// And a bunch of globals because managing state and encapsulation is hard.
let hasFocusPenalty, hasSimulacrum, favorToDonate, fulcrumHackReq, notifiedAboutDaedalus, playerInBladeburner, wasGrafting, currentBitnode;
let dictSourceFiles, dictFactionFavors, playerGang, mainLoopStart, scope, numJoinedFactions, lastTravel, crimeCount;
let firstFactions, skipFactions, completedFactions, softCompletedFactions, mostExpensiveAugByFaction, mostExpensiveDesiredAugByFaction;
let bitNodeMults = (/**@returns{BitNodeMultipliers}*/() => undefined)(); // Trick to get strong typing in mono
export function autocomplete(data, args) {
data.flags(argsSchema);
const lastFlag = args.length > 1 ? args[args.length - 2] : null;
if (lastFlag == "--first" || lastFlag == "--skip")
return factions.map(f => f.replaceAll(' ', '_')).sort();
return [];
}
// Bit of an ugly afterthought, but this is all over the place to break out of whatever we're doing and return to the main loop.
const breakToMainLoop = () => Date.now() > mainLoopStart + checkForNewPrioritiesInterval;
/** @param {NS} ns */
export async function main(ns) {
const runOptions = getConfiguration(ns, argsSchema);
if (!runOptions || await instanceCount(ns) > 1) return; // Prevent multiple instances of this script from being started, even with different args.
options = runOptions; // We don't set the global "options" until we're sure this is the only running instance
disableLogs(ns, ['sleep']);
// Reset globals whose value can persist between script restarts in weird situations
lastTravel = crimeCount = currentBitnode = 0;
notifiedAboutDaedalus = playerInBladeburner = wasGrafting = false;
// Process configuration options
firstFactions = (options['first'] || []).map(f => f.replaceAll('_', ' ')); // Factions that end up in this list will be prioritized and joined regardless of their augmentations available.
options.skip = (options.skip || []).map(f => f.replaceAll('_', ' '));
// Default desired-stats if none were specified
if (options['desired-stats'].length == 0)
options['desired-stats'] = options['crime-focus'] ? ['str', 'def', 'dex', 'agi', 'faction_rep', 'hacknet', 'crime'] :
['hacking', 'faction_rep', 'company_rep', 'charisma', 'hacknet', 'crime_money']
// Default desired-augs if none were specified
if (options['desired-augs'].length == 0)
options['desired-augs'] = default_desired_augs;
// Log some of the options in effect
ns.print(`--desired-stats matching: ${options['desired-stats'].join(", ")}`);
ns.print(`--desired-augs: ${options['desired-augs'].join(", ")}`);
if (firstFactions.length > 0) ns.print(`--first factions: ${firstFactions.join(", ")}`);
if (options.skip.length > 0) ns.print(`--skip factions: ${options.skip.join(", ")}`);
if (options['fast-crimes-only']) ns.print(`--fast-crimes-only`);
// Find out whether the user can use this script
dictSourceFiles = await getActiveSourceFiles(ns); // Find out what source files the user has unlocked
if (!(4 in dictSourceFiles))
return log(ns, "ERROR: You cannot automate working for factions until you have unlocked singularity access (SF4).", true, 'error');
else if (dictSourceFiles[4] < 3)
log(ns, `WARNING: Singularity functions are much more expensive with lower levels of SF4 (you have SF4.${dictSourceFiles[4]}). ` +
`You may encounter RAM issues with and have to wait until you have more RAM available to run this script successfully.`, false, 'warning');
let loadingComplete = false; // In the event of suboptimal RAM conditions, keep trying to start until we succeed
while (!loadingComplete) {
try {
await loadStartupData(ns);
loadingComplete = true;
} catch (err) {
log(ns, 'WARNING: work-for-factions.js caught an unhandled error while starting up. Trying again in 5 seconds...\n' + getErrorInfo(err), false, 'warning');
await ns.sleep(loopSleepInterval);
}
}
mainLoopStart = Date.now();
scope = 0;
while (true) { // After each loop, we will repeat all prevous work "strategies" to see if anything new has been unlocked, and add one more "strategy" to the queue
try {
await mainLoop(ns);
} catch (err) {
log(ns, 'WARNING: work-for-factions.js caught an unhandled error in its main loop. Trying again in 5 seconds...\n' + getErrorInfo(err), false, 'warning');
await ns.sleep(loopSleepInterval);
scope--; // Cancel out work scope increasing on the next iteration.
}
await ns.sleep(1); // Infinite loop protection in case somehow we loop without doing any meaningful work
}
}
/** @param {NS} ns */
async function loadStartupData(ns) {
favorToDonate = await getNsDataThroughFile(ns, 'ns.getFavorToDonate()');
const playerInfo = await getPlayerInfo(ns);
const allKnownFactions = factions.concat(playerInfo.factions.filter(f => !factions.includes(f)));
bitNodeMults = await tryGetBitNodeMultipliers(ns);
// Get some faction and augmentation information to decide what remains to be purchased
dictFactionFavors = await getNsDataThroughFile(ns, dictCommand('ns.singularity.getFactionFavor(o)'), '/Temp/getFactionFavors.txt', allKnownFactions);
const dictFactionAugs = await getNsDataThroughFile(ns, dictCommand('ns.singularity.getAugmentationsFromFaction(o)'), '/Temp/getAugmentationsFromFactions.txt', allKnownFactions);
const augmentationNames = [...new Set(Object.values(dictFactionAugs).flat())];
const dictAugRepReqs = await getNsDataThroughFile(ns, dictCommand('ns.singularity.getAugmentationRepReq(o)'), '/Temp/getAugmentationRepReqs.txt', augmentationNames);
const dictAugStats = await getNsDataThroughFile(ns, dictCommand('ns.singularity.getAugmentationStats(o)'), '/Temp/getAugmentationStats.txt', augmentationNames);
const ownedAugmentations = await getNsDataThroughFile(ns, `ns.singularity.getOwnedAugmentations(true)`, '/Temp/player-augs-purchased.txt');
const installedAugmentations = await getNsDataThroughFile(ns, `ns.singularity.getOwnedAugmentations()`, '/Temp/player-augs-installed.txt');
// Based on what augmentations we own, we can change our own behaviour (e.g. whether to allow work to steal focus)
hasFocusPenalty = !installedAugmentations.includes("Neuroreceptor Management Implant"); // Check if we have an augmentation that lets us not have to focus at work (always nicer if we can background it)
shouldFocus = !options['no-focus'] && hasFocusPenalty; // Focus at work for the best rate of rep gain, unless focus activities are disabled via command line
hasSimulacrum = installedAugmentations.includes("The Blade's Simulacrum");
// Find out if we're in a gang
const gangInfo = await getGangInfo(ns);
playerGang = gangInfo ? gangInfo.faction : null;
if (playerGang && !options['disable-treating-gang-as-sole-provider-of-its-augs']) {
// Whatever augmentations the gang provides are so easy to get from them, might as well ignore any other factions that have them.
const gangAugs = dictFactionAugs[playerGang];
ns.print(`Your gang ${playerGang} provides easy access to ${gangAugs.length} augs. Ignoring these augs from the original factions that provide them.`);
for (const faction of allKnownFactions.filter(f => f != playerGang))
dictFactionAugs[faction] = dictFactionAugs[faction].filter(a => !gangAugs.includes(a));
}
mostExpensiveAugByFaction = Object.fromEntries(allKnownFactions.map(f => [f,
dictFactionAugs[f].filter(aug => !ownedAugmentations.includes(aug))
.reduce((max, aug) => Math.max(max, dictAugRepReqs[aug]), -1)]));
//ns.print("Most expensive unowned aug by faction: " + JSON.stringify(mostExpensiveAugByFaction));
// TODO: Detect when the most expensive aug from two factions is the same - only need it from the first one. (Update lists and remove 'afforded' augs?)
mostExpensiveDesiredAugByFaction = Object.fromEntries(allKnownFactions.map(f => [f,
dictFactionAugs[f].filter(aug => !ownedAugmentations.includes(aug) && (
options['desired-augs'].includes(aug) ||
Object.keys(dictAugStats[aug]).length == 0 || options['desired-stats'].length == 0 ||
Object.keys(dictAugStats[aug]).some(key => options['desired-stats'].some(stat => key.includes(stat)))
)).reduce((max, aug) => Math.max(max, dictAugRepReqs[aug]), -1)]));
//ns.print("Most expensive desired aug by faction: " + JSON.stringify(mostExpensiveDesiredAugByFaction));
// Filter out factions who have no augs (or tentatively filter those with no desirable augs) unless otherwise configured. The exception is
// we will always filter the most-precluding city factions, (but not ["Chongqing", "New Tokyo", "Ishima"], which can all be joined simultaneously)
// TODO: Think this over more. need to filter e.g. chonquing if volhaven is incomplete...
const filterableFactions = (options['get-invited-to-every-faction'] ? ["Aevum", "Sector-12", "Volhaven"] : allKnownFactions);
// Unless otherwise configured, we will skip factions with no remaining augmentations
completedFactions = filterableFactions.filter(fac => mostExpensiveAugByFaction[fac] == -1);
softCompletedFactions = filterableFactions.filter(fac => mostExpensiveDesiredAugByFaction[fac] == -1 && !completedFactions.includes(fac));
skipFactions = options.skip.concat(cannotWorkForFactions).concat(completedFactions).filter(fac => !firstFactions.includes(fac));
if (completedFactions.length > 0)
ns.print(`${completedFactions.length} factions will be skipped (for having all augs purchased): ${completedFactions.join(", ")}`);
if (softCompletedFactions.length > 0)
ns.print(`${softCompletedFactions.length} factions will initially be skipped (all desired augs purchased): ${softCompletedFactions.join(", ")}`);
// TODO: If --prioritize-invites is set, we should have a preferred faction order that puts easiest-invites-to-earn at the front (e.g. all city factions)
numJoinedFactions = playerInfo.factions.length;
fulcrumHackReq = await getServerRequiredHackLevel(ns, "fulcrumassets");
}
let lastMainLoopMessage = "";
/** @param {NS} ns */
async function mainLoop(ns) {
if (!breakToMainLoop()) scope++; // Increase the scope of work if the last iteration completed early (i.e. due to all work within that scope being complete)
mainLoopStart = Date.now();
// If changing our loop scope, log a message
const loopMessage = `INFO: Currently work scope is anything <= priority level: ${scope}`;
if (loopMessage != lastMainLoopMessage)
ns.print((lastMainLoopMessage = loopMessage));
// Update information that may have changed since our last loop
const player = await getPlayerInfo(ns);
const resetInfo = await getResetInfoRd(ns);
currentBitnode = resetInfo.currentNode;
if (player.factions.length > numJoinedFactions) { // If we've recently joined a new faction, reset our work scope
scope = 1; // Back to basics until we've satisfied all highest-priority work
numJoinedFactions = player.factions.length;
}
// Immediately accept any outstanding faction invitations for factions we want to earn rep with soon
// TODO: If check if we would qualify for an invite to any factions just by travelling, and do so to start earning passive rep
const invites = await checkFactionInvites(ns);
const invitesToAccept = options['get-invited-to-every-faction'] || options['prioritize-invites'] ?
invites.filter(f => !skipFactions.includes(f)) :
invites.filter(f => !skipFactions.includes(f) && !softCompletedFactions.includes(f));
for (const invite of invitesToAccept)
await tryJoinFaction(ns, invite);
// Get some information about gangs (if unlocked)
if (2 in dictSourceFiles) {
if (!playerGang) { // Check if we've joined a gang since our last iteration
const gangInfo = await getGangInfo(ns);
playerGang = gangInfo ? gangInfo.faction : null;
// If we've only just now joined a gang, we have to reload startup data, because the augs offered by our gang faction has now changed.
if (playerGang) await loadStartupData(ns);
}
if (ns.heart.break() <= options['karma-threshold-for-gang-invites']) { // Start trying to earn gang faction invites if we're close to unlocking gangs
if (!playerGang) {
log(ns, `INFO: We are nearing the Karma required to unlock gangs (${formatNumberShort(ns.heart.break())} / -54K). Prioritize earning gang faction invites.`);
for (const factionName of desiredGangFactions)
await earnFactionInvite(ns, factionName);
}
}
}
// If something outside of this script is stealing player focus, decide whether to allow it
if (await isValidInterruption(ns))
return (await ns.sleep(loopSleepInterval));
// If we recently grafted an augmentation, it might be one that changes our behaviour, so re-load startup data
if (wasGrafting) {
await loadStartupData(ns);
wasGrafting = false;
}
// Remove Fulcrum from our "EarlyFactionOrder" if hack level is insufficient to backdoor their server
let priorityFactions = options['crime-focus'] ? preferredCrimeFactionOrder.slice() : preferredEarlyFactionOrder.slice();
if (player.skills.hacking < fulcrumHackReq - 10) { // Assume that if we're within 10, we'll get there by the time we've earned the invite
const fulcrumIdx = priorityFactions.findIndex(c => c == "Fulcrum Secret Technologies")
if (fulcrumIdx !== -1) {
priorityFactions.splice(fulcrumIdx, 1);
ns.print(`Fulcrum faction server requires ${fulcrumHackReq} hack, so removing from our initial priority list for now.`);
}
} // TODO: Otherwise, if we get Fulcrum, we have no need for a couple other company factions
// If we're in BN 10, we can purchase special Sleeve-related things from the Covenant, so we should always try join it
if (currentBitnode == 10 && !priorityFactions.includes("The Covenant")) {
priorityFactions.push("The Covenant");
ns.print(`We're in BN10, which means we should add The Covenant to our priority faction list, so you can purchase sleeves and sleeve memory.`);
}
// Strategy 1: Tackle a consolidated list of desired faction order, interleaving simple factions and megacorporations
const factionWorkOrder = firstFactions.concat(priorityFactions.filter(f => // Remove factions from our initial "work order" if we've bought all desired augmentations.
!firstFactions.includes(f) && !skipFactions.includes(f) && !softCompletedFactions.includes(f)));
for (const faction of factionWorkOrder) {
if (breakToMainLoop()) break; // Only continue on to the next faction if it isn't time for a high-level update.
let earnedNewFactionInvite = false;
if (preferredCompanyFactionOrder.includes(faction)) // If this is a company faction, we need to work for the company first
earnedNewFactionInvite = await workForMegacorpFactionInvite(ns, faction, true);
// If new work was done for a company or their faction, restart the main work loop to see if we've since unlocked a higher-priority faction in the list
if (earnedNewFactionInvite || await workForSingleFaction(ns, faction)) {
scope--; // De-increment scope so that effecitve scope doesn't increase on the next loop (i.e. it will be incremented back to what it is now)
break;
}
}
if (scope <= 1 || breakToMainLoop()) return;
// Strategy 2: Grind XP with all priority factions that are joined or can be joined, until every single one has desired REP
for (const faction of factionWorkOrder)
if (!breakToMainLoop()) await workForSingleFaction(ns, faction);
if (scope <= 2 || breakToMainLoop()) return;
// Strategy 3: Work for any megacorporations not yet completed to earn their faction invites. Once joined, we don't lose these factions on reset.
let megacorpFactions = preferredCompanyFactionOrder.filter(f => !skipFactions.includes(f));
await workForAllMegacorps(ns, megacorpFactions, false);
if (scope <= 3 || breakToMainLoop()) return;
// Strategy 4: Work for megacorps again, but this time also work for the company factions once the invite is earned
await workForAllMegacorps(ns, megacorpFactions, true);
if (scope <= 4 || breakToMainLoop()) return;
// Strategies 5+ now work towards getting an invite to *all factions in the game*
let joinedFactions = player.factions; // In case our hard-coded list of factions is missing anything, merge it with the list of all factions
let knownFactions = factions.concat(joinedFactions.filter(f => !factions.includes(f)));
let allIncompleteFactions = knownFactions.filter(f => !skipFactions.includes(f) && !completedFactions.includes(f))
.sort((a, b) => mostExpensiveAugByFaction[a] - mostExpensiveAugByFaction[b]); // sort by least-expensive final aug (correlated to easiest faction-invite requirement)
// Preserve the faction work order we've decided on previously, and only use the above sort order for every other faction added on to the end
let allFactionsWorkOrder = factionWorkOrder.filter(f => allIncompleteFactions.includes(f))
.concat(allIncompleteFactions.filter(f => !factionWorkOrder.includes(f)));
// Strategy 5: For *all factions in the game*, try to earn an invite and work for rep until we can afford the most-expensive *desired* aug (or unlock donations, whichever comes first)
for (const faction of allFactionsWorkOrder.filter(f => !softCompletedFactions.includes(f)))
if (!breakToMainLoop()) await workForSingleFaction(ns, faction);
if (scope <= 5 || breakToMainLoop()) return;
// Strategy 6: Revisit all factions until each has enough rep to unlock donations - so if we can't afford all augs this reset, at least we don't need to grind for rep on the next reset
// For this, we reverse the order of non-priority factions (ones with augs costing the most-rep to least) since these will take the most time to re-grind rep for if we can't buy them this reset.
let allFactionsWorkOrderReversed = factionWorkOrder.filter(f => allIncompleteFactions.includes(f))
.concat(allIncompleteFactions.reverse().filter(f => !factionWorkOrder.includes(f)));
for (const faction of allFactionsWorkOrderReversed)
if (!breakToMainLoop()) // Only continue on to the next faction if it isn't time for a high-level update.
await workForSingleFaction(ns, faction, true); // ForceUnlockDonations = true
if (scope <= 6 || breakToMainLoop()) return;
// Strategy 7: Next, revisit all factions and grind XP until we can afford the most expensive aug on this install, even if we are slated to unlock donations on the next reset
for (const faction of allFactionsWorkOrder)
if (!breakToMainLoop()) // Only continue on to the next faction if it isn't time for a high-level update.
await workForSingleFaction(ns, faction, true, true); // ForceBestAug = true
if (scope <= 7 || breakToMainLoop()) return;
// Strategy 8: Everything up until now will skip factions that we've *already* unlocked donations with (can donate in the current install), since we can just throw money at them for aug reputation
// But in some BNs, money might be hard to come by, so now we should proceed to grind reputation the old-fasioned way so we don't have to waste money on donations
for (const faction of allFactionsWorkOrder)
if (!breakToMainLoop()) // Only continue on to the next faction if it isn't time for a high-level update.
await workForSingleFaction(ns, faction, false, true, true); // ForceRep = true
if (scope <= 8 || breakToMainLoop()) return;
// Strategy 9: Busy ourselves for a while longer, then loop to see if there anything more we can do for the above factions
let factionsWeCanWorkFor = joinedFactions.filter(f => !options.skip.includes(f) && !cannotWorkForFactions.includes(f) && f != playerGang);
let foundWork = false;
// Work for the faction we already have the most favor with (to earn stat EXP and rep for additional neuroflux levels)
if (factionsWeCanWorkFor.length > 0 && !options['crime-focus']) { // Unless we've been asked to prioritize crime (e.g. for Karma)
let mostFavorFaction = factionsWeCanWorkFor.sort((a, b) => (dictFactionFavors[b] || 0) - (dictFactionFavors[a] || 0))[0];
let targetRep = 1000 + (await getFactionReputation(ns, mostFavorFaction)) * 1.05; // Hack: Grow rep by ~5%, plus 1000 incase it's currently 0
ns.print(`INFO: All useful work complete. Grinding an additional 5% rep (to ${formatNumberShort(targetRep)}) ` +
`with highest-favor faction: ${mostFavorFaction} (${(dictFactionFavors[mostFavorFaction] || 0).toFixed(2)} favor)`);
foundWork = await workForSingleFaction(ns, mostFavorFaction, false, false, targetRep);
}
if (!foundWork && !options['no-crime']) { // Otherwise, kill some time by doing crimes for a little while
ns.print(`INFO: Nothing to do. Doing a little crime...`);
await crimeForKillsKarmaStats(ns, 0, -ns.heart.break() + 1000 /* Hack: Decrease Karma by 1000 */, 0);
} else if (!foundWork) { // If our hands our tied, twiddle our thumbs a bit
ns.print(`INFO: Nothing to do. Sleeping for 30 seconds to see if magically we join a faction`);
await ns.sleep(30000);
}
if (scope <= 9) scope--; // Cap the 'scope' value from increasing perpetually when we're on our last strategy
}
// Ram-dodging helper, runs a command for all items in a list and returns a dictionary.
const dictCommand = (command) => `Object.fromEntries(ns.args.map(o => [o, ${command}]))`;
const requiredMoneyByFaction = {
"Tian Di Hui": 1E6, "Sector-12": 15E6, "Chongqing": 20E6, "New Tokyo": 20E6, "Ishima": 30E6, "Aevum": 40E6, "Volhaven": 50E6,
"Slum Snakes": 1E6, "Silhouette": 15E6, "The Syndicate": 10E6, "The Covenant": 75E9, "Daedalus": 100E9, "Illuminati": 150E9
};
const requiredBackdoorByFaction = { "CyberSec": "CSEC", "NiteSec": "avmnite-02h", "The Black Hand": "I.I.I.I", "BitRunners": "run4theh111z", "Fulcrum Secret Technologies": "fulcrumassets" };
const requiredHackByFaction = { "Tian Di Hui": 50, "Netburners": 80, "Speakers for the Dead": 100, "The Syndicate": 200, "The Dark Army": 300, "The Covenant": 850, "Daedalus": 2500, "Illuminati": 1500 };
const requiredCombatByFaction = { "Slum Snakes": 30, "Tetrads": 75, "Speakers for the Dead": 300, "The Syndicate": 200, "The Dark Army": 300, "The Covenant": 850, "Daedalus": 1500, "Illuminati": 1200 };
const requiredKarmaByFaction = { "Slum Snakes": 9, "Tetrads": 18, "Silhouette": 22, "Speakers for the Dead": 45, "The Dark Army": 45, "The Syndicate": 90 };
const requiredKillsByFaction = { "Speakers for the Dead": 30, "The Dark Army": 5 };
const reqHackingOrCombat = ["Daedalus"]; // Special case factions that require only hacking or combat stats, not both
// Establish some helper functions used to determine how fast we can train a stat
const title = s => s && s[0].toUpperCase() + s.slice(1); // Annoyingly bitnode multis capitalize the first letter physical stat name
/** Return the product of all multipliers affecting training the specified stat.
* @param {Player} player @param {string} stat @param {number} trainingBitnodeMult */
function heuristic(player, stat, trainingBitnodeMult) {
return Math.sqrt(player.mults[stat] * bitNodeMults[`${title(stat)}LevelMultiplier`] *
/* */ player.mults[`${stat}_exp`] * trainingBitnodeMult);
}
/** A heuristic for how long it'll take to train the specified stat via Crime. @param {Player} player @param {string} stat @param */
const crimeHeuristic = (player, stat) => heuristic(player, stat, bitNodeMults.CrimeExpGain); // When training with crime
/** A heuristic for how long it'll take to train the specified stat via Class or Gym. @param {Player} player @param {string} stat @param */
const classHeuristic = (player, stat) => heuristic(player, stat, bitNodeMults.ClassGymExpGain); // When training in university
/** @param {NS} ns */
async function earnFactionInvite(ns, factionName) {
let player = await getPlayerInfo(ns);
const joinedFactions = player.factions;
if (joinedFactions.includes(factionName)) return true;
var invitations = await checkFactionInvites(ns);
if (invitations.includes(factionName))
return await tryJoinFaction(ns, factionName);
// Can't join certain factions for various reasons
let reasonPrefix = `Cannot join faction "${factionName}" because`;
let precludingFaction;
if (["Aevum", "Sector-12"].includes(factionName) && (precludingFaction = ["Chongqing", "New Tokyo", "Ishima", "Volhaven"].find(f => joinedFactions.includes(f))) ||
["Chongqing", "New Tokyo", "Ishima"].includes(factionName) && (precludingFaction = ["Aevum", "Sector-12", "Volhaven"].find(f => joinedFactions.includes(f))) ||
["Volhaven"].includes(factionName) && (precludingFaction = ["Aevum", "Sector-12", "Chongqing", "New Tokyo", "Ishima"].find(f => joinedFactions.includes(f))))
return ns.print(`${reasonPrefix} precluding faction "${precludingFaction}"" has been joined.`);
let requirement;
// See if we can take action to earn an invite for the next faction under consideration
let workedForInvite = false;
// If committing crimes can help us join a faction - we know how to do that
let doCrime = false;
if ((requirement = requiredKarmaByFaction[factionName]) && -ns.heart.break() < requirement) {
ns.print(`${reasonPrefix} you have insufficient Karma. Need: ${-requirement}, Have: ${ns.heart.break()}`);
doCrime = true;
}
if ((requirement = requiredKillsByFaction[factionName]) && player.numPeopleKilled < requirement) {
ns.print(`${reasonPrefix} you have insufficient kills. Need: ${requirement}, Have: ${player.numPeopleKilled}`);
doCrime = true;
}
// Check on physical stat requirements
const physicalStats = ["strength", "defense", "dexterity", "agility"];
// Check which stats need to be trained up
requirement = requiredCombatByFaction[factionName];
let deficientStats = !requirement ? [] : physicalStats.map(stat => ({ stat, value: player.skills[stat] })).filter(stat => stat.value < requirement);
const hackHeuristic = classHeuristic(player, 'hacking');
const crimeHeuristics = Object.fromEntries(physicalStats.map(s => [s, crimeHeuristic(player, s)]));
// Hash for special-case factions (just 'Daedalus' for now) requiring *either* hacking *or* combat
if (reqHackingOrCombat.includes(factionName) && deficientStats.length > 0 && (
// Compare roughly how long it will take to train up our hacking stat
(requiredHackByFaction[factionName] - player.skills.hacking) / hackHeuristic <
// To the slowest time it will take to train up our deficient physical stats
Math.min(...deficientStats.map(s => (requiredCombatByFaction[factionName] - s.value) / crimeHeuristics[s.stat]))))
ns.print(`Ignoring combat requirement for ${factionName} as we are more likely to unlock them via hacking stats.`);
else if (deficientStats.length > 0) {
ns.print(`${reasonPrefix} you have insufficient combat stats. Need: ${requirement} of each, Have ` +
physicalStats.map(s => `${s.slice(0, 3)}: ${player.skills[s]}`).join(", "));
const em = requirement / options['training-stat-per-multi-threshold'];
// Hack: Create a rough heuristic suggesting how much multi we need to train physical stats in a reasonable amount of time.
// TODO: Be smarter (time-based decision), and also consider whether training physical stats via GYM might be faster
if (deficientStats.some(s => crimeHeuristics[s.stat] < em))
return ns.print("Some mults * exp_mults * bitnode mults appear to be too low to increase stats in a reasonable amount of time. " +
`You can control this with --training-stat-per-multi-threshold. Current sqrt(mult*exp_mult*bn_mult*bn_exp_mult) ` +
`should be ~${formatNumberShort(em, 2)}, have ` + deficientStats.map(s => s.stat).map(s => `${s.slice(0, 3)}: sqrt(` +
`${formatNumberShort(player.mults[s])}*${formatNumberShort(player.mults[`${s}_exp`])}*` +
`${formatNumberShort(bitNodeMults[`${title(s)}LevelMultiplier`])}*` +
`${formatNumberShort(bitNodeMults.CrimeExpGain)})=${formatNumberShort(crimeHeuristics[s])}`).join(", "));
doCrime = true; // TODO: There could be more efficient ways to gain combat stats than homicide, although at least this serves future crime factions
}
if (doCrime && options['no-crime'])
return ns.print(`${reasonPrefix} Doing crime to meet faction requirements is disabled. (--no-crime or --no-focus)`);
if (doCrime)
workedForInvite = await crimeForKillsKarmaStats(ns, requiredKillsByFaction[factionName] || 0, requiredKarmaByFaction[factionName] || 0, requiredCombatByFaction[factionName] || 0);
// Study for hack levels if that's what's keeping us
// Note: Check if we have insuffient hack to backdoor this faction server. If we have sufficient hack, we will "waitForInvite" below assuming an external script is backdooring ASAP
let serverReqHackingLevel = 0;
if (requirement = requiredBackdoorByFaction[factionName]) {
serverReqHackingLevel = await getServerRequiredHackLevel(ns, requirement);
if (player.skills.hacking < serverReqHackingLevel) {
ns.print(`${reasonPrefix} you must first backdoor ${requirement}, which needs hack: ${serverReqHackingLevel}, Have: ${player.skills.hacking}`);
}
}
requirement = Math.max(serverReqHackingLevel, requiredHackByFaction[factionName] || 0)
if (requirement && player.skills.hacking < requirement &&
// Special case (Daedalus): Don't grind for hack requirement if we previously did a grind for the physical requirements
!(reqHackingOrCombat.includes(factionName) && workedForInvite)) {
ns.print(`${reasonPrefix} you have insufficient hack level. Need: ${requirement}, Have: ${player.skills.hacking}`);
const em = requirement / options['training-stat-per-multi-threshold'];
if (options['no-studying'])
return ns.print(`--no-studying is set, nothing we can do to improve hack level.`);
if (hackHeuristic < em)
return ns.print(`Your combination of Hacking mult (${formatNumberShort(player.mults.hacking)}), exp_mult ` +
`(${formatNumberShort(player.mults.hacking_exp)}), and bitnode hacking / study exp mults ` +
`(${formatNumberShort(bitNodeMults.HackingLevelMultiplier)}) / (${formatNumberShort(bitNodeMults.ClassGymExpGain)}) ` +
`are probably too low to increase hack from ${player.skills.hacking} to ${requirement} in a reasonable amount of time ` +
`(${hackHeuristic} < ${formatNumberShort(em, 2)} - configure with --training-stat-per-multi-threshold)`);
let studying = false;
if (player.money > options['pay-for-studies-threshold']) { // If we have sufficient money, pay for the best studies
if (player.city != "Volhaven") await goToCity(ns, "Volhaven");
studying = await study(ns, false, "Algorithms");
} else if (uniByCity[player.city]) // Otherwise only go to free university if our city has a university
studying = await study(ns, false, "Study Computer Science");
else
return ns.print(`You have insufficient money (${formatMoney(player.money)} < --pay-for-studies-threshold ` +
`${formatMoney(options['pay-for-studies-threshold'])}) to travel or pay for studies, and your current ` +
`city ${player.city} does not have a university from which to take free computer science.`);
if (studying)
workedForInvite = await monitorStudies(ns, 'hacking', requirement);
// If we studied for hacking, and were awaiting a backdoor, spawn the backdoor script now
if (workedForInvite && serverReqHackingLevel) {
player = await getPlayerInfo(ns);
if (player.skills.hacking > requirement) {
ns.print(`Current hacking level ${player.skills.hacking} seems to now meet the backdoor requirement ${requirement}. Spawning backdoor-all-servers.js...`);
ns.run(getFilePath("/Tasks/backdoor-all-servers.js"));
}
}
}
if (breakToMainLoop()) return false;
// Skip factions whose remaining requirement is money. Earning money is primarily the responsibility of other scripts.
// TODO: It might be reasonable to request a temporary stock liquidation if this would get us over the edge.
if ((requirement = requiredMoneyByFaction[factionName]) && player.money < requirement)
return ns.print(`${reasonPrefix} you have insufficient money. Need: ${formatMoney(requirement)}, Have: ${formatMoney(player.money)}`);
// If travelling can help us join a faction - we can do that too
player = await getPlayerInfo(ns);
let travelledForInvite = false;
let travelToCityOrDidRecently = async city => // Helper to consider us as having travelled for an invite if we did just now, or recently
player.city != city && await goToCity(ns, city) || player.city == city && (Date.now() - lastTravel < 60000)
if (['Tian Di Hui', 'Tetrads', 'The Dark Army'].includes(factionName))
travelledForInvite = await travelToCityOrDidRecently('Chongqing');
else if (['The Syndicate'].includes(factionName))
travelledForInvite = await travelToCityOrDidRecently('Sector-12');
else if (["Aevum", "Chongqing", "Sector-12", "New Tokyo", "Ishima", "Volhaven"].includes(factionName))
travelledForInvite = await travelToCityOrDidRecently(factionName);
if (travelledForInvite) {
workedForInvite = true;
player = await getPlayerInfo(ns); // Update player.city
}
// Special case: earn a CEO position to gain an invite to Silhouette
if ("Silhouette" == factionName) {
ns.print(`You must be a CO (e.g. CEO/CTO) of a company to earn an invite to "Silhouette". This may take a while!`);
let factionConfig = companySpecificConfigs.find(f => f.name == "Silhouette"); // We set up Silhouette with a "company-specific-config" so that we can work for an invite like any megacorporation faction.
let companyNames = preferredCompanyFactionOrder.map(f => companySpecificConfigs.find(cf => cf.name == f)?.companyName || f);
let favorByCompany = await getNsDataThroughFile(ns, dictCommand('ns.singularity.getCompanyFavor(o)'), '/Temp/getCompanyFavors.txt', companyNames);
let repByCompany = await getNsDataThroughFile(ns, dictCommand('ns.singularity.getCompanyRep(o)'), '/Temp/getCompanyReps.txt', companyNames);
// Change the company to work for into whichever company we can get to CEO fastest with.
// Minimize needed_rep/rep_gain_rate. CEO job is at 3.2e6 rep, so (3.2e6-current_rep)/(100+favor).
// Also take into account that some companies will have lowered rep requirement if they are backdoored
const backdoorByServer = await backdoorStatusByServer(ns);
factionConfig.companyName = companyNames.sort((a, b) =>
((backdoorByServer[serverByCompany[a]] ? 0.75 : 1.0) * 3.2e6 - repByCompany[a]) / (100 + favorByCompany[a]) -
((backdoorByServer[serverByCompany[b]] ? 0.75 : 1.0) * 3.2e6 - repByCompany[b]) / (100 + favorByCompany[b]))[0];
// If the company we chose has a required stat modifier, we need to add it to the one for Silhouette
factionConfig.statModifier = companySpecificConfigs.find(c => (c.companyName ?? c.name) == factionConfig.companyName)?.statModifier || 0;
// If the company we chose gets backdoored, this should appear to affect Silhouette too. A hack is to add a new "serverByCompany" dict entry
serverByCompany["Silhouette"] = serverByCompany[factionConfig.companyName]
// Hack: We will be working indefinitely, so we rely on an external script (daemon + faction-manager) to join this faction for us, or for checkForNewPrioritiesInterval to elapse.
workedForInvite = await workForMegacorpFactionInvite(ns, factionName, false); // Work until CTO and the external script joins this faction, triggering an exit condition.
}
// Special case: check hacknet stats before we try to join Netburners
if ("Netburners" == factionName) {
const [totalLevels, totalRam, totalCores] = await getNsDataThroughFile(ns,
'[...Array(ns.hacknet.numNodes()).keys()].map(i => ns.hacknet.getNodeStats(i))' +
'.reduce(([l, r, c], s) => [l + s.level, r + s.ram, c + s.cores], [0, 0, 0])',
'/Temp/hacknet-Netburners-stats.txt');
if (totalLevels < 100 || totalRam < 8 || totalCores < 4)
return ns.print(`${reasonPrefix} hacknet total stats do not yet meet requirements: ` +
`${totalLevels}/100 levels, ${totalRam}/8 ram, ${totalCores}/4 cores`);
}
if (breakToMainLoop()) return false;
if (workedForInvite === true) // If we took some action to earn the faction invite, wait for it to come in
return await waitForFactionInvite(ns, factionName);
else
return ns.print(`Nothing we can do at this time to earn an invitation to faction "${factionName}"...`);
}
/** @param {NS} ns */
async function goToCity(ns, cityName) {
const player = await getPlayerInfo(ns);
if (player.city == cityName) {
ns.print(`Already in city ${cityName}`);
return true;
}
if (await getNsDataThroughFile(ns, `ns.singularity.travelToCity(ns.args[0])`, null, [cityName])) {
lastTravel = Date.now()
log(ns, `Travelled from ${player.city} to ${cityName}`, false, 'info');
return true;
}
if (player.money < 200000)
log(ns, `WARN: Insufficient funds to travel from ${player.city} to ${cityName}`, false, 'warning');
else
log(ns, `ERROR: Failed to travel from ${player.city} to ${cityName} for some reason...`, false, 'error');
return false;
}
/** @param {NS} ns */
export async function crimeForKillsKarmaStats(ns, reqKills, reqKarma, reqStats, doFastCrimesOnly = false) {
const bestCrimesByDifficulty = ["heist", "assassinate", "homicide", "mug"]; // Will change crimes as our success rate improves
const chanceThresholds = [0.75, 0.9, 0.5, 0]; // Will change crimes once we reach this probability of success for better all-round gains
doFastCrimesOnly = doFastCrimesOnly || (options ? options['fast-crimes-only'] : false);
let player = await getPlayerInfo(ns);
let strRequirements = [];
let forever = reqKills >= Number.MAX_SAFE_INTEGER || reqKarma >= Number.MAX_SAFE_INTEGER || reqStats >= Number.MAX_SAFE_INTEGER;
if (reqKills) strRequirements.push(() => `${reqKills} kills (Have ${player.numPeopleKilled})`);
if (reqKarma) strRequirements.push(() => `-${reqKarma} Karma (Have ${Math.round(ns.heart.break()).toLocaleString('en')})`);
if (reqStats) strRequirements.push(() => `${reqStats} of each combat stat (Have ` +
`Str: ${player.skills.strength}, Def: ${player.skills.defense}, Dex: ${player.skills.dexterity}, Agi: ${player.skills.agility})`);
let anyStatsDeficient = (p) => p.skills.strength < reqStats || p.skills.defense < reqStats ||
/* */ p.skills.dexterity < reqStats || p.skills.agility < reqStats;
let crime, lastCrime, crimeTime, lastStatusUpdateTime, needStats;
while (forever || (needStats = anyStatsDeficient(player)) || player.numPeopleKilled < reqKills || -ns.heart.break() < reqKarma) {
if (!forever && breakToMainLoop()) return ns.print('INFO: Interrupting crime to check on high-level priorities.');
let crimeChances = await getNsDataThroughFile(ns, `Object.fromEntries(ns.args.map(c => [c, ns.singularity.getCrimeChance(c)]))`, '/Temp/crime-chances.txt', bestCrimesByDifficulty);
let karma = -ns.heart.break();
crime = crimeCount < 2 ? (crimeChances["homicide"] > 0.75 ? "homicide" : "mug") : // Start with a few fast & easy crimes to boost stats if we're just starting
(!needStats && (player.numPeopleKilled < reqKills || karma < reqKarma)) ? "homicide" : // If *all* we need now is kills or Karma, homicide is the fastest way to do that, even at low proababilities
bestCrimesByDifficulty.find((c, index) => doFastCrimesOnly && index <= 1 ? 0 : crimeChances[c] >= chanceThresholds[index]); // Otherwise, crime based on success chance vs relative reward (precomputed)
// Warn if current crime is disrupted
let currentWork = await getCurrentWorkInfo(ns);
let crimeType = currentWork.crimeType;
if (!lastCrime || !(crimeType && crimeType.toLowerCase().includes(lastCrime))) {
if (await isValidInterruption(ns, currentWork)) return;
if (lastCrime) {
log(ns, `Committing Crime "${lastCrime}" Interrupted. (Now: ${crimeType ?? currentWork.type}) Restarting...`, false, 'warning');
if (!options['no-tail-windows']) tail(ns); // Force a tail window open to help the user kill this script if they accidentally closed the tail window and don't want to keep doing crime
}
let focusArg = shouldFocus === undefined ? true : shouldFocus; // Only undefined if running as imported function
crimeTime = await getNsDataThroughFile(ns, 'ns.singularity.commitCrime(ns.args[0], ns.args[1])', null, [crime, focusArg])
if (shouldFocus && !options['no-tail-windows']) tail(ns); // Force a tail window open when auto-criming with focus so that the user can more easily kill this script
}
// Periodic status update with progress
if (lastCrime != crime || (Date.now() - lastStatusUpdateTime) > statusUpdateInterval) {
lastCrime = crime;
lastStatusUpdateTime = Date.now();
ns.print(`Committing "${crime}" (${(100 * crimeChances[crime]).toPrecision(3)}% success) ` +
(forever ? 'forever...' : `until we reach ${strRequirements.map(r => r()).join(', ')}`));
}
// Sleep for some multiple of the crime time to avoid interrupting a crime in progress on the next status update
let sleepTime = 1 + Math.ceil(loopSleepInterval / crimeTime) * crimeTime;
await ns.sleep(sleepTime);
crimeCount++;
player = await getPlayerInfo(ns);
}
ns.print(`Done committing crimes. Reached ${strRequirements.map(r => r()).join(', ')}`);
return true;
}
/** @param {NS} ns */
async function studyForCharisma(ns, focus) {
await goToCity(ns, 'Volhaven');
return await study(ns, focus, 'Leadership', 'ZB Institute Of Technology');
}
const uniByCity = Object.fromEntries([["Aevum", "Summit University"], ["Sector-12", "Rothman University"], ["Volhaven", "ZB Institute of Technology"]]);
/** @param {NS} ns */
async function study(ns, focus, course, university = null) {
if (options['no-studying']) {
log(ns, `WARNING: Could not study '${course}' because --no-studying is set.`, false, 'warning');
return;
}
const playerCity = (await getPlayerInfo(ns)).city;
if (!university) { // Auto-detect the university in our city
university = uniByCity[playerCity];
if (!university) {
log(ns, `WARNING: Could not study '${course}' because we are in city '${playerCity}' without a university.`, false, 'warning');
return;
}
}
if (await getNsDataThroughFile(ns, `ns.singularity.universityCourse(ns.args[0], ns.args[1], ns.args[2])`, null, [university, course, focus])) {
log(ns, `Started studying '${course}' at '${university}'`, false, 'success');
return true;
}
log(ns, `ERROR: For some reason, failed to study '${course}' at university '${university}' (Not in correct city? Player is in '${playerCity}')`, false, 'error');
return false;
}
/** @param {NS} ns
* Helper to wait for studies to be complete */
async function monitorStudies(ns, stat, requirement) {
let lastStatusUpdateTime = 0;
const initialWork = await getCurrentWorkInfo(ns);
while (!breakToMainLoop()) {
const currentWork = await getCurrentWorkInfo(ns);
if (!(currentWork.classType) || currentWork.classType != initialWork.classType) {
log(ns, `WARNING: Something interrupted our studies.` +
`\nWAS: ${JSON.stringify(initialWork)}\nNOW: ${JSON.stringify(currentWork)}`, false, 'warning');
return;
}
const player = await getPlayerInfo(ns);
if (player.skills[stat] >= requirement) {
log(ns, `SUCCESS: Achieved ${stat} level ${player.skills[stat]} >= ${requirement} while studying`, false, 'info');
return true;
}
if ((Date.now() - lastStatusUpdateTime) > statusUpdateInterval) {
lastStatusUpdateTime = Date.now();
log(ns, `Studying "${currentWork.classType}" at ${currentWork.location} until ${stat} reaches ${requirement}. ` +
`Currently at ${player.skills[stat]}...`, false, 'info'); // TODO: Compute an ETA, and configure training threshold based on ETA
}
await ns.sleep(loopSleepInterval);
}
}
/** @param {NS} ns */
export async function waitForFactionInvite(ns, factionName, maxWaitTime = waitForFactionInviteTime) {
ns.print(`Waiting for invite from faction "${factionName}" (game may delay this up to ${formatDuration(maxWaitTime)})...`);
let waitTime = maxWaitTime;
let lastFactionCount = null;
do {
var invitations = await checkFactionInvites(ns);
var joinedFactions = (await getPlayerInfo(ns)).factions;
const factionCount = invitations.length + joinedFactions.length;
if (invitations.includes(factionName) || joinedFactions.includes(factionName))
break;
// If we recieved an invite, just not for the faction we wanted, reset the timer
if (lastFactionCount === null) lastFactionCount = factionCount;
if (factionCount > lastFactionCount) {
ns.print(`INFO: Recieved a new invite, but not from "${factionName}". ` +
`Invites are sent on a delay, so resetting the ${formatDuration(maxWaitTime)} timer...`);
waitTime = maxWaitTime;
lastFactionCount = factionCount;
}
await ns.sleep(loopSleepInterval);
} while (!invitations.includes(factionName) && !joinedFactions.includes(factionName) && (waitTime -= loopSleepInterval) > 0);
if (joinedFactions.includes(factionName)) // Another script may have auto-joined this faction before we could
ns.print(`An external script has joined faction "${factionName}" for us.`);
else if (!invitations.includes(factionName)) {
log(ns, `ERROR: Waited ${formatDuration(maxWaitTime)}, but still have not recieved an invite for faction: "${factionName}" (Requirements not met?)`, false, 'error');
return;
} else if (!(await tryJoinFaction(ns, factionName))) {
log(ns, `ERROR: Something went wrong. Earned "${factionName}" faction invite, but failed to join it.`, false, 'error');
return;
}
return true;
}
/** @param {NS} ns */
export async function tryJoinFaction(ns, factionName) {
var joinedFactions = (await getPlayerInfo(ns)).factions;
if (joinedFactions.includes(factionName))
return true;
if (!(await getNsDataThroughFile(ns, `ns.singularity.joinFaction(ns.args[0])`, null, [factionName])))
return false;
log(ns, `Joined faction "${factionName}"`, false, 'success');
return true;
}
/** @param {NS} ns
* @returns {Promise<Player>} the result of ns.getPlayer() */
async function getPlayerInfo(ns) {
//return ns.getPlayer(); // Note: We may decide that we call this frequently enough it is not worth ram-dodging
return await getNsDataThroughFile(ns, `ns.getPlayer()`);
}
/** @param {NS} ns
* @returns {Promise<ResetInfo>} the result of ns.getResetInfo() */
async function getResetInfoRd(ns) {
return await getNsDataThroughFile(ns, `ns.getResetInfo()`);
}
/** @param {NS} ns
* @returns {Promise<Task>} The result of ns.singularity.getCurrentWork() */
async function getCurrentWorkInfo(ns) {
return (await getNsDataThroughFile(ns, 'ns.singularity.getCurrentWork()')) ?? {}; // Easier than null-coalescing everywhere
}
/** @param {NS} ns
* @returns {Promise<string[]>} List of new faction invites */
async function checkFactionInvites(ns) {
return await getNsDataThroughFile(ns, 'ns.singularity.checkFactionInvitations()');
}
/** @param {NS} ns
* @returns {Promise<GangGenInfo|boolean>} Gang information, if we're in a gang, or False */
async function getGangInfo(ns) {
return await getNsDataThroughFile(ns, 'ns.gang.inGang() ? ns.gang.getGangInformation() : false', '/Temp/gang-stats.txt')
}
/** @param {NS} ns
* @returns {Promise<Number>} Current reputation with the specified faction */
async function getFactionReputation(ns, factionName) {
return await getNsDataThroughFile(ns, `ns.singularity.getFactionRep(ns.args[0])`, null, [factionName]);
}
/** @param {NS} ns
* @returns {Promise<Number>} Current reputation with the specified company */
async function getCompanyReputation(ns, companyName) {
return await getNsDataThroughFile(ns, `ns.singularity.getCompanyRep(ns.args[0])`, null, [companyName]);
}
/** @param {NS} ns
* @returns {Promise<Number>} Current favour with the specified faction */
async function getCurrentFactionFavour(ns, factionName) {
return await getNsDataThroughFile(ns, `ns.singularity.getFactionFavor(ns.args[0])`, null, [factionName]);
}
/** @param {NS} ns
* @returns {Promise<Number>} The hacking level required for the specified server */
async function getServerRequiredHackLevel(ns, serverName) {
return await getNsDataThroughFile(ns, `ns.getServerRequiredHackingLevel(ns.args[0])`, null, [serverName]);
}
/** A special check for when we unlock donations with Daedalus, this is usually a good time to reset.
* @param {NS} ns */
async function daedalusSpecialCheck(ns, favorRepRequired, currentReputation) {
if (favorRepRequired == 0 || currentReputation < favorRepRequired) return false;
// If we would be unlocking donations, but actually, we're pretty close to just being able to afford TRP, no impetus to reset.
if (currentReputation >= 0.9 * 2.500e6 * bitNodeMults.AugmentationRepCost) return false;
log(ns, `INFO: You have enough reputation with Daedalus (have ${formatNumberShort(currentReputation)}) that you will ` +
`unlock donations (needed ${formatNumberShort(favorRepRequired)}) with them on your next reset.`, !notifiedAboutDaedalus, "info");
ns.write("/Temp/Daedalus-donation-rep-attained.txt", "True", "w"); // HACK: To notify autopilot that we can reset for rep now.
notifiedAboutDaedalus = true;
}
let lastInterruptionNotice = "";
/** Checks whether the current work being perform qualifies as a valid interruption (true),
* or whether we should go back to what we were doing before we were interrupted (false).
* @param {NS} ns
* @param {Task?} currentWork (optional) The work the player is currently doing, if already retrieved.
* @return {bool} true if we should stop what we're doing and let the interruption continue.
*/
async function isValidInterruption(ns, currentWork = null) {
let interruptionNotice = "";
currentWork ??= await getCurrentWorkInfo(ns); // Retrieve current work (unless it was passed in)
// Never interrupt grafting
if (currentWork.type == "GRAFTING") {
interruptionNotice = "Grafting in progress. Pausing all activity to avoid interrupting...";
wasGrafting = true;
}
// If bladeburner is currently active, but we do not yet have The Blade's Simulacrum, we may choose to we pause working.
else if (7 in dictSourceFiles && !hasSimulacrum && !options['no-bladeburner-check']) {
// Heuristic: If we're in a gang, its rep will give us access to most augs, we can take a break from working in favour of bladeburner progress
// Also, if we're done all "priority" work (scope >= 2), consider letting Bladeburner take over
// TODO: Are there other situations we want to prioritize bladeburner over normal work? Perhaps if we're in a Bladeburner BN? (6 or 7)
if (playerGang || scope >= 2) {
// Check if the player has joined bladeburner (can stop checking once we see they are)
playerInBladeburner = playerInBladeburner || await getNsDataThroughFile(ns, 'ns.bladeburner.inBladeburner()');
if (playerInBladeburner) {
if (playerGang)
interruptionNotice = `Gang will give us most augs, so pausing work to allow Bladeburner to operate.`;
else
interruptionNotice = `Decided that doing Bladeburner is more important that working right now.`;
if (currentWork.type)
await stop(ns); // Stop working so bladeburner can run (bladeburner won't interrupt work for us)
}
}
}
// If we decided to pause focus-work, display a message and return true
if (interruptionNotice != "") {
if (lastInterruptionNotice != interruptionNotice) { // If we haven't already notified that we're pausing activity, do this now
log(ns, `INFO: ${interruptionNotice}`, false, 'info');
lastInterruptionNotice = interruptionNotice;
}
mainLoopStart = 0; // Ensure that any check for "break to main loop"
return true;
}
return false;
}
let lastFactionWorkStatus = "";
/** * Checks how much reputation we need with this faction to either buy all augmentations or get 150 favour, then works to that amount.
* @param {NS} ns
* @param {string} factionName The faction to work for
* @param {boolean} forceUnlockDonations Set to true to keep grinding reputation until we would earn enough favour to unlock donations on our next reset.
* If left as the default (false) we will grind rep either until we unlock donations, or can afford the most expensive desired aug, whichever is lower.
* @param {boolean} forceBestAug Set to true to a) ignore "desired" stats and just work towards the most expensive (rep) agumentation,
* and b) ignore the rep required to unlock donations and keep going until we can buy all augmentations
* Note: The exception is if donations are already unlocked, then you can already buy all augmentations (by buying rep first), so this does nothing.
* @param {boolean|number} forceRep Set to true to force working for reputation even if we have unlocked donations and could just buy reputation.
* Hack: If set to a number, we will work until that reputation amount regardless of augmentation reputation requirements.
* */
export async function workForSingleFaction(ns, factionName, forceUnlockDonations = false, forceBestAug = false, forceRep = false) {
const repToFavour = (rep) => Math.ceil(25500 * 1.02 ** (rep - 1) - 25000);
let highestRepAug = forceBestAug ? mostExpensiveAugByFaction[factionName] : mostExpensiveDesiredAugByFaction[factionName];
let startingFavor = dictFactionFavors[factionName] || 0; // How much favour do we already have with this faction?
let favorRepRequired = Math.max(0, repToFavour(favorToDonate) - repToFavour(startingFavor));
// Determine when to stop grinding faction rep (usually ~467,000 to get 150 favour) Set this lower if there are no augs requiring that much REP
let factionRepRequired = Math.min(highestRepAug, favorRepRequired); // By default, stop at whichever comes first
if (forceUnlockDonations) // If forced, ensure we earn enough reputation to unlock donations on our next reset
factionRepRequired = Math.max(factionRepRequired, favorRepRequired)
if (forceBestAug)// If forced, ensure we earn enough rep to buy the highest rep augmentation
factionRepRequired = Math.max(factionRepRequired, highestRepAug);
if (forceRep !== true && forceRep > 0) // If forceRep is a number (not just a flag 'true'), ensure we earn the specified rep amount
factionRepRequired = Math.max(factionRepRequired, forceRep)
// Check for any reasons to skip working for this faction
if (!forceRep && highestRepAug == -1 && !firstFactions.includes(factionName) && !options['get-invited-to-every-faction'])
return ns.print(`All "${factionName}" augmentations are owned. Skipping unlocking faction...`);
// Ensure we get an invite to location-based factions we might want / need
if (!await earnFactionInvite(ns, factionName))
return ns.print(`We are not yet part of faction "${factionName}". Skipping working for faction...`);
if (playerGang == factionName) // Cannot work for your own gang faction.
return ns.print(`"${factionName}" is your gang faction. You can only earn rep in your gang via respect.`);
// If we have already unlocked donations via favour, we can just buy the rep needed to unlock augmentations
// (earning money is typically faster than earning reputation), so we'll skip trying to earn further reputation
if (!forceRep && startingFavor >= favorToDonate)
return ns.print(`Donations already unlocked for "${factionName}". You should buy access to augs. Skipping working for faction...`);
// Hack: Even in "forceUnlockDonations" mode, don't ever bother unlocking donations for factions whose most expensive augmentation is <20% of the donation rep required
if (!forceRep && forceUnlockDonations && mostExpensiveAugByFaction[factionName] < 0.2 * factionRepRequired) {
ns.print(`The last "${factionName}" aug is only ${mostExpensiveAugByFaction[factionName].toLocaleString('en')} rep, ` +
`not worth grinding ${favorRepRequired.toLocaleString('en')} rep to unlock donations.`);
forceUnlockDonations = false;
factionRepRequired = highestRepAug = mostExpensiveAugByFaction[factionName];
}
let currentReputation = await getFactionReputation(ns, factionName);
// If the best faction aug is within 10% of our current rep, grind all the way to it so we can get it immediately, regardless of our current rep target
if (forceBestAug || highestRepAug <= 1.1 * Math.max(currentReputation, factionRepRequired))
factionRepRequired = Math.max(highestRepAug, factionRepRequired);
if (factionName == "Daedalus") await daedalusSpecialCheck(ns, favorRepRequired, currentReputation);
if (currentReputation >= factionRepRequired)
return ns.print(`Faction "${factionName}" required rep of ${Math.round(factionRepRequired).toLocaleString('en')} has already been attained ` +
`(Current rep: ${Math.round(currentReputation).toLocaleString('en')}). Skipping working for faction...`)
ns.print(`Faction "${factionName}" Highest Aug Req: ${highestRepAug?.toLocaleString('en')}, Current Favor (` +
`${startingFavor?.toFixed(2)}/${favorToDonate?.toFixed(2)}) Req: ${Math.round(favorRepRequired).toLocaleString('en')}`);
if (options['invites-only'])
return ns.print(`--invites-only Skipping working for faction...`);
if (options['prioritize-invites'] && !forceUnlockDonations && !forceBestAug && !forceRep)
return ns.print(`--prioritize-invites Skipping working for faction for now...`);
let lastStatusUpdateTime = 0;
let workAssigned = false; // Use to track whether work previously assigned by this script is being disrupted
let bestFactionJob = null;
while ((currentReputation = (await getFactionReputation(ns, factionName))) < factionRepRequired) {
if (breakToMainLoop()) return ns.print('INFO: Interrupting faction work to check on high-level priorities.');
const currentWork = await getCurrentWorkInfo(ns);
let factionJob = currentWork.factionWorkType;
// Detect if faction work was interrupted and log a warning
if (workAssigned && currentWork.factionName != factionName) {
if (await isValidInterruption(ns, currentWork)) return false;
log(ns, `Work for faction ${factionName} was interrupted (Now: ${JSON.stringify(currentWork)}). Restarting...`, false, 'warning');
workAssigned = false;
if (!options['no-tail-windows']) tail(ns); // Force a tail window open to help the user kill this script if they accidentally closed the tail window and don't want to keep working
}
// Periodically check again what the best faction work is (may change with stats over time)
if ((Date.now() - lastStatusUpdateTime) > statusUpdateInterval)
workAssigned = false; // This will force us to redetermine the best faction work.
// Heads up! Current implementation of "detectBestFactionWork" changes the work currently being done, so we must always re-assign work afterwards
if (!workAssigned)
bestFactionJob = await detectBestFactionWork(ns, factionName);
// For purposes of being informative, log a message if the detected "bestFactionJob" is different from what we were previously doing
if (currentWork.factionName == factionName && factionJob != bestFactionJob) {
log(ns, `INFO: Detected that "${bestFactionJob}" gives more rep than previous work "${factionJob}". Switching...`);
workAssigned = false;
}
// Ensure we are doing the best faction work (must always be done after "detect" routine is run)
if (!workAssigned) {
if (await startWorkForFaction(ns, factionName, bestFactionJob, shouldFocus)) {
workAssigned = true;
if (shouldFocus && !options['no-tail-windows']) tail(ns); // Keep a tail window open if we're stealing focus
} else {
log(ns, `ERROR: Something went wrong, failed to start "${bestFactionJob}" work for faction "${factionName}" (Is gang faction, or not joined?)`, false, 'error');
break;
}
}
let status = `Doing '${bestFactionJob}' work for "${factionName}" until ${Math.round(factionRepRequired).toLocaleString('en')} rep.`;
if (lastFactionWorkStatus != status || (Date.now() - lastStatusUpdateTime) > statusUpdateInterval) {
lastFactionWorkStatus = status;
lastStatusUpdateTime = Date.now();
// Measure approximately how quickly we're gaining reputation to give a rough ETA
const repGainRate = await measureFactionRepGainRate(ns, factionName);
const eta_milliseconds = 1000 * (factionRepRequired - currentReputation) / repGainRate;
ns.print(`${status} Currently at ${Math.round(currentReputation).toLocaleString('en')}, ` +
`earning ${formatNumberShort(repGainRate)} rep/sec. ` +
(hasFocusPenalty && !shouldFocus ? '(after 20% non-focus Penalty) ' : '') + `(ETA: ${formatDuration(eta_milliseconds)})`);
}
await tryBuyReputation(ns);
await ns.sleep(loopSleepInterval);
if (!forceBestAug && !forceRep) { // Detect our rep requirement decreasing (e.g. if we exported for our daily +1 faction rep)
let currentFavor = await getCurrentFactionFavour(ns, factionName);
if (currentFavor === undefined)
log(ns, `ERROR: WTF... getCurrentFactionFavour returned 'undefined' for factionName: ${factionName}`, true, 'error');
else if (currentFavor > startingFavor) {
startingFavor = dictFactionFavors[factionName] = currentFavor;
favorRepRequired = Math.max(0, repToFavour(favorToDonate) - repToFavour(startingFavor));
factionRepRequired = forceUnlockDonations ? favorRepRequired : Math.min(highestRepAug, favorRepRequired);