convert blade to mui

This commit is contained in:
Olivier Gagnon 2021-09-27 17:09:48 -04:00
parent 498a204c88
commit 86678b6290
50 changed files with 1921 additions and 1527 deletions

56
dist/vendor.bundle.js vendored

File diff suppressed because one or more lines are too long

@ -36,25 +36,9 @@
ga("send", "pageview"); ga("send", "pageview");
</script> </script>
<link rel="shortcut icon" href="favicon.ico" /> <link rel="shortcut icon" href="favicon.ico"><link href="dist/vendor.css" rel="stylesheet"><link href="main.css" rel="stylesheet"></head>
<link href="dist/vendor.css" rel="stylesheet" />
<link href="main.css" rel="stylesheet" />
</head>
<body> <body>
<div id="entire-game-container"> <div id="root"/>
<div id="mainmenu-container" style="display: flex; flex-direction: row"></div> <script type="text/javascript" src="dist/vendor.bundle.js"></script><script type="text/javascript" src="main.bundle.js"></script></body>
<!-- Status text -->
<div id="status-text-container">
<p id="status-text"></p>
</div>
</div>
<div id="modal-portal"></div>
<div id="unclickable" style="display: none">Click on this to upgrade your Source-File -1!</div>
<script type="text/javascript" src="dist/vendor.bundle.js"></script>
<script type="text/javascript" src="main.bundle.js"></script>
</body>
<script src="src/ThirdParty/raphael.min.js"></script> <script src="src/ThirdParty/raphael.min.js"></script>
</html> </html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -2105,13 +2105,14 @@ input[type="checkbox"] {
.popup-box-content { .popup-box-content {
background-color: var(--my-background-color); background-color: var(--my-background-color);
padding: 12px; padding: 12px;
border: 5px solid var(--my-highlight-color); border: 2px solid #adff2f;
width: 70%; width: 70%;
max-height: 80%; max-height: 80%;
overflow-y: auto; overflow-y: auto;
z-index: 11; z-index: 11;
/* Sit on top of the container */ /* Sit on top of the container */
color: var(--my-font-color); } color: var(--my-font-color);
box-shadow: 0 3px 5px -1px #090, 0 5px 8px 0 #090, 0 1px 14px 0 #090; }
.popup-box-input-div { .popup-box-input-div {
margin: 2px; } margin: 2px; }
@ -2169,12 +2170,12 @@ input[type="checkbox"] {
max-height: 50%; max-height: 50%;
z-index: 10; z-index: 10;
background-color: var(--my-background-color); background-color: var(--my-background-color);
border: 2px solid var(--my-highlight-color); } border: 2px solid #adff2f; }
.log-box-header { .log-box-header {
z-index: 1300; z-index: 1300;
background-color: #333; background-color: #333;
border: 1px solid var(--my-highlight-color); border: 2px solid #adff2f;
display: flex; display: flex;
flex: row nowrap; flex: row nowrap;
align-items: center; align-items: center;

File diff suppressed because one or more lines are too long

@ -1,3 +1,4 @@
import React from "react";
import { Player } from "../Player"; import { Player } from "../Player";
import { getRandomInt } from "../utils/helpers/getRandomInt"; import { getRandomInt } from "../utils/helpers/getRandomInt";
import { addOffset } from "../utils/helpers/addOffset"; import { addOffset } from "../utils/helpers/addOffset";
@ -20,7 +21,6 @@ class StatsMultiplier {
export interface IActionParams { export interface IActionParams {
name?: string; name?: string;
desc?: string;
level?: number; level?: number;
maxLevel?: number; maxLevel?: number;
autoLevel?: boolean; autoLevel?: boolean;
@ -43,7 +43,6 @@ export interface IActionParams {
export class Action implements IAction { export class Action implements IAction {
name = ""; name = "";
desc = "";
// Difficulty scales with level. See getDifficulty() method // Difficulty scales with level. See getDifficulty() method
level = 1; level = 1;
@ -100,7 +99,6 @@ export class Action implements IAction {
constructor(params: IActionParams | null = null) { constructor(params: IActionParams | null = null) {
// | null = null // | null = null
if (params && params.name) this.name = params.name; if (params && params.name) this.name = params.name;
if (params && params.desc) this.desc = params.desc;
if (params && params.baseDifficulty) this.baseDifficulty = addOffset(params.baseDifficulty, 10); if (params && params.baseDifficulty) this.baseDifficulty = addOffset(params.baseDifficulty, 10);
if (params && params.difficultyFac) this.difficultyFac = params.difficultyFac; if (params && params.difficultyFac) this.difficultyFac = params.difficultyFac;

@ -1,763 +0,0 @@
import { BlackOperation } from "./BlackOperation";
import { IMap } from "../types";
export const BlackOperations: IMap<BlackOperation> = {};
(function () {
BlackOperations["Operation Typhoon"] = new BlackOperation({
name: "Operation Typhoon",
desc:
"Obadiah Zenyatta is the leader of a RedWater PMC. It has long " +
"been known among the intelligence community that Zenyatta, along " +
"with the rest of the PMC, is a Synthoid.<br><br>" +
"The goal of Operation Typhoon is to find and eliminate " +
"Zenyatta and RedWater by any means necessary. After the task " +
"is completed, the actions must be covered up from the general public.",
baseDifficulty: 2000,
reqdRank: 2.5e3,
rankGain: 50,
rankLoss: 10,
hpLoss: 100,
weights: {
hack: 0.1,
str: 0.2,
def: 0.2,
dex: 0.2,
agi: 0.2,
cha: 0,
int: 0.1,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isKill: true,
});
BlackOperations["Operation Zero"] = new BlackOperation({
name: "Operation Zero",
desc:
"AeroCorp is one of the world's largest defense contractors. " +
"Its leader, Steve Watataki, is thought to be a supporter of " +
"Synthoid rights. He must be removed.<br><br>" +
"The goal of Operation Zero is to covertly infiltrate AeroCorp and " +
"uncover any incriminating evidence or " +
"information against Watataki that will cause him to be removed " +
"from his position at AeroCorp. Incriminating evidence can be " +
"fabricated as a last resort. Be warned that AeroCorp has some of " +
"the most advanced security measures in the world.",
baseDifficulty: 2500,
reqdRank: 5e3,
rankGain: 60,
rankLoss: 15,
hpLoss: 50,
weights: {
hack: 0.2,
str: 0.15,
def: 0.15,
dex: 0.2,
agi: 0.2,
cha: 0,
int: 0.1,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isStealth: true,
});
BlackOperations["Operation X"] = new BlackOperation({
name: "Operation X",
desc:
"We have recently discovered an underground publication " +
"group called Samizdat. Even though most of their publications " +
"are nonsensical conspiracy theories, the average human is " +
"gullible enough to believe them. Many of their works discuss " +
"Synthoids and pose a threat to society. The publications are spreading " +
"rapidly in China and other Eastern countries.<br><br>" +
"Samizdat has done a good job of keeping hidden and anonymous. " +
"However, we've just received intelligence that their base of " +
"operations is in Ishima's underground sewer systems. Your task is to " +
"investigate the sewer systems, and eliminate Samizdat. They must " +
"never publish anything again.",
baseDifficulty: 3000,
reqdRank: 7.5e3,
rankGain: 75,
rankLoss: 15,
hpLoss: 100,
weights: {
hack: 0.1,
str: 0.2,
def: 0.2,
dex: 0.2,
agi: 0.2,
cha: 0,
int: 0.1,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isKill: true,
});
BlackOperations["Operation Titan"] = new BlackOperation({
name: "Operation Titan",
desc:
"Several months ago Titan Laboratories' Bioengineering department " +
"was infiltrated by Synthoids. As far as we know, Titan Laboratories' " +
"management has no knowledge about this. We don't know what the " +
"Synthoids are up to, but the research that they could " +
"be conducting using Titan Laboraties' vast resources is potentially " +
"very dangerous.<br><br>" +
"Your goal is to enter and destroy the Bioengineering department's " +
"facility in Aevum. The task is not just to retire the Synthoids there, but " +
"also to destroy any information or research at the facility that " +
"is relevant to the Synthoids and their goals.",
baseDifficulty: 4000,
reqdRank: 10e3,
rankGain: 100,
rankLoss: 20,
hpLoss: 100,
weights: {
hack: 0.1,
str: 0.2,
def: 0.2,
dex: 0.2,
agi: 0.2,
cha: 0,
int: 0.1,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isKill: true,
});
BlackOperations["Operation Ares"] = new BlackOperation({
name: "Operation Ares",
desc:
"One of our undercover agents, Agent Carter, has informed us of a " +
"massive weapons deal going down in Dubai between rogue Russian " +
"militants and a radical Synthoid community. These weapons are next-gen " +
"plasma and energy weapons. It is critical for the safety of humanity " +
"that this deal does not happen.<br><br>" +
"Your task is to intercept the deal. Leave no survivors.",
baseDifficulty: 5000,
reqdRank: 12.5e3,
rankGain: 125,
rankLoss: 20,
hpLoss: 200,
weights: {
hack: 0,
str: 0.25,
def: 0.25,
dex: 0.25,
agi: 0.25,
cha: 0,
int: 0,
},
decays: {
hack: 0,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isKill: true,
});
BlackOperations["Operation Archangel"] = new BlackOperation({
name: "Operation Archangel",
desc:
"Our analysts have discovered that the popular Red Rabbit brothel in " +
"Amsterdam is run and 'staffed' by MK-VI Synthoids. Intelligence " +
"suggests that the profit from this brothel is used to fund a large " +
"black market arms trafficking operation.<br><br>" +
"The goal of this operation is to take out the leaders that are running " +
"the Red Rabbit brothel. Try to limit the number of other casualties, " +
"but do what you must to complete the mission.",
baseDifficulty: 7500,
reqdRank: 15e3,
rankGain: 200,
rankLoss: 20,
hpLoss: 25,
weights: {
hack: 0,
str: 0.2,
def: 0.2,
dex: 0.3,
agi: 0.3,
cha: 0,
int: 0,
},
decays: {
hack: 0,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isKill: true,
});
BlackOperations["Operation Juggernaut"] = new BlackOperation({
name: "Operation Juggernaut",
desc:
"The CIA has just encountered a new security threat. A new " +
"criminal group, lead by a shadowy operative who calls himself " +
"Juggernaut, has been smuggling drugs and weapons (including " +
"suspected bioweapons) into Sector-12. We also have reason " +
"to believe the tried to break into one of Universal Energy's " +
"facilities in order to cause a city-wide blackout. The CIA " +
"suspects that Juggernaut is a heavily-augmented Synthoid, and " +
"have thus enlisted our help.<br><br>" +
"Your mission is to eradicate Juggernaut and his followers.",
baseDifficulty: 10e3,
reqdRank: 20e3,
rankGain: 300,
rankLoss: 40,
hpLoss: 300,
weights: {
hack: 0,
str: 0.25,
def: 0.25,
dex: 0.25,
agi: 0.25,
cha: 0,
int: 0,
},
decays: {
hack: 0,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isKill: true,
});
BlackOperations["Operation Red Dragon"] = new BlackOperation({
name: "Operation Red Dragon",
desc:
"The Tetrads criminal organization is suspected of " +
"reverse-engineering the MK-VI Synthoid design. We believe " +
"they altered and possibly improved the design and began " +
"manufacturing their own Synthoid models in order to bolster " +
"their criminal activities.<br><br>" +
"Your task is to infiltrate and destroy the Tetrads' base of operations " +
"in Los Angeles. Intelligence tells us that their base houses " +
"one of their Synthoid manufacturing units.",
baseDifficulty: 12.5e3,
reqdRank: 25e3,
rankGain: 500,
rankLoss: 50,
hpLoss: 500,
weights: {
hack: 0.05,
str: 0.2,
def: 0.2,
dex: 0.25,
agi: 0.25,
cha: 0,
int: 0.05,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isKill: true,
});
BlackOperations["Operation K"] = new BlackOperation({
name: "Operation K",
desc:
"CODE RED SITUATION. Our intelligence tells us that VitaLife " +
"has discovered a new android cloning technology. This technology " +
"is supposedly capable of cloning Synthoid, not only physically " +
"but also their advanced AI modules. We do not believe that " +
"VitaLife is trying to use this technology illegally or " +
"maliciously, but if any Synthoids were able to infiltrate the " +
"corporation and take advantage of this technology then the " +
"results would be catastrophic.<br><br>" +
"We do not have the power or jurisdiction to shutdown this down " +
"through legal or political means, so we must resort to a covert " +
"operation. Your goal is to destroy this technology and eliminate " +
"anyone who was involved in its creation.",
baseDifficulty: 15e3,
reqdRank: 30e3,
rankGain: 750,
rankLoss: 60,
hpLoss: 1000,
weights: {
hack: 0.05,
str: 0.2,
def: 0.2,
dex: 0.25,
agi: 0.25,
cha: 0,
int: 0.05,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isKill: true,
});
BlackOperations["Operation Deckard"] = new BlackOperation({
name: "Operation Deckard",
desc:
"Despite your success in eliminating VitaLife's new android-replicating " +
"technology in Operation K, we've discovered that a small group of " +
"MK-VI Synthoids were able to make off with the schematics and design " +
"of the technology before the Operation. It is almost a certainty that " +
"these Synthoids are some of the rogue MK-VI ones from the Synthoid Uprising.<br><br>" +
"The goal of Operation Deckard is to hunt down these Synthoids and retire " +
"them. I don't need to tell you how critical this mission is.",
baseDifficulty: 20e3,
reqdRank: 40e3,
rankGain: 1e3,
rankLoss: 75,
hpLoss: 200,
weights: {
hack: 0,
str: 0.24,
def: 0.24,
dex: 0.24,
agi: 0.24,
cha: 0,
int: 0.04,
},
decays: {
hack: 0,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isKill: true,
});
BlackOperations["Operation Tyrell"] = new BlackOperation({
name: "Operation Tyrell",
desc:
"A week ago Blade Industries reported a small break-in at one " +
"of their Aevum Augmentation storage facitilities. We figured out " +
"that The Dark Army was behind the heist, and didn't think any more " +
"of it. However, we've just discovered that several known MK-VI Synthoids " +
"were part of that break-in group.<br><br>" +
"We cannot have Synthoids upgrading their already-enhanced abilities " +
"with Augmentations. Your task is to hunt down the associated Dark Army " +
"members and eliminate them.",
baseDifficulty: 25e3,
reqdRank: 50e3,
rankGain: 1.5e3,
rankLoss: 100,
hpLoss: 500,
weights: {
hack: 0.1,
str: 0.2,
def: 0.2,
dex: 0.2,
agi: 0.2,
cha: 0,
int: 0.1,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isKill: true,
});
BlackOperations["Operation Wallace"] = new BlackOperation({
name: "Operation Wallace",
desc:
"Based on information gathered from Operation Tyrell, we've discovered " +
"that The Dark Army was well aware that there were Synthoids amongst " +
"their ranks. Even worse, we believe that The Dark Army is working " +
"together with other criminal organizations such as The Syndicate and " +
"that they are planning some sort of large-scale takeover of multiple major " +
"cities, most notably Aevum. We suspect that Synthoids have infiltrated " +
"the ranks of these criminal factions and are trying to stage another " +
"Synthoid uprising.<br><br>" +
"The best way to deal with this is to prevent it before it even happens. " +
"The goal of Operation Wallace is to destroy the Dark Army and " +
"Syndicate factions in Aevum immediately. Leave no survivors.",
baseDifficulty: 30e3,
reqdRank: 75e3,
rankGain: 2e3,
rankLoss: 150,
hpLoss: 1500,
weights: {
hack: 0,
str: 0.24,
def: 0.24,
dex: 0.24,
agi: 0.24,
cha: 0,
int: 0.04,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isKill: true,
});
BlackOperations["Operation Shoulder of Orion"] = new BlackOperation({
name: "Operation Shoulder of Orion",
desc:
"China's Solaris Space Systems is secretly launching the first " +
"manned spacecraft in over a decade using Synthoids. We believe " +
"China is trying to establish the first off-world colonies.<br><br>" +
"The mission is to prevent this launch without instigating an " +
"international conflict. When you accept this mission you will be " +
"officially disavowed by the NSA and the national government until after you " +
"successfully return. In the event of failure, all of the operation's " +
"team members must not let themselves be captured alive.",
baseDifficulty: 35e3,
reqdRank: 100e3,
rankGain: 2.5e3,
rankLoss: 500,
hpLoss: 1500,
weights: {
hack: 0.1,
str: 0.2,
def: 0.2,
dex: 0.2,
agi: 0.2,
cha: 0,
int: 0.1,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isStealth: true,
});
BlackOperations["Operation Hyron"] = new BlackOperation({
name: "Operation Hyron",
desc:
"Our intelligence tells us that Fulcrum Technologies is developing " +
"a quantum supercomputer using human brains as core " +
"processors. This supercomputer " +
"is rumored to be able to store vast amounts of data and " +
"perform computations unmatched by any other supercomputer on the " +
"planet. But more importantly, the use of organic human brains " +
"means that the supercomputer may be able to reason abstractly " +
"and become self-aware.<br><br>" +
"I do not need to remind you why sentient-level AIs pose a serious " +
"threat to all of mankind.<br><br>" +
"The research for this project is being conducted at one of Fulcrum " +
"Technologies secret facilities in Aevum, codenamed 'Alpha Ranch'. " +
"Infiltrate the compound, delete and destroy the work, and then find and kill the " +
"project lead.",
baseDifficulty: 40e3,
reqdRank: 125e3,
rankGain: 3e3,
rankLoss: 1e3,
hpLoss: 500,
weights: {
hack: 0.1,
str: 0.2,
def: 0.2,
dex: 0.2,
agi: 0.2,
cha: 0,
int: 0.1,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isKill: true,
});
BlackOperations["Operation Morpheus"] = new BlackOperation({
name: "Operation Morpheus",
desc:
"DreamSense Technologies is an advertising company that uses " +
"special technology to transmit their ads into the peoples " +
"dreams and subconcious. They do this using broadcast transmitter " +
"towers. Based on information from our agents and informants in " +
"Chonqging, we have reason to believe that one of the broadcast " +
"towers there has been compromised by Synthoids and is being used " +
"to spread pro-Synthoid propaganda.<br><br>" +
"The mission is to destroy this broadcast tower. Speed and " +
"stealth are of the upmost important for this.",
baseDifficulty: 45e3,
reqdRank: 150e3,
rankGain: 4e3,
rankLoss: 1e3,
hpLoss: 100,
weights: {
hack: 0.05,
str: 0.15,
def: 0.15,
dex: 0.3,
agi: 0.3,
cha: 0,
int: 0.05,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isStealth: true,
});
BlackOperations["Operation Ion Storm"] = new BlackOperation({
name: "Operation Ion Storm",
desc:
"Our analysts have uncovered a gathering of MK-VI Synthoids " +
"that have taken up residence in the Sector-12 Slums. We " +
"don't know if they are rogue Synthoids from the Uprising, " +
"but we do know that they have been stockpiling " +
"weapons, money, and other resources. This makes them dangerous.<br><br>" +
"This is a full-scale assault operation to find and retire all of these " +
"Synthoids in the Sector-12 Slums.",
baseDifficulty: 50e3,
reqdRank: 175e3,
rankGain: 5e3,
rankLoss: 1e3,
hpLoss: 5000,
weights: {
hack: 0,
str: 0.24,
def: 0.24,
dex: 0.24,
agi: 0.24,
cha: 0,
int: 0.04,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isKill: true,
});
BlackOperations["Operation Annihilus"] = new BlackOperation({
name: "Operation Annihilus",
desc:
"Our superiors have ordered us to eradicate everything and everyone " +
"in an underground facility located in Aevum. They tell us " +
"that the facility houses many dangerous Synthoids and " +
"belongs to a terrorist organization called " +
"'The Covenant'. We have no prior intelligence about this " +
"organization, so you are going in blind.",
baseDifficulty: 55e3,
reqdRank: 200e3,
rankGain: 7.5e3,
rankLoss: 1e3,
hpLoss: 10e3,
weights: {
hack: 0,
str: 0.24,
def: 0.24,
dex: 0.24,
agi: 0.24,
cha: 0,
int: 0.04,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isKill: true,
});
BlackOperations["Operation Ultron"] = new BlackOperation({
name: "Operation Ultron",
desc:
"OmniTek Incorporated, the original designer and manufacturer of Synthoids, " +
"has notified us of a malfunction in their AI design. This malfunction, " +
"when triggered, causes MK-VI Synthoids to become radicalized and seek out " +
"the destruction of humanity. They say that this bug affects all MK-VI Synthoids, " +
"not just the rogue ones from the Uprising.<br><br>" +
"OmniTek has also told us they they believe someone has triggered this " +
"malfunction in a large group of MK-VI Synthoids, and that these newly-radicalized Synthoids " +
"are now amassing in Volhaven to form a terrorist group called Ultron.<br><br>" +
"Intelligence suggests Ultron is heavily armed and that their members are " +
"augmented. We believe Ultron is making moves to take control of " +
"and weaponize DeltaOne's Tactical High-Energy Satellite Laser Array (THESLA).<br><br>" +
"Your task is to find and destroy Ultron.",
baseDifficulty: 60e3,
reqdRank: 250e3,
rankGain: 10e3,
rankLoss: 2e3,
hpLoss: 10e3,
weights: {
hack: 0.1,
str: 0.2,
def: 0.2,
dex: 0.2,
agi: 0.2,
cha: 0,
int: 0.1,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isKill: true,
});
BlackOperations["Operation Centurion"] = new BlackOperation({
name: "Operation Centurion",
desc:
"D)@#)($M)C0293c40($*)@#D0JUMP3Rm0C<*@#)*$)#02c94830c(#$*D)<br><br>" +
"Throughout all of humanity's history, we have relied on " +
"technology to survive, conquer, and progress. Its advancement became our primary goal. " +
"And at the peak of human civilization technology turned into " +
"power. Global, absolute power.<br><br>" +
"It seems that the universe is not without a sense of irony.<br><br>" +
"D)@#)($M)C0293c40($*)@#D0JUMP3Rm0C<*@#)*$)#02c94830c(#$*D)",
baseDifficulty: 70e3,
reqdRank: 300e3,
rankGain: 15e3,
rankLoss: 5e3,
hpLoss: 10e3,
weights: {
hack: 0.1,
str: 0.2,
def: 0.2,
dex: 0.2,
agi: 0.2,
cha: 0,
int: 0.1,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
});
BlackOperations["Operation Vindictus"] = new BlackOperation({
name: "Operation Vindictus",
desc:
"D)@#)($M)C0293c40($*)@#D0JUMP3Rm0C<*@#)*$)#02c94830c(#$*D)<br><br>" +
"The bits are all around us. The daemons that hold the Node " +
"together can manifest themselves in many different ways.<br><br>" +
"D)@#)($M)C0293c40($*)@#D0JUMP3Rm0C<*@#)*$)#02c94830c(#$*D)",
baseDifficulty: 75e3,
reqdRank: 350e3,
rankGain: 20e3,
rankLoss: 20e3,
hpLoss: 20e3,
weights: {
hack: 0.1,
str: 0.2,
def: 0.2,
dex: 0.2,
agi: 0.2,
cha: 0,
int: 0.1,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
});
BlackOperations["Operation Daedalus"] = new BlackOperation({
name: "Operation Daedalus",
desc: "Yesterday we obeyed kings and bent our neck to emperors. " + "Today we kneel only to truth.",
baseDifficulty: 80e3,
reqdRank: 400e3,
rankGain: 40e3,
rankLoss: 10e3,
hpLoss: 100e3,
weights: {
hack: 0.1,
str: 0.2,
def: 0.2,
dex: 0.2,
agi: 0.2,
cha: 0,
int: 0.1,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
});
})();

@ -0,0 +1,572 @@
import React from "react";
import { BlackOperation } from "./BlackOperation";
import { IMap } from "../types";
export const BlackOperations: IMap<BlackOperation> = {};
(function () {
BlackOperations["Operation Typhoon"] = new BlackOperation({
name: "Operation Typhoon",
baseDifficulty: 2000,
reqdRank: 2.5e3,
rankGain: 50,
rankLoss: 10,
hpLoss: 100,
weights: {
hack: 0.1,
str: 0.2,
def: 0.2,
dex: 0.2,
agi: 0.2,
cha: 0,
int: 0.1,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isKill: true,
});
BlackOperations["Operation Zero"] = new BlackOperation({
name: "Operation Zero",
baseDifficulty: 2500,
reqdRank: 5e3,
rankGain: 60,
rankLoss: 15,
hpLoss: 50,
weights: {
hack: 0.2,
str: 0.15,
def: 0.15,
dex: 0.2,
agi: 0.2,
cha: 0,
int: 0.1,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isStealth: true,
});
BlackOperations["Operation X"] = new BlackOperation({
name: "Operation X",
baseDifficulty: 3000,
reqdRank: 7.5e3,
rankGain: 75,
rankLoss: 15,
hpLoss: 100,
weights: {
hack: 0.1,
str: 0.2,
def: 0.2,
dex: 0.2,
agi: 0.2,
cha: 0,
int: 0.1,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isKill: true,
});
BlackOperations["Operation Titan"] = new BlackOperation({
name: "Operation Titan",
baseDifficulty: 4000,
reqdRank: 10e3,
rankGain: 100,
rankLoss: 20,
hpLoss: 100,
weights: {
hack: 0.1,
str: 0.2,
def: 0.2,
dex: 0.2,
agi: 0.2,
cha: 0,
int: 0.1,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isKill: true,
});
BlackOperations["Operation Ares"] = new BlackOperation({
name: "Operation Ares",
baseDifficulty: 5000,
reqdRank: 12.5e3,
rankGain: 125,
rankLoss: 20,
hpLoss: 200,
weights: {
hack: 0,
str: 0.25,
def: 0.25,
dex: 0.25,
agi: 0.25,
cha: 0,
int: 0,
},
decays: {
hack: 0,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isKill: true,
});
BlackOperations["Operation Archangel"] = new BlackOperation({
name: "Operation Archangel",
baseDifficulty: 7500,
reqdRank: 15e3,
rankGain: 200,
rankLoss: 20,
hpLoss: 25,
weights: {
hack: 0,
str: 0.2,
def: 0.2,
dex: 0.3,
agi: 0.3,
cha: 0,
int: 0,
},
decays: {
hack: 0,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isKill: true,
});
BlackOperations["Operation Juggernaut"] = new BlackOperation({
name: "Operation Juggernaut",
baseDifficulty: 10e3,
reqdRank: 20e3,
rankGain: 300,
rankLoss: 40,
hpLoss: 300,
weights: {
hack: 0,
str: 0.25,
def: 0.25,
dex: 0.25,
agi: 0.25,
cha: 0,
int: 0,
},
decays: {
hack: 0,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isKill: true,
});
BlackOperations["Operation Red Dragon"] = new BlackOperation({
name: "Operation Red Dragon",
baseDifficulty: 12.5e3,
reqdRank: 25e3,
rankGain: 500,
rankLoss: 50,
hpLoss: 500,
weights: {
hack: 0.05,
str: 0.2,
def: 0.2,
dex: 0.25,
agi: 0.25,
cha: 0,
int: 0.05,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isKill: true,
});
BlackOperations["Operation K"] = new BlackOperation({
name: "Operation K",
baseDifficulty: 15e3,
reqdRank: 30e3,
rankGain: 750,
rankLoss: 60,
hpLoss: 1000,
weights: {
hack: 0.05,
str: 0.2,
def: 0.2,
dex: 0.25,
agi: 0.25,
cha: 0,
int: 0.05,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isKill: true,
});
BlackOperations["Operation Deckard"] = new BlackOperation({
name: "Operation Deckard",
baseDifficulty: 20e3,
reqdRank: 40e3,
rankGain: 1e3,
rankLoss: 75,
hpLoss: 200,
weights: {
hack: 0,
str: 0.24,
def: 0.24,
dex: 0.24,
agi: 0.24,
cha: 0,
int: 0.04,
},
decays: {
hack: 0,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isKill: true,
});
BlackOperations["Operation Tyrell"] = new BlackOperation({
name: "Operation Tyrell",
baseDifficulty: 25e3,
reqdRank: 50e3,
rankGain: 1.5e3,
rankLoss: 100,
hpLoss: 500,
weights: {
hack: 0.1,
str: 0.2,
def: 0.2,
dex: 0.2,
agi: 0.2,
cha: 0,
int: 0.1,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isKill: true,
});
BlackOperations["Operation Wallace"] = new BlackOperation({
name: "Operation Wallace",
baseDifficulty: 30e3,
reqdRank: 75e3,
rankGain: 2e3,
rankLoss: 150,
hpLoss: 1500,
weights: {
hack: 0,
str: 0.24,
def: 0.24,
dex: 0.24,
agi: 0.24,
cha: 0,
int: 0.04,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isKill: true,
});
BlackOperations["Operation Shoulder of Orion"] = new BlackOperation({
name: "Operation Shoulder of Orion",
baseDifficulty: 35e3,
reqdRank: 100e3,
rankGain: 2.5e3,
rankLoss: 500,
hpLoss: 1500,
weights: {
hack: 0.1,
str: 0.2,
def: 0.2,
dex: 0.2,
agi: 0.2,
cha: 0,
int: 0.1,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isStealth: true,
});
BlackOperations["Operation Hyron"] = new BlackOperation({
name: "Operation Hyron",
baseDifficulty: 40e3,
reqdRank: 125e3,
rankGain: 3e3,
rankLoss: 1e3,
hpLoss: 500,
weights: {
hack: 0.1,
str: 0.2,
def: 0.2,
dex: 0.2,
agi: 0.2,
cha: 0,
int: 0.1,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isKill: true,
});
BlackOperations["Operation Morpheus"] = new BlackOperation({
name: "Operation Morpheus",
baseDifficulty: 45e3,
reqdRank: 150e3,
rankGain: 4e3,
rankLoss: 1e3,
hpLoss: 100,
weights: {
hack: 0.05,
str: 0.15,
def: 0.15,
dex: 0.3,
agi: 0.3,
cha: 0,
int: 0.05,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isStealth: true,
});
BlackOperations["Operation Ion Storm"] = new BlackOperation({
name: "Operation Ion Storm",
baseDifficulty: 50e3,
reqdRank: 175e3,
rankGain: 5e3,
rankLoss: 1e3,
hpLoss: 5000,
weights: {
hack: 0,
str: 0.24,
def: 0.24,
dex: 0.24,
agi: 0.24,
cha: 0,
int: 0.04,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isKill: true,
});
BlackOperations["Operation Annihilus"] = new BlackOperation({
name: "Operation Annihilus",
baseDifficulty: 55e3,
reqdRank: 200e3,
rankGain: 7.5e3,
rankLoss: 1e3,
hpLoss: 10e3,
weights: {
hack: 0,
str: 0.24,
def: 0.24,
dex: 0.24,
agi: 0.24,
cha: 0,
int: 0.04,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isKill: true,
});
BlackOperations["Operation Ultron"] = new BlackOperation({
name: "Operation Ultron",
baseDifficulty: 60e3,
reqdRank: 250e3,
rankGain: 10e3,
rankLoss: 2e3,
hpLoss: 10e3,
weights: {
hack: 0.1,
str: 0.2,
def: 0.2,
dex: 0.2,
agi: 0.2,
cha: 0,
int: 0.1,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
isKill: true,
});
BlackOperations["Operation Centurion"] = new BlackOperation({
name: "Operation Centurion",
baseDifficulty: 70e3,
reqdRank: 300e3,
rankGain: 15e3,
rankLoss: 5e3,
hpLoss: 10e3,
weights: {
hack: 0.1,
str: 0.2,
def: 0.2,
dex: 0.2,
agi: 0.2,
cha: 0,
int: 0.1,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
});
BlackOperations["Operation Vindictus"] = new BlackOperation({
name: "Operation Vindictus",
baseDifficulty: 75e3,
reqdRank: 350e3,
rankGain: 20e3,
rankLoss: 20e3,
hpLoss: 20e3,
weights: {
hack: 0.1,
str: 0.2,
def: 0.2,
dex: 0.2,
agi: 0.2,
cha: 0,
int: 0.1,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
});
BlackOperations["Operation Daedalus"] = new BlackOperation({
name: "Operation Daedalus",
baseDifficulty: 80e3,
reqdRank: 400e3,
rankGain: 40e3,
rankLoss: 10e3,
hpLoss: 100e3,
weights: {
hack: 0.1,
str: 0.2,
def: 0.2,
dex: 0.2,
agi: 0.2,
cha: 0,
int: 0.1,
},
decays: {
hack: 0.6,
str: 0.8,
def: 0.8,
dex: 0.8,
agi: 0.8,
cha: 0,
int: 0.75,
},
});
})();

@ -1,3 +1,4 @@
import React from "react";
import { Reviver, Generic_toJSON, Generic_fromJSON } from "../utils/JSONReviver"; import { Reviver, Generic_toJSON, Generic_fromJSON } from "../utils/JSONReviver";
import { IBladeburner } from "./IBladeburner"; import { IBladeburner } from "./IBladeburner";
import { IActionIdentifier } from "./IActionIdentifier"; import { IActionIdentifier } from "./IActionIdentifier";
@ -1586,11 +1587,6 @@ export class Bladeburner implements IBladeburner {
create(): void { create(): void {
this.contracts["Tracking"] = new Contract({ this.contracts["Tracking"] = new Contract({
name: "Tracking", name: "Tracking",
desc:
"Identify and locate Synthoids. This contract involves reconnaissance " +
"and information-gathering ONLY. Do NOT engage. Stealth is of the utmost importance.<br><br>" +
"Successfully completing Tracking contracts will slightly improve your Synthoid population estimate for " +
"whatever city you are currently in.",
baseDifficulty: 125, baseDifficulty: 125,
difficultyFac: 1.02, difficultyFac: 1.02,
rewardFac: 1.041, rewardFac: 1.041,
@ -1619,10 +1615,6 @@ export class Bladeburner implements IBladeburner {
}); });
this.contracts["Bounty Hunter"] = new Contract({ this.contracts["Bounty Hunter"] = new Contract({
name: "Bounty Hunter", name: "Bounty Hunter",
desc:
"Hunt down and capture fugitive Synthoids. These Synthoids are wanted alive.<br><br>" +
"Successfully completing a Bounty Hunter contract will lower the population in your " +
"current city, and will also increase its chaos level.",
baseDifficulty: 250, baseDifficulty: 250,
difficultyFac: 1.04, difficultyFac: 1.04,
rewardFac: 1.085, rewardFac: 1.085,
@ -1651,10 +1643,6 @@ export class Bladeburner implements IBladeburner {
}); });
this.contracts["Retirement"] = new Contract({ this.contracts["Retirement"] = new Contract({
name: "Retirement", name: "Retirement",
desc:
"Hunt down and retire (kill) rogue Synthoids.<br><br>" +
"Successfully completing a Retirement contract will lower the population in your current " +
"city, and will also increase its chaos level.",
baseDifficulty: 200, baseDifficulty: 200,
difficultyFac: 1.03, difficultyFac: 1.03,
rewardFac: 1.065, rewardFac: 1.065,
@ -1684,12 +1672,6 @@ export class Bladeburner implements IBladeburner {
this.operations["Investigation"] = new Operation({ this.operations["Investigation"] = new Operation({
name: "Investigation", name: "Investigation",
desc:
"As a field agent, investigate and identify Synthoid " +
"populations, movements, and operations.<br><br>Successful " +
"Investigation ops will increase the accuracy of your " +
"synthoid data.<br><br>" +
"You will NOT lose HP from failed Investigation ops.",
baseDifficulty: 400, baseDifficulty: 400,
difficultyFac: 1.03, difficultyFac: 1.03,
rewardFac: 1.07, rewardFac: 1.07,
@ -1719,11 +1701,6 @@ export class Bladeburner implements IBladeburner {
}); });
this.operations["Undercover Operation"] = new Operation({ this.operations["Undercover Operation"] = new Operation({
name: "Undercover Operation", name: "Undercover Operation",
desc:
"Conduct undercover operations to identify hidden " +
"and underground Synthoid communities and organizations.<br><br>" +
"Successful Undercover ops will increase the accuracy of your synthoid " +
"data.",
baseDifficulty: 500, baseDifficulty: 500,
difficultyFac: 1.04, difficultyFac: 1.04,
rewardFac: 1.09, rewardFac: 1.09,
@ -1754,7 +1731,6 @@ export class Bladeburner implements IBladeburner {
}); });
this.operations["Sting Operation"] = new Operation({ this.operations["Sting Operation"] = new Operation({
name: "Sting Operation", name: "Sting Operation",
desc: "Conduct a sting operation to bait and capture particularly " + "notorious Synthoid criminals.",
baseDifficulty: 650, baseDifficulty: 650,
difficultyFac: 1.04, difficultyFac: 1.04,
rewardFac: 1.095, rewardFac: 1.095,
@ -1785,10 +1761,6 @@ export class Bladeburner implements IBladeburner {
}); });
this.operations["Raid"] = new Operation({ this.operations["Raid"] = new Operation({
name: "Raid", name: "Raid",
desc:
"Lead an assault on a known Synthoid community. Note that " +
"there must be an existing Synthoid community in your current city " +
"in order for this Operation to be successful.",
baseDifficulty: 800, baseDifficulty: 800,
difficultyFac: 1.045, difficultyFac: 1.045,
rewardFac: 1.1, rewardFac: 1.1,
@ -1819,10 +1791,6 @@ export class Bladeburner implements IBladeburner {
}); });
this.operations["Stealth Retirement Operation"] = new Operation({ this.operations["Stealth Retirement Operation"] = new Operation({
name: "Stealth Retirement Operation", name: "Stealth Retirement Operation",
desc:
"Lead a covert operation to retire Synthoids. The " +
"objective is to complete the task without " +
"drawing any attention. Stealth and discretion are key.",
baseDifficulty: 1000, baseDifficulty: 1000,
difficultyFac: 1.05, difficultyFac: 1.05,
rewardFac: 1.11, rewardFac: 1.11,
@ -1854,10 +1822,6 @@ export class Bladeburner implements IBladeburner {
}); });
this.operations["Assassination"] = new Operation({ this.operations["Assassination"] = new Operation({
name: "Assassination", name: "Assassination",
desc:
"Assassinate Synthoids that have been identified as " +
"important, high-profile social and political leaders " +
"in the Synthoid communities.",
baseDifficulty: 1500, baseDifficulty: 1500,
difficultyFac: 1.06, difficultyFac: 1.06,
rewardFac: 1.14, rewardFac: 1.14,
@ -1900,7 +1864,7 @@ export class Bladeburner implements IBladeburner {
if (this.action.type !== ActionTypes["Idle"]) { if (this.action.type !== ActionTypes["Idle"]) {
let msg = "Your Bladeburner action was cancelled because you started doing something else."; let msg = "Your Bladeburner action was cancelled because you started doing something else.";
if (this.automateEnabled) { if (this.automateEnabled) {
msg += `<br><br>Your automation was disabled as well. You will have to re-enable it through the Bladeburner console`; msg += `<br /><br />Your automation was disabled as well. You will have to re-enable it through the Bladeburner console`;
this.automateEnabled = false; this.automateEnabled = false;
} }
if (!Settings.SuppressBladeburnerPopup) { if (!Settings.SuppressBladeburnerPopup) {

@ -1,54 +0,0 @@
import { Action } from "./Action";
import { IMap } from "../types";
export const GeneralActions: IMap<Action> = {};
(function () {
// General Actions
let actionName;
actionName = "Training";
GeneralActions[actionName] = new Action({
name: actionName,
desc:
"Improve your abilities at the Bladeburner unit's specialized training " +
"center. Doing this gives experience for all combat stats and also " +
"increases your max stamina.",
});
actionName = "Field Analysis";
GeneralActions[actionName] = new Action({
name: actionName,
desc:
"Mine and analyze Synthoid-related data. This improves the " +
"Bladeburner's unit intelligence on Synthoid locations and " +
"activities. Completing this action will improve the accuracy " +
"of your Synthoid population estimated in the current city.<br><br>" +
"Does NOT require stamina.",
});
actionName = "Recruitment";
GeneralActions[actionName] = new Action({
name: actionName,
desc:
"Attempt to recruit members for your Bladeburner team. These members " +
"can help you conduct operations.<br><br>" +
"Does NOT require stamina.",
});
actionName = "Diplomacy";
GeneralActions[actionName] = new Action({
name: actionName,
desc:
"Improve diplomatic relations with the Synthoid population. " +
"Completing this action will reduce the Chaos level in your current city.<br><br>" +
"Does NOT require stamina.",
});
actionName = "Hyperbolic Regeneration Chamber";
GeneralActions[actionName] = new Action({
name: actionName,
desc:
"Enter cryogenic stasis using the Bladeburner division's hi-tech Regeneration Chamber. " +
"This will slowly heal your wounds and slightly increase your stamina.<br><br>",
});
})();

@ -0,0 +1,34 @@
import React from "react";
import { Action } from "./Action";
import { IMap } from "../types";
export const GeneralActions: IMap<Action> = {};
(function () {
// General Actions
let actionName;
actionName = "Training";
GeneralActions[actionName] = new Action({
name: actionName,
});
actionName = "Field Analysis";
GeneralActions[actionName] = new Action({
name: actionName,
});
actionName = "Recruitment";
GeneralActions[actionName] = new Action({
name: actionName,
});
actionName = "Diplomacy";
GeneralActions[actionName] = new Action({
name: actionName,
});
actionName = "Hyperbolic Regeneration Chamber";
GeneralActions[actionName] = new Action({
name: actionName,
});
})();

@ -18,7 +18,6 @@ export interface ISuccessChanceParams {
export interface IAction { export interface IAction {
name: string; name: string;
desc: string;
// Difficulty scales with level. See getDifficulty() method // Difficulty scales with level. See getDifficulty() method
level: number; level: number;

@ -0,0 +1,299 @@
import React from "react";
interface IBlackOp {
desc: JSX.Element;
}
export const BlackOperations: {
[key: string]: IBlackOp | undefined;
} = {
"Operation Typhoon": {
desc: (
<>
Obadiah Zenyatta is the leader of a RedWater PMC. It has long been known among the intelligence community that
Zenyatta, along with the rest of the PMC, is a Synthoid.
<br />
<br />
The goal of Operation Typhoon is to find and eliminate Zenyatta and RedWater by any means necessary. After the
task is completed, the actions must be covered up from the general public.
</>
),
},
"Operation Zero": {
desc: (
<>
AeroCorp is one of the world's largest defense contractors. Its leader, Steve Watataki, is thought to be a
supporter of Synthoid rights. He must be removed.
<br />
<br />
The goal of Operation Zero is to covertly infiltrate AeroCorp and uncover any incriminating evidence or
information against Watataki that will cause him to be removed from his position at AeroCorp. Incriminating
evidence can be fabricated as a last resort. Be warned that AeroCorp has some of the most advanced security
measures in the world.
</>
),
},
"Operation X": {
desc: (
<>
We have recently discovered an underground publication group called Samizdat. Even though most of their
publications are nonsensical conspiracy theories, the average human is gullible enough to believe them. Many of
their works discuss Synthoids and pose a threat to society. The publications are spreading rapidly in China and
other Eastern countries.
<br />
<br />
Samizdat has done a good job of keeping hidden and anonymous. However, we've just received intelligence that
their base of operations is in Ishima's underground sewer systems. Your task is to investigate the sewer
systems, and eliminate Samizdat. They must never publish anything again.
</>
),
},
"Operation Titan": {
desc: (
<>
Several months ago Titan Laboratories' Bioengineering department was infiltrated by Synthoids. As far as we
know, Titan Laboratories' management has no knowledge about this. We don't know what the Synthoids are up to,
but the research that they could be conducting using Titan Laboraties' vast resources is potentially very
dangerous.
<br />
<br />
Your goal is to enter and destroy the Bioengineering department's facility in Aevum. The task is not just to
retire the Synthoids there, but also to destroy any information or research at the facility that is relevant to
the Synthoids and their goals.
</>
),
},
"Operation Ares": {
desc: (
<>
One of our undercover agents, Agent Carter, has informed us of a massive weapons deal going down in Dubai
between rogue Russian militants and a radical Synthoid community. These weapons are next-gen plasma and energy
weapons. It is critical for the safety of humanity that this deal does not happen.
<br />
<br />
Your task is to intercept the deal. Leave no survivors.
</>
),
},
"Operation Archangel": {
desc: (
<>
Our analysts have discovered that the popular Red Rabbit brothel in Amsterdam is run and 'staffed' by MK-VI
Synthoids. Intelligence suggests that the profit from this brothel is used to fund a large black market arms
trafficking operation.
<br />
<br />
The goal of this operation is to take out the leaders that are running the Red Rabbit brothel. Try to limit the
number of other casualties, but do what you must to complete the mission.
</>
),
},
"Operation Juggernaut": {
desc: (
<>
The CIA has just encountered a new security threat. A new criminal group, lead by a shadowy operative who calls
himself Juggernaut, has been smuggling drugs and weapons (including suspected bioweapons) into Sector-12. We
also have reason to believe the tried to break into one of Universal Energy's facilities in order to cause a
city-wide blackout. The CIA suspects that Juggernaut is a heavily-augmented Synthoid, and have thus enlisted our
help.
<br />
<br />
Your mission is to eradicate Juggernaut and his followers.
</>
),
},
"Operation Red Dragon": {
desc: (
<>
The Tetrads criminal organization is suspected of reverse-engineering the MK-VI Synthoid design. We believe they
altered and possibly improved the design and began manufacturing their own Synthoid models in order to bolster
their criminal activities.
<br />
<br />
Your task is to infiltrate and destroy the Tetrads' base of operations in Los Angeles. Intelligence tells us
that their base houses one of their Synthoid manufacturing units.
</>
),
},
"Operation K": {
desc: (
<>
CODE RED SITUATION. Our intelligence tells us that VitaLife has discovered a new android cloning technology.
This technology is supposedly capable of cloning Synthoid, not only physically but also their advanced AI
modules. We do not believe that VitaLife is trying to use this technology illegally or maliciously, but if any
Synthoids were able to infiltrate the corporation and take advantage of this technology then the results would
be catastrophic.
<br />
<br />
We do not have the power or jurisdiction to shutdown this down through legal or political means, so we must
resort to a covert operation. Your goal is to destroy this technology and eliminate anyone who was involved in
its creation.
</>
),
},
"Operation Deckard": {
desc: (
<>
Despite your success in eliminating VitaLife's new android-replicating technology in Operation K, we've
discovered that a small group of MK-VI Synthoids were able to make off with the schematics and design of the
technology before the Operation. It is almost a certainty that these Synthoids are some of the rogue MK-VI ones
from the Synthoid Uprising.
<br />
<br />
The goal of Operation Deckard is to hunt down these Synthoids and retire them. I don't need to tell you how
critical this mission is.
</>
),
},
"Operation Tyrell": {
desc: (
<>
A week ago Blade Industries reported a small break-in at one of their Aevum Augmentation storage facitilities.
We figured out that The Dark Army was behind the heist, and didn't think any more of it. However, we've just
discovered that several known MK-VI Synthoids were part of that break-in group.
<br />
<br />
We cannot have Synthoids upgrading their already-enhanced abilities with Augmentations. Your task is to hunt
down the associated Dark Army members and eliminate them.
</>
),
},
"Operation Wallace": {
desc: (
<>
Based on information gathered from Operation Tyrell, we've discovered that The Dark Army was well aware that
there were Synthoids amongst their ranks. Even worse, we believe that The Dark Army is working together with
other criminal organizations such as The Syndicate and that they are planning some sort of large-scale takeover
of multiple major cities, most notably Aevum. We suspect that Synthoids have infiltrated the ranks of these
criminal factions and are trying to stage another Synthoid uprising.
<br />
<br />
The best way to deal with this is to prevent it before it even happens. The goal of Operation Wallace is to
destroy the Dark Army and Syndicate factions in Aevum immediately. Leave no survivors.
</>
),
},
"Operation Shoulder of Orion": {
desc: (
<>
China's Solaris Space Systems is secretly launching the first manned spacecraft in over a decade using
Synthoids. We believe China is trying to establish the first off-world colonies.
<br />
<br />
The mission is to prevent this launch without instigating an international conflict. When you accept this
mission you will be officially disavowed by the NSA and the national government until after you successfully
return. In the event of failure, all of the operation's team members must not let themselves be captured alive.
</>
),
},
"Operation Hyron": {
desc: (
<>
Our intelligence tells us that Fulcrum Technologies is developing a quantum supercomputer using human brains as
core processors. This supercomputer is rumored to be able to store vast amounts of data and perform computations
unmatched by any other supercomputer on the planet. But more importantly, the use of organic human brains means
that the supercomputer may be able to reason abstractly and become self-aware.
<br />
<br />
I do not need to remind you why sentient-level AIs pose a serious threat to all of mankind.
<br />
<br />
The research for this project is being conducted at one of Fulcrum Technologies secret facilities in Aevum,
codenamed 'Alpha Ranch'. Infiltrate the compound, delete and destroy the work, and then find and kill the
project lead.
</>
),
},
"Operation Morpheus": {
desc: (
<>
DreamSense Technologies is an advertising company that uses special technology to transmit their ads into the
peoples dreams and subconcious. They do this using broadcast transmitter towers. Based on information from our
agents and informants in Chonqging, we have reason to believe that one of the broadcast towers there has been
compromised by Synthoids and is being used to spread pro-Synthoid propaganda.
<br />
<br />
The mission is to destroy this broadcast tower. Speed and stealth are of the upmost important for this.
</>
),
},
"Operation Ion Storm": {
desc: (
<>
Our analysts have uncovered a gathering of MK-VI Synthoids that have taken up residence in the Sector-12 Slums.
We don't know if they are rogue Synthoids from the Uprising, but we do know that they have been stockpiling
weapons, money, and other resources. This makes them dangerous.
<br />
<br />
This is a full-scale assault operation to find and retire all of these Synthoids in the Sector-12 Slums.
</>
),
},
"Operation Annihilus": {
desc: (
<>
Our superiors have ordered us to eradicate everything and everyone in an underground facility located in Aevum.
They tell us that the facility houses many dangerous Synthoids and belongs to a terrorist organization called
'The Covenant'. We have no prior intelligence about this organization, so you are going in blind.
</>
),
},
"Operation Ultron": {
desc: (
<>
OmniTek Incorporated, the original designer and manufacturer of Synthoids, has notified us of a malfunction in
their AI design. This malfunction, when triggered, causes MK-VI Synthoids to become radicalized and seek out the
destruction of humanity. They say that this bug affects all MK-VI Synthoids, not just the rogue ones from the
Uprising.
<br />
<br />
OmniTek has also told us they they believe someone has triggered this malfunction in a large group of MK-VI
Synthoids, and that these newly-radicalized Synthoids are now amassing in Volhaven to form a terrorist group
called Ultron.
<br />
<br />
Intelligence suggests Ultron is heavily armed and that their members are augmented. We believe Ultron is making
moves to take control of and weaponize DeltaOne's Tactical High-Energy Satellite Laser Array (THESLA).
<br />
<br />
Your task is to find and destroy Ultron.
</>
),
},
"Operation Centurion": {
desc: (
<>
{"D)@#)($M)C0293c40($*)@#D0JUMP3Rm0C<*@#)*$)#02c94830c(#$*D)"}
<br />
<br />
Throughout all of humanity's history, we have relied on technology to survive, conquer, and progress. Its
advancement became our primary goal. And at the peak of human civilization technology turned into power. Global,
absolute power.
<br />
<br />
It seems that the universe is not without a sense of irony.
<br />
<br />
{"D)@#)($M)C0293c40($*)@#D0JUMP3Rm0C<*@#)*$)#02c94830c(#$*D)"}
</>
),
},
"Operation Vindictus": {
desc: (
<>
{"D)@#)($M)C0293c40($*)@#D0JUMP3Rm0C<*@#)*$)#02c94830c(#$*D)"}
<br />
<br />
The bits are all around us. The daemons that hold the Node together can manifest themselves in many different
ways.
<br />
<br />
{"D)@#)($M)C0293c40($*)@#D0JUMP3Rm0C<*@#)*$)#02c94830c(#$*D)"}
</>
),
},
"Operation Daedalus": {
desc: <> Yesterday we obeyed kings and bent our neck to emperors. Today we kneel only to truth.</>,
},
};

@ -0,0 +1,44 @@
import React from "react";
interface IContract {
desc: JSX.Element;
}
export const Contracts: {
[key: string]: IContract | undefined;
} = {
Tracking: {
desc: (
<>
Identify and locate Synthoids. This contract involves reconnaissance and information-gathering ONLY. Do NOT
engage. Stealth is of the utmost importance.
<br />
<br />
Successfully completing Tracking contracts will slightly improve your Synthoid population estimate for whatever
city you are currently in.
</>
),
},
"Bounty Hunter": {
desc: (
<>
Hunt down and capture fugitive Synthoids. These Synthoids are wanted alive.
<br />
<br />
Successfully completing a Bounty Hunter contract will lower the population in your current city, and will also
increase its chaos level.
</>
),
},
Retirement: {
desc: (
<>
Hunt down and retire (kill) rogue Synthoids.
<br />
<br />
Successfully completing a Retirement contract will lower the population in your current city, and will also
increase its chaos level.
</>
),
},
};

@ -0,0 +1,65 @@
import React from "react";
interface IContract {
desc: JSX.Element;
}
export const GeneralActions: {
[key: string]: IContract | undefined;
} = {
Training: {
desc: (
<>
Improve your abilities at the Bladeburner unit's specialized training center. Doing this gives experience for
all combat stats and also increases your max stamina.
</>
),
},
"Field Analysis": {
desc: (
<>
Mine and analyze Synthoid-related data. This improves the Bladeburner's unit intelligence on Synthoid locations
and activities. Completing this action will improve the accuracy of your Synthoid population estimated in the
current city.
<br />
<br />
Does NOT require stamina.
</>
),
},
Recruitment: {
desc: (
<>
Attempt to recruit members for your Bladeburner team. These members can help you conduct operations.
<br />
<br />
Does NOT require stamina.
</>
),
},
Diplomacy: {
desc: (
<>
Improve diplomatic relations with the Synthoid population. Completing this action will reduce the Chaos level in
your current city.
<br />
<br />
Does NOT require stamina.
</>
),
},
"Hyperbolic Regeneration Chamber": {
desc: (
<>
Enter cryogenic stasis using the Bladeburner division's hi-tech Regeneration Chamber. This will slowly heal your
wounds and slightly increase your stamina.
<br />
<br />
</>
),
},
};

@ -0,0 +1,60 @@
import React from "react";
interface IOperation {
desc: JSX.Element;
}
export const Operations: {
[key: string]: IOperation | undefined;
} = {
Investigation: {
desc: (
<>
As a field agent, investigate and identify Synthoid populations, movements, and operations.
<br />
<br />
Successful Investigation ops will increase the accuracy of your synthoid data.
<br />
<br />
You will NOT lose HP from failed Investigation ops.
</>
),
},
"Undercover Operation": {
desc: (
<>
Conduct undercover operations to identify hidden and underground Synthoid communities and organizations.
<br />
<br />
Successful Undercover ops will increase the accuracy of your synthoid data.
</>
),
},
"Sting Operation": {
desc: <>Conduct a sting operation to bait and capture particularly notorious Synthoid criminals.</>,
},
Raid: {
desc: (
<>
Lead an assault on a known Synthoid community. Note that there must be an existing Synthoid community in your
current city in order for this Operation to be successful.
</>
),
},
"Stealth Retirement Operation": {
desc: (
<>
Lead a covert operation to retire Synthoids. The objective is to complete the task without drawing any
attention. Stealth and discretion are key.
</>
),
},
Assassination: {
desc: (
<>
Assassinate Synthoids that have been identified as important, high-profile social and political leaders in the
Synthoid communities.
</>
),
},
};

@ -0,0 +1,75 @@
import React from "react";
import { IAction } from "../IAction";
import { IBladeburner } from "../IBladeburner";
import { BladeburnerConstants } from "../data/Constants";
import { use } from "../../ui/Context";
import Typography from "@mui/material/Typography";
import Tooltip from "@mui/material/Tooltip";
import Box from "@mui/material/Box";
import IconButton from "@mui/material/IconButton";
import ArrowDropUpIcon from "@mui/icons-material/ArrowDropUp";
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
interface IProps {
action: IAction;
isActive: boolean;
bladeburner: IBladeburner;
rerender: () => void;
}
export function ActionLevel({ action, isActive, bladeburner, rerender }: IProps): React.ReactElement {
const player = use.Player();
const canIncrease = action.level < action.maxLevel;
const canDecrease = action.level > 1;
function increaseLevel(): void {
if (!canIncrease) return;
++action.level;
if (isActive) bladeburner.startAction(player, bladeburner.action);
rerender();
}
function decreaseLevel(): void {
if (!canDecrease) return;
--action.level;
if (isActive) bladeburner.startAction(player, bladeburner.action);
rerender();
}
return (
<Box display="flex" flexDirection="row" alignItems="center">
<Box display="flex">
<Tooltip
title={
<Typography>
{action.getSuccessesNeededForNextLevel(BladeburnerConstants.ContractSuccessesPerLevel)} successes needed
for next level
</Typography>
}
>
<Typography>
Level: {action.level} / {action.maxLevel}
</Typography>
</Tooltip>
</Box>
<Tooltip
disableInteractive
title={isActive ? <Typography>WARNING: changing the level will restart the Operation</Typography> : ""}
>
<IconButton disabled={!canIncrease} onClick={increaseLevel}>
<ArrowDropUpIcon />
</IconButton>
</Tooltip>
<Tooltip
disableInteractive
title={isActive ? <Typography>WARNING: changing the level will restart the Operation</Typography> : ""}
>
<IconButton disabled={!canDecrease} onClick={decreaseLevel}>
<ArrowDropDownIcon />
</IconButton>
</Tooltip>
</Box>
);
}

@ -1,54 +1,45 @@
import React, { useState, useEffect } from "react"; import React from "react";
import { GeneralActionPage } from "./GeneralActionPage"; import { GeneralActionPage } from "./GeneralActionPage";
import { ContractPage } from "./ContractPage"; import { ContractPage } from "./ContractPage";
import { OperationPage } from "./OperationPage"; import { OperationPage } from "./OperationPage";
import { BlackOpPage } from "./BlackOpPage"; import { BlackOpPage } from "./BlackOpPage";
import { SkillPage } from "./SkillPage"; import { SkillPage } from "./SkillPage";
import { stealthIcon, killIcon } from "../data/Icons";
import { IBladeburner } from "../IBladeburner"; import { IBladeburner } from "../IBladeburner";
import { IPlayer } from "../../PersonObjects/IPlayer"; import { IPlayer } from "../../PersonObjects/IPlayer";
import Tabs from "@mui/material/Tabs";
import Tab from "@mui/material/Tab";
import Box from "@mui/material/Box";
import Paper from "@mui/material/Paper";
interface IProps { interface IProps {
bladeburner: IBladeburner; bladeburner: IBladeburner;
player: IPlayer; player: IPlayer;
} }
export function AllPages(props: IProps): React.ReactElement { export function AllPages(props: IProps): React.ReactElement {
const [page, setPage] = useState("General"); const [value, setValue] = React.useState(0);
const setRerender = useState(false)[1];
useEffect(() => { function handleChange(event: React.SyntheticEvent, tab: number): void {
const id = setInterval(() => setRerender((old) => !old), 1000); setValue(tab);
return () => clearInterval(id);
}, []);
function Header(props: { name: string }): React.ReactElement {
return (
<a
onClick={() => setPage(props.name)}
className={page !== props.name ? "bladeburner-nav-button noselect" : "bladeburner-nav-button-inactive noselect"}
>
{props.name}
</a>
);
} }
return ( return (
<> <>
<Header name={"General"} /> <Tabs variant="fullWidth" value={value} onChange={handleChange} aria-label="basic tabs example">
<Header name={"Contracts"} /> <Tab label="General" />
<Header name={"Operations"} /> <Tab label="Contracts" />
<Header name={"BlackOps"} /> <Tab label="Operations" />
<Header name={"Skills"} /> <Tab label="BlackOps" />
<div style={{ display: "block", margin: "4px", padding: "4px" }}> <Tab label="Skills" />
{page === "General" && <GeneralActionPage bladeburner={props.bladeburner} player={props.player} />} </Tabs>
{page === "Contracts" && <ContractPage bladeburner={props.bladeburner} player={props.player} />} <Box sx={{ p: 1 }}>
{page === "Operations" && <OperationPage bladeburner={props.bladeburner} player={props.player} />} {value === 0 && <GeneralActionPage bladeburner={props.bladeburner} player={props.player} />}
{page === "BlackOps" && <BlackOpPage bladeburner={props.bladeburner} player={props.player} />} {value === 1 && <ContractPage bladeburner={props.bladeburner} player={props.player} />}
{page === "Skills" && <SkillPage bladeburner={props.bladeburner} />} {value === 2 && <OperationPage bladeburner={props.bladeburner} player={props.player} />}
</div> {value === 3 && <BlackOpPage bladeburner={props.bladeburner} player={props.player} />}
<span className="text"> {value === 4 && <SkillPage bladeburner={props.bladeburner} />}
{stealthIcon} = This action requires stealth, {killIcon} = This action involves retirement </Box>
</span>
</> </>
); );
} }

@ -0,0 +1,26 @@
import React from "react";
import { IAction } from "../IAction";
import Typography from "@mui/material/Typography";
import Tooltip from "@mui/material/Tooltip";
import Box from "@mui/material/Box";
import Switch from "@mui/material/Switch";
interface IProps {
action: IAction;
rerender: () => void;
}
export function Autolevel(props: IProps): React.ReactElement {
function onAutolevel(event: React.ChangeEvent<HTMLInputElement>): void {
props.action.autoLevel = event.target.checked;
props.rerender();
}
return (
<Box display="flex" flexDirection="row" alignItems="center">
<Tooltip title={<Typography>Automatically increase operation level when possible</Typography>}>
<Typography> Autolevel:</Typography>
</Tooltip>
<Switch checked={props.action.autoLevel} onChange={onAutolevel} />
</Box>
);
}

@ -2,22 +2,29 @@ import React, { useState } from "react";
import { formatNumber, convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions"; import { formatNumber, convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions";
import { ActionTypes } from "../data/ActionTypes"; import { ActionTypes } from "../data/ActionTypes";
import { createProgressBarText } from "../../utils/helpers/createProgressBarText"; import { createProgressBarText } from "../../utils/helpers/createProgressBarText";
import { stealthIcon, killIcon } from "../data/Icons"; import { TeamSizeButton } from "./TeamSizeButton";
import { createPopup } from "../../ui/React/createPopup";
import { TeamSizePopup } from "./TeamSizePopup";
import { IBladeburner } from "../IBladeburner"; import { IBladeburner } from "../IBladeburner";
import { BlackOperation } from "../BlackOperation";
import { BlackOperations } from "../data/BlackOperations";
import { IPlayer } from "../../PersonObjects/IPlayer"; import { IPlayer } from "../../PersonObjects/IPlayer";
import { SuccessChance } from "./SuccessChance";
import { CopyableText } from "../../ui/React/CopyableText"; import { CopyableText } from "../../ui/React/CopyableText";
import { SuccessChance } from "./SuccessChance";
import { StartButton } from "./StartButton";
import Typography from "@mui/material/Typography";
import Paper from "@mui/material/Paper";
interface IProps { interface IProps {
bladeburner: IBladeburner; bladeburner: IBladeburner;
player: IPlayer; player: IPlayer;
action: any; action: BlackOperation;
} }
export function BlackOpElem(props: IProps): React.ReactElement { export function BlackOpElem(props: IProps): React.ReactElement {
const setRerender = useState(false)[1]; const setRerender = useState(false)[1];
function rerender(): void {
setRerender((old) => !old);
}
const isCompleted = props.bladeburner.blackops[props.action.name] != null; const isCompleted = props.bladeburner.blackops[props.action.name] != null;
if (isCompleted) { if (isCompleted) {
return <h2 style={{ display: "block" }}>{props.action.name} (COMPLETED)</h2>; return <h2 style={{ display: "block" }}>{props.action.name} (COMPLETED)</h2>;
@ -26,7 +33,6 @@ export function BlackOpElem(props: IProps): React.ReactElement {
const isActive = const isActive =
props.bladeburner.action.type === ActionTypes["BlackOperation"] && props.bladeburner.action.type === ActionTypes["BlackOperation"] &&
props.action.name === props.bladeburner.action.name; props.action.name === props.bladeburner.action.name;
const estimatedSuccessChance = props.action.getEstSuccessChance(props.bladeburner);
const actionTime = props.action.getActionTime(props.bladeburner); const actionTime = props.action.getActionTime(props.bladeburner);
const hasReqdRank = props.bladeburner.rank >= props.action.reqdRank; const hasReqdRank = props.bladeburner.rank >= props.action.reqdRank;
const computedActionTimeCurrent = Math.min( const computedActionTimeCurrent = Math.min(
@ -34,70 +40,54 @@ export function BlackOpElem(props: IProps): React.ReactElement {
props.bladeburner.actionTimeToComplete, props.bladeburner.actionTimeToComplete,
); );
function onStart(): void { const actionData = BlackOperations[props.action.name];
props.bladeburner.action.type = ActionTypes.BlackOperation; if (actionData === undefined) {
props.bladeburner.action.name = props.action.name; throw new Error(`Cannot find data for ${props.action.name}`);
props.bladeburner.startAction(props.player, props.bladeburner.action);
setRerender((old) => !old);
}
function onTeam(): void {
const popupId = "bladeburner-operation-set-team-size-popup";
createPopup(popupId, TeamSizePopup, {
bladeburner: props.bladeburner,
action: props.action,
popupId: popupId,
});
} }
return ( return (
<> <Paper sx={{ my: 1, p: 1 }}>
<h2 style={{ display: "inline-block" }}> <Typography>
{isActive ? ( {isActive ? (
<>
<> <>
<CopyableText value={props.action.name} /> (IN PROGRESS - {formatNumber(computedActionTimeCurrent, 0)} /{" "} <CopyableText value={props.action.name} /> (IN PROGRESS - {formatNumber(computedActionTimeCurrent, 0)} /{" "}
{formatNumber(props.bladeburner.actionTimeToComplete, 0)}) {formatNumber(props.bladeburner.actionTimeToComplete, 0)})
</>
) : (
<CopyableText value={props.action.name} />
)}
</h2>
{isActive ? (
<p style={{ display: "block" }}> <p style={{ display: "block" }}>
{createProgressBarText({ {createProgressBarText({
progress: computedActionTimeCurrent / props.bladeburner.actionTimeToComplete, progress: computedActionTimeCurrent / props.bladeburner.actionTimeToComplete,
})} })}
</p> </p>
</>
</>
) : ( ) : (
<> <>
<a <CopyableText value={props.action.name} />
className={hasReqdRank ? "a-link-button" : "a-link-button-inactive"}
style={{ margin: "3px", padding: "3px" }} <StartButton
onClick={onStart} bladeburner={props.bladeburner}
> type={ActionTypes.BlackOperation}
Start name={props.action.name}
</a> rerender={rerender}
<a onClick={onTeam} style={{ margin: "3px", padding: "3px" }} className="a-link-button"> />
Set Team Size (Curr Size: {formatNumber(props.action.teamCount, 0)}) <TeamSizeButton action={props.action} bladeburner={props.bladeburner} />
</a>
</> </>
)} )}
</Typography>
<br /> <br />
<br /> <br />
<p style={{ display: "inline-block" }} dangerouslySetInnerHTML={{ __html: props.action.desc }} /> <Typography>{actionData.desc}</Typography>
<br /> <br />
<br /> <br />
<p style={{ display: "block", color: hasReqdRank ? "white" : "red" }}> <Typography color={hasReqdRank ? "primary" : "error"}>
Required Rank: {formatNumber(props.action.reqdRank, 0)} Required Rank: {formatNumber(props.action.reqdRank, 0)}
</p> </Typography>
<br /> <br />
<pre style={{ display: "inline-block" }}> <Typography>
Estimated Success Chance: <SuccessChance chance={estimatedSuccessChance} />{" "} <SuccessChance action={props.action} bladeburner={props.bladeburner} />
{props.action.isStealth ? stealthIcon : <></>}
{props.action.isKill ? killIcon : <></>}
<br /> <br />
Time Required: {convertTimeMsToTimeElapsedString(actionTime * 1000)} Time Required: {convertTimeMsToTimeElapsedString(actionTime * 1000)}
</pre> </Typography>
</> </Paper>
); );
} }

@ -35,9 +35,7 @@ export function BlackOpList(props: IProps): React.ReactElement {
return ( return (
<> <>
{blackops.map((blackop: BlackOperation) => ( {blackops.map((blackop: BlackOperation) => (
<li key={blackop.name} className="bladeburner-action"> <BlackOpElem key={blackop.name} bladeburner={props.bladeburner} action={blackop} player={props.player} />
<BlackOpElem bladeburner={props.bladeburner} action={blackop} player={props.player} />
</li>
))} ))}
</> </>
); );

@ -2,6 +2,7 @@ import * as React from "react";
import { BlackOpList } from "./BlackOpList"; import { BlackOpList } from "./BlackOpList";
import { IBladeburner } from "../IBladeburner"; import { IBladeburner } from "../IBladeburner";
import { IPlayer } from "../../PersonObjects/IPlayer"; import { IPlayer } from "../../PersonObjects/IPlayer";
import Typography from "@mui/material/Typography";
interface IProps { interface IProps {
bladeburner: IBladeburner; bladeburner: IBladeburner;
@ -11,7 +12,7 @@ interface IProps {
export function BlackOpPage(props: IProps): React.ReactElement { export function BlackOpPage(props: IProps): React.ReactElement {
return ( return (
<> <>
<p style={{ display: "block", margin: "4px", padding: "4px" }}> <Typography>
Black Operations (Black Ops) are special, one-time covert operations. Each Black Op must be unlocked Black Operations (Black Ops) are special, one-time covert operations. Each Black Op must be unlocked
successively by completing the one before it. successively by completing the one before it.
<br /> <br />
@ -21,7 +22,7 @@ export function BlackOpPage(props: IProps): React.ReactElement {
<br /> <br />
Like normal operations, you may use a team for Black Ops. Failing a black op will incur heavy HP and rank Like normal operations, you may use a team for Black Ops. Failing a black op will incur heavy HP and rank
losses. losses.
</p> </Typography>
<BlackOpList bladeburner={props.bladeburner} player={props.player} /> <BlackOpList bladeburner={props.bladeburner} player={props.player} />
</> </>
); );

@ -1,42 +1,39 @@
import React from "react"; import React, { useState, useEffect } from "react";
import { Stats } from "./Stats"; import { Stats } from "./Stats";
import { Console } from "./Console"; import { Console } from "./Console";
import { AllPages } from "./AllPages"; import { AllPages } from "./AllPages";
import { use } from "../../ui/Context"; import { use } from "../../ui/Context";
import Grid from "@mui/material/Grid";
import Box from "@mui/material/Box";
export function BladeburnerRoot(): React.ReactElement { export function BladeburnerRoot(): React.ReactElement {
const player = use.Player(); const player = use.Player();
const router = use.Router(); const router = use.Router();
const setRerender = useState(false)[1];
function rerender(): void {
setRerender((old) => !old);
}
useEffect(() => {
const id = setInterval(rerender, 200);
return () => clearInterval(id);
}, []);
const bladeburner = player.bladeburner; const bladeburner = player.bladeburner;
if (bladeburner === null) return <></>; if (bladeburner === null) return <></>;
return ( return (
<div className="bladeburner-container"> <Box display="flex" flexDirection="column">
<div style={{ height: "60%", display: "block", position: "relative" }}> <Grid container>
<div <Grid item xs={6}>
style={{
height: "100%",
width: "30%",
display: "inline-block",
border: "1px solid white",
}}
>
<Stats bladeburner={bladeburner} player={player} router={router} /> <Stats bladeburner={bladeburner} player={player} router={router} />
</div> </Grid>
<Grid item xs={6}>
<Console bladeburner={bladeburner} player={player} /> <Console bladeburner={bladeburner} player={player} />
</div> </Grid>
<div </Grid>
style={{
width: "70%",
display: "block",
border: "1px solid white",
marginTop: "6px",
padding: "6px",
position: "relative",
}}
>
<AllPages bladeburner={bladeburner} player={player} /> <AllPages bladeburner={bladeburner} player={player} />
</div> </Box>
</div>
); );
} }

@ -2,18 +2,48 @@ import React, { useState, useRef, useEffect } from "react";
import { IBladeburner } from "../IBladeburner"; import { IBladeburner } from "../IBladeburner";
import { IPlayer } from "../../PersonObjects/IPlayer"; import { IPlayer } from "../../PersonObjects/IPlayer";
import Paper from "@mui/material/Paper";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import TextField from "@mui/material/TextField";
import { Theme } from "@mui/material/styles";
import makeStyles from "@mui/styles/makeStyles";
import createStyles from "@mui/styles/createStyles";
interface ILineProps { interface ILineProps {
content: any; content: any;
} }
const useStyles = makeStyles((theme: Theme) =>
createStyles({
textfield: {
margin: theme.spacing(0),
width: "100%",
},
input: {
backgroundColor: "#000",
},
nopadding: {
padding: theme.spacing(0),
},
preformatted: {
whiteSpace: "pre-wrap",
margin: theme.spacing(0),
},
list: {
padding: theme.spacing(0),
height: "100%",
},
}),
);
function Line(props: ILineProps): React.ReactElement { function Line(props: ILineProps): React.ReactElement {
return ( return (
<tr> <ListItem sx={{ p: 0 }}>
<td className="bladeburner-console-line" style={{ color: "var(--my-font-color)", whiteSpace: "pre-wrap" }}> <Typography>{props.content}</Typography>
{props.content} </ListItem>
</td>
</tr>
); );
} }
@ -23,15 +53,21 @@ interface IProps {
} }
export function Console(props: IProps): React.ReactElement { export function Console(props: IProps): React.ReactElement {
const lastRef = useRef<HTMLDivElement>(null); const classes = useStyles();
const scrollHook = useRef<HTMLDivElement>(null);
const [command, setCommand] = useState("");
const setRerender = useState(false)[1]; const setRerender = useState(false)[1];
function handleCommandChange(event: React.ChangeEvent<HTMLInputElement>): void {
setCommand(event.target.value);
}
const [consoleHistoryIndex, setConsoleHistoryIndex] = useState(props.bladeburner.consoleHistory.length); const [consoleHistoryIndex, setConsoleHistoryIndex] = useState(props.bladeburner.consoleHistory.length);
// TODO: Figure out how to actually make the scrolling work correctly. // TODO: Figure out how to actually make the scrolling work correctly.
function scrollToBottom(): void { function scrollToBottom(): void {
if (!lastRef.current) return; if (!scrollHook.current) return;
lastRef.current.scrollTop = lastRef.current.scrollHeight; scrollHook.current.scrollTop = scrollHook.current.scrollHeight;
} }
function rerender(): void { function rerender(): void {
@ -50,13 +86,11 @@ export function Console(props: IProps): React.ReactElement {
function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void { function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
if (event.keyCode === 13) { if (event.keyCode === 13) {
event.preventDefault(); event.preventDefault();
const command = event.currentTarget.value;
event.currentTarget.value = "";
if (command.length > 0) { if (command.length > 0) {
props.bladeburner.postToConsole("> " + command); props.bladeburner.postToConsole("> " + command);
props.bladeburner.executeConsoleCommands(props.player, command); props.bladeburner.executeConsoleCommands(props.player, command);
setConsoleHistoryIndex(props.bladeburner.consoleHistory.length); setConsoleHistoryIndex(props.bladeburner.consoleHistory.length);
rerender(); setCommand("");
} }
} }
@ -105,31 +139,35 @@ export function Console(props: IProps): React.ReactElement {
} }
return ( return (
<div ref={lastRef} className="bladeburner-console-div"> <Box height={"60vh"} display={"flex"} alignItems={"stretch"} component={Paper}>
<table className="bladeburner-console-table"> <Box>
<tbody> <List sx={{ height: "100%", overflow: "auto" }}>
{/*
TODO: optimize this.
using `i` as a key here isn't great because it'll re-render everything
everytime the console reaches max length.
*/}
{props.bladeburner.consoleLogs.map((log: any, i: number) => ( {props.bladeburner.consoleLogs.map((log: any, i: number) => (
<Line key={i} content={log} /> <Line key={i} content={log} />
))} ))}
<tr key="input" id="bladeburner-console-input-row" className="bladeburner-console-input-row"> <TextField
<td className="bladeburner-console-input-cell"> classes={{ root: classes.textfield }}
<pre>{"> "}</pre>
<input
autoFocus autoFocus
className="bladeburner-console-input" variant="standard"
tabIndex={1} tabIndex={1}
type="text" type="text"
value={command}
onChange={handleCommandChange}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
InputProps={{
// for players to hook in
className: classes.input,
startAdornment: (
<>
<Typography>&gt;&nbsp;</Typography>
</>
),
spellCheck: false,
}}
/> />
</td> </List>
</tr> <div ref={scrollHook}></div>
</tbody> </Box>
</table> </Box>
</div>
); );
} }

@ -2,113 +2,78 @@ import React, { useState } from "react";
import { ActionTypes } from "../data/ActionTypes"; import { ActionTypes } from "../data/ActionTypes";
import { createProgressBarText } from "../../utils/helpers/createProgressBarText"; import { createProgressBarText } from "../../utils/helpers/createProgressBarText";
import { formatNumber, convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions"; import { formatNumber, convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions";
import { stealthIcon, killIcon } from "../data/Icons"; import { Contracts } from "../data/Contracts";
import { BladeburnerConstants } from "../data/Constants";
import { IBladeburner } from "../IBladeburner"; import { IBladeburner } from "../IBladeburner";
import { IAction } from "../IAction";
import { IPlayer } from "../../PersonObjects/IPlayer"; import { IPlayer } from "../../PersonObjects/IPlayer";
import { SuccessChance } from "./SuccessChance"; import { SuccessChance } from "./SuccessChance";
import { CopyableText } from "../../ui/React/CopyableText"; import { CopyableText } from "../../ui/React/CopyableText";
import { ActionLevel } from "./ActionLevel";
import { Autolevel } from "./Autolevel";
import { StartButton } from "./StartButton";
import Typography from "@mui/material/Typography";
import Paper from "@mui/material/Paper";
interface IProps { interface IProps {
bladeburner: IBladeburner; bladeburner: IBladeburner;
player: IPlayer; player: IPlayer;
action: any; action: IAction;
} }
export function ContractElem(props: IProps): React.ReactElement { export function ContractElem(props: IProps): React.ReactElement {
const setRerender = useState(false)[1]; const setRerender = useState(false)[1];
function rerender(): void {
setRerender((old) => !old);
}
const isActive = const isActive =
props.bladeburner.action.type === ActionTypes["Contract"] && props.action.name === props.bladeburner.action.name; props.bladeburner.action.type === ActionTypes["Contract"] && props.action.name === props.bladeburner.action.name;
const estimatedSuccessChance = props.action.getEstSuccessChance(props.bladeburner);
const computedActionTimeCurrent = Math.min( const computedActionTimeCurrent = Math.min(
props.bladeburner.actionTimeCurrent + props.bladeburner.actionTimeOverflow, props.bladeburner.actionTimeCurrent + props.bladeburner.actionTimeOverflow,
props.bladeburner.actionTimeToComplete, props.bladeburner.actionTimeToComplete,
); );
const maxLevel = props.action.level >= props.action.maxLevel;
const actionTime = props.action.getActionTime(props.bladeburner); const actionTime = props.action.getActionTime(props.bladeburner);
const autolevelCheckboxId = `bladeburner-${props.action.name}-autolevel-checkbox`;
function onStart(): void { const actionData = Contracts[props.action.name];
props.bladeburner.action.type = ActionTypes.Contract; if (actionData === undefined) {
props.bladeburner.action.name = props.action.name; throw new Error(`Cannot find data for ${props.action.name}`);
props.bladeburner.startAction(props.player, props.bladeburner.action);
setRerender((old) => !old);
}
function increaseLevel(): void {
++props.action.level;
if (isActive) props.bladeburner.startAction(props.player, props.bladeburner.action);
setRerender((old) => !old);
}
function decreaseLevel(): void {
--props.action.level;
if (isActive) props.bladeburner.startAction(props.player, props.bladeburner.action);
setRerender((old) => !old);
}
function onAutolevel(event: React.ChangeEvent<HTMLInputElement>): void {
props.action.autoLevel = event.target.checked;
setRerender((old) => !old);
} }
return ( return (
<> <Paper sx={{ my: 1, p: 1 }}>
<h2 style={{ display: "inline-block" }}>
{isActive ? ( {isActive ? (
<> <>
<Typography>
<CopyableText value={props.action.name} /> (IN PROGRESS - {formatNumber(computedActionTimeCurrent, 0)} /{" "} <CopyableText value={props.action.name} /> (IN PROGRESS - {formatNumber(computedActionTimeCurrent, 0)} /{" "}
{formatNumber(props.bladeburner.actionTimeToComplete, 0)}) {formatNumber(props.bladeburner.actionTimeToComplete, 0)})
</> </Typography>
) : ( <Typography>
<CopyableText value={props.action.name} />
)}
</h2>
{isActive ? (
<p style={{ display: "block" }}>
{createProgressBarText({ {createProgressBarText({
progress: computedActionTimeCurrent / props.bladeburner.actionTimeToComplete, progress: computedActionTimeCurrent / props.bladeburner.actionTimeToComplete,
})} })}
</p> </Typography>
</>
) : ( ) : (
<> <>
<a onClick={onStart} className="a-link-button" style={{ margin: "3px", padding: "3px" }}> <CopyableText value={props.action.name} />
Start <StartButton
</a> bladeburner={props.bladeburner}
type={ActionTypes.Contract}
name={props.action.name}
rerender={rerender}
/>
</> </>
)} )}
<br /> <br />
<br /> <br />
<pre className="tooltip" style={{ display: "inline-block" }}> <ActionLevel action={props.action} bladeburner={props.bladeburner} isActive={isActive} rerender={rerender} />
<span className="tooltiptext">
{props.action.getSuccessesNeededForNextLevel(BladeburnerConstants.ContractSuccessesPerLevel)} successes needed
for next level
</span>
Level: {props.action.level} / {props.action.maxLevel}
</pre>
<a
onClick={increaseLevel}
style={{ padding: "2px", margin: "2px" }}
className={`tooltip ${maxLevel ? "a-link-button-inactive" : "a-link-button"}`}
>
{isActive && <span className="tooltiptext">WARNING: changing the level will restart the Operation</span>}
</a>
<a
onClick={decreaseLevel}
style={{ padding: "2px", margin: "2px" }}
className={`tooltip ${props.action.level <= 1 ? "a-link-button-inactive" : "a-link-button"}`}
>
{isActive && <span className="tooltiptext">WARNING: changing the level will restart the Operation</span>}
</a>
<br /> <br />
<br /> <br />
<pre style={{ display: "inline-block" }}> <Typography>
<span dangerouslySetInnerHTML={{ __html: props.action.desc }} /> {actionData.desc}
<br /> <br />
<br /> <br />
Estimated success chance: <SuccessChance chance={estimatedSuccessChance} />{" "} <SuccessChance action={props.action} bladeburner={props.bladeburner} />
{props.action.isStealth ? stealthIcon : <></>}
{props.action.isKill ? killIcon : <></>}
<br /> <br />
Time Required: {convertTimeMsToTimeElapsedString(actionTime * 1000)} Time Required: {convertTimeMsToTimeElapsedString(actionTime * 1000)}
<br /> <br />
@ -117,13 +82,9 @@ export function ContractElem(props: IProps): React.ReactElement {
Successes: {props.action.successes} Successes: {props.action.successes}
<br /> <br />
Failures: {props.action.failures} Failures: {props.action.failures}
</pre> </Typography>
<br /> <br />
<label className="tooltip" style={{ color: "white" }} htmlFor={autolevelCheckboxId}> <Autolevel rerender={rerender} action={props.action} />
Autolevel: </Paper>
<span className="tooltiptext">Automatically increase operation level when possible</span>
</label>
<input type="checkbox" id={autolevelCheckboxId} checked={props.action.autoLevel} onChange={onAutolevel} />
</>
); );
} }

@ -14,9 +14,7 @@ export function ContractList(props: IProps): React.ReactElement {
return ( return (
<> <>
{names.map((name: string) => ( {names.map((name: string) => (
<li key={name} className="bladeburner-action"> <ContractElem key={name} bladeburner={props.bladeburner} action={contracts[name]} player={props.player} />
<ContractElem bladeburner={props.bladeburner} action={contracts[name]} player={props.player} />
</li>
))} ))}
</> </>
); );

@ -2,6 +2,7 @@ import * as React from "react";
import { ContractList } from "./ContractList"; import { ContractList } from "./ContractList";
import { IBladeburner } from "../IBladeburner"; import { IBladeburner } from "../IBladeburner";
import { IPlayer } from "../../PersonObjects/IPlayer"; import { IPlayer } from "../../PersonObjects/IPlayer";
import Typography from "@mui/material/Typography";
interface IProps { interface IProps {
bladeburner: IBladeburner; bladeburner: IBladeburner;
@ -11,14 +12,14 @@ interface IProps {
export function ContractPage(props: IProps): React.ReactElement { export function ContractPage(props: IProps): React.ReactElement {
return ( return (
<> <>
<p style={{ display: "block", margin: "4px", padding: "4px" }}> <Typography>
Complete contracts in order to increase your Bladeburner rank and earn money. Failing a contract will cause you Complete contracts in order to increase your Bladeburner rank and earn money. Failing a contract will cause you
to lose HP, which can lead to hospitalization. to lose HP, which can lead to hospitalization.
<br /> <br />
<br /> <br />
You can unlock higher-level contracts by successfully completing them. Higher-level contracts are more You can unlock higher-level contracts by successfully completing them. Higher-level contracts are more
difficult, but grant more rank, experience, and money. difficult, but grant more rank, experience, and money.
</p> </Typography>
<ContractList bladeburner={props.bladeburner} player={props.player} /> <ContractList bladeburner={props.bladeburner} player={props.player} />
</> </>
); );

@ -3,17 +3,28 @@ import { ActionTypes } from "../data/ActionTypes";
import { createProgressBarText } from "../../utils/helpers/createProgressBarText"; import { createProgressBarText } from "../../utils/helpers/createProgressBarText";
import { formatNumber, convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions"; import { formatNumber, convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions";
import { IBladeburner } from "../IBladeburner"; import { IBladeburner } from "../IBladeburner";
import { IAction } from "../IAction";
import { GeneralActions } from "../data/GeneralActions";
import { IPlayer } from "../../PersonObjects/IPlayer"; import { IPlayer } from "../../PersonObjects/IPlayer";
import { CopyableText } from "../../ui/React/CopyableText"; import { CopyableText } from "../../ui/React/CopyableText";
import { StartButton } from "./StartButton";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
import Paper from "@mui/material/Paper";
interface IProps { interface IProps {
bladeburner: IBladeburner; bladeburner: IBladeburner;
player: IPlayer; player: IPlayer;
action: any; action: IAction;
} }
export function GeneralActionElem(props: IProps): React.ReactElement { export function GeneralActionElem(props: IProps): React.ReactElement {
const setRerender = useState(false)[1]; const setRerender = useState(false)[1];
function rerender(): void {
setRerender((old) => !old);
}
const isActive = props.action.name === props.bladeburner.action.name; const isActive = props.action.name === props.bladeburner.action.name;
const computedActionTimeCurrent = Math.min( const computedActionTimeCurrent = Math.min(
props.bladeburner.actionTimeCurrent + props.bladeburner.actionTimeOverflow, props.bladeburner.actionTimeCurrent + props.bladeburner.actionTimeOverflow,
@ -37,44 +48,44 @@ export function GeneralActionElem(props: IProps): React.ReactElement {
? Math.max(0, Math.min(props.bladeburner.getRecruitmentSuccessChance(props.player), 1)) ? Math.max(0, Math.min(props.bladeburner.getRecruitmentSuccessChance(props.player), 1))
: -1; : -1;
function onStart(): void { const actionData = GeneralActions[props.action.name];
props.bladeburner.action.type = ActionTypes[props.action.name as string]; if (actionData === undefined) {
props.bladeburner.action.name = props.action.name; throw new Error(`Cannot find data for ${props.action.name}`);
props.bladeburner.startAction(props.player, props.bladeburner.action);
setRerender((old) => !old);
} }
return ( return (
<> <Paper sx={{ my: 1, p: 1 }}>
<h2 style={{ display: "inline-block" }}>
{isActive ? ( {isActive ? (
<> <>
<Typography>
<CopyableText value={props.action.name} /> (IN PROGRESS - {formatNumber(computedActionTimeCurrent, 0)} /{" "} <CopyableText value={props.action.name} /> (IN PROGRESS - {formatNumber(computedActionTimeCurrent, 0)} /{" "}
{formatNumber(props.bladeburner.actionTimeToComplete, 0)}) {formatNumber(props.bladeburner.actionTimeToComplete, 0)})
</> </Typography>
) : ( <Typography>
<CopyableText value={props.action.name} />
)}
</h2>
{isActive ? (
<p style={{ display: "block" }}>
{createProgressBarText({ {createProgressBarText({
progress: computedActionTimeCurrent / props.bladeburner.actionTimeToComplete, progress: computedActionTimeCurrent / props.bladeburner.actionTimeToComplete,
})} })}
</p> </Typography>
) : (
<>
<a onClick={onStart} className="a-link-button" style={{ margin: "3px", padding: "3px" }}>
Start
</a>
</> </>
) : (
<Box display="flex" flexDirection="row" alignItems="center">
<Typography>
<CopyableText value={props.action.name} />
</Typography>
<StartButton
bladeburner={props.bladeburner}
type={ActionTypes[props.action.name as string]}
name={props.action.name}
rerender={rerender}
/>
</Box>
)} )}
<br /> <br />
<br /> <br />
<pre style={{ display: "inline-block" }} dangerouslySetInnerHTML={{ __html: props.action.desc }}></pre> <Typography>{actionData.desc}</Typography>
<br /> <br />
<br /> <br />
<pre style={{ display: "inline-block" }}> <Typography>
Time Required: {convertTimeMsToTimeElapsedString(actionTime * 1000)} Time Required: {convertTimeMsToTimeElapsedString(actionTime * 1000)}
{successChance !== -1 && ( {successChance !== -1 && (
<> <>
@ -82,7 +93,7 @@ export function GeneralActionElem(props: IProps): React.ReactElement {
Estimated success chance: {formatNumber(successChance * 100, 1)}% Estimated success chance: {formatNumber(successChance * 100, 1)}%
</> </>
)} )}
</pre> </Typography>
</> </Paper>
); );
} }

@ -20,9 +20,7 @@ export function GeneralActionList(props: IProps): React.ReactElement {
return ( return (
<> <>
{actions.map((action: Action) => ( {actions.map((action: Action) => (
<li key={action.name} className="bladeburner-action"> <GeneralActionElem key={action.name} bladeburner={props.bladeburner} action={action} player={props.player} />
<GeneralActionElem bladeburner={props.bladeburner} action={action} player={props.player} />
</li>
))} ))}
</> </>
); );

@ -2,6 +2,7 @@ import * as React from "react";
import { GeneralActionList } from "./GeneralActionList"; import { GeneralActionList } from "./GeneralActionList";
import { IBladeburner } from "../IBladeburner"; import { IBladeburner } from "../IBladeburner";
import { IPlayer } from "../../PersonObjects/IPlayer"; import { IPlayer } from "../../PersonObjects/IPlayer";
import Typography from "@mui/material/Typography";
interface IProps { interface IProps {
bladeburner: IBladeburner; bladeburner: IBladeburner;
@ -11,10 +12,7 @@ interface IProps {
export function GeneralActionPage(props: IProps): React.ReactElement { export function GeneralActionPage(props: IProps): React.ReactElement {
return ( return (
<> <>
<p style={{ display: "block", margin: "4px", padding: "4px" }}> <Typography>These are generic actions that will assist you in your Bladeburner duties.</Typography>
These are generic actions that will assist you in your Bladeburner duties. They will not affect your Bladeburner
rank in any way.
</p>
<GeneralActionList bladeburner={props.bladeburner} player={props.player} /> <GeneralActionList bladeburner={props.bladeburner} player={props.player} />
</> </>
); );

@ -0,0 +1,13 @@
import React from "react";
import { killIcon } from "../data/Icons";
import Typography from "@mui/material/Typography";
import Tooltip from "@mui/material/Tooltip";
export function KillIcon(): React.ReactElement {
return (
<Tooltip disableInteractive title={<Typography>This action involves retirement</Typography>}>
{killIcon}
</Tooltip>
);
}

@ -2,127 +2,81 @@ import React, { useState } from "react";
import { ActionTypes } from "../data/ActionTypes"; import { ActionTypes } from "../data/ActionTypes";
import { createProgressBarText } from "../../utils/helpers/createProgressBarText"; import { createProgressBarText } from "../../utils/helpers/createProgressBarText";
import { formatNumber, convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions"; import { formatNumber, convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions";
import { stealthIcon, killIcon } from "../data/Icons";
import { BladeburnerConstants } from "../data/Constants";
import { createPopup } from "../../ui/React/createPopup";
import { TeamSizePopup } from "./TeamSizePopup";
import { IBladeburner } from "../IBladeburner";
import { IPlayer } from "../../PersonObjects/IPlayer";
import { SuccessChance } from "./SuccessChance"; import { SuccessChance } from "./SuccessChance";
import { ActionLevel } from "./ActionLevel";
import { Autolevel } from "./Autolevel";
import { StartButton } from "./StartButton";
import { TeamSizeButton } from "./TeamSizeButton";
import { IBladeburner } from "../IBladeburner";
import { Operation } from "../Operation";
import { Operations } from "../data/Operations";
import { IPlayer } from "../../PersonObjects/IPlayer";
import { CopyableText } from "../../ui/React/CopyableText"; import { CopyableText } from "../../ui/React/CopyableText";
import Typography from "@mui/material/Typography";
import Paper from "@mui/material/Paper";
interface IProps { interface IProps {
bladeburner: IBladeburner; bladeburner: IBladeburner;
player: IPlayer; player: IPlayer;
action: any; action: Operation;
} }
export function OperationElem(props: IProps): React.ReactElement { export function OperationElem(props: IProps): React.ReactElement {
const setRerender = useState(false)[1]; const setRerender = useState(false)[1];
function rerender(): void {
setRerender((old) => !old);
}
const isActive = const isActive =
props.bladeburner.action.type === ActionTypes["Operation"] && props.action.name === props.bladeburner.action.name; props.bladeburner.action.type === ActionTypes["Operation"] && props.action.name === props.bladeburner.action.name;
const estimatedSuccessChance = props.action.getEstSuccessChance(props.bladeburner);
const computedActionTimeCurrent = Math.min( const computedActionTimeCurrent = Math.min(
props.bladeburner.actionTimeCurrent + props.bladeburner.actionTimeOverflow, props.bladeburner.actionTimeCurrent + props.bladeburner.actionTimeOverflow,
props.bladeburner.actionTimeToComplete, props.bladeburner.actionTimeToComplete,
); );
const maxLevel = props.action.level >= props.action.maxLevel;
const actionTime = props.action.getActionTime(props.bladeburner); const actionTime = props.action.getActionTime(props.bladeburner);
const autolevelCheckboxId = `bladeburner-${props.action.name}-autolevel-checkbox`;
function onStart(): void { const actionData = Operations[props.action.name];
props.bladeburner.action.type = ActionTypes.Operation; if (actionData === undefined) {
props.bladeburner.action.name = props.action.name; throw new Error(`Cannot find data for ${props.action.name}`);
props.bladeburner.startAction(props.player, props.bladeburner.action);
setRerender((old) => !old);
}
function onTeam(): void {
const popupId = "bladeburner-operation-set-team-size-popup";
createPopup(popupId, TeamSizePopup, {
bladeburner: props.bladeburner,
action: props.action,
popupId: popupId,
});
}
function increaseLevel(): void {
++props.action.level;
if (isActive) props.bladeburner.startAction(props.player, props.bladeburner.action);
setRerender((old) => !old);
}
function decreaseLevel(): void {
--props.action.level;
if (isActive) props.bladeburner.startAction(props.player, props.bladeburner.action);
setRerender((old) => !old);
}
function onAutolevel(event: React.ChangeEvent<HTMLInputElement>): void {
props.action.autoLevel = event.target.checked;
setRerender((old) => !old);
} }
return ( return (
<> <Paper sx={{ my: 1, p: 1 }}>
<h2 style={{ display: "inline-block" }}>
{isActive ? ( {isActive ? (
<> <>
<Typography>
<CopyableText value={props.action.name} /> (IN PROGRESS - {formatNumber(computedActionTimeCurrent, 0)} /{" "} <CopyableText value={props.action.name} /> (IN PROGRESS - {formatNumber(computedActionTimeCurrent, 0)} /{" "}
{formatNumber(props.bladeburner.actionTimeToComplete, 0)}) {formatNumber(props.bladeburner.actionTimeToComplete, 0)})
</> </Typography>
) : ( <Typography>
<CopyableText value={props.action.name} />
)}
</h2>
{isActive ? (
<p style={{ display: "block" }}>
{createProgressBarText({ {createProgressBarText({
progress: computedActionTimeCurrent / props.bladeburner.actionTimeToComplete, progress: computedActionTimeCurrent / props.bladeburner.actionTimeToComplete,
})} })}
</p> </Typography>
</>
) : ( ) : (
<> <>
<a onClick={onStart} className="a-link-button" style={{ margin: "3px", padding: "3px" }}> <CopyableText value={props.action.name} />
Start <StartButton
</a> bladeburner={props.bladeburner}
<a onClick={onTeam} style={{ margin: "3px", padding: "3px" }} className="a-link-button"> type={ActionTypes.Operation}
Set Team Size (Curr Size: {formatNumber(props.action.teamCount, 0)}) name={props.action.name}
</a> rerender={rerender}
/>
<TeamSizeButton action={props.action} bladeburner={props.bladeburner} />
</> </>
)} )}
<br /> <br />
<br /> <br />
<pre className="tooltip" style={{ display: "inline-block" }}>
<span className="tooltiptext"> <ActionLevel action={props.action} bladeburner={props.bladeburner} isActive={isActive} rerender={rerender} />
{props.action.getSuccessesNeededForNextLevel(BladeburnerConstants.OperationSuccessesPerLevel)} successes
needed for next level
</span>
Level: {props.action.level} / {props.action.maxLevel}
</pre>
<a
onClick={increaseLevel}
style={{ padding: "2px", margin: "2px" }}
className={`tooltip ${maxLevel ? "a-link-button-inactive" : "a-link-button"}`}
>
{isActive && <span className="tooltiptext">WARNING: changing the level will restart the Operation</span>}
</a>
<a
onClick={decreaseLevel}
style={{ padding: "2px", margin: "2px" }}
className={`tooltip ${props.action.level <= 1 ? "a-link-button-inactive" : "a-link-button"}`}
>
{isActive && <span className="tooltiptext">WARNING: changing the level will restart the Operation</span>}
</a>
<br /> <br />
<br /> <br />
<pre style={{ display: "inline-block" }}> <Typography>
<span dangerouslySetInnerHTML={{ __html: props.action.desc }} /> {actionData.desc}
<br /> <br />
<br /> <br />
Estimated success chance: <SuccessChance chance={estimatedSuccessChance} />{" "} <SuccessChance action={props.action} bladeburner={props.bladeburner} />
{props.action.isStealth ? stealthIcon : <></>}
{props.action.isKill ? killIcon : <></>}
<br /> <br />
Time Required: {convertTimeMsToTimeElapsedString(actionTime * 1000)} Time Required: {convertTimeMsToTimeElapsedString(actionTime * 1000)}
<br /> <br />
@ -131,13 +85,9 @@ export function OperationElem(props: IProps): React.ReactElement {
Successes: {props.action.successes} Successes: {props.action.successes}
<br /> <br />
Failures: {props.action.failures} Failures: {props.action.failures}
</pre> </Typography>
<br /> <br />
<label className="tooltip" style={{ color: "white" }} htmlFor={autolevelCheckboxId}> <Autolevel rerender={rerender} action={props.action} />
Autolevel: </Paper>
<span className="tooltiptext">Automatically increase operation level when possible</span>
</label>
<input type="checkbox" id={autolevelCheckboxId} checked={props.action.autoLevel} onChange={onAutolevel} />
</>
); );
} }

@ -14,9 +14,7 @@ export function OperationList(props: IProps): React.ReactElement {
return ( return (
<> <>
{names.map((name: string) => ( {names.map((name: string) => (
<li key={name} className="bladeburner-action"> <OperationElem key={name} bladeburner={props.bladeburner} action={operations[name]} player={props.player} />
<OperationElem bladeburner={props.bladeburner} action={operations[name]} player={props.player} />
</li>
))} ))}
</> </>
); );

@ -2,6 +2,7 @@ import * as React from "react";
import { OperationList } from "./OperationList"; import { OperationList } from "./OperationList";
import { IBladeburner } from "../IBladeburner"; import { IBladeburner } from "../IBladeburner";
import { IPlayer } from "../../PersonObjects/IPlayer"; import { IPlayer } from "../../PersonObjects/IPlayer";
import Typography from "@mui/material/Typography";
interface IProps { interface IProps {
bladeburner: IBladeburner; bladeburner: IBladeburner;
@ -11,7 +12,7 @@ interface IProps {
export function OperationPage(props: IProps): React.ReactElement { export function OperationPage(props: IProps): React.ReactElement {
return ( return (
<> <>
<p style={{ display: "block", margin: "4px", padding: "4px" }}> <Typography>
Carry out operations for the Bladeburner division. Failing an operation will reduce your Bladeburner rank. It Carry out operations for the Bladeburner division. Failing an operation will reduce your Bladeburner rank. It
will also cause you to lose HP, which can lead to hospitalization. In general, operations are harder and more will also cause you to lose HP, which can lead to hospitalization. In general, operations are harder and more
punishing than contracts, but are also more rewarding. punishing than contracts, but are also more rewarding.
@ -27,7 +28,7 @@ export function OperationPage(props: IProps): React.ReactElement {
<br /> <br />
You can unlock higher-level operations by successfully completing them. Higher-level operations are more You can unlock higher-level operations by successfully completing them. Higher-level operations are more
difficult, but grant more rank and experience. difficult, but grant more rank and experience.
</p> </Typography>
<OperationList bladeburner={props.bladeburner} player={props.player} /> <OperationList bladeburner={props.bladeburner} player={props.player} />
</> </>
); );

@ -3,6 +3,13 @@ import { CopyableText } from "../../ui/React/CopyableText";
import { formatNumber } from "../../utils/StringHelperFunctions"; import { formatNumber } from "../../utils/StringHelperFunctions";
import { IBladeburner } from "../IBladeburner"; import { IBladeburner } from "../IBladeburner";
import Typography from "@mui/material/Typography";
import IconButton from "@mui/material/IconButton";
import Box from "@mui/material/Box";
import Paper from "@mui/material/Paper";
import AddIcon from "@mui/icons-material/Add";
import CloseIcon from "@mui/icons-material/Close";
interface IProps { interface IProps {
skill: any; skill: any;
bladeburner: IBladeburner; bladeburner: IBladeburner;
@ -28,26 +35,30 @@ export function SkillElem(props: IProps): React.ReactElement {
} }
return ( return (
<> <Paper sx={{ my: 1, p: 1 }}>
<h2 style={{ display: "inline-block" }}> <Box display="flex" flexDirection="row" alignItems="center">
<Typography>
<CopyableText value={props.skill.name} /> <CopyableText value={props.skill.name} />
</h2> </Typography>
<a {!canLevel || maxLvl ? (
onClick={onClick} <IconButton disabled>
style={{ display: "inline-block", margin: "3px", padding: "3px" }} <CloseIcon />
className={canLevel && !maxLvl ? "a-link-button" : "a-link-button-inactive"} </IconButton>
>
Level
</a>
<br />
<br />
<p style={{ display: "block" }}>Level: {currentLevel}</p>
{maxLvl ? (
<p style={{ color: "red", display: "block" }}>MAX LEVEL</p>
) : ( ) : (
<p style={{ display: "block" }}>Skill Points required: {formatNumber(pointCost, 0)}</p> <IconButton onClick={onClick}>
<AddIcon />
</IconButton>
)} )}
<p style={{ display: "inline-block" }} dangerouslySetInnerHTML={{ __html: props.skill.desc }} /> </Box>
</> <br />
<br />
<Typography>Level: {currentLevel}</Typography>
{maxLvl ? (
<Typography>MAX LEVEL</Typography>
) : (
<Typography>Skill Points required: {formatNumber(pointCost, 0)}</Typography>
)}
<Typography>{props.skill.desc}</Typography>
</Paper>
); );
} }

@ -12,9 +12,7 @@ export function SkillList(props: IProps): React.ReactElement {
return ( return (
<> <>
{Object.keys(Skills).map((skill: string) => ( {Object.keys(Skills).map((skill: string) => (
<li key={skill} className="bladeburner-action"> <SkillElem key={skill} bladeburner={props.bladeburner} skill={Skills[skill]} onUpgrade={props.onUpgrade} />
<SkillElem bladeburner={props.bladeburner} skill={Skills[skill]} onUpgrade={props.onUpgrade} />
</li>
))} ))}
</> </>
); );

@ -3,7 +3,8 @@ import { SkillList } from "./SkillList";
import { BladeburnerConstants } from "../data/Constants"; import { BladeburnerConstants } from "../data/Constants";
import { formatNumber } from "../../utils/StringHelperFunctions"; import { formatNumber } from "../../utils/StringHelperFunctions";
import { IBladeburner } from "../IBladeburner"; import { IBladeburner } from "../IBladeburner";
import Typography from "@mui/material/Typography";
import { BitNodeMultipliers } from "../../BitNode/BitNodeMultipliers";
interface IProps { interface IProps {
bladeburner: IBladeburner; bladeburner: IBladeburner;
} }
@ -18,45 +19,48 @@ export function SkillPage(props: IProps): React.ReactElement {
return ( return (
<> <>
<p> <Typography>
<strong>Skill Points: {formatNumber(props.bladeburner.skillPoints, 0)}</strong> <strong>Skill Points: {formatNumber(props.bladeburner.skillPoints, 0)}</strong>
</p> </Typography>
<p> <Typography>
You will gain one skill point every {BladeburnerConstants.RanksPerSkillPoint} ranks. You will gain one skill point every{" "}
{BladeburnerConstants.RanksPerSkillPoint * BitNodeMultipliers.BladeburnerSkillCost} ranks.
<br /> <br />
<br /> <br />
Note that when upgrading a skill, the benefit for that skill is additive. However, the effects of different Note that when upgrading a skill, the benefit for that skill is additive. However, the effects of different
skills with each other is multiplicative. skills with each other is multiplicative.
<br /> <br />
</p> </Typography>
<br /> <br />
{valid(mults["successChanceAll"]) && <p>Total Success Chance: x{formatNumber(mults["successChanceAll"], 3)}</p>} {valid(mults["successChanceAll"]) && (
<Typography>Total Success Chance: x{formatNumber(mults["successChanceAll"], 3)}</Typography>
)}
{valid(mults["successChanceStealth"]) && ( {valid(mults["successChanceStealth"]) && (
<p>Stealth Success Chance: x{formatNumber(mults["successChanceStealth"], 3)}</p> <Typography>Stealth Success Chance: x{formatNumber(mults["successChanceStealth"], 3)}</Typography>
)} )}
{valid(mults["successChanceKill"]) && ( {valid(mults["successChanceKill"]) && (
<p>Retirement Success Chance: x{formatNumber(mults["successChanceKill"], 3)}</p> <Typography>Retirement Success Chance: x{formatNumber(mults["successChanceKill"], 3)}</Typography>
)} )}
{valid(mults["successChanceContract"]) && ( {valid(mults["successChanceContract"]) && (
<p>Contract Success Chance: x{formatNumber(mults["successChanceContract"], 3)}</p> <Typography>Contract Success Chance: x{formatNumber(mults["successChanceContract"], 3)}</Typography>
)} )}
{valid(mults["successChanceOperation"]) && ( {valid(mults["successChanceOperation"]) && (
<p>Operation Success Chance: x{formatNumber(mults["successChanceOperation"], 3)}</p> <Typography>Operation Success Chance: x{formatNumber(mults["successChanceOperation"], 3)}</Typography>
)} )}
{valid(mults["successChanceEstimate"]) && ( {valid(mults["successChanceEstimate"]) && (
<p>Synthoid Data Estimate: x{formatNumber(mults["successChanceEstimate"], 3)}</p> <Typography>Synthoid Data Estimate: x{formatNumber(mults["successChanceEstimate"], 3)}</Typography>
)} )}
{valid(mults["actionTime"]) && <p>Action Time: x{formatNumber(mults["actionTime"], 3)}</p>} {valid(mults["actionTime"]) && <Typography>Action Time: x{formatNumber(mults["actionTime"], 3)}</Typography>}
{valid(mults["effHack"]) && <p>Hacking Skill: x{formatNumber(mults["effHack"], 3)}</p>} {valid(mults["effHack"]) && <Typography>Hacking Skill: x{formatNumber(mults["effHack"], 3)}</Typography>}
{valid(mults["effStr"]) && <p>Strength: x{formatNumber(mults["effStr"], 3)}</p>} {valid(mults["effStr"]) && <Typography>Strength: x{formatNumber(mults["effStr"], 3)}</Typography>}
{valid(mults["effDef"]) && <p>Defense: x{formatNumber(mults["effDef"], 3)}</p>} {valid(mults["effDef"]) && <Typography>Defense: x{formatNumber(mults["effDef"], 3)}</Typography>}
{valid(mults["effDex"]) && <p>Dexterity: x{formatNumber(mults["effDex"], 3)}</p>} {valid(mults["effDex"]) && <Typography>Dexterity: x{formatNumber(mults["effDex"], 3)}</Typography>}
{valid(mults["effAgi"]) && <p>Agility: x{formatNumber(mults["effAgi"], 3)}</p>} {valid(mults["effAgi"]) && <Typography>Agility: x{formatNumber(mults["effAgi"], 3)}</Typography>}
{valid(mults["effCha"]) && <p>Charisma: x{formatNumber(mults["effCha"], 3)}</p>} {valid(mults["effCha"]) && <Typography>Charisma: x{formatNumber(mults["effCha"], 3)}</Typography>}
{valid(mults["effInt"]) && <p>Intelligence: x{formatNumber(mults["effInt"], 3)}</p>} {valid(mults["effInt"]) && <Typography>Intelligence: x{formatNumber(mults["effInt"], 3)}</Typography>}
{valid(mults["stamina"]) && <p>Stamina: x{formatNumber(mults["stamina"], 3)}</p>} {valid(mults["stamina"]) && <Typography>Stamina: x{formatNumber(mults["stamina"], 3)}</Typography>}
{valid(mults["money"]) && <p>Contract Money: x{formatNumber(mults["money"], 3)}</p>} {valid(mults["money"]) && <Typography>Contract Money: x{formatNumber(mults["money"], 3)}</Typography>}
{valid(mults["expGain"]) && <p>Exp Gain: x{formatNumber(mults["expGain"], 3)}</p>} {valid(mults["expGain"]) && <Typography>Exp Gain: x{formatNumber(mults["expGain"], 3)}</Typography>}
<br /> <br />
<SkillList bladeburner={props.bladeburner} onUpgrade={() => setRerender((old) => !old)} /> <SkillList bladeburner={props.bladeburner} onUpgrade={() => setRerender((old) => !old)} />
</> </>

@ -0,0 +1,44 @@
import React from "react";
import { IBladeburner } from "../IBladeburner";
import { BlackOperation } from "../BlackOperation";
import { use } from "../../ui/Context";
import Button from "@mui/material/Button";
interface IProps {
bladeburner: IBladeburner;
type: number;
name: string;
rerender: () => void;
}
export function StartButton(props: IProps): React.ReactElement {
const player = use.Player();
const action = props.bladeburner.getActionObject({ name: props.name, type: props.type });
if (action == null) {
throw new Error("Failed to get Operation Object for: " + props.name);
}
let disabled = false;
if (action.count < 1) {
disabled = true;
}
if (props.name === "Raid" && props.bladeburner.getCurrentCity().comms === 0) {
disabled = true;
}
if (action instanceof BlackOperation && props.bladeburner.rank < action.reqdRank) {
disabled = true;
}
function onStart(): void {
if (disabled) return;
props.bladeburner.action.type = props.type;
props.bladeburner.action.name = props.name;
props.bladeburner.startAction(player, props.bladeburner.action);
props.rerender();
}
return (
<Button sx={{ mx: 1 }} disabled={disabled} onClick={onStart}>
Start
</Button>
);
}

@ -5,14 +5,17 @@ import { IPlayer } from "../../PersonObjects/IPlayer";
import { Money } from "../../ui/React/Money"; import { Money } from "../../ui/React/Money";
import { StatsTable } from "../../ui/React/StatsTable"; import { StatsTable } from "../../ui/React/StatsTable";
import { numeralWrapper } from "../../ui/numeralFormat"; import { numeralWrapper } from "../../ui/numeralFormat";
import { dialogBoxCreate } from "../../ui/React/DialogBox";
import { createPopup } from "../../ui/React/createPopup";
import { Factions } from "../../Faction/Factions"; import { Factions } from "../../Faction/Factions";
import { IRouter } from "../../ui/Router"; import { IRouter } from "../../ui/Router";
import { joinFaction } from "../../Faction/FactionHelpers"; import { joinFaction } from "../../Faction/FactionHelpers";
import { IBladeburner } from "../IBladeburner"; import { IBladeburner } from "../IBladeburner";
import { TravelPopup } from "./TravelPopup"; import { TravelModal } from "./TravelModal";
import Typography from "@mui/material/Typography";
import Tooltip from "@mui/material/Tooltip";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Paper from "@mui/material/Paper";
interface IProps { interface IProps {
bladeburner: IBladeburner; bladeburner: IBladeburner;
@ -21,131 +24,146 @@ interface IProps {
} }
export function Stats(props: IProps): React.ReactElement { export function Stats(props: IProps): React.ReactElement {
const [travelOpen, setTravelOpen] = useState(false);
const setRerender = useState(false)[1]; const setRerender = useState(false)[1];
const inFaction = props.bladeburner.rank >= BladeburnerConstants.RankNeededForFaction;
useEffect(() => { useEffect(() => {
const id = setInterval(() => setRerender((old) => !old), 1000); const id = setInterval(() => setRerender((old) => !old), 1000);
return () => clearInterval(id); return () => clearInterval(id);
}, []); }, []);
function openStaminaHelp(): void {
dialogBoxCreate(
"Performing actions will use up your stamina.<br><br>" +
"Your max stamina is determined primarily by your agility stat.<br><br>" +
"Your stamina gain rate is determined by both your agility and your " +
"max stamina. Higher max stamina leads to a higher gain rate.<br><br>" +
"Once your " +
"stamina falls below 50% of its max value, it begins to negatively " +
"affect the success rate of your contracts/operations. This penalty " +
"is shown in the overview panel. If the penalty is 15%, then this means " +
"your success rate would be multipled by 85% (100 - 15).<br><br>" +
"Your max stamina and stamina gain rate can also be increased by " +
"training, or through skills and Augmentation upgrades.",
);
}
function openPopulationHelp(): void {
dialogBoxCreate(
"The success rate of your contracts/operations depends on " +
"the population of Synthoids in your current city. " +
"The success rate that is shown to you is only an estimate, " +
"and it is based on your Synthoid population estimate.<br><br>" +
"Therefore, it is important that this Synthoid population estimate " +
"is accurate so that you have a better idea of your " +
"success rate for contracts/operations. Certain " +
"actions will increase the accuracy of your population " +
"estimate.<br><br>" +
"The Synthoid populations of cities can change due to your " +
"actions or random events. If random events occur, they will " +
"be logged in the Bladeburner Console.",
);
}
function openTravel(): void {
const popupId = "bladeburner-travel-popup";
createPopup(popupId, TravelPopup, {
bladeburner: props.bladeburner,
popupId: popupId,
});
}
function openFaction(): void { function openFaction(): void {
if (!inFaction) return;
const faction = Factions["Bladeburners"]; const faction = Factions["Bladeburners"];
if (faction.isMember) { if (!faction.isMember) {
props.router.toFaction(faction);
} else {
if (props.bladeburner.rank >= BladeburnerConstants.RankNeededForFaction) {
joinFaction(faction); joinFaction(faction);
dialogBoxCreate("Congratulations! You were accepted into the Bladeburners faction");
} else {
dialogBoxCreate("You need a rank of 25 to join the Bladeburners Faction!");
}
} }
props.router.toFaction(faction);
} }
return ( return (
<> <Paper sx={{ p: 1 }}>
<p className="tooltip" style={{ display: "inline-block" }}> <Box display="flex">
Rank: {formatNumber(props.bladeburner.rank, 2)} <Tooltip disableInteractive title={<Typography>Your rank within the Bladeburner division.</Typography>}>
<span className="tooltiptext">Your rank within the Bladeburner division.</span> <Typography>Rank: {formatNumber(props.bladeburner.rank, 2)}</Typography>
</p> </Tooltip>
</Box>
<br /> <br />
<p> <Box display="flex">
<Tooltip
disableInteractive
title={
<Typography>
Performing actions will use up your stamina.
<br />
<br />
Your max stamina is determined primarily by your agility stat.
<br />
<br />
Your stamina gain rate is determined by both your agility and your max stamina. Higher max stamina leads
to a higher gain rate.
<br />
<br />
Once your stamina falls below 50% of its max value, it begins to negatively affect the success rate of
your contracts/operations. This penalty is shown in the overview panel. If the penalty is 15%, then this
means your success rate would be multipled by 85% (100 - 15).
<br />
<br />
Your max stamina and stamina gain rate can also be increased by training, or through skills and
Augmentation upgrades.
</Typography>
}
>
<Typography>
Stamina: {formatNumber(props.bladeburner.stamina, 3)} / {formatNumber(props.bladeburner.maxStamina, 3)} Stamina: {formatNumber(props.bladeburner.stamina, 3)} / {formatNumber(props.bladeburner.maxStamina, 3)}
</p> </Typography>
<div className="help-tip" onClick={openStaminaHelp}> </Tooltip>
? </Box>
</div>
<br /> <br />
<p>Stamina Penalty: {formatNumber((1 - props.bladeburner.calculateStaminaPenalty()) * 100, 1)}%</p> <Typography>
Stamina Penalty: {formatNumber((1 - props.bladeburner.calculateStaminaPenalty()) * 100, 1)}%
</Typography>
<br /> <br />
<p>Team Size: {formatNumber(props.bladeburner.teamSize, 0)}</p> <Typography>Team Size: {formatNumber(props.bladeburner.teamSize, 0)}</Typography>
<p>Team Members Lost: {formatNumber(props.bladeburner.teamLost, 0)}</p> <Typography>Team Members Lost: {formatNumber(props.bladeburner.teamLost, 0)}</Typography>
<br /> <br />
<p>Num Times Hospitalized: {props.bladeburner.numHosp}</p> <Typography>Num Times Hospitalized: {props.bladeburner.numHosp}</Typography>
<p> <Typography>
Money Lost From Hospitalizations: <Money money={props.bladeburner.moneyLost} /> Money Lost From Hospitalizations: <Money money={props.bladeburner.moneyLost} />
</p> </Typography>
<br /> <br />
<p>Current City: {props.bladeburner.city}</p> <Typography>Current City: {props.bladeburner.city}</Typography>
<p className="tooltip" style={{ display: "inline-block" }}> <Box display="flex">
<Tooltip
disableInteractive
title={
<Typography>
This is your Bladeburner division's estimate of how many Synthoids exist in your current city. An accurate
population count increases success rate estimates.
</Typography>
}
>
<Typography>
Est. Synthoid Population: {numeralWrapper.formatPopulation(props.bladeburner.getCurrentCity().popEst)} Est. Synthoid Population: {numeralWrapper.formatPopulation(props.bladeburner.getCurrentCity().popEst)}
<span className="tooltiptext"> </Typography>
This is your Bladeburner division's estimate of how many Synthoids exist in your current city. </Tooltip>
</span> </Box>
</p>
<div className="help-tip" onClick={openPopulationHelp}>
?
</div>
<br /> <br />
<p className="tooltip" style={{ display: "inline-block" }}> <Box display="flex">
Est. Synthoid Communities: {formatNumber(props.bladeburner.getCurrentCity().comms, 0)} <Tooltip
<span className="tooltiptext"> disableInteractive
title={
<Typography>
This is your Bladeburner divison's estimate of how many Synthoid communities exist in your current city. This is your Bladeburner divison's estimate of how many Synthoid communities exist in your current city.
</span> </Typography>
</p> }
>
<Typography>
Est. Synthoid Communities: {formatNumber(props.bladeburner.getCurrentCity().comms, 0)}
</Typography>
</Tooltip>
</Box>
<br /> <br />
<p className="tooltip" style={{ display: "inline-block" }}> <Box display="flex">
City Chaos: {formatNumber(props.bladeburner.getCurrentCity().chaos)} <Tooltip
<span className="tooltiptext"> disableInteractive
The city's chaos level due to tensions and conflicts between humans and Synthoids. Having too high of a chaos title={
level can make contracts and operations harder. <Typography>
</span> The city's chaos level due to tensions and conflicts between humans and Synthoids. Having too high of a
</p> chaos level can make contracts and operations harder.
</Typography>
}
>
<Typography>City Chaos: {formatNumber(props.bladeburner.getCurrentCity().chaos)}</Typography>
</Tooltip>
</Box>
<br /> <br />
<br /> {(props.bladeburner.storedCycles / BladeburnerConstants.CyclesPerSecond) * 1000 > 15000 && (
<p className="tooltip" style={{ display: "inline-block" }}> <>
<Box display="flex">
<Tooltip
disableInteractive
title={
<Typography>
You gain bonus time while offline or when the game is inactive (e.g. when the tab is throttled by
browser). Bonus time makes the Bladeburner mechanic progress faster, up to 5x the normal speed.
</Typography>
}
>
<Typography>
Bonus time:{" "} Bonus time:{" "}
{convertTimeMsToTimeElapsedString( {convertTimeMsToTimeElapsedString(
(props.bladeburner.storedCycles / BladeburnerConstants.CyclesPerSecond) * 1000, (props.bladeburner.storedCycles / BladeburnerConstants.CyclesPerSecond) * 1000,
)} )}
</Typography>
</Tooltip>
</Box>
<br /> <br />
<span className="tooltiptext"> </>
You gain bonus time while offline or when the game is inactive (e.g. when the tab is throttled by browser). )}
Bonus time makes the Bladeburner mechanic progress faster, up to 5x the normal speed. <Typography>Skill Points: {formatNumber(props.bladeburner.skillPoints, 0)}</Typography>
</span>
</p>
<p>Skill Points: {formatNumber(props.bladeburner.skillPoints, 0)}</p>
<br /> <br />
<StatsTable <StatsTable
rows={[ rows={[
@ -156,17 +174,15 @@ export function Stats(props: IProps): React.ReactElement {
]} ]}
/> />
<br /> <br />
<a onClick={openTravel} className="a-link-button" style={{ display: "inline-block" }}> <Button onClick={() => setTravelOpen(true)}>Travel</Button>
Travel <Tooltip title={!inFaction ? <Typography>Rank 25 required.</Typography> : ""}>
</a> <span>
<a onClick={openFaction} className="a-link-button tooltip" style={{ display: "inline-block" }}> <Button disabled={!inFaction} onClick={openFaction}>
<span className="tooltiptext">
Apply to the Bladeburner Faction, or go to the faction page if you are already a member
</span>
Faction Faction
</a> </Button>
<br /> </span>
<br /> </Tooltip>
</> <TravelModal open={travelOpen} onClose={() => setTravelOpen(false)} bladeburner={props.bladeburner} />
</Paper>
); );
} }

@ -0,0 +1,13 @@
import React from "react";
import { stealthIcon } from "../data/Icons";
import Typography from "@mui/material/Typography";
import Tooltip from "@mui/material/Tooltip";
export function StealthIcon(): React.ReactElement {
return (
<Tooltip disableInteractive title={<Typography>This action involves stealth</Typography>}>
{stealthIcon}
</Tooltip>
);
}

@ -1,18 +1,33 @@
import React from "react"; import React from "react";
import { formatNumber } from "../../utils/StringHelperFunctions"; import { formatNumber } from "../../utils/StringHelperFunctions";
import { StealthIcon } from "./StealthIcon";
import { KillIcon } from "./KillIcon";
import { IAction } from "../IAction";
import { IBladeburner } from "../IBladeburner";
interface IProps { interface IProps {
chance: number[]; bladeburner: IBladeburner;
action: IAction;
} }
export function SuccessChance(props: IProps): React.ReactElement { export function SuccessChance(props: IProps): React.ReactElement {
if (props.chance[0] === props.chance[1]) { const estimatedSuccessChance = props.action.getEstSuccessChance(props.bladeburner);
return <>{formatNumber(props.chance[0] * 100, 1)}%</>;
let chance = <></>;
if (estimatedSuccessChance[0] === estimatedSuccessChance[1]) {
chance = <>{formatNumber(estimatedSuccessChance[0] * 100, 1)}%</>;
} else {
chance = (
<>
{formatNumber(estimatedSuccessChance[0] * 100, 1)}% ~ {formatNumber(estimatedSuccessChance[1] * 100, 1)}%
</>
);
} }
return ( return (
<> <>
{formatNumber(props.chance[0] * 100, 1)}% ~ {formatNumber(props.chance[1] * 100, 1)}% Estimated success chance: {chance} {props.action.isStealth ? <StealthIcon /> : <></>}
{props.action.isKill ? <KillIcon /> : <></>}
</> </>
); );
} }

@ -0,0 +1,22 @@
import React, { useState } from "react";
import { Operation } from "../Operation";
import { IBladeburner } from "../IBladeburner";
import { TeamSizeModal } from "./TeamSizeModal";
import { formatNumber } from "../../utils/StringHelperFunctions";
import Button from "@mui/material/Button";
interface IProps {
action: Operation;
bladeburner: IBladeburner;
}
export function TeamSizeButton(props: IProps): React.ReactElement {
const [open, setOpen] = useState(false);
return (
<>
<Button disabled={props.bladeburner.teamSize === 0} onClick={() => setOpen(true)}>
Set Team Size (Curr Size: {formatNumber(props.action.teamCount, 0)})
</Button>
<TeamSizeModal open={open} onClose={() => setOpen(false)} action={props.action} bladeburner={props.bladeburner} />
</>
);
}

@ -1,16 +1,20 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { removePopup } from "../../ui/React/createPopup";
import { dialogBoxCreate } from "../../ui/React/DialogBox"; import { dialogBoxCreate } from "../../ui/React/DialogBox";
import { Modal } from "../../ui/React/Modal";
import { Action } from "../Action"; import { Action } from "../Action";
import { IBladeburner } from "../IBladeburner"; import { IBladeburner } from "../IBladeburner";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField";
interface IProps { interface IProps {
bladeburner: IBladeburner; bladeburner: IBladeburner;
action: Action; action: Action;
popupId: string; open: boolean;
onClose: () => void;
} }
export function TeamSizePopup(props: IProps): React.ReactElement { export function TeamSizeModal(props: IProps): React.ReactElement {
const [teamSize, setTeamSize] = useState<number | undefined>(); const [teamSize, setTeamSize] = useState<number | undefined>();
function confirmTeamSize(): void { function confirmTeamSize(): void {
@ -21,25 +25,32 @@ export function TeamSizePopup(props: IProps): React.ReactElement {
} else { } else {
props.action.teamCount = num; props.action.teamCount = num;
} }
removePopup(props.popupId); props.onClose();
}
function onTeamSize(event: React.ChangeEvent<HTMLInputElement>): void {
const x = parseFloat(event.target.value);
if (x > props.bladeburner.teamSize) setTeamSize(props.bladeburner.teamSize);
else setTeamSize(x);
} }
return ( return (
<> <Modal open={props.open} onClose={props.onClose}>
<p> <Typography>
Enter the amount of team members you would like to take on this Op. If you do not have the specified number of Enter the amount of team members you would like to take on this Op. If you do not have the specified number of
team members, then as many as possible will be used. Note that team members may be lost during operations. team members, then as many as possible will be used. Note that team members may be lost during operations.
</p> </Typography>
<input <TextField
autoFocus autoFocus
variant="standard"
type="number" type="number"
placeholder="Team size" placeholder="Team size"
className="text-input" value={teamSize}
onChange={(event) => setTeamSize(parseFloat(event.target.value))} onChange={onTeamSize}
/> />
<a className="a-link-button" onClick={confirmTeamSize}> <Button sx={{ mx: 2 }} onClick={confirmTeamSize}>
Confirm Confirm
</a> </Button>
</> </Modal>
); );
} }

@ -0,0 +1,41 @@
import React from "react";
import { IBladeburner } from "../IBladeburner";
import { WorldMap } from "../../ui/React/WorldMap";
import { Modal } from "../../ui/React/Modal";
import { CityName } from "../../Locations/data/CityNames";
import { Settings } from "../../Settings/Settings";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
interface IProps {
bladeburner: IBladeburner;
open: boolean;
onClose: () => void;
}
export function TravelModal(props: IProps): React.ReactElement {
function travel(city: string): void {
props.bladeburner.city = city;
props.onClose();
}
return (
<Modal open={props.open} onClose={props.onClose}>
<>
<Typography>
Travel to a different city for your Bladeburner activities. This does not cost any money. The city you are in
for your Bladeburner duties does not affect your location in the game otherwise.
</Typography>
{Settings.DisableASCIIArt ? (
Object.values(CityName).map((city: CityName) => (
<Button key={city} onClick={() => travel(city)}>
{city}
</Button>
))
) : (
<WorldMap currentCity={props.bladeburner.city as CityName} onTravel={(city: CityName) => travel(city)} />
)}
</>
</Modal>
);
}

@ -1,36 +0,0 @@
import React from "react";
import { removePopup } from "../../ui/React/createPopup";
import { IBladeburner } from "../IBladeburner";
import { WorldMap } from "../../ui/React/WorldMap";
import { CityName } from "../../Locations/data/CityNames";
import { Settings } from "../../Settings/Settings";
interface IProps {
bladeburner: IBladeburner;
popupId: string;
}
export function TravelPopup(props: IProps): React.ReactElement {
function travel(city: string): void {
props.bladeburner.city = city;
removePopup(props.popupId);
}
return (
<>
<p>
Travel to a different city for your Bladeburner activities. This does not cost any money. The city you are in
for your Bladeburner duties does not affect your location in the game otherwise.
</p>
{Settings.DisableASCIIArt ? (
Object.values(CityName).map((city: CityName) => (
<button key={city} className="std-button" onClick={() => travel(city)}>
{city}
</button>
))
) : (
<WorldMap currentCity={props.bladeburner.city as CityName} onTravel={(city: CityName) => travel(city)} />
)}
</>
);
}

@ -3,7 +3,7 @@
* This is the component for displaying a single faction's UI, not the list of all * This is the component for displaying a single faction's UI, not the list of all
* accessible factions * accessible factions
*/ */
import React, { useState } from "react"; import React, { useState, useEffect } from "react";
import { AugmentationsPage } from "./AugmentationsPage"; import { AugmentationsPage } from "./AugmentationsPage";
import { DonateOption } from "./DonateOption"; import { DonateOption } from "./DonateOption";
@ -71,6 +71,12 @@ export function FactionRoot(props: IProps): React.ReactElement {
function rerender(): void { function rerender(): void {
setRerender((old) => !old); setRerender((old) => !old);
} }
useEffect(() => {
const id = setInterval(rerender, 200);
return () => clearInterval(id);
}, []);
const faction = props.faction; const faction = props.faction;
const player = use.Player(); const player = use.Player();

@ -1,4 +1,5 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import Typography from "@mui/material/Typography";
interface IProps { interface IProps {
text: string; text: string;
@ -34,10 +35,10 @@ export function CinematicLine(props: IProps): React.ReactElement {
}); });
return ( return (
<> <>
<pre> <Typography>
{props.text.slice(0, length)} {props.text.slice(0, length)}
{!done && <span>&#9608;</span>} {!done && <span>&#9608;</span>}
</pre> </Typography>
</> </>
); );
} }

@ -1,6 +1,8 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { CinematicLine } from "./CinematicLine"; import { CinematicLine } from "./CinematicLine";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
interface IProps { interface IProps {
lines: string[]; lines: string[];
@ -24,14 +26,10 @@ export function CinematicText(props: IProps): React.ReactElement {
return ( return (
<div> <div>
{props.lines.slice(0, i).map((line, i) => ( {props.lines.slice(0, i).map((line, i) => (
<pre key={i}>{line}</pre> <Typography key={i}>{line}</Typography>
))} ))}
{props.lines.length > i && <CinematicLine key={i} text={props.lines[i]} onDone={advance} />} {props.lines.length > i && <CinematicLine key={i} text={props.lines[i]} onDone={advance} />}
{!props.auto && props.onDone && done && ( {!props.auto && props.onDone && done && <Button onClick={props.onDone}>Continue ...</Button>}
<button className="std-button" onClick={props.onDone}>
Continue ...
</button>
)}
</div> </div>
); );
} }

@ -261,6 +261,16 @@ export function refreshTheme(): void {
}, },
}, },
}, },
MuiTab: {
styleOverrides: {
textColorPrimary: {
color: Settings.theme.secondary,
"&.Mui-selected": {
color: Settings.theme.primary,
},
},
},
},
}, },
}); });
} }