BLADEBURNER: Typesafety / refactoring (#1154)

This commit is contained in:
Snarling 2024-03-28 21:52:37 -04:00 committed by GitHub
parent 5f1a94a9d3
commit 6669c4da6a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
79 changed files with 3876 additions and 5462 deletions

@ -1,14 +1,13 @@
import { import {
AugmentationName, AugmentationName,
BlackOperationName, BladeSkillName,
CityName, CityName,
CompletedProgramName, CompletedProgramName,
CorpUnlockName, CorpUnlockName,
FactionName, FactionName,
IndustryType, IndustryType,
} from "@enums"; } from "@enums";
import { SkillNames } from "../Bladeburner/data/SkillNames"; import { Skills } from "../Bladeburner/data/Skills";
import { Skills } from "../Bladeburner/Skills";
import { CONSTANTS } from "../Constants"; import { CONSTANTS } from "../Constants";
import { Exploit } from "../Exploits/Exploit"; import { Exploit } from "../Exploits/Exploit";
import { Factions } from "../Faction/Factions"; import { Factions } from "../Faction/Factions";
@ -31,6 +30,7 @@ import { workerScripts } from "../Netscript/WorkerScripts";
import { getRecordValues } from "../Types/Record"; import { getRecordValues } from "../Types/Record";
import { ServerConstants } from "../Server/data/Constants"; import { ServerConstants } from "../Server/data/Constants";
import { blackOpsArray } from "../Bladeburner/data/BlackOperations";
// Unable to correctly cast the JSON data into AchievementDataJson type otherwise... // Unable to correctly cast the JSON data into AchievementDataJson type otherwise...
const achievementData = (<AchievementDataJson>(<unknown>data)).achievements; const achievementData = (<AchievementDataJson>(<unknown>data)).achievements;
@ -65,7 +65,7 @@ function bitNodeFinishedState(): boolean {
const wd = GetServer(SpecialServers.WorldDaemon); const wd = GetServer(SpecialServers.WorldDaemon);
if (!(wd instanceof Server)) return false; if (!(wd instanceof Server)) return false;
if (wd.backdoorInstalled) return true; if (wd.backdoorInstalled) return true;
return Player.bladeburner !== null && BlackOperationName.OperationDaedalus in Player.bladeburner.blackops; return Player.bladeburner !== null && Player.bladeburner.numBlackOpsComplete >= blackOpsArray.length;
} }
function hasAccessToSF(bn: number): boolean { function hasAccessToSF(bn: number): boolean {
@ -432,8 +432,7 @@ export const achievements: Record<string, Achievement> = {
Icon: "BLADEOVERCLOCK", Icon: "BLADEOVERCLOCK",
Visible: () => hasAccessToSF(6), Visible: () => hasAccessToSF(6),
Condition: () => Condition: () =>
Player.bladeburner !== null && Player.bladeburner?.getSkillLevel(BladeSkillName.overclock) === Skills[BladeSkillName.overclock].maxLvl,
Player.bladeburner.skills[SkillNames.Overclock] === Skills[SkillNames.Overclock].maxLvl,
}, },
BLADEBURNER_UNSPENT_100000: { BLADEBURNER_UNSPENT_100000: {
...achievementData.BLADEBURNER_UNSPENT_100000, ...achievementData.BLADEBURNER_UNSPENT_100000,

@ -1,306 +0,0 @@
import { getRandomInt } from "../utils/helpers/getRandomInt";
import { addOffset } from "../utils/helpers/addOffset";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
import { BladeburnerConstants } from "./data/Constants";
import { Bladeburner } from "./Bladeburner";
import { Person } from "../PersonObjects/Person";
import { calculateIntelligenceBonus } from "../PersonObjects/formulas/intelligence";
interface ISuccessChanceParams {
est: boolean;
}
class StatsMultiplier {
[key: string]: number;
hack = 0;
str = 0;
def = 0;
dex = 0;
agi = 0;
cha = 0;
int = 0;
}
export interface IActionParams {
name?: string;
level?: number;
maxLevel?: number;
autoLevel?: boolean;
baseDifficulty?: number;
difficultyFac?: number;
rewardFac?: number;
successes?: number;
failures?: number;
rankGain?: number;
rankLoss?: number;
hpLoss?: number;
hpLost?: number;
isStealth?: boolean;
isKill?: boolean;
count?: number;
weights?: StatsMultiplier;
decays?: StatsMultiplier;
teamCount?: number;
}
export class Action {
name = "";
// Difficulty scales with level. See getDifficulty() method
level = 1;
maxLevel = 1;
autoLevel = true;
baseDifficulty = 100;
difficultyFac = 1.01;
// Rank increase/decrease is affected by this exponent
rewardFac = 1.02;
successes = 0;
failures = 0;
// All of these scale with level/difficulty
rankGain = 0;
rankLoss = 0;
hpLoss = 0;
hpLost = 0;
// Action Category. Current categories are stealth and kill
isStealth = false;
isKill = false;
/**
* Number of this contract remaining, and its growth rate
* Growth rate is an integer and the count will increase by that integer every "cycle"
*/
count: number = getRandomInt(1e3, 25e3);
// Weighting of each stat in determining action success rate
weights: StatsMultiplier = {
hack: 1 / 7,
str: 1 / 7,
def: 1 / 7,
dex: 1 / 7,
agi: 1 / 7,
cha: 1 / 7,
int: 1 / 7,
};
// Diminishing returns of stats (stat ^ decay where 0 <= decay <= 1)
decays: StatsMultiplier = {
hack: 0.9,
str: 0.9,
def: 0.9,
dex: 0.9,
agi: 0.9,
cha: 0.9,
int: 0.9,
};
teamCount = 0;
// Base Class for Contracts, Operations, and BlackOps
constructor(params: IActionParams | null = null) {
// | null = null
if (params && params.name) this.name = params.name;
if (params && params.baseDifficulty) this.baseDifficulty = addOffset(params.baseDifficulty, 10);
if (params && params.difficultyFac) this.difficultyFac = params.difficultyFac;
if (params && params.rewardFac) this.rewardFac = params.rewardFac;
if (params && params.rankGain) this.rankGain = params.rankGain;
if (params && params.rankLoss) this.rankLoss = params.rankLoss;
if (params && params.hpLoss) this.hpLoss = params.hpLoss;
if (params && params.isStealth) this.isStealth = params.isStealth;
if (params && params.isKill) this.isKill = params.isKill;
if (params && params.count) this.count = params.count;
if (params && params.weights) this.weights = params.weights;
if (params && params.decays) this.decays = params.decays;
// Check to make sure weights are summed properly
let sum = 0;
for (const weight of Object.keys(this.weights)) {
if (Object.hasOwn(this.weights, weight)) {
sum += this.weights[weight];
}
}
if (sum - 1 >= 10 * Number.EPSILON) {
throw new Error(
"Invalid weights when constructing Action " +
this.name +
". The weights should sum up to 1. They sum up to :" +
1,
);
}
for (const decay of Object.keys(this.decays)) {
if (Object.hasOwn(this.decays, decay)) {
if (this.decays[decay] > 1) {
throw new Error(`Invalid decays when constructing Action ${this.name}. Decay value cannot be greater than 1`);
}
}
}
}
getDifficulty(): number {
const difficulty = this.baseDifficulty * Math.pow(this.difficultyFac, this.level - 1);
if (isNaN(difficulty)) {
throw new Error("Calculated NaN in Action.getDifficulty()");
}
return difficulty;
}
/**
* Tests for success. Should be called when an action has completed
* @param inst {Bladeburner} - Bladeburner instance
*/
attempt(inst: Bladeburner, person: Person): boolean {
return Math.random() < this.getSuccessChance(inst, person);
}
// To be implemented by subtypes
getActionTimePenalty(): number {
return 1;
}
getActionTime(inst: Bladeburner, person: Person): number {
const difficulty = this.getDifficulty();
let baseTime = difficulty / BladeburnerConstants.DifficultyToTimeFactor;
const skillFac = inst.skillMultipliers.actionTime; // Always < 1
const effAgility = person.skills.agility * inst.skillMultipliers.effAgi;
const effDexterity = person.skills.dexterity * inst.skillMultipliers.effDex;
const statFac =
0.5 *
(Math.pow(effAgility, BladeburnerConstants.EffAgiExponentialFactor) +
Math.pow(effDexterity, BladeburnerConstants.EffDexExponentialFactor) +
effAgility / BladeburnerConstants.EffAgiLinearFactor +
effDexterity / BladeburnerConstants.EffDexLinearFactor); // Always > 1
baseTime = Math.max(1, (baseTime * skillFac) / statFac);
return Math.ceil(baseTime * this.getActionTimePenalty());
}
// Subtypes of Action implement these differently
getTeamSuccessBonus(__inst: Bladeburner): number {
return 1;
}
getActionTypeSkillSuccessBonus(__inst: Bladeburner): number {
return 1;
}
getChaosCompetencePenalty(inst: Bladeburner, params: ISuccessChanceParams): number {
const city = inst.getCurrentCity();
if (params.est) {
return Math.pow(city.popEst / BladeburnerConstants.PopulationThreshold, BladeburnerConstants.PopulationExponent);
} else {
return Math.pow(city.pop / BladeburnerConstants.PopulationThreshold, BladeburnerConstants.PopulationExponent);
}
}
getChaosDifficultyBonus(inst: Bladeburner /*, params: ISuccessChanceParams*/): number {
const city = inst.getCurrentCity();
if (city.chaos > BladeburnerConstants.ChaosThreshold) {
const diff = 1 + (city.chaos - BladeburnerConstants.ChaosThreshold);
const mult = Math.pow(diff, 0.5);
return mult;
}
return 1;
}
getEstSuccessChance(inst: Bladeburner, person: Person): [number, number] {
function clamp(x: number): number {
return Math.max(0, Math.min(x, 1));
}
const est = this.getSuccessChance(inst, person, { est: true });
const real = this.getSuccessChance(inst, person);
const diff = Math.abs(real - est);
let low = real - diff;
let high = real + diff;
const city = inst.getCurrentCity();
let r = city.pop / city.popEst;
if (Number.isNaN(r)) r = 0;
if (r < 1) low *= r;
else high *= r;
return [clamp(low), clamp(high)];
}
/**
* @inst - Bladeburner Object
* @params - options:
* est (bool): Get success chance estimate instead of real success chance
*/
getSuccessChance(inst: Bladeburner, person: Person, params: ISuccessChanceParams = { est: false }): number {
if (inst == null) {
throw new Error("Invalid Bladeburner instance passed into Action.getSuccessChance");
}
let difficulty = this.getDifficulty();
let competence = 0;
for (const stat of Object.keys(this.weights)) {
if (Object.hasOwn(this.weights, stat)) {
const playerStatLvl = person.queryStatFromString(stat);
const key = "eff" + stat.charAt(0).toUpperCase() + stat.slice(1);
let effMultiplier = inst.skillMultipliers[key];
if (effMultiplier == null) {
console.error(`Failed to find Bladeburner Skill multiplier for: ${stat}`);
effMultiplier = 1;
}
competence += this.weights[stat] * Math.pow(effMultiplier * playerStatLvl, this.decays[stat]);
}
}
competence *= calculateIntelligenceBonus(person.skills.intelligence, 0.75);
competence *= inst.calculateStaminaPenalty();
competence *= this.getTeamSuccessBonus(inst);
competence *= this.getChaosCompetencePenalty(inst, params);
difficulty *= this.getChaosDifficultyBonus(inst);
if (this.name == "Raid" && inst.getCurrentCity().comms <= 0) {
return 0;
}
// Factor skill multipliers into success chance
competence *= inst.skillMultipliers.successChanceAll;
competence *= this.getActionTypeSkillSuccessBonus(inst);
if (this.isStealth) {
competence *= inst.skillMultipliers.successChanceStealth;
}
if (this.isKill) {
competence *= inst.skillMultipliers.successChanceKill;
}
// Augmentation multiplier
competence *= person.mults.bladeburner_success_chance;
if (isNaN(competence)) {
throw new Error("Competence calculated as NaN in Action.getSuccessChance()");
}
return Math.min(1, competence / difficulty);
}
getSuccessesNeededForNextLevel(baseSuccessesPerLevel: number): number {
return Math.ceil(0.5 * this.maxLevel * (2 * baseSuccessesPerLevel + (this.maxLevel - 1)));
}
setMaxLevel(baseSuccessesPerLevel: number): void {
if (this.successes >= this.getSuccessesNeededForNextLevel(baseSuccessesPerLevel)) {
++this.maxLevel;
}
}
toJSON(): IReviverValue {
return Generic_toJSON("Action", this);
}
static fromJSON(value: IReviverValue): Action {
return Generic_fromJSON(Action, value.data);
}
}
constructorsForReviver.Action = Action;

@ -1,26 +0,0 @@
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
interface IParams {
name?: string;
type?: number;
}
export class ActionIdentifier {
name = "";
type = -1;
constructor(params: IParams = {}) {
if (params.name) this.name = params.name;
if (params.type) this.type = params.type;
}
toJSON(): IReviverValue {
return Generic_toJSON("ActionIdentifier", this);
}
static fromJSON(value: IReviverValue): ActionIdentifier {
return Generic_fromJSON(ActionIdentifier, value.data);
}
}
constructorsForReviver.ActionIdentifier = ActionIdentifier;

@ -0,0 +1,183 @@
import type { Bladeburner } from "../Bladeburner";
import type { Person } from "../../PersonObjects/Person";
import type { Availability, SuccessChanceParams } from "../Types";
import type { Skills as PersonSkills } from "../../PersonObjects/Skills";
import { addOffset } from "../../utils/helpers/addOffset";
import { BladeburnerConstants } from "../data/Constants";
import { calculateIntelligenceBonus } from "../../PersonObjects/formulas/intelligence";
import { BladeMultName } from "../Enums";
import { getRecordKeys } from "../../Types/Record";
export interface ActionParams {
desc: string;
baseDifficulty?: number;
rewardFac?: number;
rankGain?: number;
rankLoss?: number;
hpLoss?: number;
isStealth?: boolean;
isKill?: boolean;
weights?: PersonSkills;
decays?: PersonSkills;
}
export abstract class ActionClass {
desc = "";
// For LevelableActions, the base difficulty can be increased based on action level
baseDifficulty = 100;
// All of these scale with level/difficulty
rankGain = 0;
rankLoss = 0;
hpLoss = 0;
// Action Category. Current categories are stealth and kill
isStealth = false;
isKill = false;
// Weighting of each stat in determining action success rate
weights: PersonSkills = {
hacking: 1 / 7,
strength: 1 / 7,
defense: 1 / 7,
dexterity: 1 / 7,
agility: 1 / 7,
charisma: 1 / 7,
intelligence: 1 / 7,
};
// Diminishing returns of stats (stat ^ decay where 0 <= decay <= 1)
decays: PersonSkills = {
hacking: 0.9,
strength: 0.9,
defense: 0.9,
dexterity: 0.9,
agility: 0.9,
charisma: 0.9,
intelligence: 0.9,
};
constructor(params: ActionParams | null = null) {
if (!params) return;
this.desc = params.desc;
if (params.baseDifficulty) this.baseDifficulty = addOffset(params.baseDifficulty, 10);
if (params.rankGain) this.rankGain = params.rankGain;
if (params.rankLoss) this.rankLoss = params.rankLoss;
if (params.hpLoss) this.hpLoss = params.hpLoss;
if (params.isStealth) this.isStealth = params.isStealth;
if (params.isKill) this.isKill = params.isKill;
if (params.weights) this.weights = params.weights;
if (params.decays) this.decays = params.decays;
}
/** Tests for success. Should be called when an action has completed */
attempt(bladeburner: Bladeburner, person: Person): boolean {
return Math.random() < this.getSuccessChance(bladeburner, person);
}
// All the functions below are overwritten by certain subtypes of action, e.g. BlackOps ignore city stats
getPopulationSuccessFactor(bladeburner: Bladeburner, { est }: SuccessChanceParams): number {
const city = bladeburner.getCurrentCity();
const pop = est ? city.popEst : city.pop;
return Math.pow(pop / BladeburnerConstants.PopulationThreshold, BladeburnerConstants.PopulationExponent);
}
getChaosSuccessFactor(bladeburner: Bladeburner): number {
const city = bladeburner.getCurrentCity();
if (city.chaos > BladeburnerConstants.ChaosThreshold) {
const diff = 1 + (city.chaos - BladeburnerConstants.ChaosThreshold);
const mult = Math.pow(diff, 0.5);
return mult;
}
return 1;
}
getActionTime(bladeburner: Bladeburner, person: Person): number {
const difficulty = this.getDifficulty();
let baseTime = difficulty / BladeburnerConstants.DifficultyToTimeFactor;
const skillFac = bladeburner.getSkillMult(BladeMultName.actionTime); // Always < 1
const effAgility = bladeburner.getEffectiveSkillLevel(person, "agility");
const effDexterity = bladeburner.getEffectiveSkillLevel(person, "dexterity");
const statFac =
0.5 *
(Math.pow(effAgility, BladeburnerConstants.EffAgiExponentialFactor) +
Math.pow(effDexterity, BladeburnerConstants.EffDexExponentialFactor) +
effAgility / BladeburnerConstants.EffAgiLinearFactor +
effDexterity / BladeburnerConstants.EffDexLinearFactor); // Always > 1
baseTime = Math.max(1, (baseTime * skillFac) / statFac);
return Math.ceil(baseTime * this.getActionTimePenalty());
}
getTeamSuccessBonus(__bladeburner: Bladeburner): number {
return 1;
}
getActionTypeSkillSuccessBonus(__bladeburner: Bladeburner): number {
return 1;
}
getAvailability(__bladeburner: Bladeburner): Availability {
return { available: true };
}
getActionTimePenalty(): number {
return 1;
}
getDifficulty(): number {
return this.baseDifficulty;
}
getSuccessRange(bladeburner: Bladeburner, person: Person): [minChance: number, maxChance: number] {
function clamp(x: number): number {
return Math.max(0, Math.min(x, 1));
}
const est = this.getSuccessChance(bladeburner, person, { est: true });
const real = this.getSuccessChance(bladeburner, person);
const diff = Math.abs(real - est);
let low = real - diff;
let high = real + diff;
const city = bladeburner.getCurrentCity();
let r = city.pop / city.popEst;
if (Number.isNaN(r)) r = 0;
if (r < 1) low *= r;
else high *= r;
return [clamp(low), clamp(high)];
}
getSuccessChance(inst: Bladeburner, person: Person, { est }: SuccessChanceParams = { est: false }): number {
let difficulty = this.getDifficulty();
let competence = 0;
for (const stat of getRecordKeys(person.skills)) {
competence += this.weights[stat] * Math.pow(inst.getEffectiveSkillLevel(person, stat), this.decays[stat]);
}
competence *= calculateIntelligenceBonus(person.skills.intelligence, 0.75);
competence *= inst.calculateStaminaPenalty();
competence *= this.getTeamSuccessBonus(inst);
competence *= this.getPopulationSuccessFactor(inst, { est });
difficulty *= this.getChaosSuccessFactor(inst);
// Factor skill multipliers into success chance
competence *= inst.getSkillMult(BladeMultName.successChanceAll);
competence *= this.getActionTypeSkillSuccessBonus(inst);
if (this.isStealth) competence *= inst.getSkillMult(BladeMultName.successChanceStealth);
if (this.isKill) competence *= inst.getSkillMult(BladeMultName.successChanceKill);
// Augmentation multiplier
competence *= person.mults.bladeburner_success_chance;
if (isNaN(competence)) {
throw new Error("Competence calculated as NaN in Action.getSuccessChance()");
}
return Math.min(1, competence / difficulty);
}
}

@ -0,0 +1,51 @@
import type { Bladeburner } from "../Bladeburner";
import type { Availability, ActionIdentifier } from "../Types";
import { BladeActionType, BladeBlackOpName } from "@enums";
import { ActionClass, ActionParams } from "./Action";
import { operationSkillSuccessBonus, operationTeamSuccessBonus } from "./Operation";
interface BlackOpParams {
name: BladeBlackOpName;
reqdRank: number;
n: number;
}
export class BlackOperation extends ActionClass {
type: BladeActionType.blackOp = BladeActionType.blackOp;
name: BladeBlackOpName;
n: number;
reqdRank: number;
teamCount = 0;
get id(): ActionIdentifier {
return { type: this.type, name: this.name };
}
constructor(params: ActionParams & BlackOpParams) {
super(params);
this.name = params.name;
this.reqdRank = params.reqdRank;
this.n = params.n;
}
getAvailability(bladeburner: Bladeburner): Availability {
if (bladeburner.numBlackOpsComplete < this.n) return { error: "Have not completed the previous Black Operation" };
if (bladeburner.numBlackOpsComplete > this.n) return { error: "Already completed" };
if (bladeburner.rank < this.reqdRank) return { error: "Insufficient rank" };
return { available: true };
}
// To be implemented by subtypes
getActionTimePenalty(): number {
return 1.5;
}
getPopulationSuccessFactor(/*inst: Bladeburner, params: ISuccessChanceParams*/): number {
return 1;
}
getChaosSuccessFactor(/*inst: Bladeburner, params: ISuccessChanceParams*/): number {
return 1;
}
getTeamSuccessBonus = operationTeamSuccessBonus;
getActionTypeSkillSuccessBonus = operationSkillSuccessBonus;
}

@ -0,0 +1,33 @@
import type { Bladeburner } from "../Bladeburner";
import type { ActionIdentifier } from "../Types";
import { Generic_fromJSON, IReviverValue, constructorsForReviver } from "../../utils/JSONReviver";
import { BladeActionType, BladeContractName, BladeMultName } from "../Enums";
import { LevelableActionClass, LevelableActionParams } from "./LevelableAction";
export class Contract extends LevelableActionClass {
type: BladeActionType.contract = BladeActionType.contract;
name: BladeContractName = BladeContractName.tracking;
get id(): ActionIdentifier {
return { type: this.type, name: this.name };
}
constructor(params: (LevelableActionParams & { name: BladeContractName }) | null = null) {
super(params);
if (params) this.name = params.name;
}
getActionTypeSkillSuccessBonus(inst: Bladeburner): number {
return inst.getSkillMult(BladeMultName.successChanceContract);
}
toJSON(): IReviverValue {
return this.save("Contract");
}
static fromJSON(value: IReviverValue): Contract {
return Generic_fromJSON(Contract, value.data);
}
}
constructorsForReviver.Contract = Contract;

@ -0,0 +1,35 @@
import type { Person } from "../../PersonObjects/Person";
import type { Bladeburner } from "../Bladeburner";
import type { ActionIdentifier } from "../Types";
import { BladeActionType, BladeGeneralActionName } from "@enums";
import { ActionClass, ActionParams } from "./Action";
type GeneralActionParams = ActionParams & {
name: BladeGeneralActionName;
getActionTime: (bladeburner: Bladeburner, person: Person) => number;
getSuccessChance?: (bladeburner: Bladeburner, person: Person) => number;
};
export class GeneralAction extends ActionClass {
type: BladeActionType.general = BladeActionType.general;
name: BladeGeneralActionName;
get id(): ActionIdentifier {
return { type: this.type, name: this.name };
}
constructor(params: GeneralActionParams) {
super(params);
this.name = params.name;
this.getActionTime = params.getActionTime;
if (params.getSuccessChance) this.getSuccessChance = params.getSuccessChance;
}
getSuccessChance(__bladeburner: Bladeburner, __person: Person): number {
return 1;
}
getSuccessRange(bladeburner: Bladeburner, person: Person): [minChance: number, maxChance: number] {
const chance = this.getSuccessChance(bladeburner, person);
return [chance, chance];
}
}

@ -0,0 +1,111 @@
import type { Bladeburner } from "../Bladeburner";
import type { IReviverValue } from "../../utils/JSONReviver";
import type { Availability } from "../Types";
import { ActionClass, ActionParams } from "./Action";
import { getRandomInt } from "../../utils/helpers/getRandomInt";
import { clampInteger } from "../../utils/helpers/clampNumber";
export type LevelableActionParams = ActionParams & {
growthFunction: () => number;
difficultyFac?: number;
rewardFac?: number;
minCount?: number;
maxCount?: number;
};
export abstract class LevelableActionClass extends ActionClass {
// Static info, not included in save
difficultyFac = 1.01;
rewardFac = 1.02;
growthFunction = () => 0;
minCount = 1;
maxCount = 150;
// Dynamic properties included in save
count = 0;
level = 1;
maxLevel = 1;
autoLevel = true;
successes = 0;
failures = 0;
constructor(params: LevelableActionParams | null = null) {
super(params);
if (!params) return;
if (params.minCount) this.minCount = params.minCount;
if (params.maxCount) this.maxCount = params.maxCount;
if (params.difficultyFac) this.difficultyFac = params.difficultyFac;
if (params.rewardFac) this.rewardFac = params.rewardFac;
this.count = getRandomInt(this.minCount, this.maxCount);
this.growthFunction = params.growthFunction;
}
getAvailability(__bladeburner: Bladeburner): Availability {
if (this.count < 1) return { error: "Insufficient action count" };
return { available: true };
}
setMaxLevel(baseSuccessesPerLevel: number): void {
if (this.successes >= this.getSuccessesNeededForNextLevel(baseSuccessesPerLevel)) {
++this.maxLevel;
}
}
getSuccessesNeededForNextLevel(baseSuccessesPerLevel: number): number {
return Math.ceil(0.5 * this.maxLevel * (2 * baseSuccessesPerLevel + (this.maxLevel - 1)));
}
getDifficulty() {
const difficulty = this.baseDifficulty * Math.pow(this.difficultyFac, this.level - 1);
if (isNaN(difficulty)) {
throw new Error("Calculated NaN in Action.getDifficulty()");
}
return difficulty;
}
/** Reset a levelable action's tracked stats */
reset() {
this.count = getRandomInt(this.minCount, this.maxCount);
this.level = 1;
this.maxLevel = 1;
this.autoLevel = true;
this.successes = 0;
this.failures = 0;
}
/** These are not loaded the same way as most game objects, to allow better typechecking on load + partially static loading */
loadData(loadedObject: LevelableActionClass) {
this.maxLevel = clampInteger(loadedObject.maxLevel, 1);
this.level = clampInteger(loadedObject.level, 1, this.maxLevel);
this.count = clampInteger(loadedObject.count, 0);
this.autoLevel = !!loadedObject.autoLevel;
this.successes = clampInteger(loadedObject.successes, 0);
this.failures = clampInteger(loadedObject.failures, 0);
}
/** Create a basic object just containing the relevant data for a levelable action */
save<T extends LevelableActionClass>(
this: T,
ctorName: string,
...extraParams: (keyof T)[]
): IReviverValue<LevelableActionSaveData> {
const data = {
...Object.fromEntries(extraParams.map((param) => [param, this[param]])),
count: this.count,
level: this.level,
maxLevel: this.maxLevel,
autoLevel: this.autoLevel,
successes: this.successes,
failures: this.failures,
};
return { ctor: ctorName, data };
}
}
export interface LevelableActionSaveData {
count: number;
level: number;
maxLevel: number;
autoLevel: boolean;
successes: number;
failures: number;
}

@ -0,0 +1,84 @@
import type { Person } from "../../PersonObjects/Person";
import type { BlackOperation } from "./BlackOperation";
import type { Bladeburner } from "../Bladeburner";
import type { Availability, ActionIdentifier, SuccessChanceParams } from "../Types";
import { BladeActionType, BladeMultName, BladeOperationName } from "@enums";
import { BladeburnerConstants } from "../data/Constants";
import { ActionClass } from "./Action";
import { Generic_fromJSON, IReviverValue, constructorsForReviver } from "../../utils/JSONReviver";
import { LevelableActionClass, LevelableActionParams } from "./LevelableAction";
import { clampInteger } from "../../utils/helpers/clampNumber";
export interface OperationParams extends LevelableActionParams {
name: BladeOperationName;
getAvailability?: (bladeburner: Bladeburner) => Availability;
}
export class Operation extends LevelableActionClass {
type: BladeActionType.operation = BladeActionType.operation;
name = BladeOperationName.investigation;
teamCount = 0;
get id(): ActionIdentifier {
return { type: this.type, name: this.name };
}
constructor(params: OperationParams | null = null) {
super(params);
if (!params) return;
this.name = params.name;
if (params.getAvailability) this.getAvailability = params.getAvailability;
}
// These functions are shared between operations and blackops, so they are defined outside of Operation
getTeamSuccessBonus = operationTeamSuccessBonus;
getActionTypeSkillSuccessBonus = operationSkillSuccessBonus;
getChaosSuccessFactor(inst: Bladeburner /*, params: ISuccessChanceParams*/): number {
const city = inst.getCurrentCity();
if (city.chaos > BladeburnerConstants.ChaosThreshold) {
const diff = 1 + (city.chaos - BladeburnerConstants.ChaosThreshold);
const mult = Math.pow(diff, 0.5);
return mult;
}
return 1;
}
getSuccessChance(inst: Bladeburner, person: Person, params: SuccessChanceParams) {
if (this.name == BladeOperationName.raid && inst.getCurrentCity().comms <= 0) return 0;
return ActionClass.prototype.getSuccessChance.call(this, inst, person, params);
}
reset() {
LevelableActionClass.prototype.reset.call(this);
this.teamCount = 0;
}
toJSON(): IReviverValue {
return this.save("Operation", "teamCount");
}
loadData(loadedObject: Operation): void {
this.teamCount = clampInteger(loadedObject.teamCount, 0);
LevelableActionClass.prototype.loadData.call(this, loadedObject);
}
static fromJSON(value: IReviverValue): Operation {
return Generic_fromJSON(Operation, value.data);
}
}
constructorsForReviver.Operation = Operation;
// shared member functions for Operation and BlackOperation
export const operationSkillSuccessBonus = (inst: Bladeburner) => {
return inst.getSkillMult(BladeMultName.successChanceOperation);
};
export function operationTeamSuccessBonus(this: Operation | BlackOperation, inst: Bladeburner) {
if (this.teamCount && this.teamCount > 0) {
this.teamCount = Math.min(this.teamCount, inst.teamSize);
const teamMultiplier = Math.pow(this.teamCount, 0.05);
return teamMultiplier;
}
return 1;
}

@ -0,0 +1,7 @@
// Barrel file for easier importing
export { ActionClass } from "./Action";
export { BlackOperation } from "./BlackOperation";
export { Contract } from "./Contract";
export { GeneralAction } from "./GeneralAction";
export { Operation } from "./Operation";
export { LevelableActionClass } from "./LevelableAction";

@ -1,32 +0,0 @@
import { Operation, IOperationParams } from "./Operation";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
export class BlackOperation extends Operation {
constructor(params: IOperationParams | null = null) {
super(params);
this.count = 1;
}
// To be implemented by subtypes
getActionTimePenalty(): number {
return 1.5;
}
getChaosCompetencePenalty(/*inst: Bladeburner, params: ISuccessChanceParams*/): number {
return 1;
}
getChaosDifficultyBonus(/*inst: Bladeburner, params: ISuccessChanceParams*/): number {
return 1;
}
toJSON(): IReviverValue {
return Generic_toJSON("BlackOperation", this);
}
static fromJSON(value: IReviverValue): Operation {
return Generic_fromJSON(BlackOperation, value.data);
}
}
constructorsForReviver.BlackOperation = BlackOperation;

@ -1,571 +0,0 @@
import { BlackOperation } from "./BlackOperation";
import { BlackOperationName } from "@enums";
export const BlackOperations: Record<string, BlackOperation> = {};
(function () {
BlackOperations[BlackOperationName.OperationTyphoon] = new BlackOperation({
name: BlackOperationName.OperationTyphoon,
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[BlackOperationName.OperationZero] = new BlackOperation({
name: BlackOperationName.OperationZero,
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[BlackOperationName.OperationX] = new BlackOperation({
name: BlackOperationName.OperationX,
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[BlackOperationName.OperationTitan] = new BlackOperation({
name: BlackOperationName.OperationTitan,
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[BlackOperationName.OperationAres] = new BlackOperation({
name: BlackOperationName.OperationAres,
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[BlackOperationName.OperationArchangel] = new BlackOperation({
name: BlackOperationName.OperationArchangel,
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[BlackOperationName.OperationJuggernaut] = new BlackOperation({
name: BlackOperationName.OperationJuggernaut,
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[BlackOperationName.OperationRedDragon] = new BlackOperation({
name: BlackOperationName.OperationRedDragon,
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[BlackOperationName.OperationK] = new BlackOperation({
name: BlackOperationName.OperationK,
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[BlackOperationName.OperationDeckard] = new BlackOperation({
name: BlackOperationName.OperationDeckard,
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[BlackOperationName.OperationTyrell] = new BlackOperation({
name: BlackOperationName.OperationTyrell,
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[BlackOperationName.OperationWallace] = new BlackOperation({
name: BlackOperationName.OperationWallace,
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[BlackOperationName.OperationShoulderOfOrion] = new BlackOperation({
name: BlackOperationName.OperationShoulderOfOrion,
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[BlackOperationName.OperationHyron] = new BlackOperation({
name: BlackOperationName.OperationHyron,
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[BlackOperationName.OperationMorpheus] = new BlackOperation({
name: BlackOperationName.OperationMorpheus,
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[BlackOperationName.OperationIonStorm] = new BlackOperation({
name: BlackOperationName.OperationIonStorm,
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[BlackOperationName.OperationAnnihilus] = new BlackOperation({
name: BlackOperationName.OperationAnnihilus,
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[BlackOperationName.OperationUltron] = new BlackOperation({
name: BlackOperationName.OperationUltron,
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[BlackOperationName.OperationCenturion] = new BlackOperation({
name: BlackOperationName.OperationCenturion,
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[BlackOperationName.OperationVindictus] = new BlackOperation({
name: BlackOperationName.OperationVindictus,
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[BlackOperationName.OperationDaedalus] = new BlackOperation({
name: BlackOperationName.OperationDaedalus,
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,
},
});
})();

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -1,35 +1,15 @@
import { CityName } from "@enums";
import { BladeburnerConstants } from "./data/Constants"; import { BladeburnerConstants } from "./data/Constants";
import { getRandomInt } from "../utils/helpers/getRandomInt"; import { getRandomInt } from "../utils/helpers/getRandomInt";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver"; import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
import { addOffset } from "../utils/helpers/addOffset"; import { addOffset } from "../utils/helpers/addOffset";
import { CityName } from "@enums"; import { clampInteger, clampNumber } from "../utils/helpers/clampNumber";
interface IChangePopulationByCountParams {
/** How much the estimate should change by. */
estChange: number;
/** Add offset to estimate (offset by percentage). */
estOffset: number;
}
interface IChangePopulationByPercentageParams {
nonZero: boolean;
changeEstEqually: boolean;
}
export class City { export class City {
/** Name of the city. */
name: CityName; name: CityName;
pop = 0; // Population
/** Population of the city. */ popEst = 0; // Population estimate
pop = 0; comms = 0; // Number of communities
/** Population estimation of the city. */
popEst = 0;
/** Number of communities in the city. */
comms = 0;
/** Chaos level of the city. */
chaos = 0; chaos = 0;
constructor(name = CityName.Sector12) { constructor(name = CityName.Sector12) {
@ -44,118 +24,63 @@ export class City {
this.chaos = 0; this.chaos = 0;
} }
/** p is the percentage, not the multiplier (e.g. pass in p = 5 for 5%) */ /** @param {number} p - the percentage change, not the multiplier. e.g. pass in p = 5 for 5% */
changeChaosByPercentage(p: number): void { changeChaosByPercentage(p: number): void {
if (isNaN(p)) { this.chaos = clampNumber(this.chaos * (1 + p / 100), 0);
throw new Error("NaN passed into City.chaosChaosByPercentage()");
}
if (p === 0) {
return;
}
this.chaos += this.chaos * (p / 100);
if (this.chaos < 0) {
this.chaos = 0;
}
} }
improvePopulationEstimateByCount(n: number): void { improvePopulationEstimateByCount(n: number): void {
if (isNaN(n)) { n = clampInteger(n, 0);
throw new Error("NaN passed into City.improvePopulationEstimateByCount()"); const diff = Math.abs(this.popEst - this.pop);
} // Chgnge would overshoot actual population -> make estimate accurate
if (this.popEst < this.pop) { if (diff <= n) this.popEst = this.pop;
this.popEst += n; // Otherwise make enstimate closer by n
if (this.popEst > this.pop) { else if (this.popEst < this.pop) this.popEst += n;
this.popEst = this.pop; else this.popEst -= n;
}
} else if (this.popEst > this.pop) {
this.popEst -= n;
if (this.popEst < this.pop) {
this.popEst = this.pop;
}
}
} }
/** p is the percentage, not the multiplier (e.g. pass in p = 5 for 5%) */ /** @param {number} p - the percentage change, not the multiplier. e.g. pass in p = 5 for 5% */
improvePopulationEstimateByPercentage(p: number, skillMult = 1): void { improvePopulationEstimateByPercentage(p: number, skillMult = 1): void {
p = p * skillMult; p = clampNumber((p * skillMult) / 100);
if (isNaN(p)) { const diff = Math.abs(this.popEst - this.pop);
throw new Error("NaN passed into City.improvePopulationEstimateByPercentage()"); // Chgnge would overshoot actual population -> make estimate accurate
} if (diff <= p * this.popEst) this.popEst = this.pop;
if (this.popEst < this.pop) { // Otherwise make enstimate closer by n
++this.popEst; // In case estimate is 0 else if (this.popEst < this.pop) this.popEst = clampNumber(this.popEst * (1 + p));
this.popEst *= 1 + p / 100; else this.popEst = clampNumber(this.popEst * (1 - p));
if (this.popEst > this.pop) {
this.popEst = this.pop;
}
} else if (this.popEst > this.pop) {
this.popEst *= 1 - p / 100;
if (this.popEst < this.pop) {
this.popEst = this.pop;
}
}
} }
changePopulationByCount(n: number, params: IChangePopulationByCountParams = { estChange: 0, estOffset: 0 }): void { /**
if (isNaN(n)) { * @param params.estChange - Number to change the estimate by
throw new Error("NaN passed into City.changePopulationByCount()"); * @param params.estOffset - Offset percentage to apply to estimate */
} changePopulationByCount(n: number, params = { estChange: 0, estOffset: 0 }): void {
this.pop += n; n = clampInteger(n);
this.pop = clampInteger(this.pop + n, 0);
if (params.estChange && !isNaN(params.estChange)) { if (params.estChange && !isNaN(params.estChange)) {
this.popEst += params.estChange; this.popEst += params.estChange;
} }
if (params.estOffset) { if (params.estOffset) {
this.popEst = addOffset(this.popEst, params.estOffset); this.popEst = addOffset(this.popEst, params.estOffset);
} }
this.popEst = Math.max(this.popEst, 0); this.popEst = clampInteger(this.popEst, 0);
} }
/** /**
* @p is the percentage, not the multiplier. e.g. pass in p = 5 for 5% * @param {number} p - the percentage change, not the multiplier. e.g. pass in p = 5 for 5%
* @params options: * @param {boolean} params.changeEstEqually - Whether to change the population estimate by an equal amount
* changeEstEqually(bool) - Change the population estimate by an equal amount * @param {boolean} params.nonZero - Whether to ensure that population always changes by at least 1 */
* nonZero (bool) - Set to true to ensure that population always changes by at least 1 changePopulationByPercentage(p: number, params = { nonZero: false, changeEstEqually: false }): number {
*/ let change = clampInteger(this.pop * (p / 100));
changePopulationByPercentage(
p: number,
params: IChangePopulationByPercentageParams = {
nonZero: false,
changeEstEqually: false,
},
): number {
if (isNaN(p)) {
throw new Error("NaN passed into City.changePopulationByPercentage()");
}
if (p === 0) {
return 0;
}
let change = Math.round(this.pop * (p / 100));
// Population always changes by at least 1 if (params.nonZero && change === 0) change = p > 0 ? 1 : -1;
if (params.nonZero && change === 0) {
p > 0 ? (change = 1) : (change = -1);
}
this.pop += change; this.pop = clampInteger(this.pop + change, 0);
if (params.changeEstEqually) { if (params.changeEstEqually) this.popEst = clampInteger(this.popEst + change, 0);
this.popEst += change;
if (this.popEst < 0) {
this.popEst = 0;
}
}
return change; return change;
} }
changeChaosByCount(n: number): void { changeChaosByCount(n: number): void {
if (isNaN(n)) { this.chaos = clampNumber(this.chaos + n, 0);
throw new Error("NaN passed into City.changeChaosByCount()");
}
if (n === 0) {
return;
}
this.chaos += n;
if (this.chaos < 0) {
this.chaos = 0;
}
} }
/** Serialize the current object to a JSON save state. */ /** Serialize the current object to a JSON save state. */

@ -1,23 +0,0 @@
import { Bladeburner } from "./Bladeburner";
import { Action, IActionParams } from "./Action";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
export class Contract extends Action {
constructor(params: IActionParams | null = null) {
super(params);
}
getActionTypeSkillSuccessBonus(inst: Bladeburner): number {
return inst.skillMultipliers.successChanceContract;
}
toJSON(): IReviverValue {
return Generic_toJSON("Contract", this);
}
static fromJSON(value: IReviverValue): Contract {
return Generic_fromJSON(Contract, value.data);
}
}
constructorsForReviver.Contract = Contract;

@ -1,4 +1,31 @@
export enum BlackOperationName { export enum BladeActionType {
general = "General",
contract = "Contracts",
operation = "Operations",
blackOp = "Black Operations",
}
export enum BladeGeneralActionName {
training = "Training",
fieldAnalysis = "Field Analysis",
recruitment = "Recruitment",
diplomacy = "Diplomacy",
hyperbolicRegen = "Hyperbolic Regeneration Chamber",
inciteViolence = "Incite Violence",
}
export enum BladeContractName {
tracking = "Tracking",
bountyHunter = "Bounty Hunter",
retirement = "Retirement",
}
export enum BladeOperationName {
investigation = "Investigation",
undercover = "Undercover Operation",
sting = "Sting Operation",
raid = "Raid",
stealthRetirement = "Stealth Retirement Operation",
assassination = "Assassination",
}
export enum BladeBlackOpName {
OperationTyphoon = "Operation Typhoon", OperationTyphoon = "Operation Typhoon",
OperationZero = "Operation Zero", OperationZero = "Operation Zero",
OperationX = "Operation X", OperationX = "Operation X",
@ -21,3 +48,36 @@ export enum BlackOperationName {
OperationVindictus = "Operation Vindictus", OperationVindictus = "Operation Vindictus",
OperationDaedalus = "Operation Daedalus", OperationDaedalus = "Operation Daedalus",
} }
export enum BladeSkillName {
bladesIntuition = "Blade's Intuition",
cloak = "Cloak",
shortCircuit = "Short-Circuit",
digitalObserver = "Digital Observer",
tracer = "Tracer",
overclock = "Overclock",
reaper = "Reaper",
evasiveSystem = "Evasive System",
datamancer = "Datamancer",
cybersEdge = "Cyber's Edge",
handsOfMidas = "Hands of Midas",
hyperdrive = "Hyperdrive",
}
export enum BladeMultName {
successChanceAll = "Total Success Chance",
successChanceStealth = "Stealth Success Chance",
successChanceKill = "Retirement Success Chance",
successChanceContract = "Contract Success Chance",
successChanceOperation = "Operation Success Chance",
successChanceEstimate = "Synthoid Data Estimate",
actionTime = "Action Time",
effStr = "Effective Strength",
effDef = "Effective Defense",
effDex = "Effective Dexterity",
effAgi = "Effective Agility",
effCha = "Effective Charisma",
stamina = "Stamina",
money = "Contract Money",
expGain = "Experience Gain",
}

@ -1,18 +0,0 @@
import { Action } from "./Action";
export const GeneralActions: Record<string, Action> = {};
const actionNames: string[] = [
"Training",
"Field Analysis",
"Recruitment",
"Diplomacy",
"Hyperbolic Regeneration Chamber",
"Incite Violence",
];
for (const actionName of actionNames) {
GeneralActions[actionName] = new Action({
name: actionName,
});
}

@ -1,56 +0,0 @@
import { Bladeburner } from "./Bladeburner";
import { BladeburnerConstants } from "./data/Constants";
import { Action, IActionParams } from "./Action";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
export interface IOperationParams extends IActionParams {
reqdRank?: number;
teamCount?: number;
}
export class Operation extends Action {
reqdRank = 100;
teamCount = 0;
constructor(params: IOperationParams | null = null) {
super(params);
if (params && params.reqdRank) this.reqdRank = params.reqdRank;
if (params && params.teamCount) this.teamCount = params.teamCount;
}
// For actions that have teams. To be implemented by subtypes.
getTeamSuccessBonus(inst: Bladeburner): number {
if (this.teamCount && this.teamCount > 0) {
this.teamCount = Math.min(this.teamCount, inst.teamSize);
const teamMultiplier = Math.pow(this.teamCount, 0.05);
return teamMultiplier;
}
return 1;
}
getActionTypeSkillSuccessBonus(inst: Bladeburner): number {
return inst.skillMultipliers.successChanceOperation;
}
getChaosDifficultyBonus(inst: Bladeburner /*, params: ISuccessChanceParams*/): number {
const city = inst.getCurrentCity();
if (city.chaos > BladeburnerConstants.ChaosThreshold) {
const diff = 1 + (city.chaos - BladeburnerConstants.ChaosThreshold);
const mult = Math.pow(diff, 0.5);
return mult;
}
return 1;
}
toJSON(): IReviverValue {
return Generic_toJSON("Operation", this);
}
static fromJSON(value: IReviverValue): Operation {
return Generic_fromJSON(Operation, value.data);
}
}
constructorsForReviver.Operation = Operation;

@ -1,149 +1,51 @@
import type { BladeMultName, BladeSkillName } from "@enums";
import { currentNodeMults } from "../BitNode/BitNodeMultipliers"; import { currentNodeMults } from "../BitNode/BitNodeMultipliers";
import { Bladeburner } from "./Bladeburner";
import { Availability } from "./Types";
import { PositiveSafeInteger, isPositiveSafeInteger } from "../types";
import { PartialRecord, getRecordEntries } from "../Types/Record";
interface ISkillParams { interface SkillParams {
name: string; name: BladeSkillName;
desc: string; desc: string;
baseCost?: number; baseCost?: number;
costInc?: number; costInc?: number;
maxLvl?: number; maxLvl?: number;
mults: PartialRecord<BladeMultName, number>;
successChanceAll?: number;
successChanceStealth?: number;
successChanceKill?: number;
successChanceContract?: number;
successChanceOperation?: number;
successChanceEstimate?: number;
actionTime?: number;
effHack?: number;
effStr?: number;
effDef?: number;
effDex?: number;
effAgi?: number;
effCha?: number;
stamina?: number;
money?: number;
expGain?: number;
} }
export class Skill { export class Skill {
name: string; name: BladeSkillName;
desc: string; desc: string;
// Cost is in Skill Points // Cost is in Skill Points
baseCost = 1; baseCost = 1;
// Additive cost increase per level // Additive cost increase per level
costInc = 1; costInc = 1;
maxLvl = 0; maxLvl = Number.MAX_SAFE_INTEGER;
mults: PartialRecord<BladeMultName, number> = {};
/** constructor(params: SkillParams) {
* These benefits are additive. So total multiplier will be level (handled externally) times the
* effects below
*/
successChanceAll = 0;
successChanceStealth = 0;
successChanceKill = 0;
successChanceContract = 0;
successChanceOperation = 0;
/**
* This multiplier affects everything that increases synthoid population/community estimate
* e.g. Field analysis, Investigation Op, Undercover Op
*/
successChanceEstimate = 0;
actionTime = 0;
effHack = 0;
effStr = 0;
effDef = 0;
effDex = 0;
effAgi = 0;
effCha = 0;
stamina = 0;
money = 0;
expGain = 0;
constructor(params: ISkillParams = { name: "foo", desc: "foo" }) {
if (!params.name) {
throw new Error("Failed to initialize Bladeburner Skill. No name was specified in ctor");
}
if (!params.desc) {
throw new Error("Failed to initialize Bladeburner Skills. No desc was specified in ctor");
}
this.name = params.name; this.name = params.name;
this.desc = params.desc; this.desc = params.desc;
this.baseCost = params.baseCost ? params.baseCost : 1; this.baseCost = params.baseCost ?? 1;
this.costInc = params.costInc ? params.costInc : 1; this.costInc = params.costInc ?? 1;
this.maxLvl = params.maxLvl ?? 1;
if (params.maxLvl) { for (const [multName, mult] of getRecordEntries(params.mults)) this.mults[multName] = mult;
this.maxLvl = params.maxLvl;
} }
if (params.successChanceAll) { calculateCost(currentLevel: number, count = 1 as PositiveSafeInteger): number {
this.successChanceAll = params.successChanceAll; if (currentLevel + count > this.maxLvl) return Infinity;
}
if (params.successChanceStealth) {
this.successChanceStealth = params.successChanceStealth;
}
if (params.successChanceKill) {
this.successChanceKill = params.successChanceKill;
}
if (params.successChanceContract) {
this.successChanceContract = params.successChanceContract;
}
if (params.successChanceOperation) {
this.successChanceOperation = params.successChanceOperation;
}
if (params.successChanceEstimate) {
this.successChanceEstimate = params.successChanceEstimate;
}
if (params.actionTime) {
this.actionTime = params.actionTime;
}
if (params.effHack) {
this.effHack = params.effHack;
}
if (params.effStr) {
this.effStr = params.effStr;
}
if (params.effDef) {
this.effDef = params.effDef;
}
if (params.effDex) {
this.effDex = params.effDex;
}
if (params.effAgi) {
this.effAgi = params.effAgi;
}
if (params.effCha) {
this.effCha = params.effCha;
}
if (params.stamina) {
this.stamina = params.stamina;
}
if (params.money) {
this.money = params.money;
}
if (params.expGain) {
this.expGain = params.expGain;
}
}
calculateCost(currentLevel: number, count = 1): number {
//Recursive mode does not handle invalid inputs properly, but it should never //Recursive mode does not handle invalid inputs properly, but it should never
//be possible for it to run with them. For the sake of not crashing the game, //be possible for it to run with them. For the sake of not crashing the game,
const recursiveMode = (currentLevel: number, count: number): number => { const recursiveMode = (currentLevel: number, count: PositiveSafeInteger): number => {
if (count <= 1) { if (count <= 1) {
return Math.floor((this.baseCost + currentLevel * this.costInc) * currentNodeMults.BladeburnerSkillCost); return Math.floor((this.baseCost + currentLevel * this.costInc) * currentNodeMults.BladeburnerSkillCost);
} else { } else {
const thisUpgrade = Math.floor( const thisUpgrade = Math.floor(
(this.baseCost + currentLevel * this.costInc) * currentNodeMults.BladeburnerSkillCost, (this.baseCost + currentLevel * this.costInc) * currentNodeMults.BladeburnerSkillCost,
); );
return this.calculateCost(currentLevel + 1, count - 1) + thisUpgrade; return this.calculateCost(currentLevel + 1, (count - 1) as PositiveSafeInteger) + thisUpgrade;
} }
}; };
@ -166,26 +68,16 @@ export class Skill {
} }
} }
getMultiplier(name: string): number { canUpgrade(bladeburner: Bladeburner, count = 1): Availability<{ cost: number }> {
if (name === "successChanceAll") return this.successChanceAll; const currentLevel = bladeburner.skills[this.name] ?? 0;
if (name === "successChanceStealth") return this.successChanceStealth; if (!isPositiveSafeInteger(count)) return { error: `Invalid upgrade count ${count}` };
if (name === "successChanceKill") return this.successChanceKill; if (currentLevel + count > this.maxLvl) return { error: `Upgraded level ${currentLevel + count} exceeds max` };
if (name === "successChanceContract") return this.successChanceContract; const cost = this.calculateCost(currentLevel, count);
if (name === "successChanceOperation") return this.successChanceOperation; if (cost > bladeburner.skillPoints) return { error: `Insufficient skill points for upgrade` };
if (name === "successChanceEstimate") return this.successChanceEstimate; return { available: true, cost };
}
if (name === "actionTime") return this.actionTime; getMultiplier(name: BladeMultName): number {
return this.mults[name] ?? 0;
if (name === "effHack") return this.effHack;
if (name === "effStr") return this.effStr;
if (name === "effDef") return this.effDef;
if (name === "effDex") return this.effDex;
if (name === "effAgi") return this.effAgi;
if (name === "effCha") return this.effCha;
if (name === "stamina") return this.stamina;
if (name === "money") return this.money;
if (name === "expGain") return this.expGain;
return 0;
} }
} }

31
src/Bladeburner/Types.ts Normal file

@ -0,0 +1,31 @@
import type { BlackOperation, Contract, GeneralAction, Operation } from "./Actions";
import type {
BladeActionType,
BladeBlackOpName,
BladeContractName,
BladeOperationName,
BladeGeneralActionName,
} from "@enums";
export interface SuccessChanceParams {
/** Whether the success chance should be based on estimated statistics */
est: boolean;
}
type AvailabilitySuccess<T extends object> = { available: true } & T;
type AvailabilityFailure = { available?: undefined; error: string };
export type Availability<T extends object = object> = AvailabilitySuccess<T> | AvailabilityFailure;
type AttemptSuccess<T extends object> = { success: true; message?: string } & T;
type AttemptFailure = { success?: undefined; message: string };
export type Attempt<T extends object = object> = AttemptSuccess<T> | AttemptFailure;
export type Action = Contract | Operation | BlackOperation | GeneralAction;
export type ActionIdentifier =
| { type: BladeActionType.blackOp; name: BladeBlackOpName }
| { type: BladeActionType.contract; name: BladeContractName }
| { type: BladeActionType.operation; name: BladeOperationName }
| { type: BladeActionType.general; name: BladeGeneralActionName };
export type LevelableAction = Contract | Operation;

@ -1,29 +0,0 @@
// Action Identifier enum
export const ActionTypes: {
[key: string]: number;
Idle: number;
Contract: number;
Operation: number;
BlackOp: number;
BlackOperation: number;
Training: number;
Recruitment: number;
FieldAnalysis: number;
"Field Analysis": number;
Diplomacy: number;
"Hyperbolic Regeneration Chamber": number;
"Incite Violence": number;
} = {
Idle: 1,
Contract: 2,
Operation: 3,
BlackOp: 4,
BlackOperation: 4,
Training: 5,
Recruitment: 6,
FieldAnalysis: 7,
"Field Analysis": 7,
Diplomacy: 8,
"Hyperbolic Regeneration Chamber": 9,
"Incite Violence": 10,
};

@ -0,0 +1,737 @@
import { BlackOperation } from "../Actions/BlackOperation";
import { BladeBlackOpName, CityName, FactionName } from "@enums";
export const BlackOperations: Record<BladeBlackOpName, BlackOperation> = {
[BladeBlackOpName.OperationTyphoon]: new BlackOperation({
name: BladeBlackOpName.OperationTyphoon,
n: 0,
baseDifficulty: 2000,
reqdRank: 2.5e3,
rankGain: 50,
rankLoss: 10,
hpLoss: 100,
weights: {
hacking: 0.1,
strength: 0.2,
defense: 0.2,
dexterity: 0.2,
agility: 0.2,
charisma: 0,
intelligence: 0.1,
},
decays: {
hacking: 0.6,
strength: 0.8,
defense: 0.8,
dexterity: 0.8,
agility: 0.8,
charisma: 0,
intelligence: 0.75,
},
isKill: true,
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.\n\n" +
`The goal of ${BladeBlackOpName.OperationTyphoon} 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.",
}),
[BladeBlackOpName.OperationZero]: new BlackOperation({
name: BladeBlackOpName.OperationZero,
n: 1,
baseDifficulty: 2500,
reqdRank: 5e3,
rankGain: 60,
rankLoss: 15,
hpLoss: 50,
weights: {
hacking: 0.2,
strength: 0.15,
defense: 0.15,
dexterity: 0.2,
agility: 0.2,
charisma: 0,
intelligence: 0.1,
},
decays: {
hacking: 0.6,
strength: 0.8,
defense: 0.8,
dexterity: 0.8,
agility: 0.8,
charisma: 0,
intelligence: 0.75,
},
isStealth: true,
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.\n\n" +
`The goal of ${BladeBlackOpName.OperationZero} 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.",
}),
[BladeBlackOpName.OperationX]: new BlackOperation({
name: BladeBlackOpName.OperationX,
n: 2,
baseDifficulty: 3000,
reqdRank: 7.5e3,
rankGain: 75,
rankLoss: 15,
hpLoss: 100,
weights: {
hacking: 0.1,
strength: 0.2,
defense: 0.2,
dexterity: 0.2,
agility: 0.2,
charisma: 0,
intelligence: 0.1,
},
decays: {
hacking: 0.6,
strength: 0.8,
defense: 0.8,
dexterity: 0.8,
agility: 0.8,
charisma: 0,
intelligence: 0.75,
},
isKill: true,
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.\n\n" +
"Samizdat has done a good job of keeping hidden and anonymous. However, we've just received intelligence that " +
`their base of operations is in ${CityName.Ishima}'s underground sewer systems. Your task is to investigate the ` +
"sewer systems, and eliminate Samizdat. They must never publish anything again.",
}),
[BladeBlackOpName.OperationTitan]: new BlackOperation({
name: BladeBlackOpName.OperationTitan,
n: 3,
baseDifficulty: 4000,
reqdRank: 10e3,
rankGain: 100,
rankLoss: 20,
hpLoss: 100,
weights: {
hacking: 0.1,
strength: 0.2,
defense: 0.2,
dexterity: 0.2,
agility: 0.2,
charisma: 0,
intelligence: 0.1,
},
decays: {
hacking: 0.6,
strength: 0.8,
defense: 0.8,
dexterity: 0.8,
agility: 0.8,
charisma: 0,
intelligence: 0.75,
},
isKill: true,
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 Laboratories' vast resources is potentially very " +
"dangerous.\n\n" +
`Your goal is to enter and destroy the Bioengineering department's facility in ${CityName.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.",
}),
[BladeBlackOpName.OperationAres]: new BlackOperation({
name: BladeBlackOpName.OperationAres,
n: 4,
baseDifficulty: 5000,
reqdRank: 12.5e3,
rankGain: 125,
rankLoss: 20,
hpLoss: 200,
weights: {
hacking: 0,
strength: 0.25,
defense: 0.25,
dexterity: 0.25,
agility: 0.25,
charisma: 0,
intelligence: 0,
},
decays: {
hacking: 0,
strength: 0.8,
defense: 0.8,
dexterity: 0.8,
agility: 0.8,
charisma: 0,
intelligence: 0.75,
},
isKill: true,
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.\n\n" +
"Your task is to intercept the deal. Leave no survivors.",
}),
[BladeBlackOpName.OperationArchangel]: new BlackOperation({
name: BladeBlackOpName.OperationArchangel,
n: 5,
baseDifficulty: 7500,
reqdRank: 15e3,
rankGain: 200,
rankLoss: 20,
hpLoss: 25,
weights: {
hacking: 0,
strength: 0.2,
defense: 0.2,
dexterity: 0.3,
agility: 0.3,
charisma: 0,
intelligence: 0,
},
decays: {
hacking: 0,
strength: 0.8,
defense: 0.8,
dexterity: 0.8,
agility: 0.8,
charisma: 0,
intelligence: 0.75,
},
isKill: true,
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.\n\n" +
"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.",
}),
[BladeBlackOpName.OperationJuggernaut]: new BlackOperation({
name: BladeBlackOpName.OperationJuggernaut,
n: 6,
baseDifficulty: 10e3,
reqdRank: 20e3,
rankGain: 300,
rankLoss: 40,
hpLoss: 300,
weights: {
hacking: 0,
strength: 0.25,
defense: 0.25,
dexterity: 0.25,
agility: 0.25,
charisma: 0,
intelligence: 0,
},
decays: {
hacking: 0,
strength: 0.8,
defense: 0.8,
dexterity: 0.8,
agility: 0.8,
charisma: 0,
intelligence: 0.75,
},
isKill: true,
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 " +
`${CityName.Sector12}. We also have reason to believe they 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.\n\n" +
"Your mission is to eradicate Juggernaut and his followers.",
}),
[BladeBlackOpName.OperationRedDragon]: new BlackOperation({
name: BladeBlackOpName.OperationRedDragon,
n: 7,
baseDifficulty: 12.5e3,
reqdRank: 25e3,
rankGain: 500,
rankLoss: 50,
hpLoss: 500,
weights: {
hacking: 0.05,
strength: 0.2,
defense: 0.2,
dexterity: 0.25,
agility: 0.25,
charisma: 0,
intelligence: 0.05,
},
decays: {
hacking: 0.6,
strength: 0.8,
defense: 0.8,
dexterity: 0.8,
agility: 0.8,
charisma: 0,
intelligence: 0.75,
},
isKill: true,
desc:
`The ${FactionName.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.\n\n" +
`Your task is to infiltrate and destroy the ${FactionName.Tetrads}' base of operations in Los Angeles. ` +
"Intelligence tells us that their base houses one of their Synthoid manufacturing units.",
}),
[BladeBlackOpName.OperationK]: new BlackOperation({
name: BladeBlackOpName.OperationK,
n: 8,
baseDifficulty: 15e3,
reqdRank: 30e3,
rankGain: 750,
rankLoss: 60,
hpLoss: 1000,
weights: {
hacking: 0.05,
strength: 0.2,
defense: 0.2,
dexterity: 0.25,
agility: 0.25,
charisma: 0,
intelligence: 0.05,
},
decays: {
hacking: 0.6,
strength: 0.8,
defense: 0.8,
dexterity: 0.8,
agility: 0.8,
charisma: 0,
intelligence: 0.75,
},
isKill: true,
desc:
"CODE RED SITUATION. Our intelligence tells us that VitaLife has discovered a new android cloning technology. " +
"This technology is supposedly capable of cloning Synthoids, 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.\n\n" +
"We do not have the power or jurisdiction to shut 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.",
}),
[BladeBlackOpName.OperationDeckard]: new BlackOperation({
name: BladeBlackOpName.OperationDeckard,
n: 9,
baseDifficulty: 20e3,
reqdRank: 40e3,
rankGain: 1e3,
rankLoss: 75,
hpLoss: 200,
weights: {
hacking: 0,
strength: 0.24,
defense: 0.24,
dexterity: 0.24,
agility: 0.24,
charisma: 0,
intelligence: 0.04,
},
decays: {
hacking: 0,
strength: 0.8,
defense: 0.8,
dexterity: 0.8,
agility: 0.8,
charisma: 0,
intelligence: 0.75,
},
isKill: true,
desc:
"Despite your success in eliminating VitaLife's new android-replicating technology in " +
`${BladeBlackOpName.OperationK}, 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.\n\n" +
`The goal of ${BladeBlackOpName.OperationDeckard} is to hunt down these Synthoids and retire them. I don't need to ` +
"tell you how critical this mission is.",
}),
[BladeBlackOpName.OperationTyrell]: new BlackOperation({
name: BladeBlackOpName.OperationTyrell,
n: 10,
baseDifficulty: 25e3,
reqdRank: 50e3,
rankGain: 1.5e3,
rankLoss: 100,
hpLoss: 500,
weights: {
hacking: 0.1,
strength: 0.2,
defense: 0.2,
dexterity: 0.2,
agility: 0.2,
charisma: 0,
intelligence: 0.1,
},
decays: {
hacking: 0.6,
strength: 0.8,
defense: 0.8,
dexterity: 0.8,
agility: 0.8,
charisma: 0,
intelligence: 0.75,
},
isKill: true,
desc:
`A week ago ${FactionName.BladeIndustries} reported a small break-in at one of their ${CityName.Aevum} ` +
`Augmentation storage facilities. We figured out that ${FactionName.TheDarkArmy} 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.\n\n" +
"We cannot have Synthoids upgrading their already-enhanced abilities with Augmentations. Your task is to hunt " +
`down associated ${FactionName.TheDarkArmy} members and eliminate them.`,
}),
[BladeBlackOpName.OperationWallace]: new BlackOperation({
name: BladeBlackOpName.OperationWallace,
n: 11,
baseDifficulty: 30e3,
reqdRank: 75e3,
rankGain: 2e3,
rankLoss: 150,
hpLoss: 1500,
weights: {
hacking: 0,
strength: 0.24,
defense: 0.24,
dexterity: 0.24,
agility: 0.24,
charisma: 0,
intelligence: 0.04,
},
decays: {
hacking: 0.6,
strength: 0.8,
defense: 0.8,
dexterity: 0.8,
agility: 0.8,
charisma: 0,
intelligence: 0.75,
},
isKill: true,
desc:
`Based on information gathered from ${BladeBlackOpName.OperationTyrell}, we've discovered that ` +
`${FactionName.TheDarkArmy} was well aware that there were Synthoids amongst their ranks. Even worse, we believe ` +
`that ${FactionName.TheDarkArmy} is working together with other criminal organizations such as ` +
`${FactionName.TheSyndicate} and that they are planning some sort of large-scale takeover of multiple major ` +
`cities, most notably ${CityName.Aevum}. We suspect that Synthoids have infiltrated the ranks of these criminal ` +
"factions and are trying to stage another Synthoid uprising.\n\n" +
"The best way to deal with this is to prevent it before it even happens. The goal of " +
`${BladeBlackOpName.OperationWallace} is to destroy ${FactionName.TheDarkArmy} and Syndicate factions in ` +
`${CityName.Aevum} immediately. Leave no survivors.`,
}),
[BladeBlackOpName.OperationShoulderOfOrion]: new BlackOperation({
name: BladeBlackOpName.OperationShoulderOfOrion,
n: 12,
baseDifficulty: 35e3,
reqdRank: 100e3,
rankGain: 2.5e3,
rankLoss: 500,
hpLoss: 1500,
weights: {
hacking: 0.1,
strength: 0.2,
defense: 0.2,
dexterity: 0.2,
agility: 0.2,
charisma: 0,
intelligence: 0.1,
},
decays: {
hacking: 0.6,
strength: 0.8,
defense: 0.8,
dexterity: 0.8,
agility: 0.8,
charisma: 0,
intelligence: 0.75,
},
isStealth: true,
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.\n\n" +
"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.",
}),
[BladeBlackOpName.OperationHyron]: new BlackOperation({
name: BladeBlackOpName.OperationHyron,
n: 13,
baseDifficulty: 40e3,
reqdRank: 125e3,
rankGain: 3e3,
rankLoss: 1e3,
hpLoss: 500,
weights: {
hacking: 0.1,
strength: 0.2,
defense: 0.2,
dexterity: 0.2,
agility: 0.2,
charisma: 0,
intelligence: 0.1,
},
decays: {
hacking: 0.6,
strength: 0.8,
defense: 0.8,
dexterity: 0.8,
agility: 0.8,
charisma: 0,
intelligence: 0.75,
},
isKill: true,
desc:
`Our intelligence tells us that ${FactionName.FulcrumSecretTechnologies} 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.\n\n" +
"I do not need to remind you why sentient-level AIs pose a serious threat to all of mankind.\n\n" +
`The research for this project is being conducted at one of ${FactionName.FulcrumSecretTechnologies} secret ` +
`facilities in ${CityName.Aevum}, codenamed 'Alpha Ranch'. Infiltrate the compound, delete and destroy the work, ` +
"and then find and kill the project lead.",
}),
[BladeBlackOpName.OperationMorpheus]: new BlackOperation({
name: BladeBlackOpName.OperationMorpheus,
n: 14,
baseDifficulty: 45e3,
reqdRank: 150e3,
rankGain: 4e3,
rankLoss: 1e3,
hpLoss: 100,
weights: {
hacking: 0.05,
strength: 0.15,
defense: 0.15,
dexterity: 0.3,
agility: 0.3,
charisma: 0,
intelligence: 0.05,
},
decays: {
hacking: 0.6,
strength: 0.8,
defense: 0.8,
dexterity: 0.8,
agility: 0.8,
charisma: 0,
intelligence: 0.75,
},
isStealth: true,
desc:
"DreamSense Technologies is an advertising company that uses special technology to transmit their ads into the " +
"people's dreams and subconscious. They do this using broadcast transmitter towers. Based on information from our " +
`agents and informants in ${CityName.Chongqing}, 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.\n\n" +
"The mission is to destroy this broadcast tower. Speed and stealth are of the utmost importance for this.",
}),
[BladeBlackOpName.OperationIonStorm]: new BlackOperation({
name: BladeBlackOpName.OperationIonStorm,
n: 15,
baseDifficulty: 50e3,
reqdRank: 175e3,
rankGain: 5e3,
rankLoss: 1e3,
hpLoss: 5000,
weights: {
hacking: 0,
strength: 0.24,
defense: 0.24,
dexterity: 0.24,
agility: 0.24,
charisma: 0,
intelligence: 0.04,
},
decays: {
hacking: 0.6,
strength: 0.8,
defense: 0.8,
dexterity: 0.8,
agility: 0.8,
charisma: 0,
intelligence: 0.75,
},
isKill: true,
desc:
"Our analysts have uncovered a gathering of MK-VI Synthoids that have taken up residence in the " +
`${CityName.Sector12} 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.\n\n" +
`This is a full-scale assault operation to find and retire all of these Synthoids in the ${CityName.Sector12} ` +
"Slums.",
}),
[BladeBlackOpName.OperationAnnihilus]: new BlackOperation({
name: BladeBlackOpName.OperationAnnihilus,
n: 16,
baseDifficulty: 55e3,
reqdRank: 200e3,
rankGain: 7.5e3,
rankLoss: 1e3,
hpLoss: 10e3,
weights: {
hacking: 0,
strength: 0.24,
defense: 0.24,
dexterity: 0.24,
agility: 0.24,
charisma: 0,
intelligence: 0.04,
},
decays: {
hacking: 0.6,
strength: 0.8,
defense: 0.8,
dexterity: 0.8,
agility: 0.8,
charisma: 0,
intelligence: 0.75,
},
isKill: true,
desc:
"Our superiors have ordered us to eradicate everything and everyone in an underground facility located in " +
`${CityName.Aevum}. They tell us that the facility houses many dangerous Synthoids and belongs to a terrorist ` +
`organization called '${FactionName.TheCovenant}'. We have no prior intelligence about this organization, so you ` +
"are going in blind.",
}),
[BladeBlackOpName.OperationUltron]: new BlackOperation({
name: BladeBlackOpName.OperationUltron,
n: 17,
baseDifficulty: 60e3,
reqdRank: 250e3,
rankGain: 10e3,
rankLoss: 2e3,
hpLoss: 10e3,
weights: {
hacking: 0.1,
strength: 0.2,
defense: 0.2,
dexterity: 0.2,
agility: 0.2,
charisma: 0,
intelligence: 0.1,
},
decays: {
hacking: 0.6,
strength: 0.8,
defense: 0.8,
dexterity: 0.8,
agility: 0.8,
charisma: 0,
intelligence: 0.75,
},
isKill: true,
desc:
`${FactionName.OmniTekIncorporated}, 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.\n\n" +
`${FactionName.OmniTekIncorporated} has also told us 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 " +
`${CityName.Volhaven} to form a terrorist group called Ultron.\n\n` +
"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).\n\n" +
"Your task is to find and destroy Ultron.",
}),
[BladeBlackOpName.OperationCenturion]: new BlackOperation({
name: BladeBlackOpName.OperationCenturion,
n: 18,
baseDifficulty: 70e3,
reqdRank: 300e3,
rankGain: 15e3,
rankLoss: 5e3,
hpLoss: 10e3,
weights: {
hacking: 0.1,
strength: 0.2,
defense: 0.2,
dexterity: 0.2,
agility: 0.2,
charisma: 0,
intelligence: 0.1,
},
decays: {
hacking: 0.6,
strength: 0.8,
defense: 0.8,
dexterity: 0.8,
agility: 0.8,
charisma: 0,
intelligence: 0.75,
},
desc:
"D)@#)($M)C0293c40($*)@#D0JUMP3Rm0C<*@#)*$)#02c94830c(#$*D)\n\n" +
"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.\n\n" +
"It seems that the universe is not without a sense of irony.\n\n" +
"D)@#)($M)C0293c40($*)@#D0JUMP3Rm0C<*@#)*$)#02c94830c(#$*D)",
}),
[BladeBlackOpName.OperationVindictus]: new BlackOperation({
name: BladeBlackOpName.OperationVindictus,
n: 19,
baseDifficulty: 75e3,
reqdRank: 350e3,
rankGain: 20e3,
rankLoss: 20e3,
hpLoss: 20e3,
weights: {
hacking: 0.1,
strength: 0.2,
defense: 0.2,
dexterity: 0.2,
agility: 0.2,
charisma: 0,
intelligence: 0.1,
},
decays: {
hacking: 0.6,
strength: 0.8,
defense: 0.8,
dexterity: 0.8,
agility: 0.8,
charisma: 0,
intelligence: 0.75,
},
desc:
"D)@#)($M)C0293c40($*)@#D0JUMP3Rm0C<*@#)*$)#02c94830c(#$*D)\n\n" +
"The bits are all around us. The daemons that hold the Node together can manifest themselves in many different " +
"ways.\n\n" +
"D)@#)($M)C0293c40($*)@#D0JUMP3Rm0C<*@#)*$)#02c94830c(#$*D)",
}),
[BladeBlackOpName.OperationDaedalus]: new BlackOperation({
name: BladeBlackOpName.OperationDaedalus,
n: 20,
baseDifficulty: 80e3,
reqdRank: 400e3,
rankGain: 40e3,
rankLoss: 10e3,
hpLoss: 100e3,
weights: {
hacking: 0.1,
strength: 0.2,
defense: 0.2,
dexterity: 0.2,
agility: 0.2,
charisma: 0,
intelligence: 0.1,
},
decays: {
hacking: 0.6,
strength: 0.8,
defense: 0.8,
dexterity: 0.8,
agility: 0.8,
charisma: 0,
intelligence: 0.75,
},
desc: "Yesterday we obeyed kings and bent our neck to emperors. Today we kneel only to truth.",
}),
};
/** Array for quick lookup by blackop number */
export const blackOpsArray = Object.values(BlackOperations).sort((a, b) => (a.n < b.n ? -1 : 1));
// Verify that all "n" properties match the index in the array
if (!blackOpsArray.every((blackOp, i) => blackOp.n === i)) {
throw new Error("blackOpsArray did not initialize with correct indices");
}

@ -1,302 +0,0 @@
import React from "react";
import { BlackOperationName, CityName, FactionName } from "@enums";
interface IBlackOp {
desc: JSX.Element;
}
export const BlackOperations: Record<string, IBlackOp | undefined> = {
[BlackOperationName.OperationTyphoon]: {
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 {BlackOperationName.OperationTyphoon} 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.
</>
),
},
[BlackOperationName.OperationZero]: {
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 {BlackOperationName.OperationZero} 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.
</>
),
},
[BlackOperationName.OperationX]: {
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 {CityName.Ishima}'s underground sewer systems. Your task is to investigate the
sewer systems, and eliminate Samizdat. They must never publish anything again.
</>
),
},
[BlackOperationName.OperationTitan]: {
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 Laboratories' vast resources is potentially very
dangerous.
<br />
<br />
Your goal is to enter and destroy the Bioengineering department's facility in {CityName.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.
</>
),
},
[BlackOperationName.OperationAres]: {
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.
</>
),
},
[BlackOperationName.OperationArchangel]: {
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.
</>
),
},
[BlackOperationName.OperationJuggernaut]: {
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{" "}
{CityName.Sector12}. We also have reason to believe they 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.
</>
),
},
[BlackOperationName.OperationRedDragon]: {
desc: (
<>
The {FactionName.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 {FactionName.Tetrads}' base of operations in Los Angeles.
Intelligence tells us that their base houses one of their Synthoid manufacturing units.
</>
),
},
[BlackOperationName.OperationK]: {
desc: (
<>
CODE RED SITUATION. Our intelligence tells us that VitaLife has discovered a new android cloning technology.
This technology is supposedly capable of cloning Synthoids, 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 shut 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.
</>
),
},
[BlackOperationName.OperationDeckard]: {
desc: (
<>
Despite your success in eliminating VitaLife's new android-replicating technology in{" "}
{BlackOperationName.OperationK}, 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 {BlackOperationName.OperationDeckard} is to hunt down these Synthoids and retire them. I don't need
to tell you how critical this mission is.
</>
),
},
[BlackOperationName.OperationTyrell]: {
desc: (
<>
A week ago {FactionName.BladeIndustries} reported a small break-in at one of their {CityName.Aevum} Augmentation
storage facilities. We figured out that {FactionName.TheDarkArmy} 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 associated {FactionName.TheDarkArmy} members and eliminate them.
</>
),
},
[BlackOperationName.OperationWallace]: {
desc: (
<>
Based on information gathered from {BlackOperationName.OperationTyrell}, we've discovered that{" "}
{FactionName.TheDarkArmy} was well aware that there were Synthoids amongst their ranks. Even worse, we believe
that {FactionName.TheDarkArmy} is working together with other criminal organizations such as{" "}
{FactionName.TheSyndicate} and that they are planning some sort of large-scale takeover of multiple major
cities, most notably {CityName.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{" "}
{BlackOperationName.OperationWallace} is to destroy {FactionName.TheDarkArmy} and Syndicate factions in{" "}
{CityName.Aevum} immediately. Leave no survivors.
</>
),
},
[BlackOperationName.OperationShoulderOfOrion]: {
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.
</>
),
},
[BlackOperationName.OperationHyron]: {
desc: (
<>
Our intelligence tells us that {FactionName.FulcrumSecretTechnologies} 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 {FactionName.FulcrumSecretTechnologies} secret
facilities in {CityName.Aevum}, codenamed 'Alpha Ranch'. Infiltrate the compound, delete and destroy the work,
and then find and kill the project lead.
</>
),
},
[BlackOperationName.OperationMorpheus]: {
desc: (
<>
DreamSense Technologies is an advertising company that uses special technology to transmit their ads into the
people's dreams and subconscious. They do this using broadcast transmitter towers. Based on information from our
agents and informants in {CityName.Chongqing}, 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 utmost importance for this.
</>
),
},
[BlackOperationName.OperationIonStorm]: {
desc: (
<>
Our analysts have uncovered a gathering of MK-VI Synthoids that have taken up residence in the{" "}
{CityName.Sector12} 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 {CityName.Sector12}{" "}
Slums.
</>
),
},
[BlackOperationName.OperationAnnihilus]: {
desc: (
<>
Our superiors have ordered us to eradicate everything and everyone in an underground facility located in{" "}
{CityName.Aevum}. They tell us that the facility houses many dangerous Synthoids and belongs to a terrorist
organization called '{FactionName.TheCovenant}'. We have no prior intelligence about this organization, so you
are going in blind.
</>
),
},
[BlackOperationName.OperationUltron]: {
desc: (
<>
{FactionName.OmniTekIncorporated}, 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 />
{FactionName.OmniTekIncorporated} has also told us 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{" "}
{CityName.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.
</>
),
},
[BlackOperationName.OperationCenturion]: {
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)"}
</>
),
},
[BlackOperationName.OperationVindictus]: {
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)"}
</>
),
},
[BlackOperationName.OperationDaedalus]: {
desc: <> Yesterday we obeyed kings and bent our neck to emperors. Today we kneel only to truth.</>,
},
};

@ -0,0 +1,121 @@
import { BladeContractName } from "@enums";
import { Contract } from "../Actions/Contract";
import { getRandomInt } from "../../utils/helpers/getRandomInt";
import { assertLoadingType } from "../../utils/TypeAssertion";
export function createContracts(): Record<BladeContractName, Contract> {
return {
[BladeContractName.tracking]: new Contract({
name: BladeContractName.tracking,
desc:
"Identify and locate Synthoids. This contract involves reconnaissance and information-gathering ONLY. Do NOT " +
"engage. Stealth is of the utmost importance.\n\n" +
"Successfully completing Tracking contracts will slightly improve your Synthoid population estimate for whatever " +
"city you are currently in.",
baseDifficulty: 125,
difficultyFac: 1.02,
rewardFac: 1.041,
rankGain: 0.3,
hpLoss: 0.5,
weights: {
hacking: 0,
strength: 0.05,
defense: 0.05,
dexterity: 0.35,
agility: 0.35,
charisma: 0.1,
intelligence: 0.05,
},
decays: {
hacking: 0,
strength: 0.91,
defense: 0.91,
dexterity: 0.91,
agility: 0.91,
charisma: 0.9,
intelligence: 1,
},
isStealth: true,
growthFunction: () => getRandomInt(5, 75) / 10,
minCount: 25,
}),
[BladeContractName.bountyHunter]: new Contract({
name: BladeContractName.bountyHunter,
desc:
"Hunt down and capture fugitive Synthoids. These Synthoids are wanted alive.\n\n" +
"Successfully completing a Bounty Hunter contract will lower the population in your current city, and will also " +
"increase its chaos level.",
baseDifficulty: 250,
difficultyFac: 1.04,
rewardFac: 1.085,
rankGain: 0.9,
hpLoss: 1,
weights: {
hacking: 0,
strength: 0.15,
defense: 0.15,
dexterity: 0.25,
agility: 0.25,
charisma: 0.1,
intelligence: 0.1,
},
decays: {
hacking: 0,
strength: 0.91,
defense: 0.91,
dexterity: 0.91,
agility: 0.91,
charisma: 0.8,
intelligence: 0.9,
},
isKill: true,
growthFunction: () => getRandomInt(5, 75) / 10,
minCount: 5,
}),
[BladeContractName.retirement]: new Contract({
name: BladeContractName.retirement,
desc:
"Hunt down and retire (kill) rogue Synthoids.\n\n" +
"Successfully completing a Retirement contract will lower the population in your current city, and will also " +
"increase its chaos level.",
baseDifficulty: 200,
difficultyFac: 1.03,
rewardFac: 1.065,
rankGain: 0.6,
hpLoss: 1,
weights: {
hacking: 0,
strength: 0.2,
defense: 0.2,
dexterity: 0.2,
agility: 0.2,
charisma: 0.1,
intelligence: 0.1,
},
decays: {
hacking: 0,
strength: 0.91,
defense: 0.91,
dexterity: 0.91,
agility: 0.91,
charisma: 0.8,
intelligence: 0.9,
},
isKill: true,
growthFunction: () => getRandomInt(5, 75) / 10,
minCount: 5,
}),
};
}
export function loadContractsData(data: unknown, contracts: Record<BladeContractName, Contract>) {
// loading data as "unknown" and typechecking it down is probably not necessary
// but this will prevent crashes even with malformed savedata
if (!data || typeof data !== "object") return;
assertLoadingType<Record<BladeContractName, unknown>>(data);
for (const contractName of Object.values(BladeContractName)) {
const loadedContract = data[contractName];
if (!(loadedContract instanceof Contract)) continue;
contracts[contractName].loadData(loadedContract);
}
}

@ -1,42 +0,0 @@
import React from "react";
interface IContract {
desc: JSX.Element;
}
export const Contracts: Record<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,58 @@
import { BladeGeneralActionName } from "@enums";
import { GeneralAction } from "../Actions/GeneralAction";
import { BladeburnerConstants } from "./Constants";
export const GeneralActions: Record<BladeGeneralActionName, GeneralAction> = {
[BladeGeneralActionName.training]: new GeneralAction({
name: BladeGeneralActionName.training,
getActionTime: () => 30,
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.",
}),
[BladeGeneralActionName.fieldAnalysis]: new GeneralAction({
name: BladeGeneralActionName.fieldAnalysis,
getActionTime: () => 30,
desc:
"Mine and analyze Synthoid-related data. This improves the Bladeburner unit's intelligence on Synthoid locations " +
"and activities. Completing this action will improve the accuracy of your Synthoid population estimated in the " +
"current city.\n\n" +
"Does NOT require stamina.",
}),
[BladeGeneralActionName.recruitment]: new GeneralAction({
name: BladeGeneralActionName.recruitment,
getActionTime: function (bladeburner, person) {
const effCharisma = bladeburner.getEffectiveSkillLevel(person, "charisma");
const charismaFactor = Math.pow(effCharisma, 0.81) + effCharisma / 90;
return Math.max(10, Math.round(BladeburnerConstants.BaseRecruitmentTimeNeeded - charismaFactor));
},
getSuccessChance: function (bladeburner, person) {
return Math.pow(person.skills.charisma, 0.45) / (bladeburner.teamSize - bladeburner.sleeveSize + 1);
},
desc:
"Attempt to recruit members for your Bladeburner team. These members can help you conduct operations.\n\n" +
"Does NOT require stamina.",
}),
[BladeGeneralActionName.diplomacy]: new GeneralAction({
name: BladeGeneralActionName.diplomacy,
getActionTime: () => 60,
desc:
"Improve diplomatic relations with the Synthoid population. Completing this action will reduce the Chaos level in " +
"your current city.\n\n" +
"Does NOT require stamina.",
}),
[BladeGeneralActionName.hyperbolicRegen]: new GeneralAction({
name: BladeGeneralActionName.hyperbolicRegen,
getActionTime: () => 60,
desc:
"Enter cryogenic stasis using the Bladeburner division's hi-tech Regeneration Chamber. This will slowly heal your " +
"wounds and slightly increase your stamina.",
}),
[BladeGeneralActionName.inciteViolence]: new GeneralAction({
name: BladeGeneralActionName.inciteViolence,
getActionTime: () => 60,
desc:
"Purposefully stir trouble in the synthoid community in order to gain a political edge. This will generate " +
"additional contracts and operations, at the cost of increased Chaos.",
}),
};

@ -1,97 +0,0 @@
import React from "react";
import { newWorkStats, WorkStats } from "../../Work/WorkStats";
interface IGeneral {
desc: JSX.Element;
exp: WorkStats;
}
export const GeneralActions: Record<string, IGeneral | 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.
</>
),
exp: newWorkStats({
strExp: 30,
defExp: 30,
dexExp: 30,
agiExp: 30,
}),
},
"Field Analysis": {
desc: (
<>
Mine and analyze Synthoid-related data. This improves the Bladeburner unit's 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.
</>
),
exp: newWorkStats({
hackExp: 20,
chaExp: 20,
}),
},
Recruitment: {
desc: (
<>
Attempt to recruit members for your Bladeburner team. These members can help you conduct operations.
<br />
<br />
Does NOT require stamina.
</>
),
exp: newWorkStats({
chaExp: 120,
}),
},
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.
</>
),
exp: newWorkStats({
chaExp: 120,
}),
},
"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 />
</>
),
exp: newWorkStats(),
},
"Incite Violence": {
desc: (
<>
Purposefully stir trouble in the synthoid community in order to gain a political edge. This will generate
additional contracts and operations, at the cost of increased Chaos.
</>
),
exp: newWorkStats({
strExp: 10,
defExp: 10,
dexExp: 10,
agiExp: 10,
chaExp: 10,
}),
},
};

@ -1,24 +0,0 @@
import { getRandomInt } from "../../utils/helpers/getRandomInt";
export const Growths: {
[key: string]: (() => number) | undefined;
["Tracking"]: () => number;
["Bounty Hunter"]: () => number;
["Retirement"]: () => number;
["Investigation"]: () => number;
["Undercover Operation"]: () => number;
["Sting Operation"]: () => number;
["Raid"]: () => number;
["Stealth Retirement Operation"]: () => number;
["Assassination"]: () => number;
} = {
Tracking: () => getRandomInt(5, 75) / 10,
"Bounty Hunter": () => getRandomInt(5, 75) / 10,
Retirement: () => getRandomInt(5, 75) / 10,
Investigation: () => getRandomInt(10, 40) / 10,
"Undercover Operation": () => getRandomInt(10, 40) / 10,
"Sting Operation": () => getRandomInt(3, 40) / 10,
Raid: () => getRandomInt(2, 40) / 10,
"Stealth Retirement Operation": () => getRandomInt(1, 20) / 10,
Assassination: () => getRandomInt(1, 20) / 10,
};

@ -0,0 +1,220 @@
import { BladeOperationName } from "@enums";
import { Operation } from "../Actions/Operation";
import { getRandomInt } from "../../utils/helpers/getRandomInt";
import { LevelableActionClass } from "../Actions/LevelableAction";
import { assertLoadingType } from "../../utils/TypeAssertion";
export function createOperations(): Record<BladeOperationName, Operation> {
return {
[BladeOperationName.investigation]: new Operation({
name: BladeOperationName.investigation,
desc:
"As a field agent, investigate and identify Synthoid populations, movements, and operations.\n\n" +
"Successful Investigation ops will increase the accuracy of your synthoid data.\n\n" +
"You will NOT lose HP from failed Investigation ops.",
baseDifficulty: 400,
difficultyFac: 1.03,
rewardFac: 1.07,
rankGain: 2.2,
rankLoss: 0.2,
weights: {
hacking: 0.25,
strength: 0.05,
defense: 0.05,
dexterity: 0.2,
agility: 0.1,
charisma: 0.25,
intelligence: 0.1,
},
decays: {
hacking: 0.85,
strength: 0.9,
defense: 0.9,
dexterity: 0.9,
agility: 0.9,
charisma: 0.7,
intelligence: 0.9,
},
isStealth: true,
growthFunction: () => getRandomInt(10, 40) / 10,
maxCount: 100,
}),
[BladeOperationName.undercover]: new Operation({
name: BladeOperationName.undercover,
desc:
"Conduct undercover operations to identify hidden and underground Synthoid communities and organizations.\n\n" +
"Successful Undercover ops will increase the accuracy of your synthoid data.",
baseDifficulty: 500,
difficultyFac: 1.04,
rewardFac: 1.09,
rankGain: 4.4,
rankLoss: 0.4,
hpLoss: 2,
weights: {
hacking: 0.2,
strength: 0.05,
defense: 0.05,
dexterity: 0.2,
agility: 0.2,
charisma: 0.2,
intelligence: 0.1,
},
decays: {
hacking: 0.8,
strength: 0.9,
defense: 0.9,
dexterity: 0.9,
agility: 0.9,
charisma: 0.7,
intelligence: 0.9,
},
isStealth: true,
growthFunction: () => getRandomInt(10, 40) / 10,
maxCount: 100,
}),
[BladeOperationName.sting]: new Operation({
name: BladeOperationName.sting,
desc: "Conduct a sting operation to bait and capture particularly notorious Synthoid criminals.",
baseDifficulty: 650,
difficultyFac: 1.04,
rewardFac: 1.095,
rankGain: 5.5,
rankLoss: 0.5,
hpLoss: 2.5,
weights: {
hacking: 0.25,
strength: 0.05,
defense: 0.05,
dexterity: 0.25,
agility: 0.1,
charisma: 0.2,
intelligence: 0.1,
},
decays: {
hacking: 0.8,
strength: 0.85,
defense: 0.85,
dexterity: 0.85,
agility: 0.85,
charisma: 0.7,
intelligence: 0.9,
},
isStealth: true,
growthFunction: () => getRandomInt(3, 40) / 10,
}),
[BladeOperationName.raid]: new Operation({
name: BladeOperationName.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,
difficultyFac: 1.045,
rewardFac: 1.1,
rankGain: 55,
rankLoss: 2.5,
hpLoss: 50,
weights: {
hacking: 0.1,
strength: 0.2,
defense: 0.2,
dexterity: 0.2,
agility: 0.2,
charisma: 0,
intelligence: 0.1,
},
decays: {
hacking: 0.7,
strength: 0.8,
defense: 0.8,
dexterity: 0.8,
agility: 0.8,
charisma: 0,
intelligence: 0.9,
},
isKill: true,
growthFunction: () => getRandomInt(2, 40) / 10,
getAvailability: function (bladeburner) {
if (bladeburner.getCurrentCity().comms < 1) return { error: "No Synthoid communities in current city" };
return LevelableActionClass.prototype.getAvailability.call(this, bladeburner);
},
}),
[BladeOperationName.stealthRetirement]: new Operation({
name: BladeOperationName.stealthRetirement,
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,
difficultyFac: 1.05,
rewardFac: 1.11,
rankGain: 22,
rankLoss: 2,
hpLoss: 10,
weights: {
hacking: 0.1,
strength: 0.1,
defense: 0.1,
dexterity: 0.3,
agility: 0.3,
charisma: 0,
intelligence: 0.1,
},
decays: {
hacking: 0.7,
strength: 0.8,
defense: 0.8,
dexterity: 0.8,
agility: 0.8,
charisma: 0,
intelligence: 0.9,
},
isStealth: true,
isKill: true,
growthFunction: () => getRandomInt(1, 20) / 10,
}),
[BladeOperationName.assassination]: new Operation({
name: BladeOperationName.assassination,
desc:
"Assassinate Synthoids that have been identified as important, high-profile social and political leaders in the " +
"Synthoid communities.",
baseDifficulty: 1500,
difficultyFac: 1.06,
rewardFac: 1.14,
rankGain: 44,
rankLoss: 4,
hpLoss: 5,
weights: {
hacking: 0.1,
strength: 0.1,
defense: 0.1,
dexterity: 0.3,
agility: 0.3,
charisma: 0,
intelligence: 0.1,
},
decays: {
hacking: 0.6,
strength: 0.8,
defense: 0.8,
dexterity: 0.8,
agility: 0.8,
charisma: 0,
intelligence: 0.8,
},
isStealth: true,
isKill: true,
growthFunction: () => getRandomInt(1, 20) / 10,
}),
};
}
export function loadOperationsData(data: unknown, operations: Record<BladeOperationName, Operation>) {
// loading data as "unknown" and typechecking it down is probably not necessary
// but this will prevent crashes even with malformed savedata
if (!data || typeof data !== "object") return;
assertLoadingType<Record<BladeOperationName, unknown>>(data);
for (const operationName of Object.values(BladeOperationName)) {
const loadedOperation = data[operationName];
if (!(loadedOperation instanceof Operation)) continue;
operations[operationName].loadData(loadedOperation);
}
}

@ -1,58 +0,0 @@
import React from "react";
interface IOperation {
desc: JSX.Element;
}
export const Operations: Record<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.
</>
),
},
};

@ -1,16 +0,0 @@
export const SkillNames = {
BladesIntuition: "Blade's Intuition",
Cloak: "Cloak",
Marksman: "Marksman",
WeaponProficiency: "Weapon Proficiency",
ShortCircuit: "Short-Circuit",
DigitalObserver: "Digital Observer",
Tracer: "Tracer",
Overclock: "Overclock",
Reaper: "Reaper",
EvasiveSystem: "Evasive System",
Datamancer: "Datamancer",
CybersEdge: "Cyber's Edge",
HandsOfMidas: "Hands of Midas",
Hyperdrive: "Hyperdrive",
};

@ -1,78 +1,77 @@
import { Skill } from "./Skill"; import { BladeMultName, BladeSkillName } from "@enums";
import { SkillNames } from "./data/SkillNames"; import { Skill } from "../Skill";
export const Skills: Record<string, Skill> = {}; export const Skills: Record<BladeSkillName, Skill> = {
[BladeSkillName.bladesIntuition]: new Skill({
(function () { name: BladeSkillName.bladesIntuition,
Skills[SkillNames.BladesIntuition] = new Skill({
name: SkillNames.BladesIntuition,
desc: "Each level of this skill increases your success chance for all Contracts, Operations, and BlackOps by 3%", desc: "Each level of this skill increases your success chance for all Contracts, Operations, and BlackOps by 3%",
baseCost: 3, baseCost: 3,
costInc: 2.1, costInc: 2.1,
successChanceAll: 3, mults: { [BladeMultName.successChanceAll]: 3 },
}); }),
Skills[SkillNames.Cloak] = new Skill({ [BladeSkillName.cloak]: new Skill({
name: SkillNames.Cloak, name: BladeSkillName.cloak,
desc: desc:
"Each level of this skill increases your " + "Each level of this skill increases your " +
"success chance in stealth-related Contracts, Operations, and BlackOps by 5.5%", "success chance in stealth-related Contracts, Operations, and BlackOps by 5.5%",
baseCost: 2, baseCost: 2,
costInc: 1.1, costInc: 1.1,
successChanceStealth: 5.5, mults: { [BladeMultName.successChanceStealth]: 5.5 },
}); }),
Skills[SkillNames.ShortCircuit] = new Skill({ [BladeSkillName.shortCircuit]: new Skill({
name: SkillNames.ShortCircuit, name: BladeSkillName.shortCircuit,
desc: desc:
"Each level of this skill increases your success chance " + "Each level of this skill increases your success chance " +
"in Contracts, Operations, and BlackOps that involve retirement by 5.5%", "in Contracts, Operations, and BlackOps that involve retirement by 5.5%",
baseCost: 2, baseCost: 2,
costInc: 2.1, costInc: 2.1,
successChanceKill: 5.5, mults: { [BladeMultName.successChanceKill]: 5.5 },
}); }),
Skills[SkillNames.DigitalObserver] = new Skill({ [BladeSkillName.digitalObserver]: new Skill({
name: SkillNames.DigitalObserver, name: BladeSkillName.digitalObserver,
desc: "Each level of this skill increases your success chance in all Operations and BlackOps by 4%", desc: "Each level of this skill increases your success chance in all Operations and BlackOps by 4%",
baseCost: 2, baseCost: 2,
costInc: 2.1, costInc: 2.1,
successChanceOperation: 4, mults: { [BladeMultName.successChanceOperation]: 4 },
}); }),
Skills[SkillNames.Tracer] = new Skill({ [BladeSkillName.tracer]: new Skill({
name: SkillNames.Tracer, name: BladeSkillName.tracer,
desc: "Each level of this skill increases your success chance in all Contracts by 4%", desc: "Each level of this skill increases your success chance in all Contracts by 4%",
baseCost: 2, baseCost: 2,
costInc: 2.1, costInc: 2.1,
successChanceContract: 4, mults: { [BladeMultName.successChanceContract]: 4 },
}); }),
Skills[SkillNames.Overclock] = new Skill({ [BladeSkillName.overclock]: new Skill({
name: SkillNames.Overclock, name: BladeSkillName.overclock,
desc: desc:
"Each level of this skill decreases the time it takes " + "Each level of this skill decreases the time it takes " +
"to attempt a Contract, Operation, and BlackOp by 1% (Max Level: 90)", "to attempt a Contract, Operation, and BlackOp by 1% (Max Level: 90)",
baseCost: 3, baseCost: 3,
costInc: 1.4, costInc: 1.4,
maxLvl: 90, maxLvl: 90,
actionTime: 1, mults: { [BladeMultName.actionTime]: -1 },
}); }),
Skills[SkillNames.Reaper] = new Skill({ [BladeSkillName.reaper]: new Skill({
name: SkillNames.Reaper, name: BladeSkillName.reaper,
desc: "Each level of this skill increases your effective combat stats for Bladeburner actions by 2%", desc: "Each level of this skill increases your effective combat stats for Bladeburner actions by 2%",
baseCost: 2, baseCost: 2,
costInc: 2.1, costInc: 2.1,
effStr: 2, mults: {
effDef: 2, [BladeMultName.effStr]: 2,
effDex: 2, [BladeMultName.effDef]: 2,
effAgi: 2, [BladeMultName.effDex]: 2,
}); [BladeMultName.effAgi]: 2,
Skills[SkillNames.EvasiveSystem] = new Skill({ },
name: SkillNames.EvasiveSystem, }),
[BladeSkillName.evasiveSystem]: new Skill({
name: BladeSkillName.evasiveSystem,
desc: "Each level of this skill increases your effective dexterity and agility for Bladeburner actions by 4%", desc: "Each level of this skill increases your effective dexterity and agility for Bladeburner actions by 4%",
baseCost: 2, baseCost: 2,
costInc: 2.1, costInc: 2.1,
effDex: 4, mults: { [BladeMultName.effDex]: 4, [BladeMultName.effAgi]: 4 },
effAgi: 4, }),
}); [BladeSkillName.datamancer]: new Skill({
Skills[SkillNames.Datamancer] = new Skill({ name: BladeSkillName.datamancer,
name: SkillNames.Datamancer,
desc: desc:
"Each level of this skill increases your effectiveness in " + "Each level of this skill increases your effectiveness in " +
"synthoid population analysis and investigation by 5%. " + "synthoid population analysis and investigation by 5%. " +
@ -80,27 +79,27 @@ export const Skills: Record<string, Skill> = {};
"the accuracy of your synthoid population/community estimates.", "the accuracy of your synthoid population/community estimates.",
baseCost: 3, baseCost: 3,
costInc: 1, costInc: 1,
successChanceEstimate: 5, mults: { [BladeMultName.successChanceEstimate]: 5 },
}); }),
Skills[SkillNames.CybersEdge] = new Skill({ [BladeSkillName.cybersEdge]: new Skill({
name: SkillNames.CybersEdge, name: BladeSkillName.cybersEdge,
desc: "Each level of this skill increases your max stamina by 2%", desc: "Each level of this skill increases your max stamina by 2%",
baseCost: 1, baseCost: 1,
costInc: 3, costInc: 3,
stamina: 2, mults: { [BladeMultName.stamina]: 2 },
}); }),
Skills[SkillNames.HandsOfMidas] = new Skill({ [BladeSkillName.handsOfMidas]: new Skill({
name: SkillNames.HandsOfMidas, name: BladeSkillName.handsOfMidas,
desc: "Each level of this skill increases the amount of money you receive from Contracts by 10%", desc: "Each level of this skill increases the amount of money you receive from Contracts by 10%",
baseCost: 2, baseCost: 2,
costInc: 2.5, costInc: 2.5,
money: 10, mults: { [BladeMultName.money]: 10 },
}); }),
Skills[SkillNames.Hyperdrive] = new Skill({ [BladeSkillName.hyperdrive]: new Skill({
name: SkillNames.Hyperdrive, name: BladeSkillName.hyperdrive,
desc: "Each level of this skill increases the experience earned from Contracts, Operations, and BlackOps by 10%", desc: "Each level of this skill increases the experience earned from Contracts, Operations, and BlackOps by 10%",
baseCost: 1, baseCost: 1,
costInc: 2.5, costInc: 2.5,
expGain: 10, mults: { [BladeMultName.expGain]: 10 },
}); }),
})(); };

@ -1,23 +1,21 @@
import React from "react"; import type { Bladeburner } from "../Bladeburner";
import { Action } from "../Action"; import type { LevelableAction } from "../Types";
import { Bladeburner } from "../Bladeburner";
import { BladeburnerConstants } from "../data/Constants";
import Typography from "@mui/material/Typography"; import React from "react";
import Tooltip from "@mui/material/Tooltip"; import { Box, IconButton, Tooltip, Typography } from "@mui/material";
import Box from "@mui/material/Box";
import IconButton from "@mui/material/IconButton";
import ArrowDropUpIcon from "@mui/icons-material/ArrowDropUp"; import ArrowDropUpIcon from "@mui/icons-material/ArrowDropUp";
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
interface IProps { import { BladeburnerConstants } from "../data/Constants";
action: Action;
interface ActionLevelProps {
action: LevelableAction;
isActive: boolean; isActive: boolean;
bladeburner: Bladeburner; bladeburner: Bladeburner;
rerender: () => void; rerender: () => void;
} }
export function ActionLevel({ action, isActive, bladeburner, rerender }: IProps): React.ReactElement { export function ActionLevel({ action, isActive, bladeburner, rerender }: ActionLevelProps): React.ReactElement {
const canIncrease = action.level < action.maxLevel; const canIncrease = action.level < action.maxLevel;
const canDecrease = action.level > 1; const canDecrease = action.level > 1;

@ -1,4 +1,6 @@
import React from "react"; import React from "react";
import { Box, Tab, Tabs } from "@mui/material";
import { GeneralActionPage } from "./GeneralActionPage"; import { GeneralActionPage } from "./GeneralActionPage";
import { ContractPage } from "./ContractPage"; import { ContractPage } from "./ContractPage";
import { OperationPage } from "./OperationPage"; import { OperationPage } from "./OperationPage";
@ -6,15 +8,11 @@ import { BlackOpPage } from "./BlackOpPage";
import { SkillPage } from "./SkillPage"; import { SkillPage } from "./SkillPage";
import { Bladeburner } from "../Bladeburner"; import { Bladeburner } from "../Bladeburner";
import Tabs from "@mui/material/Tabs";
import Tab from "@mui/material/Tab";
import Box from "@mui/material/Box";
interface IProps { interface IProps {
bladeburner: Bladeburner; bladeburner: Bladeburner;
} }
export function AllPages(props: IProps): React.ReactElement { export function AllPages({ bladeburner }: IProps): React.ReactElement {
const [value, setValue] = React.useState(0); const [value, setValue] = React.useState(0);
function handleChange(event: React.SyntheticEvent, tab: number): void { function handleChange(event: React.SyntheticEvent, tab: number): void {
@ -31,11 +29,11 @@ export function AllPages(props: IProps): React.ReactElement {
<Tab label="Skills" /> <Tab label="Skills" />
</Tabs> </Tabs>
<Box sx={{ p: 1 }}> <Box sx={{ p: 1 }}>
{value === 0 && <GeneralActionPage bladeburner={props.bladeburner} />} {value === 0 && <GeneralActionPage bladeburner={bladeburner} />}
{value === 1 && <ContractPage bladeburner={props.bladeburner} />} {value === 1 && <ContractPage bladeburner={bladeburner} />}
{value === 2 && <OperationPage bladeburner={props.bladeburner} />} {value === 2 && <OperationPage bladeburner={bladeburner} />}
{value === 3 && <BlackOpPage bladeburner={props.bladeburner} />} {value === 3 && <BlackOpPage bladeburner={bladeburner} />}
{value === 4 && <SkillPage bladeburner={props.bladeburner} />} {value === 4 && <SkillPage bladeburner={bladeburner} />}
</Box> </Box>
</> </>
); );

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

@ -1,93 +1,79 @@
import React from "react"; import React from "react";
import { Paper, Typography } from "@mui/material";
import { Player } from "@player";
import { formatNumberNoSuffix } from "../../ui/formatNumber"; import { formatNumberNoSuffix } from "../../ui/formatNumber";
import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions"; import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions";
import { ActionTypes } from "../data/ActionTypes";
import { createProgressBarText } from "../../utils/helpers/createProgressBarText"; import { createProgressBarText } from "../../utils/helpers/createProgressBarText";
import { TeamSizeButton } from "./TeamSizeButton"; import { TeamSizeButton } from "./TeamSizeButton";
import { Bladeburner } from "../Bladeburner"; import { Bladeburner } from "../Bladeburner";
import { BlackOperation } from "../BlackOperation"; import { BlackOperation } from "../Actions/BlackOperation";
import { BlackOperations } from "../data/BlackOperations";
import { Player } from "@player";
import { CopyableText } from "../../ui/React/CopyableText"; import { CopyableText } from "../../ui/React/CopyableText";
import { SuccessChance } from "./SuccessChance"; import { SuccessChance } from "./SuccessChance";
import { StartButton } from "./StartButton"; import { StartButton } from "./StartButton";
import Typography from "@mui/material/Typography";
import Paper from "@mui/material/Paper";
import { useRerender } from "../../ui/React/hooks"; import { useRerender } from "../../ui/React/hooks";
interface IProps { interface BlackOpElemProps {
bladeburner: Bladeburner; bladeburner: Bladeburner;
action: BlackOperation; blackOp: BlackOperation;
} }
export function BlackOpElem(props: IProps): React.ReactElement { export function BlackOpElem({ bladeburner, blackOp }: BlackOpElemProps): React.ReactElement {
const rerender = useRerender(); const rerender = useRerender();
const isCompleted = props.bladeburner.blackops[props.action.name] != null; const isCompleted = bladeburner.numBlackOpsComplete > blackOp.n;
if (isCompleted) { if (isCompleted) {
return ( return (
<Paper sx={{ my: 1, p: 1 }}> <Paper sx={{ my: 1, p: 1 }}>
<Typography>{props.action.name} (COMPLETED)</Typography> <Typography>{blackOp.name} (COMPLETED)</Typography>
</Paper> </Paper>
); );
} }
const isActive = const isActive = bladeburner.action?.name === blackOp.name;
props.bladeburner.action.type === ActionTypes.BlackOperation && props.action.name === props.bladeburner.action.name; const actionTime = blackOp.getActionTime(bladeburner, Player);
const actionTime = props.action.getActionTime(props.bladeburner, Player); const hasReqdRank = bladeburner.rank >= blackOp.reqdRank;
const hasReqdRank = props.bladeburner.rank >= props.action.reqdRank;
const computedActionTimeCurrent = Math.min( const computedActionTimeCurrent = Math.min(
props.bladeburner.actionTimeCurrent + props.bladeburner.actionTimeOverflow, bladeburner.actionTimeCurrent + bladeburner.actionTimeOverflow,
props.bladeburner.actionTimeToComplete, bladeburner.actionTimeToComplete,
); );
const actionData = BlackOperations[props.action.name];
if (actionData === undefined) {
throw new Error(`Cannot find data for ${props.action.name}`);
}
return ( return (
<Paper sx={{ my: 1, p: 1 }}> <Paper sx={{ my: 1, p: 1 }}>
{isActive ? ( {isActive ? (
<> <>
<> <>
<CopyableText value={props.action.name} /> <CopyableText value={blackOp.name} />
<Typography> <Typography>
(IN PROGRESS - {formatNumberNoSuffix(computedActionTimeCurrent, 0)} /{" "} (IN PROGRESS - {formatNumberNoSuffix(computedActionTimeCurrent, 0)} /{" "}
{formatNumberNoSuffix(props.bladeburner.actionTimeToComplete, 0)}) {formatNumberNoSuffix(bladeburner.actionTimeToComplete, 0)})
</Typography> </Typography>
<Typography> <Typography>
{createProgressBarText({ {createProgressBarText({
progress: computedActionTimeCurrent / props.bladeburner.actionTimeToComplete, progress: computedActionTimeCurrent / bladeburner.actionTimeToComplete,
})} })}
</Typography> </Typography>
</> </>
</> </>
) : ( ) : (
<> <>
<CopyableText value={props.action.name} /> <CopyableText value={blackOp.name} />
<StartButton <StartButton bladeburner={bladeburner} action={blackOp} rerender={rerender} />
bladeburner={props.bladeburner} <TeamSizeButton action={blackOp} bladeburner={bladeburner} />
type={ActionTypes.BlackOperation}
name={props.action.name}
rerender={rerender}
/>
<TeamSizeButton action={props.action} bladeburner={props.bladeburner} />
</> </>
)} )}
<br /> <br />
<br /> <br />
<Typography>{actionData.desc}</Typography> <Typography whiteSpace={"pre-wrap"}>{blackOp.desc}</Typography>
<br /> <br />
<br /> <br />
<Typography color={hasReqdRank ? "primary" : "error"}> <Typography color={hasReqdRank ? "primary" : "error"}>
Required Rank: {formatNumberNoSuffix(props.action.reqdRank, 0)} Required Rank: {formatNumberNoSuffix(blackOp.reqdRank, 0)}
</Typography> </Typography>
<br /> <br />
<Typography> <Typography>
<SuccessChance action={props.action} bladeburner={props.bladeburner} /> <SuccessChance action={blackOp} bladeburner={bladeburner} />
<br /> <br />
Time Required: {convertTimeMsToTimeElapsedString(actionTime * 1000)} Time Required: {convertTimeMsToTimeElapsedString(actionTime * 1000)}
</Typography> </Typography>

@ -1,39 +1,20 @@
import React from "react"; import React from "react";
import { BlackOperations } from "../BlackOperations"; import { blackOpsArray } from "../data/BlackOperations";
import { BlackOperation } from "../BlackOperation"; import { BlackOperation } from "../Actions/BlackOperation";
import { BlackOpElem } from "./BlackOpElem"; import { BlackOpElem } from "./BlackOpElem";
import { Bladeburner } from "../Bladeburner"; import { Bladeburner } from "../Bladeburner";
interface IProps { interface BlackOpListProps {
bladeburner: Bladeburner; bladeburner: Bladeburner;
} }
export function BlackOpList(props: IProps): React.ReactElement { export function BlackOpList({ bladeburner }: BlackOpListProps): React.ReactElement {
let blackops: BlackOperation[] = []; const blackOps = blackOpsArray.slice(0, bladeburner.numBlackOpsComplete + 1);
for (const blackopName of Object.keys(BlackOperations)) {
if (Object.hasOwn(BlackOperations, blackopName)) {
blackops.push(BlackOperations[blackopName]);
}
}
blackops.sort(function (a, b) {
return a.reqdRank - b.reqdRank;
});
blackops = blackops.filter(
(blackop: BlackOperation, i: number) =>
!(
props.bladeburner.blackops[blackops[i].name] == null &&
i !== 0 &&
props.bladeburner.blackops[blackops[i - 1].name] == null
),
);
blackops = blackops.reverse();
return ( return (
<> <>
{blackops.map((blackop: BlackOperation) => ( {blackOps.map((blackOp: BlackOperation) => (
<BlackOpElem key={blackop.name} bladeburner={props.bladeburner} action={blackop} /> <BlackOpElem key={blackOp.name} bladeburner={bladeburner} blackOp={blackOp} />
))} ))}
</> </>
); );

@ -1,17 +1,18 @@
import * as React from "react"; import * as React from "react";
import { Button, Typography } from "@mui/material"; import { Button, Typography } from "@mui/material";
import { BlackOperationName, FactionName } from "@enums"; import { FactionName } from "@enums";
import { BlackOpList } from "./BlackOpList"; import { BlackOpList } from "./BlackOpList";
import { Bladeburner } from "../Bladeburner"; import { Bladeburner } from "../Bladeburner";
import { Router } from "../../ui/GameRoot"; import { Router } from "../../ui/GameRoot";
import { Page } from "../../ui/Router"; import { Page } from "../../ui/Router";
import { CorruptableText } from "../../ui/React/CorruptableText"; import { CorruptableText } from "../../ui/React/CorruptableText";
import { blackOpsArray } from "../data/BlackOperations";
interface IProps { interface BlackOpPageProps {
bladeburner: Bladeburner; bladeburner: Bladeburner;
} }
export function BlackOpPage(props: IProps): React.ReactElement { export function BlackOpPage({ bladeburner }: BlackOpPageProps): React.ReactElement {
return ( return (
<> <>
<Typography> <Typography>
@ -28,12 +29,12 @@ export function BlackOpPage(props: IProps): React.ReactElement {
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.
</Typography> </Typography>
{props.bladeburner.blackops[BlackOperationName.OperationDaedalus] ? ( {bladeburner.numBlackOpsComplete >= blackOpsArray.length ? (
<Button sx={{ my: 1, p: 1 }} onClick={() => Router.toPage(Page.BitVerse, { flume: false, quick: false })}> <Button sx={{ my: 1, p: 1 }} onClick={() => Router.toPage(Page.BitVerse, { flume: false, quick: false })}>
<CorruptableText content="Destroy w0rld_d34mon" spoiler={false}></CorruptableText> <CorruptableText content="Destroy w0rld_d34mon" spoiler={false}></CorruptableText>
</Button> </Button>
) : ( ) : (
<BlackOpList bladeburner={props.bladeburner} /> <BlackOpList bladeburner={bladeburner} />
)} )}
</> </>
); );

@ -1,10 +1,7 @@
import React from "react"; import React from "react";
import { ActionTypes } from "../data/ActionTypes";
import { createProgressBarText } from "../../utils/helpers/createProgressBarText"; import { createProgressBarText } from "../../utils/helpers/createProgressBarText";
import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions"; import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions";
import { Contracts } from "../data/Contracts";
import { Bladeburner } from "../Bladeburner"; import { Bladeburner } from "../Bladeburner";
import { Action } from "../Action";
import { Player } from "@player"; import { Player } from "@player";
import { SuccessChance } from "./SuccessChance"; import { SuccessChance } from "./SuccessChance";
import { CopyableText } from "../../ui/React/CopyableText"; import { CopyableText } from "../../ui/React/CopyableText";
@ -15,74 +12,64 @@ import { formatNumberNoSuffix, formatBigNumber } from "../../ui/formatNumber";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Paper from "@mui/material/Paper"; import Paper from "@mui/material/Paper";
import { useRerender } from "../../ui/React/hooks"; import { useRerender } from "../../ui/React/hooks";
import { Contract } from "../Actions/Contract";
import { getEnumHelper } from "../../utils/EnumHelper";
interface IProps { interface ContractElemProps {
bladeburner: Bladeburner; bladeburner: Bladeburner;
action: Action; action: Contract;
} }
export function ContractElem(props: IProps): React.ReactElement { export function ContractElem({ bladeburner, action }: ContractElemProps): React.ReactElement {
const rerender = useRerender(); const rerender = useRerender();
const isActive = // Temp special return
props.bladeburner.action.type === ActionTypes.Contract && props.action.name === props.bladeburner.action.name; if (!getEnumHelper("BladeContractName").isMember(action.name)) return <></>;
const isActive = action.name === bladeburner.action?.name;
const computedActionTimeCurrent = Math.min( const computedActionTimeCurrent = Math.min(
props.bladeburner.actionTimeCurrent + props.bladeburner.actionTimeOverflow, bladeburner.actionTimeCurrent + bladeburner.actionTimeOverflow,
props.bladeburner.actionTimeToComplete, bladeburner.actionTimeToComplete,
); );
const actionTime = props.action.getActionTime(props.bladeburner, Player); const actionTime = action.getActionTime(bladeburner, Player);
const actionData = Contracts[props.action.name];
if (actionData === undefined) {
throw new Error(`Cannot find data for ${props.action.name}`);
}
return ( return (
<Paper sx={{ my: 1, p: 1 }}> <Paper sx={{ my: 1, p: 1 }}>
{isActive ? ( {isActive ? (
<> <>
<Typography> <CopyableText value={action.name} /> (IN PROGRESS - {formatNumberNoSuffix(computedActionTimeCurrent, 0)} /{" "}
<CopyableText value={props.action.name} /> (IN PROGRESS -{" "} {formatNumberNoSuffix(bladeburner.actionTimeToComplete, 0)})
{formatNumberNoSuffix(computedActionTimeCurrent, 0)} /{" "}
{formatNumberNoSuffix(props.bladeburner.actionTimeToComplete, 0)})
</Typography>
<Typography> <Typography>
{createProgressBarText({ {createProgressBarText({
progress: computedActionTimeCurrent / props.bladeburner.actionTimeToComplete, progress: computedActionTimeCurrent / bladeburner.actionTimeToComplete,
})} })}
</Typography> </Typography>
</> </>
) : ( ) : (
<> <>
<CopyableText value={props.action.name} /> <CopyableText value={action.name} />
<StartButton <StartButton bladeburner={bladeburner} action={action} rerender={rerender} />
bladeburner={props.bladeburner}
type={ActionTypes.Contract}
name={props.action.name}
rerender={rerender}
/>
</> </>
)} )}
<br /> <br />
<br /> <br />
<ActionLevel action={props.action} bladeburner={props.bladeburner} isActive={isActive} rerender={rerender} /> <ActionLevel action={action} bladeburner={bladeburner} isActive={isActive} rerender={rerender} />
<br /> <br />
<br /> <br />
<Typography> <Typography whiteSpace={"pre-wrap"}>
{actionData.desc} {action.desc}
<br /> <br />
<br /> <br />
<SuccessChance action={props.action} bladeburner={props.bladeburner} /> <SuccessChance action={action} bladeburner={bladeburner} />
<br /> <br />
Time Required: {convertTimeMsToTimeElapsedString(actionTime * 1000)} Time Required: {convertTimeMsToTimeElapsedString(actionTime * 1000)}
<br /> <br />
Contracts remaining: {formatBigNumber(Math.floor(props.action.count))} Contracts remaining: {formatBigNumber(Math.floor(action.count))}
<br /> <br />
Successes: {formatBigNumber(props.action.successes)} Successes: {formatBigNumber(action.successes)}
<br /> <br />
Failures: {formatBigNumber(props.action.failures)} Failures: {formatBigNumber(action.failures)}
</Typography> </Typography>
<br /> <br />
<Autolevel rerender={rerender} action={props.action} /> <Autolevel rerender={rerender} action={action} />
</Paper> </Paper>
); );
} }

@ -1,18 +1,14 @@
import React from "react"; import React from "react";
import { BladeContractName } from "@enums";
import { ContractElem } from "./ContractElem"; import { ContractElem } from "./ContractElem";
import { Bladeburner } from "../Bladeburner"; import { Bladeburner } from "../Bladeburner";
interface IProps { export function ContractList({ bladeburner }: { bladeburner: Bladeburner }): React.ReactElement {
bladeburner: Bladeburner; const names = Object.values(BladeContractName);
}
export function ContractList(props: IProps): React.ReactElement {
const names = Object.keys(props.bladeburner.contracts);
const contracts = props.bladeburner.contracts;
return ( return (
<> <>
{names.map((name: string) => ( {names.map((name) => (
<ContractElem key={name} bladeburner={props.bladeburner} action={contracts[name]} /> <ContractElem key={name} bladeburner={bladeburner} action={bladeburner.contracts[name]} />
))} ))}
</> </>
); );

@ -1,11 +1,10 @@
import type { GeneralAction } from "../Actions/GeneralAction";
import React from "react"; import React from "react";
import { ActionTypes } from "../data/ActionTypes";
import { createProgressBarText } from "../../utils/helpers/createProgressBarText"; import { createProgressBarText } from "../../utils/helpers/createProgressBarText";
import { formatNumberNoSuffix } from "../../ui/formatNumber"; import { formatNumberNoSuffix } from "../../ui/formatNumber";
import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions"; import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions";
import { Bladeburner } from "../Bladeburner"; import { Bladeburner } from "../Bladeburner";
import { Action } from "../Action";
import { GeneralActions } from "../data/GeneralActions";
import { Player } from "@player"; import { Player } from "@player";
import { CopyableText } from "../../ui/React/CopyableText"; import { CopyableText } from "../../ui/React/CopyableText";
@ -16,71 +15,46 @@ import Box from "@mui/material/Box";
import Paper from "@mui/material/Paper"; import Paper from "@mui/material/Paper";
import { useRerender } from "../../ui/React/hooks"; import { useRerender } from "../../ui/React/hooks";
interface IProps { interface GeneralActionElemProps {
bladeburner: Bladeburner; bladeburner: Bladeburner;
action: Action; action: GeneralAction;
} }
export function GeneralActionElem(props: IProps): React.ReactElement { export function GeneralActionElem({ bladeburner, action }: GeneralActionElemProps): React.ReactElement {
const rerender = useRerender(); const rerender = useRerender();
const isActive = props.action.name === props.bladeburner.action.name; const isActive = action.name === bladeburner.action?.name;
const computedActionTimeCurrent = Math.min( const computedActionTimeCurrent = Math.min(
props.bladeburner.actionTimeCurrent + props.bladeburner.actionTimeOverflow, bladeburner.actionTimeCurrent + bladeburner.actionTimeOverflow,
props.bladeburner.actionTimeToComplete, bladeburner.actionTimeToComplete,
); );
const actionTime = (function (): number { const actionTime = action.getActionTime(bladeburner, Player);
switch (props.action.name) {
case "Training":
case "Field Analysis":
return 30;
case "Diplomacy":
case "Hyperbolic Regeneration Chamber":
case "Incite Violence":
return 60;
case "Recruitment":
return props.bladeburner.getRecruitmentTime(Player);
}
return -1; // dead code
})();
const successChance = const successChance =
props.action.name === "Recruitment" action.name === "Recruitment" ? Math.max(0, Math.min(bladeburner.getRecruitmentSuccessChance(Player), 1)) : -1;
? Math.max(0, Math.min(props.bladeburner.getRecruitmentSuccessChance(Player), 1))
: -1;
const actionData = GeneralActions[props.action.name];
if (actionData === undefined) {
throw new Error(`Cannot find data for ${props.action.name}`);
}
return ( return (
<Paper sx={{ my: 1, p: 1 }}> <Paper sx={{ my: 1, p: 1 }}>
{isActive ? ( {isActive ? (
<> <>
<CopyableText value={props.action.name} /> <CopyableText value={action.name} />
<Typography> <Typography>
(IN PROGRESS - {formatNumberNoSuffix(computedActionTimeCurrent, 0)} /{" "} (IN PROGRESS - {formatNumberNoSuffix(computedActionTimeCurrent, 0)} /{" "}
{formatNumberNoSuffix(props.bladeburner.actionTimeToComplete, 0)}) {formatNumberNoSuffix(bladeburner.actionTimeToComplete, 0)})
</Typography> </Typography>
<Typography> <Typography>
{createProgressBarText({ {createProgressBarText({
progress: computedActionTimeCurrent / props.bladeburner.actionTimeToComplete, progress: computedActionTimeCurrent / bladeburner.actionTimeToComplete,
})} })}
</Typography> </Typography>
</> </>
) : ( ) : (
<Box display="flex" flexDirection="row" alignItems="center"> <Box display="flex" flexDirection="row" alignItems="center">
<CopyableText value={props.action.name} /> <CopyableText value={action.name} />
<StartButton <StartButton bladeburner={bladeburner} action={action} rerender={rerender} />
bladeburner={props.bladeburner}
type={ActionTypes[props.action.name]}
name={props.action.name}
rerender={rerender}
/>
</Box> </Box>
)} )}
<br /> <br />
<br /> <br />
<Typography>{actionData.desc}</Typography> <Typography>{action.desc}</Typography>
<br /> <br />
<br /> <br />
<Typography> <Typography>

@ -1,24 +1,18 @@
import React from "react"; import React from "react";
import { GeneralActionElem } from "./GeneralActionElem"; import { GeneralActionElem } from "./GeneralActionElem";
import { Action } from "../Action"; import { GeneralActions } from "../data/GeneralActions";
import { GeneralActions } from "../GeneralActions";
import { Bladeburner } from "../Bladeburner"; import { Bladeburner } from "../Bladeburner";
interface IProps { interface GeneralActionListProps {
bladeburner: Bladeburner; bladeburner: Bladeburner;
} }
export function GeneralActionList(props: IProps): React.ReactElement { export function GeneralActionList({ bladeburner }: GeneralActionListProps): React.ReactElement {
const actions: Action[] = []; const actions = Object.values(GeneralActions);
for (const name of Object.keys(GeneralActions)) {
if (Object.hasOwn(GeneralActions, name)) {
actions.push(GeneralActions[name]);
}
}
return ( return (
<> <>
{actions.map((action: Action) => ( {actions.map((action) => (
<GeneralActionElem key={action.name} bladeburner={props.bladeburner} action={action} /> <GeneralActionElem key={action.name} bladeburner={bladeburner} action={action} />
))} ))}
</> </>
); );

@ -1,5 +1,10 @@
import type { Bladeburner } from "../Bladeburner";
import type { Operation } from "../Actions/Operation";
import React from "react"; import React from "react";
import { ActionTypes } from "../data/ActionTypes"; import { Paper, Typography } from "@mui/material";
import { Player } from "@player";
import { createProgressBarText } from "../../utils/helpers/createProgressBarText"; import { createProgressBarText } from "../../utils/helpers/createProgressBarText";
import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions"; import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions";
import { SuccessChance } from "./SuccessChance"; import { SuccessChance } from "./SuccessChance";
@ -7,85 +12,69 @@ import { ActionLevel } from "./ActionLevel";
import { Autolevel } from "./Autolevel"; import { Autolevel } from "./Autolevel";
import { StartButton } from "./StartButton"; import { StartButton } from "./StartButton";
import { TeamSizeButton } from "./TeamSizeButton"; import { TeamSizeButton } from "./TeamSizeButton";
import { Bladeburner } from "../Bladeburner";
import { Operation } from "../Operation";
import { Operations } from "../data/Operations";
import { Player } from "@player";
import { CopyableText } from "../../ui/React/CopyableText"; import { CopyableText } from "../../ui/React/CopyableText";
import { formatNumberNoSuffix, formatBigNumber } from "../../ui/formatNumber"; import { formatNumberNoSuffix, formatBigNumber } from "../../ui/formatNumber";
import Typography from "@mui/material/Typography";
import Paper from "@mui/material/Paper";
import { useRerender } from "../../ui/React/hooks"; import { useRerender } from "../../ui/React/hooks";
import { BladeActionType } from "@enums";
interface IProps { interface OperationElemProps {
bladeburner: Bladeburner; bladeburner: Bladeburner;
action: Operation; operation: Operation;
} }
export function OperationElem(props: IProps): React.ReactElement { export function OperationElem({ bladeburner, operation }: OperationElemProps): React.ReactElement {
const rerender = useRerender(); const rerender = useRerender();
const isActive = const isActive =
props.bladeburner.action.type === ActionTypes.Operation && props.action.name === props.bladeburner.action.name; bladeburner.action?.type === BladeActionType.operation && operation.name === bladeburner.action?.name;
const computedActionTimeCurrent = Math.min( const computedActionTimeCurrent = Math.min(
props.bladeburner.actionTimeCurrent + props.bladeburner.actionTimeOverflow, bladeburner.actionTimeCurrent + bladeburner.actionTimeOverflow,
props.bladeburner.actionTimeToComplete, bladeburner.actionTimeToComplete,
); );
const actionTime = props.action.getActionTime(props.bladeburner, Player); const actionTime = operation.getActionTime(bladeburner, Player);
const actionData = Operations[props.action.name];
if (actionData === undefined) {
throw new Error(`Cannot find data for ${props.action.name}`);
}
return ( return (
<Paper sx={{ my: 1, p: 1 }}> <Paper sx={{ my: 1, p: 1 }}>
{isActive ? ( {isActive ? (
<> <>
<Typography> <Typography>
<CopyableText value={props.action.name} /> (IN PROGRESS -{" "} <CopyableText value={operation.name} /> (IN PROGRESS - {formatNumberNoSuffix(computedActionTimeCurrent, 0)}{" "}
{formatNumberNoSuffix(computedActionTimeCurrent, 0)} /{" "} / {formatNumberNoSuffix(bladeburner.actionTimeToComplete, 0)})
{formatNumberNoSuffix(props.bladeburner.actionTimeToComplete, 0)})
</Typography> </Typography>
<Typography> <Typography>
{createProgressBarText({ {createProgressBarText({
progress: computedActionTimeCurrent / props.bladeburner.actionTimeToComplete, progress: computedActionTimeCurrent / bladeburner.actionTimeToComplete,
})} })}
</Typography> </Typography>
</> </>
) : ( ) : (
<> <>
<CopyableText value={props.action.name} /> <CopyableText value={operation.name} />
<StartButton <StartButton bladeburner={bladeburner} action={operation} rerender={rerender} />
bladeburner={props.bladeburner} <TeamSizeButton action={operation} bladeburner={bladeburner} />
type={ActionTypes.Operation}
name={props.action.name}
rerender={rerender}
/>
<TeamSizeButton action={props.action} bladeburner={props.bladeburner} />
</> </>
)} )}
<br /> <br />
<br /> <br />
<ActionLevel action={props.action} bladeburner={props.bladeburner} isActive={isActive} rerender={rerender} /> <ActionLevel action={operation} bladeburner={bladeburner} isActive={isActive} rerender={rerender} />
<br /> <br />
<br /> <br />
<Typography> <Typography whiteSpace={"pre-wrap"}>
{actionData.desc} {operation.desc}
<br /> <br />
<br /> <br />
<SuccessChance action={props.action} bladeburner={props.bladeburner} /> <SuccessChance action={operation} bladeburner={bladeburner} />
<br /> <br />
Time Required: {convertTimeMsToTimeElapsedString(actionTime * 1000)} Time Required: {convertTimeMsToTimeElapsedString(actionTime * 1000)}
<br /> <br />
Operations remaining: {formatBigNumber(Math.floor(props.action.count))} Operations remaining: {formatBigNumber(Math.floor(operation.count))}
<br /> <br />
Successes: {formatBigNumber(props.action.successes)} Successes: {formatBigNumber(operation.successes)}
<br /> <br />
Failures: {formatBigNumber(props.action.failures)} Failures: {formatBigNumber(operation.failures)}
</Typography> </Typography>
<br /> <br />
<Autolevel rerender={rerender} action={props.action} /> <Autolevel rerender={rerender} action={operation} />
</Paper> </Paper>
); );
} }

@ -1,18 +1,14 @@
import React from "react"; import React from "react";
import { BladeOperationName } from "@enums";
import { OperationElem } from "./OperationElem"; import { OperationElem } from "./OperationElem";
import { Bladeburner } from "../Bladeburner"; import { Bladeburner } from "../Bladeburner";
interface IProps { export function OperationList({ bladeburner }: { bladeburner: Bladeburner }): React.ReactElement {
bladeburner: Bladeburner; const names = Object.values(BladeOperationName);
}
export function OperationList(props: IProps): React.ReactElement {
const names = Object.keys(props.bladeburner.operations);
const operations = props.bladeburner.operations;
return ( return (
<> <>
{names.map((name: string) => ( {names.map((name) => (
<OperationElem key={name} bladeburner={props.bladeburner} action={operations[name]} /> <OperationElem key={name} bladeburner={bladeburner} operation={bladeburner.operations[name]} />
))} ))}
</> </>
); );

@ -1,4 +1,4 @@
import React from "react"; import React, { useMemo } from "react";
import { CopyableText } from "../../ui/React/CopyableText"; import { CopyableText } from "../../ui/React/CopyableText";
import { formatBigNumber } from "../../ui/formatNumber"; import { formatBigNumber } from "../../ui/formatNumber";
import { Bladeburner } from "../Bladeburner"; import { Bladeburner } from "../Bladeburner";
@ -10,34 +10,29 @@ import AddIcon from "@mui/icons-material/Add";
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import { Skill } from "../Skill"; import { Skill } from "../Skill";
interface IProps { interface SkillElemProps {
skill: Skill; skill: Skill;
bladeburner: Bladeburner; bladeburner: Bladeburner;
onUpgrade: () => void; onUpgrade: () => void;
} }
export function SkillElem(props: IProps): React.ReactElement { export function SkillElem({ skill, bladeburner, onUpgrade }: SkillElemProps): React.ReactElement {
const skillName = props.skill.name; const skillName = skill.name;
let currentLevel = 0; const skillLevel = bladeburner.getSkillLevel(skillName);
if (props.bladeburner.skills[skillName] && !isNaN(props.bladeburner.skills[skillName])) { const pointCost = useMemo(() => skill.calculateCost(skillLevel), [skill, skillLevel]);
currentLevel = props.bladeburner.skills[skillName];
}
const pointCost = props.skill.calculateCost(currentLevel);
const canLevel = props.bladeburner.skillPoints >= pointCost; const canLevel = bladeburner.skillPoints >= pointCost;
const maxLvl = props.skill.maxLvl ? currentLevel >= props.skill.maxLvl : false; const maxLvl = skill.maxLvl ? skillLevel >= skill.maxLvl : false;
function onClick(): void { function onClick(): void {
if (props.bladeburner.skillPoints < pointCost) return; bladeburner.upgradeSkill(skillName);
props.bladeburner.skillPoints -= pointCost; onUpgrade();
props.bladeburner.upgradeSkill(props.skill);
props.onUpgrade();
} }
return ( return (
<Paper sx={{ my: 1, p: 1 }}> <Paper sx={{ my: 1, p: 1 }}>
<Box display="flex" flexDirection="row" alignItems="center"> <Box display="flex" flexDirection="row" alignItems="center">
<CopyableText variant="h6" color="primary" value={props.skill.name} /> <CopyableText variant="h6" color="primary" value={skillName} />
{!canLevel || maxLvl ? ( {!canLevel || maxLvl ? (
<IconButton disabled> <IconButton disabled>
<CloseIcon /> <CloseIcon />
@ -48,13 +43,13 @@ export function SkillElem(props: IProps): React.ReactElement {
</IconButton> </IconButton>
)} )}
</Box> </Box>
<Typography>Level: {formatBigNumber(currentLevel)}</Typography> <Typography>Level: {formatBigNumber(skillLevel)}</Typography>
{maxLvl ? ( {maxLvl ? (
<Typography>MAX LEVEL</Typography> <Typography>MAX LEVEL</Typography>
) : ( ) : (
<Typography>Skill Points required: {formatBigNumber(pointCost)}</Typography> <Typography>Skill Points required: {formatBigNumber(pointCost)}</Typography>
)} )}
<Typography>{props.skill.desc}</Typography> <Typography>{skill.desc}</Typography>
</Paper> </Paper>
); );
} }

@ -1,19 +0,0 @@
import * as React from "react";
import { SkillElem } from "./SkillElem";
import { Skills } from "../Skills";
import { Bladeburner } from "../Bladeburner";
interface IProps {
bladeburner: Bladeburner;
onUpgrade: () => void;
}
export function SkillList(props: IProps): React.ReactElement {
return (
<>
{Object.keys(Skills).map((skill: string) => (
<SkillElem key={skill} bladeburner={props.bladeburner} skill={Skills[skill]} onUpgrade={props.onUpgrade} />
))}
</>
);
}

@ -1,25 +1,24 @@
import React, { useState } from "react"; import React from "react";
import { SkillList } from "./SkillList";
import { BladeburnerConstants } from "../data/Constants"; import { BladeburnerConstants } from "../data/Constants";
import { Bladeburner } from "../Bladeburner"; import { Bladeburner } from "../Bladeburner";
import { formatBigNumber } from "../../ui/formatNumber"; import { formatBigNumber } from "../../ui/formatNumber";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
interface IProps { import { useRerender } from "../../ui/React/hooks";
import { SkillElem } from "./SkillElem";
import { Skills } from "../data/Skills";
interface SkillPageProps {
bladeburner: Bladeburner; bladeburner: Bladeburner;
} }
export function SkillPage(props: IProps): React.ReactElement { export function SkillPage({ bladeburner }: SkillPageProps): React.ReactElement {
const setRerender = useState(false)[1]; const rerender = useRerender();
const mults = props.bladeburner.skillMultipliers; const multDisplays = bladeburner.getSkillMultsDisplay();
function valid(mult: number | undefined): boolean {
return mult !== undefined && mult !== 1;
}
return ( return (
<> <>
<Typography> <Typography>
<strong>Skill Points: {formatBigNumber(props.bladeburner.skillPoints)}</strong> <strong>Skill Points: {formatBigNumber(bladeburner.skillPoints)}</strong>
</Typography> </Typography>
<Typography> <Typography>
You will gain one skill point every {BladeburnerConstants.RanksPerSkillPoint} ranks. You will gain one skill point every {BladeburnerConstants.RanksPerSkillPoint} ranks.
@ -27,53 +26,12 @@ export function SkillPage(props: IProps): React.ReactElement {
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.
</Typography> </Typography>
{valid(mults.successChanceAll) && ( {multDisplays.map((multDisplay, i) => (
<Typography>Total Success Chance: x{formatBigNumber(mults.successChanceAll)}</Typography> <Typography key={i}>{multDisplay}</Typography>
)} ))}
{valid(mults.successChanceStealth) && ( {Object.values(Skills).map((skill) => (
<Typography>Stealth Success Chance: x{formatBigNumber(mults.successChanceStealth)}</Typography> <SkillElem key={skill.name} bladeburner={bladeburner} skill={skill} onUpgrade={rerender} />
)} ))}
{valid(mults.successChanceKill) && (
<Typography>Retirement Success Chance: x{formatBigNumber(mults.successChanceKill)}</Typography>
)}
{valid(mults.successChanceContract) && (
<Typography>Contract Success Chance: x{formatBigNumber(mults.successChanceContract)}</Typography>
)}
{valid(mults.successChanceOperation) && (
<Typography>Operation Success Chance: x{formatBigNumber(mults.successChanceOperation)}</Typography>
)}
{valid(mults.successChanceEstimate) && (
<Typography>Synthoid Data Estimate: x{formatBigNumber(mults.successChanceEstimate)}</Typography>
)}
{valid(mults.actionTime) && <Typography>Action Time: x{formatBigNumber(mults.actionTime)}</Typography>}
{valid(mults.effHack) && <Typography>Hacking Skill: x{formatBigNumber(mults.effHack)}</Typography>}
{valid(mults.effStr) && <Typography>Strength: x{formatBigNumber(mults.effStr)}</Typography>}
{valid(mults.effDef) && <Typography>Defense: x{formatBigNumber(mults.effDef)}</Typography>}
{valid(mults.effDex) && <Typography>Dexterity: x{formatBigNumber(mults.effDex)}</Typography>}
{valid(mults.effAgi) && <Typography>Agility: x{formatBigNumber(mults.effAgi)}</Typography>}
{valid(mults.effCha) && <Typography>Charisma: x{formatBigNumber(mults.effCha)}</Typography>}
{valid(mults.effInt) && <Typography>Intelligence: x{formatBigNumber(mults.effInt)}</Typography>}
{valid(mults.stamina) && <Typography>Stamina: x{formatBigNumber(mults.stamina)}</Typography>}
{valid(mults.money) && <Typography>Contract Money: x{formatBigNumber(mults.money)}</Typography>}
{valid(mults.expGain) && <Typography>Exp Gain: x{formatBigNumber(mults.expGain)}</Typography>}
<SkillList bladeburner={props.bladeburner} onUpgrade={() => setRerender((old) => !old)} />
</> </>
); );
} }
/*
var multKeys = Object.keys(this.skillMultipliers);
for (var i = 0; i < multKeys.length; ++i) {
var mult = this.skillMultipliers[multKeys[i]];
if (mult && mult !== 1) {
mult = formatNumber(mult, 3);
switch(multKeys[i]) {
}
}
}
*/

@ -1,47 +1,27 @@
import type { Bladeburner } from "../Bladeburner";
import type { Action } from "../Types";
import React from "react"; import React from "react";
import { ButtonWithTooltip } from "../../ui/Components/ButtonWithTooltip";
import { Bladeburner } from "../Bladeburner"; interface StartButtonProps {
import { BlackOperation } from "../BlackOperation";
import { Player } from "@player";
import Button from "@mui/material/Button";
import { AugmentationName } from "@enums";
import { ActionIdentifier } from "../ActionIdentifier";
interface IProps {
bladeburner: Bladeburner; bladeburner: Bladeburner;
type: number; action: Action;
name: string;
rerender: () => void; rerender: () => void;
} }
export function StartButton(props: IProps): React.ReactElement { export function StartButton({ bladeburner, action, rerender }: StartButtonProps): React.ReactElement {
const action = props.bladeburner.getActionObject(new ActionIdentifier({ name: props.name, type: props.type })); const availability = action.getAvailability(bladeburner);
if (action == null) { const disabledReason = availability.available ? "" : availability.error;
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 { function onStart(): void {
if (disabled) return; if (disabledReason) return;
const action = new ActionIdentifier(); bladeburner.startAction(action.id);
action.type = props.type; rerender();
action.name = props.name;
if (!Player.hasAugmentation(AugmentationName.BladesSimulacrum, true)) Player.finishWork(true);
props.bladeburner.startAction(action);
props.rerender();
} }
return ( return (
<Button sx={{ mx: 1 }} disabled={disabled} onClick={onStart}> <ButtonWithTooltip disabledTooltip={disabledReason} onClick={onStart}>
Start Start
</Button> </ButtonWithTooltip>
); );
} }

@ -10,29 +10,23 @@ import { formatNumberNoSuffix, formatPopulation, formatBigNumber } from "../../u
import { Factions } from "../../Faction/Factions"; import { Factions } from "../../Faction/Factions";
import { Router } from "../../ui/GameRoot"; import { Router } from "../../ui/GameRoot";
import { Page } from "../../ui/Router"; import { Page } from "../../ui/Router";
import { joinFaction } from "../../Faction/FactionHelpers";
import { Bladeburner } from "../Bladeburner"; import { Bladeburner } from "../Bladeburner";
import { TravelModal } from "./TravelModal"; import { TravelModal } from "./TravelModal";
interface IProps { interface StatsProps {
bladeburner: Bladeburner; bladeburner: Bladeburner;
} }
export function Stats(props: IProps): React.ReactElement { export function Stats({ bladeburner }: StatsProps): React.ReactElement {
const [travelOpen, setTravelOpen] = useState(false); const [travelOpen, setTravelOpen] = useState(false);
useRerender(1000); useRerender(1000);
const inFaction = props.bladeburner.rank >= BladeburnerConstants.RankNeededForFaction; const inFaction = bladeburner.rank >= BladeburnerConstants.RankNeededForFaction;
function openFaction(): void { function openFaction(): void {
if (!inFaction) return; const success = bladeburner.joinFaction();
const faction = Factions[FactionName.Bladeburners]; if (success) Router.toPage(Page.Faction, { faction: Factions[FactionName.Bladeburners] });
if (!faction.isMember) {
joinFaction(faction);
}
Router.toPage(Page.Faction, { faction });
} }
return ( return (
@ -49,11 +43,11 @@ export function Stats(props: IProps): React.ReactElement {
</Button> </Button>
</span> </span>
</Tooltip> </Tooltip>
<TravelModal open={travelOpen} onClose={() => setTravelOpen(false)} bladeburner={props.bladeburner} /> <TravelModal open={travelOpen} onClose={() => setTravelOpen(false)} bladeburner={bladeburner} />
</Box> </Box>
<Box display="flex"> <Box display="flex">
<Tooltip title={<Typography>Your rank within the Bladeburner division.</Typography>}> <Tooltip title={<Typography>Your rank within the Bladeburner division.</Typography>}>
<Typography>Rank: {formatBigNumber(props.bladeburner.rank)}</Typography> <Typography>Rank: {formatBigNumber(bladeburner.rank)}</Typography>
</Tooltip> </Tooltip>
</Box> </Box>
<br /> <br />
@ -82,23 +76,23 @@ export function Stats(props: IProps): React.ReactElement {
} }
> >
<Typography> <Typography>
Stamina: {formatBigNumber(props.bladeburner.stamina)} / {formatBigNumber(props.bladeburner.maxStamina)} Stamina: {formatBigNumber(bladeburner.stamina)} / {formatBigNumber(bladeburner.maxStamina)}
</Typography> </Typography>
</Tooltip> </Tooltip>
</Box> </Box>
<Typography> <Typography>
Stamina Penalty: {formatNumberNoSuffix((1 - props.bladeburner.calculateStaminaPenalty()) * 100, 1)}% Stamina Penalty: {formatNumberNoSuffix((1 - bladeburner.calculateStaminaPenalty()) * 100, 1)}%
</Typography> </Typography>
<br /> <br />
<Typography>Team Size: {formatNumberNoSuffix(props.bladeburner.teamSize, 0)}</Typography> <Typography>Team Size: {formatNumberNoSuffix(bladeburner.teamSize, 0)}</Typography>
<Typography>Team Members Lost: {formatNumberNoSuffix(props.bladeburner.teamLost, 0)}</Typography> <Typography>Team Members Lost: {formatNumberNoSuffix(bladeburner.teamLost, 0)}</Typography>
<br /> <br />
<Typography>Num Times Hospitalized: {props.bladeburner.numHosp}</Typography> <Typography>Num Times Hospitalized: {bladeburner.numHosp}</Typography>
<Typography> <Typography>
Money Lost From Hospitalizations: <Money money={props.bladeburner.moneyLost} /> Money Lost From Hospitalizations: <Money money={bladeburner.moneyLost} />
</Typography> </Typography>
<br /> <br />
<Typography>Current City: {props.bladeburner.city}</Typography> <Typography>Current City: {bladeburner.city}</Typography>
<Box display="flex"> <Box display="flex">
<Tooltip <Tooltip
title={ title={
@ -108,9 +102,7 @@ export function Stats(props: IProps): React.ReactElement {
</Typography> </Typography>
} }
> >
<Typography> <Typography>Est. Synthoid Population: {formatPopulation(bladeburner.getCurrentCity().popEst)}</Typography>
Est. Synthoid Population: {formatPopulation(props.bladeburner.getCurrentCity().popEst)}
</Typography>
</Tooltip> </Tooltip>
</Box> </Box>
<Box display="flex"> <Box display="flex">
@ -122,9 +114,7 @@ export function Stats(props: IProps): React.ReactElement {
</Typography> </Typography>
} }
> >
<Typography> <Typography>Synthoid Communities: {formatNumberNoSuffix(bladeburner.getCurrentCity().comms, 0)}</Typography>
Synthoid Communities: {formatNumberNoSuffix(props.bladeburner.getCurrentCity().comms, 0)}
</Typography>
</Tooltip> </Tooltip>
</Box> </Box>
<Box display="flex"> <Box display="flex">
@ -136,11 +126,11 @@ export function Stats(props: IProps): React.ReactElement {
</Typography> </Typography>
} }
> >
<Typography>City Chaos: {formatBigNumber(props.bladeburner.getCurrentCity().chaos)}</Typography> <Typography>City Chaos: {formatBigNumber(bladeburner.getCurrentCity().chaos)}</Typography>
</Tooltip> </Tooltip>
</Box> </Box>
<br /> <br />
{(props.bladeburner.storedCycles / BladeburnerConstants.CyclesPerSecond) * 1000 > 15000 && ( {(bladeburner.storedCycles / BladeburnerConstants.CyclesPerSecond) * 1000 > 15000 && (
<> <>
<Box display="flex"> <Box display="flex">
<Tooltip <Tooltip
@ -154,7 +144,7 @@ export function Stats(props: IProps): React.ReactElement {
<Typography> <Typography>
Bonus time:{" "} Bonus time:{" "}
{convertTimeMsToTimeElapsedString( {convertTimeMsToTimeElapsedString(
(props.bladeburner.storedCycles / BladeburnerConstants.CyclesPerSecond) * 1000, (bladeburner.storedCycles / BladeburnerConstants.CyclesPerSecond) * 1000,
)} )}
</Typography> </Typography>
</Tooltip> </Tooltip>
@ -162,7 +152,7 @@ export function Stats(props: IProps): React.ReactElement {
<br /> <br />
</> </>
)} )}
<Typography>Skill Points: {formatBigNumber(props.bladeburner.skillPoints)}</Typography> <Typography>Skill Points: {formatBigNumber(bladeburner.skillPoints)}</Typography>
<br /> <br />
<Typography> <Typography>
Aug. Success Chance mult: {formatNumberNoSuffix(Player.mults.bladeburner_success_chance * 100, 1)}% Aug. Success Chance mult: {formatNumberNoSuffix(Player.mults.bladeburner_success_chance * 100, 1)}%

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

@ -1,35 +1,27 @@
import type { Bladeburner } from "../Bladeburner";
import type { Action } from "../Types";
import React from "react"; import React from "react";
import { formatNumberNoSuffix } from "../../ui/formatNumber";
import { Player } from "@player";
import { formatPercent } from "../../ui/formatNumber";
import { StealthIcon } from "./StealthIcon"; import { StealthIcon } from "./StealthIcon";
import { KillIcon } from "./KillIcon"; import { KillIcon } from "./KillIcon";
import { Action } from "../Action";
import { Bladeburner } from "../Bladeburner";
import { Player } from "@player";
interface IProps { interface SuccessChanceProps {
bladeburner: Bladeburner; bladeburner: Bladeburner;
action: Action; action: Action;
} }
export function SuccessChance(props: IProps): React.ReactElement { export function SuccessChance({ bladeburner, action }: SuccessChanceProps): React.ReactElement {
const estimatedSuccessChance = props.action.getEstSuccessChance(props.bladeburner, Player); const [minChance, maxChance] = action.getSuccessRange(bladeburner, Player);
let chance = <></>; const chance = formatPercent(minChance, 1) + (minChance === maxChance ? "" : ` ~ ${formatPercent(maxChance, 1)}`);
if (estimatedSuccessChance[0] === estimatedSuccessChance[1]) {
chance = <>{formatNumberNoSuffix(estimatedSuccessChance[0] * 100, 1)}%</>;
} else {
chance = (
<>
{formatNumberNoSuffix(estimatedSuccessChance[0] * 100, 1)}% ~{" "}
{formatNumberNoSuffix(estimatedSuccessChance[1] * 100, 1)}%
</>
);
}
return ( return (
<> <>
Estimated success chance: {chance} {props.action.isStealth ? <StealthIcon /> : <></>} Estimated success chance: {chance} {action.isStealth ? <StealthIcon /> : <></>}
{props.action.isKill ? <KillIcon /> : <></>} {action.isKill ? <KillIcon /> : <></>}
</> </>
); );
} }

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

@ -1,5 +1,5 @@
import type { Action } from "../Action";
import type { Bladeburner } from "../Bladeburner"; import type { Bladeburner } from "../Bladeburner";
import type { BlackOperation, Operation } from "../Actions";
import React, { useState } from "react"; import React, { useState } from "react";
import { dialogBoxCreate } from "../../ui/React/DialogBox"; import { dialogBoxCreate } from "../../ui/React/DialogBox";
@ -8,7 +8,7 @@ import { Button, TextField, Typography } from "@mui/material";
interface TeamSizeModalProps { interface TeamSizeModalProps {
bladeburner: Bladeburner; bladeburner: Bladeburner;
action: Action; action: Operation | BlackOperation;
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
} }

@ -0,0 +1,25 @@
import type { ActionIdentifier } from "../Types";
import { BladeActionType } from "@enums";
import { assertLoadingType } from "../../utils/TypeAssertion";
import { getEnumHelper } from "../../utils/EnumHelper";
/** Loads an action identifier
* This is used for loading ActionIdentifier class objects from pre-2.6.1
* Should load both the old format and the new format */
export function loadActionIdentifier(identifier: unknown): ActionIdentifier | null {
if (!identifier || typeof identifier !== "object") return null;
assertLoadingType<ActionIdentifier>(identifier);
if (getEnumHelper("BladeBlackOpName").isMember(identifier.name)) {
return { type: BladeActionType.blackOp, name: identifier.name };
}
if (getEnumHelper("BladeContractName").isMember(identifier.name)) {
return { type: BladeActionType.contract, name: identifier.name };
}
if (getEnumHelper("BladeOperationName").isMember(identifier.name)) {
return { type: BladeActionType.operation, name: identifier.name };
}
if (getEnumHelper("BladeGeneralActionName").isMember(identifier.name)) {
return { type: BladeActionType.general, name: identifier.name };
}
return null;
}

@ -1,7 +1,8 @@
// Constructs all CompanyPosition objects using the metadata in data/companypositions.ts // Constructs all CompanyPosition objects using the metadata in data/companypositions.ts
import { getCompaniesMetadata } from "./data/CompaniesMetadata"; import { getCompaniesMetadata } from "./data/CompaniesMetadata";
import { Company } from "./Company"; import { Company } from "./Company";
import { Reviver, assertLoadingType } from "../utils/JSONReviver"; import { Reviver } from "../utils/JSONReviver";
import { assertLoadingType } from "../utils/TypeAssertion";
import { CompanyName } from "./Enums"; import { CompanyName } from "./Enums";
import { PartialRecord, createEnumKeyedRecord } from "../Types/Record"; import { PartialRecord, createEnumKeyedRecord } from "../Types/Record";
import { getEnumHelper } from "../utils/EnumHelper"; import { getEnumHelper } from "../utils/EnumHelper";

@ -1,4 +1,4 @@
import { Member } from "src/types"; import type { Member } from "../types";
export enum IndustryType { export enum IndustryType {
Water = "Water Utilities", Water = "Water Utilities",

@ -15,10 +15,10 @@ import Select, { SelectChangeEvent } from "@mui/material/Select";
import DeleteIcon from "@mui/icons-material/Delete"; import DeleteIcon from "@mui/icons-material/Delete";
import { Adjuster } from "./Adjuster"; import { Adjuster } from "./Adjuster";
import { Player } from "@player"; import { Player } from "@player";
import { CityName } from "@enums"; import { BladeSkillName, CityName } from "@enums";
import { Skills as AllSkills } from "../../Bladeburner/Skills"; import { Skills as AllSkills } from "../../Bladeburner/data/Skills";
import { SkillNames } from "../../Bladeburner/data/SkillNames";
import { Bladeburner } from "../../Bladeburner/Bladeburner"; import { Bladeburner } from "../../Bladeburner/Bladeburner";
import { getEnumHelper } from "../../utils/EnumHelper";
const bigNumber = 1e27; const bigNumber = 1e27;
@ -57,24 +57,23 @@ export function BladeburnerDev({ bladeburner }: { bladeburner: Bladeburner }): R
}; };
// Skill functions // Skill functions
const [skill, setSkill] = useState(SkillNames.BladesIntuition); const [skillName, setSkillName] = useState(BladeSkillName.bladesIntuition);
function setSkillDropdown(event: SelectChangeEvent): void { function setSkillDropdown(event: SelectChangeEvent): void {
setSkill(event.target.value); if (!getEnumHelper("BladeSkillName").isMember(event.target.value)) return;
setSkillName(event.target.value);
} }
const modifySkill = (modifier: number) => (levelchange: number) => { const modifySkill = (modifier: number) => (levelchange: number) => {
if (bladeburner.skills[AllSkills[skill].name] == null) resetSkill();
if (!isNaN(levelchange)) { if (!isNaN(levelchange)) {
bladeburner.skills[AllSkills[skill].name] += levelchange * modifier; bladeburner.setSkillLevel(skillName, bladeburner.getSkillLevel(skillName) + levelchange * modifier);
bladeburner.updateSkillMultipliers(); bladeburner.updateSkillMultipliers();
} }
}; };
const addTonsOfSkill = () => { const addTonsOfSkill = () => {
if (bladeburner.skills[AllSkills[skill].name] == null) resetSkill(); bladeburner.setSkillLevel(skillName, bladeburner.getSkillLevel(skillName) + bigNumber);
bladeburner.skills[AllSkills[skill].name] += bigNumber;
bladeburner.updateSkillMultipliers(); bladeburner.updateSkillMultipliers();
}; };
const resetSkill = () => { const resetSkill = () => {
bladeburner.skills[AllSkills[skill].name] = 0; bladeburner.setSkillLevel(skillName, 0);
bladeburner.updateSkillMultipliers(); bladeburner.updateSkillMultipliers();
}; };
@ -82,6 +81,7 @@ export function BladeburnerDev({ bladeburner }: { bladeburner: Bladeburner }): R
const AllContracts = bladeburner.contracts; const AllContracts = bladeburner.contracts;
const [contractTarget, setContract] = useState(AllContracts.Tracking.name); const [contractTarget, setContract] = useState(AllContracts.Tracking.name);
function setContractDropdown(event: SelectChangeEvent): void { function setContractDropdown(event: SelectChangeEvent): void {
if (!getEnumHelper("BladeContractName").isMember(event.target.value)) return;
setContract(event.target.value); setContract(event.target.value);
} }
const modifyContractLevel = (modifier: number) => (levelchange: number) => { const modifyContractLevel = (modifier: number) => (levelchange: number) => {
@ -117,6 +117,7 @@ export function BladeburnerDev({ bladeburner }: { bladeburner: Bladeburner }): R
const AllOperations = bladeburner.operations; const AllOperations = bladeburner.operations;
const [operationTarget, setOperation] = useState(AllOperations.Investigation.name); const [operationTarget, setOperation] = useState(AllOperations.Investigation.name);
function setOperationDropdown(event: SelectChangeEvent): void { function setOperationDropdown(event: SelectChangeEvent): void {
if (!getEnumHelper("BladeOperationName").isMember(event.target.value)) return;
setOperation(event.target.value); setOperation(event.target.value);
} }
const modifyOperationLevel = (modifier: number) => (levelchange: number) => { const modifyOperationLevel = (modifier: number) => (levelchange: number) => {
@ -232,7 +233,7 @@ export function BladeburnerDev({ bladeburner }: { bladeburner: Bladeburner }): R
<td align="center"> <td align="center">
<FormControl> <FormControl>
<InputLabel id="skills-select"></InputLabel> <InputLabel id="skills-select"></InputLabel>
<Select labelId="skills-select" id="skills-dropdown" onChange={setSkillDropdown} value={skill}> <Select labelId="skills-select" id="skills-dropdown" onChange={setSkillDropdown} value={skillName}>
{Object.values(AllSkills).map((skill) => ( {Object.values(AllSkills).map((skill) => (
<MenuItem key={skill.name} value={skill.name}> <MenuItem key={skill.name} value={skill.name}>
{skill.name} {skill.name}

@ -20,7 +20,6 @@ import { NumberInput } from "../../ui/React/NumberInput";
import { Hashes } from "../../ui/React/Hashes"; import { Hashes } from "../../ui/React/Hashes";
import { Router } from "../../ui/GameRoot"; import { Router } from "../../ui/GameRoot";
import { Page } from "../../ui/Router"; import { Page } from "../../ui/Router";
import { Bladeburner } from "../../Bladeburner/Bladeburner";
import { GangConstants } from "../../Gang/data/Constants"; import { GangConstants } from "../../Gang/data/Constants";
import { checkForMessagesToSend } from "../../Message/MessageHelpers"; import { checkForMessagesToSend } from "../../Message/MessageHelpers";
import { getEnumHelper } from "../../utils/EnumHelper"; import { getEnumHelper } from "../../utils/EnumHelper";
@ -72,7 +71,7 @@ export function General({ parentRerender }: { parentRerender: () => void }): Rea
// Blade functions // Blade functions
const joinBladeburner = () => { const joinBladeburner = () => {
Player.bladeburner = new Bladeburner(); Player.startBladeburner();
parentRerender(); parentRerender();
}; };
const leaveBladeburner = () => { const leaveBladeburner = () => {

@ -3,7 +3,8 @@ import type { PlayerObject } from "../PersonObjects/Player/PlayerObject";
import { FactionName, FactionDiscovery } from "@enums"; import { FactionName, FactionDiscovery } from "@enums";
import { Faction } from "./Faction"; import { Faction } from "./Faction";
import { Reviver, assertLoadingType } from "../utils/JSONReviver"; import { Reviver } from "../utils/JSONReviver";
import { assertLoadingType } from "../utils/TypeAssertion";
import { PartialRecord, createEnumKeyedRecord, getRecordValues } from "../Types/Record"; import { PartialRecord, createEnumKeyedRecord, getRecordValues } from "../Types/Record";
import { Augmentations } from "../Augmentation/Augmentations"; import { Augmentations } from "../Augmentation/Augmentations";
import { getEnumHelper } from "../utils/EnumHelper"; import { getEnumHelper } from "../utils/EnumHelper";

@ -5,7 +5,7 @@ import { Truthy } from "lodash";
import { GoColor, GoOpponent } from "@enums"; import { GoColor, GoOpponent } from "@enums";
import { Go } from "./Go"; import { Go } from "./Go";
import { boardStateFromSimpleBoard, simpleBoardFromBoard } from "./boardAnalysis/boardAnalysis"; import { boardStateFromSimpleBoard, simpleBoardFromBoard } from "./boardAnalysis/boardAnalysis";
import { assertLoadingType } from "../utils/JSONReviver"; import { assertLoadingType } from "../utils/TypeAssertion";
import { getEnumHelper } from "../utils/EnumHelper"; import { getEnumHelper } from "../utils/EnumHelper";
import { boardSizes } from "./Constants"; import { boardSizes } from "./Constants";
import { isInteger, isNumber } from "../types"; import { isInteger, isNumber } from "../types";

@ -32,7 +32,15 @@ import { arrayToString } from "../utils/helpers/ArrayHelpers";
import { HacknetServer } from "../Hacknet/HacknetServer"; import { HacknetServer } from "../Hacknet/HacknetServer";
import { BaseServer } from "../Server/BaseServer"; import { BaseServer } from "../Server/BaseServer";
import { RamCostConstants } from "./RamCostGenerator"; import { RamCostConstants } from "./RamCostGenerator";
import { isPositiveInteger, PositiveInteger, Unknownify, isPositiveNumber, PositiveNumber } from "../types"; import {
isPositiveInteger,
PositiveInteger,
Unknownify,
isPositiveNumber,
PositiveNumber,
PositiveSafeInteger,
isPositiveSafeInteger,
} from "../types";
import { Engine } from "../engine"; import { Engine } from "../engine";
import { resolveFilePath, FilePath } from "../Paths/FilePath"; import { resolveFilePath, FilePath } from "../Paths/FilePath";
import { hasScriptExtension, ScriptFilePath } from "../Paths/ScriptFilePath"; import { hasScriptExtension, ScriptFilePath } from "../Paths/ScriptFilePath";
@ -45,6 +53,7 @@ export const helpers = {
string, string,
number, number,
positiveInteger, positiveInteger,
positiveSafeInteger,
scriptArgs, scriptArgs,
runOptions, runOptions,
spawnOptions, spawnOptions,
@ -120,6 +129,16 @@ function positiveInteger(ctx: NetscriptContext, argName: string, v: unknown): Po
} }
return n; return n;
} }
/** Convert provided value v for argument argName to a positive safe integer, throwing if it looks like something else. */
function positiveSafeInteger(ctx: NetscriptContext, argName: string, v: unknown): PositiveSafeInteger {
const n = number(ctx, argName, v);
if (!isPositiveSafeInteger(n)) {
throw errorMessage(ctx, `${argName} should be a positive safe integer, was ${n}`, "TYPE");
}
return n;
}
/** Convert provided value v for argument argName to a positive number, throwing if it looks like something else. */ /** Convert provided value v for argument argName to a positive number, throwing if it looks like something else. */
function positiveNumber(ctx: NetscriptContext, argName: string, v: unknown): PositiveNumber { function positiveNumber(ctx: NetscriptContext, argName: string, v: unknown): PositiveNumber {
const n = number(ctx, argName, v); const n = number(ctx, argName, v);

@ -1,13 +1,16 @@
import type { Bladeburner as INetscriptBladeburner } from "@nsdefs"; import type { Bladeburner as INetscriptBladeburner } from "@nsdefs";
import type { Action } from "../Bladeburner/Action"; import type { Action, LevelableAction } from "../Bladeburner/Types";
import type { InternalAPI, NetscriptContext } from "../Netscript/APIWrapper"; import type { InternalAPI, NetscriptContext } from "../Netscript/APIWrapper";
import { Player } from "@player"; import { Player } from "@player";
import { BladeActionType, BladeContractName, BladeGeneralActionName, BladeOperationName, BladeSkillName } from "@enums";
import { Bladeburner, BladeburnerPromise } from "../Bladeburner/Bladeburner"; import { Bladeburner, BladeburnerPromise } from "../Bladeburner/Bladeburner";
import { currentNodeMults } from "../BitNode/BitNodeMultipliers"; import { currentNodeMults } from "../BitNode/BitNodeMultipliers";
import { BlackOperation } from "../Bladeburner/BlackOperation";
import { helpers } from "../Netscript/NetscriptHelpers"; import { helpers } from "../Netscript/NetscriptHelpers";
import { getEnumHelper } from "../utils/EnumHelper"; import { getEnumHelper } from "../utils/EnumHelper";
import { Skills } from "../Bladeburner/data/Skills";
import { assertString } from "../Netscript/TypeAssertion";
import { BlackOperations, blackOpsArray } from "../Bladeburner/data/BlackOperations";
export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> { export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
const checkBladeburnerAccess = function (ctx: NetscriptContext): void { const checkBladeburnerAccess = function (ctx: NetscriptContext): void {
@ -25,88 +28,88 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
return bladeburner; return bladeburner;
}; };
const getBladeburnerActionObject = function (ctx: NetscriptContext, type: string, name: string): Action { function getAction(ctx: NetscriptContext, type: unknown, name: unknown): Action {
const bladeburner = Player.bladeburner; const bladeburner = Player.bladeburner;
assertString(ctx, "type", type);
assertString(ctx, "name", name);
if (bladeburner === null) throw new Error("Must have joined bladeburner"); if (bladeburner === null) throw new Error("Must have joined bladeburner");
const actionId = bladeburner.getActionIdFromTypeAndName(type, name); const action = bladeburner.getActionFromTypeAndName(type, name);
if (!actionId) { if (!action) throw helpers.errorMessage(ctx, `Invalid action type='${type}', name='${name}'`);
throw helpers.errorMessage(ctx, `Invalid action type='${type}', name='${name}'`); return action;
}
const actionObj = bladeburner.getActionObject(actionId);
if (!actionObj) {
throw helpers.errorMessage(ctx, `Invalid action type='${type}', name='${name}'`);
} }
return actionObj; function isLevelableAction(action: Action): action is LevelableAction {
}; return action.type === BladeActionType.contract || action.type === BladeActionType.operation;
}
function getLevelableAction(ctx: NetscriptContext, type: unknown, name: unknown): LevelableAction {
const action = getAction(ctx, type, name);
if (!isLevelableAction(action)) {
throw helpers.errorMessage(
ctx,
`Actions of type ${action.type} are not levelable, ${ctx.functionPath} requires a levelable action`,
);
}
return action;
}
return { return {
inBladeburner: () => () => !!Player.bladeburner, inBladeburner: () => () => !!Player.bladeburner,
getContractNames: (ctx) => () => { getContractNames: (ctx) => () => {
const bladeburner = getBladeburner(ctx); getBladeburner(ctx);
return bladeburner.getContractNamesNetscriptFn(); return Object.values(BladeContractName);
}, },
getOperationNames: (ctx) => () => { getOperationNames: (ctx) => () => {
const bladeburner = getBladeburner(ctx); getBladeburner(ctx);
return bladeburner.getOperationNamesNetscriptFn(); return Object.values(BladeOperationName);
}, },
getBlackOpNames: (ctx) => () => { getBlackOpNames: (ctx) => () => {
const bladeburner = getBladeburner(ctx); getBladeburner(ctx);
return bladeburner.getBlackOpNamesNetscriptFn(); // Ensures they are sent in the correct order
return blackOpsArray.map((blackOp) => blackOp.name);
}, },
getNextBlackOp: (ctx) => () => { getNextBlackOp: (ctx) => () => {
const bladeburner = getBladeburner(ctx); const bladeburner = getBladeburner(ctx);
return bladeburner.getNextBlackOp(); if (bladeburner.numBlackOpsComplete >= blackOpsArray.length) return null;
const blackOp = blackOpsArray[bladeburner.numBlackOpsComplete];
return { name: blackOp.name, rank: blackOp.reqdRank };
}, },
getBlackOpRank: (ctx) => (_blackOpName) => { getBlackOpRank: (ctx) => (_blackOpName) => {
const blackOpName = helpers.string(ctx, "blackOpName", _blackOpName);
checkBladeburnerAccess(ctx); checkBladeburnerAccess(ctx);
const action = getBladeburnerActionObject(ctx, "blackops", blackOpName); const blackOpName = getEnumHelper("BladeBlackOpName").nsGetMember(ctx, _blackOpName);
if (!(action instanceof BlackOperation)) throw new Error("action was not a black operation"); return BlackOperations[blackOpName].reqdRank;
return action.reqdRank;
}, },
getGeneralActionNames: (ctx) => () => { getGeneralActionNames: (ctx) => () => {
const bladeburner = getBladeburner(ctx); getBladeburner(ctx);
return bladeburner.getGeneralActionNamesNetscriptFn(); return Object.values(BladeGeneralActionName);
}, },
getSkillNames: (ctx) => () => { getSkillNames: (ctx) => () => {
const bladeburner = getBladeburner(ctx); getBladeburner(ctx);
return bladeburner.getSkillNamesNetscriptFn(); return Object.values(BladeSkillName);
}, },
startAction: (ctx) => (_type, _name) => { startAction: (ctx) => (type, name) => {
const type = helpers.string(ctx, "type", _type);
const name = helpers.string(ctx, "name", _name);
const bladeburner = getBladeburner(ctx); const bladeburner = getBladeburner(ctx);
try { const action = getAction(ctx, type, name);
return bladeburner.startActionNetscriptFn(type, name, ctx.workerScript); const attempt = bladeburner.startAction(action.id);
} catch (e: unknown) { helpers.log(ctx, () => attempt.message);
throw helpers.errorMessage(ctx, String(e)); return !!attempt.success;
}
}, },
stopBladeburnerAction: (ctx) => () => { stopBladeburnerAction: (ctx) => () => {
const bladeburner = getBladeburner(ctx); const bladeburner = getBladeburner(ctx);
helpers.log(ctx, () => `Stopping current Bladeburner action.`);
return bladeburner.resetAction(); return bladeburner.resetAction();
}, },
getCurrentAction: (ctx) => () => { getCurrentAction: (ctx) => () => {
const bladeburner = getBladeburner(ctx); const bladeburner = getBladeburner(ctx);
return bladeburner.getTypeAndNameFromActionId(bladeburner.action); // Temporary bad return type to not be an API break (idle should just return null)
if (!bladeburner.action) return { type: "Idle", name: "Idle" };
return { ...bladeburner.action };
}, },
getActionTime: (ctx) => (_type, _name) => { getActionTime: (ctx) => (type, name) => {
const type = helpers.string(ctx, "type", _type);
const name = helpers.string(ctx, "name", _name);
const bladeburner = getBladeburner(ctx); const bladeburner = getBladeburner(ctx);
try { const action = getAction(ctx, type, name);
const time = bladeburner.getActionTimeNetscriptFn(Player, type, name); // return ms instead of seconds
if (typeof time === "string") { return action.getActionTime(bladeburner, Player) * 1000;
const errorLogText = `Invalid action: type='${type}' name='${name}'`;
helpers.log(ctx, () => errorLogText);
return -1;
} else {
return time;
}
} catch (e: unknown) {
throw helpers.errorMessage(ctx, String(e));
}
}, },
getActionCurrentTime: (ctx) => () => { getActionCurrentTime: (ctx) => () => {
const bladeburner = getBladeburner(ctx); const bladeburner = getBladeburner(ctx);
@ -119,92 +122,69 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
throw helpers.errorMessage(ctx, String(e)); throw helpers.errorMessage(ctx, String(e));
} }
}, },
getActionEstimatedSuccessChance: (ctx) => (_type, _name) => { getActionEstimatedSuccessChance: (ctx) => (type, name) => {
const bladeburner = getBladeburner(ctx); const bladeburner = getBladeburner(ctx);
const type = helpers.string(ctx, "type", _type); const action = getAction(ctx, type, name);
const name = helpers.string(ctx, "name", _name); return action.getSuccessRange(bladeburner, Player);
try {
const chance = bladeburner.getActionEstimatedSuccessChanceNetscriptFn(Player, type, name);
if (typeof chance === "string") {
const errorLogText = `Invalid action: type='${type}' name='${name}'`;
helpers.log(ctx, () => errorLogText);
return [-1, -1];
} else {
return chance;
}
} catch (e: unknown) {
throw helpers.errorMessage(ctx, String(e));
}
}, },
getActionRepGain: (ctx) => (_type, _name, _level) => { getActionRepGain: (ctx) => (type, name, _level) => {
checkBladeburnerAccess(ctx); checkBladeburnerAccess(ctx);
const type = helpers.string(ctx, "type", _type); const action = getAction(ctx, type, name);
const name = helpers.string(ctx, "name", _name); const level = isLevelableAction(action) ? helpers.number(ctx, "level", _level ?? action.level) : 1;
const action = getBladeburnerActionObject(ctx, type, name); const rewardMultiplier = isLevelableAction(action) ? Math.pow(action.rewardFac, level - 1) : 1;
const level = _level === undefined ? action.level : helpers.number(ctx, "level", _level);
const rewardMultiplier = Math.pow(action.rewardFac, level - 1);
return action.rankGain * rewardMultiplier * currentNodeMults.BladeburnerRank; return action.rankGain * rewardMultiplier * currentNodeMults.BladeburnerRank;
}, },
getActionCountRemaining: (ctx) => (_type, _name) => { getActionCountRemaining: (ctx) => (type, name) => {
const bladeburner = getBladeburner(ctx); const bladeburner = getBladeburner(ctx);
const type = helpers.string(ctx, "type", _type); const action = getAction(ctx, type, name);
const name = helpers.string(ctx, "name", _name); switch (action.type) {
try { case BladeActionType.general:
return bladeburner.getActionCountRemainingNetscriptFn(type, name, ctx.workerScript); return Infinity;
} catch (e: unknown) { case BladeActionType.blackOp:
throw helpers.errorMessage(ctx, String(e)); return bladeburner.numBlackOpsComplete > action.n ? 0 : 1;
case BladeActionType.contract:
case BladeActionType.operation:
return action.count;
} }
}, },
getActionMaxLevel: (ctx) => (_type, _name) => { getActionMaxLevel: (ctx) => (type, name) => {
const type = helpers.string(ctx, "type", _type);
const name = helpers.string(ctx, "name", _name);
checkBladeburnerAccess(ctx); checkBladeburnerAccess(ctx);
const action = getBladeburnerActionObject(ctx, type, name); const action = getLevelableAction(ctx, type, name);
return action.maxLevel; return action.maxLevel;
}, },
getActionCurrentLevel: (ctx) => (_type, _name) => { getActionCurrentLevel: (ctx) => (type, name) => {
const type = helpers.string(ctx, "type", _type);
const name = helpers.string(ctx, "name", _name);
checkBladeburnerAccess(ctx); checkBladeburnerAccess(ctx);
const action = getBladeburnerActionObject(ctx, type, name); const action = getLevelableAction(ctx, type, name);
return action.level; return action.level;
}, },
getActionAutolevel: (ctx) => (_type, _name) => { getActionAutolevel: (ctx) => (type, name) => {
const type = helpers.string(ctx, "type", _type);
const name = helpers.string(ctx, "name", _name);
checkBladeburnerAccess(ctx); checkBladeburnerAccess(ctx);
const action = getBladeburnerActionObject(ctx, type, name); const action = getLevelableAction(ctx, type, name);
return action.autoLevel; return action.autoLevel;
}, },
getActionSuccesses: (ctx) => (_type, _name) => { getActionSuccesses: (ctx) => (type, name) => {
const type = helpers.string(ctx, "type", _type);
const name = helpers.string(ctx, "name", _name);
checkBladeburnerAccess(ctx); checkBladeburnerAccess(ctx);
const action = getBladeburnerActionObject(ctx, type, name); const action = getLevelableAction(ctx, type, name);
return action.successes; return action.successes;
}, },
setActionAutolevel: setActionAutolevel:
(ctx) => (ctx) =>
(_type, _name, _autoLevel = true) => { (type, name, _autoLevel = true) => {
const type = helpers.string(ctx, "type", _type);
const name = helpers.string(ctx, "name", _name);
const autoLevel = !!_autoLevel; const autoLevel = !!_autoLevel;
checkBladeburnerAccess(ctx); checkBladeburnerAccess(ctx);
const action = getBladeburnerActionObject(ctx, type, name); const action = getLevelableAction(ctx, type, name);
action.autoLevel = autoLevel; action.autoLevel = autoLevel;
helpers.log(ctx, () => `Autolevel for ${action.name} has been ${autoLevel ? "enabled" : "disabled"}`);
}, },
setActionLevel: setActionLevel: (ctx) => (type, name, _level) => {
(ctx) => const level = helpers.positiveInteger(ctx, "level", _level ?? 1);
(_type, _name, _level = 1) => {
const type = helpers.string(ctx, "type", _type);
const name = helpers.string(ctx, "name", _name);
const level = helpers.number(ctx, "level", _level);
checkBladeburnerAccess(ctx); checkBladeburnerAccess(ctx);
const action = getBladeburnerActionObject(ctx, type, name); const action = getLevelableAction(ctx, type, name);
if (level < 1 || level > action.maxLevel) { if (level < 1 || level > action.maxLevel) {
throw helpers.errorMessage(ctx, `Level must be between 1 and ${action.maxLevel}, is ${level}`); throw helpers.errorMessage(ctx, `Level must be between 1 and ${action.maxLevel}, is ${level}`);
} }
action.level = level; action.level = level;
helpers.log(ctx, () => `Set level for ${action.name} to ${level}`);
}, },
getRank: (ctx) => () => { getRank: (ctx) => () => {
const bladeburner = getBladeburner(ctx); const bladeburner = getBladeburner(ctx);
@ -215,57 +195,57 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
return bladeburner.skillPoints; return bladeburner.skillPoints;
}, },
getSkillLevel: (ctx) => (_skillName) => { getSkillLevel: (ctx) => (_skillName) => {
const skillName = helpers.string(ctx, "skillName", _skillName);
const bladeburner = getBladeburner(ctx); const bladeburner = getBladeburner(ctx);
try { const skillName = getEnumHelper("BladeSkillName").nsGetMember(ctx, _skillName, "skillName");
return bladeburner.getSkillLevelNetscriptFn(skillName, ctx.workerScript); return bladeburner.getSkillLevel(skillName);
} catch (e: unknown) { },
throw helpers.errorMessage(ctx, String(e)); getSkillUpgradeCost: (ctx) => (_skillName, _count) => {
const bladeburner = getBladeburner(ctx);
const skillName = getEnumHelper("BladeSkillName").nsGetMember(ctx, _skillName, "skillName");
const count = helpers.positiveSafeInteger(ctx, "count", _count ?? 1);
const currentLevel = bladeburner.getSkillLevel(skillName);
return Skills[skillName].calculateCost(currentLevel, count);
},
upgradeSkill: (ctx) => (_skillName, _count) => {
const bladeburner = getBladeburner(ctx);
const skillName = getEnumHelper("BladeSkillName").nsGetMember(ctx, _skillName, "skillName");
const count = helpers.positiveSafeInteger(ctx, "count", _count ?? 1);
const attempt = bladeburner.upgradeSkill(skillName, count);
helpers.log(ctx, () => attempt.message);
return !!attempt.success;
},
getTeamSize: (ctx) => (type, name) => {
const bladeburner = getBladeburner(ctx);
if (!type && !name) return bladeburner.teamSize;
const action = getAction(ctx, type, name);
switch (action.type) {
case BladeActionType.general:
case BladeActionType.contract:
return 0;
case BladeActionType.blackOp:
case BladeActionType.operation:
return action.teamCount;
} }
}, },
getSkillUpgradeCost: setTeamSize: (ctx) => (type, name, _size) => {
(ctx) =>
(_skillName, _count = 1) => {
const bladeburner = getBladeburner(ctx); const bladeburner = getBladeburner(ctx);
const skillName = helpers.string(ctx, "skillName", _skillName); const action = getAction(ctx, type, name);
const count = helpers.number(ctx, "count", _count); const size = helpers.positiveInteger(ctx, "size", _size);
try { if (size > bladeburner.teamSize) {
return bladeburner.getSkillUpgradeCostNetscriptFn(skillName, count, ctx.workerScript); helpers.log(ctx, () => `Failed to set team size due to not enough team members.`);
} catch (e: unknown) { return -1;
throw helpers.errorMessage(ctx, String(e));
} }
}, switch (action.type) {
upgradeSkill: case BladeActionType.contract:
(ctx) => case BladeActionType.general:
(_skillName, _count = 1) => { helpers.log(ctx, () => "Only valid for Operations and Black Operations");
const bladeburner = getBladeburner(ctx); return -1;
const skillName = helpers.string(ctx, "skillName", _skillName); case BladeActionType.blackOp:
const count = helpers.number(ctx, "count", _count); case BladeActionType.operation: {
try { action.teamCount = size;
return bladeburner.upgradeSkillNetscriptFn(skillName, count, ctx.workerScript); helpers.log(ctx, () => `Set team size for ${action.name} to ${size}`);
} catch (e: unknown) { return size;
throw helpers.errorMessage(ctx, String(e));
} }
},
getTeamSize: (ctx) => (_type, _name) => {
const bladeburner = getBladeburner(ctx);
const type = helpers.string(ctx, "type", _type);
const name = helpers.string(ctx, "name", _name);
try {
return bladeburner.getTeamSizeNetscriptFn(type, name, ctx.workerScript);
} catch (e: unknown) {
throw helpers.errorMessage(ctx, String(e));
}
},
setTeamSize: (ctx) => (_type, _name, _size) => {
const bladeburner = getBladeburner(ctx);
const type = helpers.string(ctx, "type", _type);
const name = helpers.string(ctx, "name", _name);
const size = helpers.number(ctx, "size", _size);
try {
return bladeburner.setTeamSizeNetscriptFn(type, name, size, ctx.workerScript);
} catch (e: unknown) {
throw helpers.errorMessage(ctx, String(e));
} }
}, },
getCityEstimatedPopulation: (ctx) => (_cityName) => { getCityEstimatedPopulation: (ctx) => (_cityName) => {
@ -299,7 +279,9 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
}, },
joinBladeburnerFaction: (ctx) => () => { joinBladeburnerFaction: (ctx) => () => {
const bladeburner = getBladeburner(ctx); const bladeburner = getBladeburner(ctx);
return bladeburner.joinBladeburnerFactionNetscriptFn(ctx.workerScript); const attempt = bladeburner.joinFaction();
helpers.log(ctx, () => attempt.message);
return !!attempt.success;
}, },
joinBladeburnerDivision: (ctx) => () => { joinBladeburnerDivision: (ctx) => () => {
if (Player.bitNodeN === 7 || Player.sourceFileLvl(7) > 0) { if (Player.bitNodeN === 7 || Player.sourceFileLvl(7) > 0) {
@ -314,7 +296,7 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
Player.skills.dexterity >= 100 && Player.skills.dexterity >= 100 &&
Player.skills.agility >= 100 Player.skills.agility >= 100
) { ) {
Player.bladeburner = new Bladeburner(); Player.startBladeburner();
helpers.log(ctx, () => "You have been accepted into the Bladeburner division"); helpers.log(ctx, () => "You have been accepted into the Bladeburner division");
return true; return true;

@ -3,7 +3,6 @@ import type { Singularity as ISingularity, Task as ITask } from "@nsdefs";
import { Player } from "@player"; import { Player } from "@player";
import { import {
AugmentationName, AugmentationName,
BlackOperationName,
CityName, CityName,
FactionName, FactionName,
FactionWorkType, FactionWorkType,
@ -57,6 +56,7 @@ import { root } from "../Paths/Directory";
import { getRecordEntries } from "../Types/Record"; import { getRecordEntries } from "../Types/Record";
import { JobTracks } from "../Company/data/JobTracks"; import { JobTracks } from "../Company/data/JobTracks";
import { ServerConstants } from "../Server/data/Constants"; import { ServerConstants } from "../Server/data/Constants";
import { blackOpsArray } from "../Bladeburner/data/BlackOperations";
export function NetscriptSingularity(): InternalAPI<ISingularity> { export function NetscriptSingularity(): InternalAPI<ISingularity> {
const runAfterReset = function (cbScript: ScriptFilePath) { const runAfterReset = function (cbScript: ScriptFilePath) {
@ -1122,7 +1122,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
}; };
const bladeburnerRequirements = () => { const bladeburnerRequirements = () => {
if (!Player.bladeburner) return false; if (!Player.bladeburner) return false;
return Player.bladeburner.blackops[BlackOperationName.OperationDaedalus]; return Player.bladeburner.numBlackOpsComplete >= blackOpsArray.length;
}; };
if (!hackingRequirements() && !bladeburnerRequirements()) { if (!hackingRequirements() && !bladeburnerRequirements()) {

@ -1,17 +1,20 @@
import type { Augmentation } from "../Augmentation/Augmentation"; import type { Augmentation } from "../Augmentation/Augmentation";
import type { Sleeve as NetscriptSleeve } from "@nsdefs"; import type { Sleeve as NetscriptSleeve } from "@nsdefs";
import type { ActionIdentifier } from "../Bladeburner/Types";
import { Player } from "@player"; import { Player } from "@player";
import { BladeActionType } from "@enums";
import { Augmentations } from "../Augmentation/Augmentations"; import { Augmentations } from "../Augmentation/Augmentations";
import { findCrime } from "../Crime/CrimeHelpers"; import { findCrime } from "../Crime/CrimeHelpers";
import { getEnumHelper } from "../utils/EnumHelper"; import { getEnumHelper } from "../utils/EnumHelper";
import { InternalAPI, NetscriptContext, setRemovedFunctions } from "../Netscript/APIWrapper"; import { InternalAPI, NetscriptContext, setRemovedFunctions } from "../Netscript/APIWrapper";
import { isSleeveBladeburnerWork } from "../PersonObjects/Sleeve/Work/SleeveBladeburnerWork"; import { SleeveBladeburnerWork } from "../PersonObjects/Sleeve/Work/SleeveBladeburnerWork";
import { isSleeveFactionWork } from "../PersonObjects/Sleeve/Work/SleeveFactionWork"; import { isSleeveFactionWork } from "../PersonObjects/Sleeve/Work/SleeveFactionWork";
import { isSleeveCompanyWork } from "../PersonObjects/Sleeve/Work/SleeveCompanyWork"; import { isSleeveCompanyWork } from "../PersonObjects/Sleeve/Work/SleeveCompanyWork";
import { helpers } from "../Netscript/NetscriptHelpers"; import { helpers } from "../Netscript/NetscriptHelpers";
import { getAugCost } from "../Augmentation/AugmentationHelpers"; import { getAugCost } from "../Augmentation/AugmentationHelpers";
import { Factions } from "../Faction/Factions"; import { Factions } from "../Faction/Factions";
import { SleeveWorkType } from "../PersonObjects/Sleeve/Work/Work";
export function NetscriptSleeve(): InternalAPI<NetscriptSleeve> { export function NetscriptSleeve(): InternalAPI<NetscriptSleeve> {
const checkSleeveAPIAccess = function (ctx: NetscriptContext) { const checkSleeveAPIAccess = function (ctx: NetscriptContext) {
@ -233,32 +236,25 @@ export function NetscriptSleeve(): InternalAPI<NetscriptSleeve> {
setToBladeburnerAction: (ctx) => (_sleeveNumber, _action, _contract?) => { setToBladeburnerAction: (ctx) => (_sleeveNumber, _action, _contract?) => {
const sleeveNumber = helpers.number(ctx, "sleeveNumber", _sleeveNumber); const sleeveNumber = helpers.number(ctx, "sleeveNumber", _sleeveNumber);
const action = helpers.string(ctx, "action", _action); const action = helpers.string(ctx, "action", _action);
let contract: string;
if (typeof _contract === "undefined") {
contract = "------";
} else {
contract = helpers.string(ctx, "contract", _contract);
}
checkSleeveAPIAccess(ctx); checkSleeveAPIAccess(ctx);
checkSleeveNumber(ctx, sleeveNumber); checkSleeveNumber(ctx, sleeveNumber);
// Cannot Take on Contracts if another sleeve is performing that action
if (action === "Take on contracts") { if (action === "Take on contracts") {
const contract = getEnumHelper("BladeContractName").nsGetMember(ctx, _contract);
for (let i = 0; i < Player.sleeves.length; ++i) { for (let i = 0; i < Player.sleeves.length; ++i) {
if (i === sleeveNumber) { if (i === sleeveNumber) continue;
continue; const otherWork = Player.sleeves[i].currentWork;
} if (otherWork?.type === SleeveWorkType.BLADEBURNER && otherWork.actionId.name === contract) {
const other = Player.sleeves[i];
if (isSleeveBladeburnerWork(other.currentWork) && other.currentWork.actionName === contract) {
throw helpers.errorMessage( throw helpers.errorMessage(
ctx, ctx,
`Sleeve ${sleeveNumber} cannot take on contracts because Sleeve ${i} is already performing that action.`, `Sleeve ${sleeveNumber} cannot take on contracts because Sleeve ${i} is already performing that action.`,
); );
} }
} }
const actionId: ActionIdentifier = { type: BladeActionType.contract, name: contract };
Player.sleeves[sleeveNumber].startWork(new SleeveBladeburnerWork({ actionId }));
} }
return Player.sleeves[sleeveNumber].bladeburner(action, contract); return Player.sleeves[sleeveNumber].bladeburner(action);
}, },
}; };

@ -49,7 +49,6 @@ export abstract class Person implements IPerson {
gainIntelligenceExp = personMethods.gainIntelligenceExp; gainIntelligenceExp = personMethods.gainIntelligenceExp;
gainStats = personMethods.gainStats; gainStats = personMethods.gainStats;
regenerateHp = personMethods.regenerateHp; regenerateHp = personMethods.regenerateHp;
queryStatFromString = personMethods.queryStatFromString;
updateSkillLevels = personMethods.updateSkillLevels; updateSkillLevels = personMethods.updateSkillLevels;
hasAugmentation = personMethods.hasAugmentation; hasAugmentation = personMethods.hasAugmentation;
calculateSkill = calculateSkill; //Class version is equal to imported version calculateSkill = calculateSkill; //Class version is equal to imported version

@ -114,33 +114,6 @@ export function gainStats(this: Person, retValue: WorkStats): void {
this.gainIntelligenceExp(retValue.intExp); this.gainIntelligenceExp(retValue.intExp);
} }
//Given a string expression like "str" or "strength", returns the given stat
export function queryStatFromString(this: Person, str: string): number {
const tempStr = str.toLowerCase();
if (tempStr.includes("hack")) {
return this.skills.hacking;
}
if (tempStr.includes("str")) {
return this.skills.strength;
}
if (tempStr.includes("def")) {
return this.skills.defense;
}
if (tempStr.includes("dex")) {
return this.skills.dexterity;
}
if (tempStr.includes("agi")) {
return this.skills.agility;
}
if (tempStr.includes("cha")) {
return this.skills.charisma;
}
if (tempStr.includes("int")) {
return this.skills.intelligence;
}
return 0;
}
export function regenerateHp(this: Person, amt: number): void { export function regenerateHp(this: Person, amt: number): void {
if (typeof amt !== "number") { if (typeof amt !== "number") {
console.warn(`Player.regenerateHp() called without a numeric argument: ${amt}`); console.warn(`Player.regenerateHp() called without a numeric argument: ${amt}`);

@ -8,4 +8,5 @@ export function canAccessBladeburner(this: PlayerObject): boolean {
export function startBladeburner(this: PlayerObject): void { export function startBladeburner(this: PlayerObject): void {
this.bladeburner = new Bladeburner(); this.bladeburner = new Bladeburner();
this.bladeburner.init();
} }

@ -14,7 +14,6 @@ import type { SleeveWork } from "./Work/Work";
import { Player } from "@player"; import { Player } from "@player";
import { Person } from "../Person"; import { Person } from "../Person";
import { Contracts } from "../../Bladeburner/data/Contracts";
import { CONSTANTS } from "../../Constants"; import { CONSTANTS } from "../../Constants";
import { import {
ClassType, ClassType,
@ -26,12 +25,13 @@ import {
UniversityClassType, UniversityClassType,
CompanyName, CompanyName,
FactionName, FactionName,
BladeActionType,
BladeGeneralActionName,
} from "@enums"; } from "@enums";
import { Factions } from "../../Faction/Factions"; import { Factions } from "../../Faction/Factions";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../../utils/JSONReviver"; import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../../utils/JSONReviver";
import { formatPercent } from "../../ui/formatNumber";
import { SleeveClassWork } from "./Work/SleeveClassWork"; import { SleeveClassWork } from "./Work/SleeveClassWork";
import { SleeveSynchroWork } from "./Work/SleeveSynchroWork"; import { SleeveSynchroWork } from "./Work/SleeveSynchroWork";
import { SleeveRecoveryWork } from "./Work/SleeveRecoveryWork"; import { SleeveRecoveryWork } from "./Work/SleeveRecoveryWork";
@ -391,24 +391,44 @@ export class Sleeve extends Person implements SleevePerson {
} }
/** Begin a bladeburner task */ /** Begin a bladeburner task */
bladeburner(action: string, contract: string): boolean { bladeburner(action: string, contract?: string): boolean {
if (!Player.bladeburner) return false; if (!Player.bladeburner) return false;
switch (action) { switch (action) {
case "Training": case "Training":
this.startWork(new SleeveBladeburnerWork({ type: "General", name: "Training" })); this.startWork(
new SleeveBladeburnerWork({
actionId: { type: BladeActionType.general, name: BladeGeneralActionName.training },
}),
);
return true; return true;
case "Field analysis": case "Field analysis":
case "Field Analysis": case "Field Analysis":
this.startWork(new SleeveBladeburnerWork({ type: "General", name: "Field Analysis" })); this.startWork(
new SleeveBladeburnerWork({
actionId: { type: BladeActionType.general, name: BladeGeneralActionName.fieldAnalysis },
}),
);
return true; return true;
case "Recruitment": case "Recruitment":
this.startWork(new SleeveBladeburnerWork({ type: "General", name: "Recruitment" })); this.startWork(
new SleeveBladeburnerWork({
actionId: { type: BladeActionType.general, name: BladeGeneralActionName.recruitment },
}),
);
return true; return true;
case "Diplomacy": case "Diplomacy":
this.startWork(new SleeveBladeburnerWork({ type: "General", name: "Diplomacy" })); this.startWork(
new SleeveBladeburnerWork({
actionId: { type: BladeActionType.general, name: BladeGeneralActionName.diplomacy },
}),
);
return true; return true;
case "Hyperbolic Regeneration Chamber": case "Hyperbolic Regeneration Chamber":
this.startWork(new SleeveBladeburnerWork({ type: "General", name: "Hyperbolic Regeneration Chamber" })); this.startWork(
new SleeveBladeburnerWork({
actionId: { type: BladeActionType.general, name: BladeGeneralActionName.hyperbolicRegen },
}),
);
return true; return true;
case "Infiltrate synthoids": case "Infiltrate synthoids":
case "Infiltrate Synthoids": case "Infiltrate Synthoids":
@ -418,36 +438,13 @@ export class Sleeve extends Person implements SleevePerson {
this.startWork(new SleeveSupportWork()); this.startWork(new SleeveSupportWork());
return true; return true;
case "Take on contracts": case "Take on contracts":
if (!Contracts[contract]) return false; if (!getEnumHelper("BladeContractName").isMember(contract)) return false;
this.startWork(new SleeveBladeburnerWork({ type: "Contracts", name: contract })); this.startWork(new SleeveBladeburnerWork({ actionId: { type: BladeActionType.contract, name: contract } }));
return true; return true;
} }
return false; return false;
} }
recruitmentSuccessChance(): number {
return Math.max(0, Math.min(1, Player.bladeburner?.getRecruitmentSuccessChance(this) ?? 0));
}
contractSuccessChance(type: string, name: string): string {
const bb = Player.bladeburner;
if (bb === null) {
const errorLogText = `bladeburner is null`;
console.error(`Function: sleeves.contractSuccessChance; Message: '${errorLogText}'`);
return "0%";
}
const chances = bb.getActionEstimatedSuccessChanceNetscriptFn(this, type, name);
if (typeof chances === "string") {
console.error(`Function: sleeves.contractSuccessChance; Message: '${chances}'`);
return "0%";
}
if (chances[0] >= 1) {
return "100%";
} else {
return `${formatPercent(chances[0])} - ${formatPercent(chances[1])}`;
}
}
takeDamage(amt: number): boolean { takeDamage(amt: number): boolean {
if (typeof amt !== "number") { if (typeof amt !== "number") {
console.warn(`Player.takeDamage() called without a numeric argument: ${amt}`); console.warn(`Player.takeDamage() called without a numeric argument: ${amt}`);

@ -1,39 +1,40 @@
import type { Sleeve } from "../Sleeve";
import type { ActionIdentifier } from "../../../Bladeburner/Types";
import type { PromisePair } from "../../../Types/Promises"; import type { PromisePair } from "../../../Types/Promises";
import { Player } from "@player"; import { Player } from "@player";
import { BladeActionType, BladeGeneralActionName } from "@enums";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../../../utils/JSONReviver"; import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../../../utils/JSONReviver";
import { Sleeve } from "../Sleeve";
import { applySleeveGains, SleeveWorkClass, SleeveWorkType } from "./Work"; import { applySleeveGains, SleeveWorkClass, SleeveWorkType } from "./Work";
import { CONSTANTS } from "../../../Constants"; import { CONSTANTS } from "../../../Constants";
import { GeneralActions } from "../../../Bladeburner/data/GeneralActions";
import { scaleWorkStats } from "../../../Work/WorkStats"; import { scaleWorkStats } from "../../../Work/WorkStats";
import { getKeyList } from "../../../utils/helpers/getKeyList"; import { getKeyList } from "../../../utils/helpers/getKeyList";
import { loadActionIdentifier } from "../../../Bladeburner/utils/loadActionIdentifier";
import { invalidWork } from "../../../Work/InvalidWork";
interface SleeveBladeburnerWorkParams { interface SleeveBladeburnerWorkParams {
type: "General" | "Contracts"; actionId: ActionIdentifier & { type: BladeActionType.general | BladeActionType.contract };
name: string;
} }
export const isSleeveBladeburnerWork = (w: SleeveWorkClass | null): w is SleeveBladeburnerWork => export const isSleeveBladeburnerWork = (w: SleeveWorkClass | null): w is SleeveBladeburnerWork =>
w !== null && w.type === SleeveWorkType.BLADEBURNER; w?.type === SleeveWorkType.BLADEBURNER;
export class SleeveBladeburnerWork extends SleeveWorkClass { export class SleeveBladeburnerWork extends SleeveWorkClass {
type: SleeveWorkType.BLADEBURNER = SleeveWorkType.BLADEBURNER; type: SleeveWorkType.BLADEBURNER = SleeveWorkType.BLADEBURNER;
tasksCompleted = 0; tasksCompleted = 0;
cyclesWorked = 0; cyclesWorked = 0;
actionType: "General" | "Contracts"; actionId: ActionIdentifier & { type: BladeActionType.general | BladeActionType.contract };
actionName: string;
nextCompletionPair: PromisePair<void> = { promise: null, resolve: null }; nextCompletionPair: PromisePair<void> = { promise: null, resolve: null };
constructor(params?: SleeveBladeburnerWorkParams) { constructor(params?: SleeveBladeburnerWorkParams) {
super(); super();
this.actionType = params?.type ?? "General"; this.actionId = params?.actionId ?? { type: BladeActionType.general, name: BladeGeneralActionName.fieldAnalysis };
this.actionName = params?.name ?? "Field Analysis";
} }
cyclesNeeded(sleeve: Sleeve): number { cyclesNeeded(sleeve: Sleeve): number {
const ret = Player.bladeburner?.getActionTimeNetscriptFn(sleeve, this.actionType, this.actionName); if (!Player.bladeburner) return Infinity;
if (!ret || typeof ret === "string") throw new Error(`Error querying ${this.actionName} time`); const action = Player.bladeburner.getActionObject(this.actionId);
return ret / CONSTANTS.MilliPerCycle; const timeInMs = action.getActionTime(Player.bladeburner, sleeve) * 1000;
return timeInMs / CONSTANTS.MilliPerCycle;
} }
finish() { finish() {
@ -47,30 +48,19 @@ export class SleeveBladeburnerWork extends SleeveWorkClass {
process(sleeve: Sleeve, cycles: number) { process(sleeve: Sleeve, cycles: number) {
if (!Player.bladeburner) return sleeve.stopWork(); if (!Player.bladeburner) return sleeve.stopWork();
this.cyclesWorked += cycles; this.cyclesWorked += cycles;
const actionIdent = Player.bladeburner.getActionIdFromTypeAndName(this.actionType, this.actionName); if (this.actionId.type === BladeActionType.contract) {
if (!actionIdent) throw new Error(`Error getting ${this.actionName} action`); const action = Player.bladeburner.getActionObject(this.actionId);
if (this.actionType === "Contracts") {
const action = Player.bladeburner.getActionObject(actionIdent);
if (!action) throw new Error(`Error getting ${this.actionName} action object`);
if (action.count < 1) return sleeve.stopWork(); if (action.count < 1) return sleeve.stopWork();
} }
while (this.cyclesWorked >= this.cyclesNeeded(sleeve)) { while (this.cyclesWorked >= this.cyclesNeeded(sleeve)) {
if (this.actionType === "Contracts") { if (this.actionId.type === BladeActionType.contract) {
const action = Player.bladeburner.getActionObject(actionIdent); const action = Player.bladeburner.getActionObject(this.actionId);
if (!action) throw new Error(`Error getting ${this.actionName} action object`);
if (action.count < 1) return sleeve.stopWork(); if (action.count < 1) return sleeve.stopWork();
} }
const retValue = Player.bladeburner.completeAction(sleeve, actionIdent, false); const retValue = Player.bladeburner.completeAction(sleeve, this.actionId, false);
if (this.actionType === "General") {
const exp = GeneralActions[this.actionName]?.exp;
if (!exp) throw new Error(`Somehow there was no exp for action ${this.actionType} ${this.actionName}`);
applySleeveGains(sleeve, scaleWorkStats(exp, sleeve.shockBonus(), false));
}
if (this.actionType === "Contracts") {
applySleeveGains(sleeve, scaleWorkStats(retValue, sleeve.shockBonus(), false)); applySleeveGains(sleeve, scaleWorkStats(retValue, sleeve.shockBonus(), false));
}
this.tasksCompleted++; this.tasksCompleted++;
this.cyclesWorked -= this.cyclesNeeded(sleeve); this.cyclesWorked -= this.cyclesNeeded(sleeve);
// Resolve and reset nextCompletion promise // Resolve and reset nextCompletion promise
@ -86,8 +76,8 @@ export class SleeveBladeburnerWork extends SleeveWorkClass {
APICopy(sleeve: Sleeve) { APICopy(sleeve: Sleeve) {
return { return {
type: SleeveWorkType.BLADEBURNER as const, type: SleeveWorkType.BLADEBURNER as const,
actionType: this.actionType, actionType: this.actionId.type,
actionName: this.actionName, actionName: this.actionId.name,
tasksCompleted: this.tasksCompleted, tasksCompleted: this.tasksCompleted,
cyclesWorked: this.cyclesWorked, cyclesWorked: this.cyclesWorked,
cyclesNeeded: this.cyclesNeeded(sleeve), cyclesNeeded: this.cyclesNeeded(sleeve),
@ -104,6 +94,9 @@ export class SleeveBladeburnerWork extends SleeveWorkClass {
/** Initializes a BladeburnerWork object from a JSON save state. */ /** Initializes a BladeburnerWork object from a JSON save state. */
static fromJSON(value: IReviverValue): SleeveBladeburnerWork { static fromJSON(value: IReviverValue): SleeveBladeburnerWork {
const actionId = loadActionIdentifier(value.data?.actionId);
if (!actionId) return invalidWork();
value.data.actionId = actionId;
return Generic_fromJSON(SleeveBladeburnerWork, value.data, SleeveBladeburnerWork.savedKeys); return Generic_fromJSON(SleeveBladeburnerWork, value.data, SleeveBladeburnerWork.savedKeys);
} }
} }

@ -31,7 +31,7 @@ function getWorkDescription(sleeve: Sleeve, progress: number): string {
return "This sleeve is currently set to synchronize with the original consciousness. This causes the Sleeve's synchronization to increase."; return "This sleeve is currently set to synchronize with the original consciousness. This causes the Sleeve's synchronization to increase.";
case SleeveWorkType.BLADEBURNER: case SleeveWorkType.BLADEBURNER:
return ( return (
`This sleeve is currently attempting to perform ${work.actionName}.\n\nTasks Completed: ${formatInt( `This sleeve is currently attempting to perform ${work.actionId.name}.\n\nTasks Completed: ${formatInt(
work.tasksCompleted, work.tasksCompleted,
)}\n \n` + `Progress: ${formatPercent(progress)}` )}\n \n` + `Progress: ${formatPercent(progress)}`
); );

@ -1,14 +1,20 @@
import type { Sleeve } from "../Sleeve";
import React, { useState } from "react"; import React, { useState } from "react";
import { Sleeve } from "../Sleeve"; import { MenuItem, Select, SelectChangeEvent } from "@mui/material";
import { Player } from "@player"; import { Player } from "@player";
import {
BladeActionType,
BladeContractName,
CityName,
FactionName,
FactionWorkType,
GymType,
LocationName,
} from "@enums";
import { Crimes } from "../../../Crime/Crimes"; import { Crimes } from "../../../Crime/Crimes";
import { CityName, FactionName, FactionWorkType, GymType, LocationName } from "@enums";
import { Factions } from "../../../Faction/Factions"; import { Factions } from "../../../Faction/Factions";
import Select, { SelectChangeEvent } from "@mui/material/Select";
import MenuItem from "@mui/material/MenuItem";
import { isSleeveFactionWork } from "../Work/SleeveFactionWork";
import { isSleeveCompanyWork } from "../Work/SleeveCompanyWork";
import { isSleeveBladeburnerWork } from "../Work/SleeveBladeburnerWork";
import { getEnumHelper } from "../../../utils/EnumHelper"; import { getEnumHelper } from "../../../utils/EnumHelper";
import { SleeveWorkType } from "../Work/Work"; import { SleeveWorkType } from "../Work/Work";
@ -51,7 +57,7 @@ function possibleJobs(sleeve: Sleeve): string[] {
if (sleeve === otherSleeve) { if (sleeve === otherSleeve) {
continue; continue;
} }
if (isSleeveCompanyWork(otherSleeve.currentWork)) { if (otherSleeve.currentWork?.type === SleeveWorkType.COMPANY) {
forbiddenCompanies.push(otherSleeve.currentWork.companyName); forbiddenCompanies.push(otherSleeve.currentWork.companyName);
} }
} }
@ -70,7 +76,7 @@ function possibleFactions(sleeve: Sleeve): string[] {
if (sleeve === otherSleeve) { if (sleeve === otherSleeve) {
continue; continue;
} }
if (isSleeveFactionWork(otherSleeve.currentWork)) { if (otherSleeve.currentWork?.type === SleeveWorkType.FACTION) {
forbiddenFactions.push(otherSleeve.currentWork.factionName); forbiddenFactions.push(otherSleeve.currentWork.factionName);
} }
} }
@ -90,24 +96,24 @@ function possibleFactions(sleeve: Sleeve): string[] {
}); });
} }
function possibleContracts(sleeve: Sleeve): string[] { function possibleContracts(sleeve: Sleeve): BladeContractName[] | ["------"] {
const bb = Player.bladeburner; const bb = Player.bladeburner;
if (bb === null) { if (bb === null) {
return ["------"]; return ["------"];
} }
let contracts = bb.getContractNamesNetscriptFn(); let contracts = Object.values(BladeContractName);
for (const otherSleeve of Player.sleeves) { for (const otherSleeve of Player.sleeves) {
if (sleeve === otherSleeve) { if (sleeve === otherSleeve) {
continue; continue;
} }
if (isSleeveBladeburnerWork(otherSleeve.currentWork) && otherSleeve.currentWork.actionType === "Contracts") { if (
otherSleeve.currentWork?.type === SleeveWorkType.BLADEBURNER &&
otherSleeve.currentWork.actionId.type === BladeActionType.contract
) {
const w = otherSleeve.currentWork; const w = otherSleeve.currentWork;
contracts = contracts.filter((x) => x != w.actionName); contracts = contracts.filter((x) => x != w.actionId.name);
} }
} }
if (contracts.length === 0) {
return ["------"];
}
return contracts; return contracts;
} }
@ -256,10 +262,10 @@ function getABC(sleeve: Sleeve): [string, string, string] {
return ["Work for Faction", work.factionName, workNames[work.factionWorkType] ?? ""]; return ["Work for Faction", work.factionName, workNames[work.factionWorkType] ?? ""];
} }
case SleeveWorkType.BLADEBURNER: case SleeveWorkType.BLADEBURNER:
if (work.actionType === "Contracts") { if (work.actionId.type === BladeActionType.contract) {
return ["Perform Bladeburner Actions", "Take on contracts", work.actionName]; return ["Perform Bladeburner Actions", "Take on contracts", work.actionId.name];
} }
return ["Perform Bladeburner Actions", work.actionName, "------"]; return ["Perform Bladeburner Actions", work.actionId.name, "------"];
case SleeveWorkType.CLASS: { case SleeveWorkType.CLASS: {
if (!work.isGym()) return ["Take University Course", work.classType, work.location]; if (!work.isGym()) return ["Take University Course", work.classType, work.location];
const gymNames: Record<GymType, string> = { const gymNames: Record<GymType, string> = {

@ -131,7 +131,7 @@ export function prestigeAugmentation(): void {
// Cancel Bladeburner action // Cancel Bladeburner action
if (Player.bladeburner) { if (Player.bladeburner) {
Player.bladeburner.prestige(); Player.bladeburner.prestigeAugmentation();
} }
// BitNode 8: Ghost of Wall Street // BitNode 8: Ghost of Wall Street

@ -1,4 +1,4 @@
import type { BaseServer } from "src/Server/BaseServer"; import type { BaseServer } from "../Server/BaseServer";
export class RFAMessage { export class RFAMessage {
jsonrpc = "2.0"; // Transmits version of JSON-RPC. Compliance maybe allows some funky interaction with external tools? jsonrpc = "2.0"; // Transmits version of JSON-RPC. Compliance maybe allows some funky interaction with external tools?

@ -1,33 +1,26 @@
import type { ActionIdentifier } from "../../Bladeburner/Types";
// Root React Component for the Corporation UI // Root React Component for the Corporation UI
import React, { useMemo, useState, useEffect, ReactNode } from "react"; import React, { useMemo, useState, useEffect, ReactNode } from "react";
import { Box, Button, IconButton, Table, TableBody, TableCell, TableRow, Tooltip, Typography } from "@mui/material";
import SaveIcon from "@mui/icons-material/Save";
import ClearAllIcon from "@mui/icons-material/ClearAll";
import { Theme, useTheme } from "@mui/material/styles"; import { Theme, useTheme } from "@mui/material/styles";
import makeStyles from "@mui/styles/makeStyles"; import makeStyles from "@mui/styles/makeStyles";
import createStyles from "@mui/styles/createStyles"; import createStyles from "@mui/styles/createStyles";
import { Player } from "@player";
import { formatHp, formatMoney, formatSkill } from "../formatNumber"; import { formatHp, formatMoney, formatSkill } from "../formatNumber";
import { Reputation } from "./Reputation"; import { Reputation } from "./Reputation";
import { KillScriptsModal } from "./KillScriptsModal"; import { KillScriptsModal } from "./KillScriptsModal";
import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions"; import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableRow from "@mui/material/TableRow";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import IconButton from "@mui/material/IconButton";
import SaveIcon from "@mui/icons-material/Save";
import ClearAllIcon from "@mui/icons-material/ClearAll";
import { Settings } from "../../Settings/Settings"; import { Settings } from "../../Settings/Settings";
import { Router } from "../GameRoot"; import { Router } from "../GameRoot";
import { Page } from "../Router"; import { Page } from "../Router";
import { Player } from "@player";
import { StatsProgressOverviewCell } from "./StatsProgressBar"; import { StatsProgressOverviewCell } from "./StatsProgressBar";
import { currentNodeMults } from "../../BitNode/BitNodeMultipliers"; import { currentNodeMults } from "../../BitNode/BitNodeMultipliers";
import { Box, Tooltip } from "@mui/material";
import { isClassWork } from "../../Work/ClassWork"; import { isClassWork } from "../../Work/ClassWork";
import { CONSTANTS } from "../../Constants"; import { CONSTANTS } from "../../Constants";
import { isCreateProgramWork } from "../../Work/CreateProgramWork"; import { isCreateProgramWork } from "../../Work/CreateProgramWork";
@ -36,7 +29,6 @@ import { isFactionWork } from "../../Work/FactionWork";
import { ReputationRate } from "./ReputationRate"; import { ReputationRate } from "./ReputationRate";
import { isCompanyWork } from "../../Work/CompanyWork"; import { isCompanyWork } from "../../Work/CompanyWork";
import { isCrimeWork } from "../../Work/CrimeWork"; import { isCrimeWork } from "../../Work/CrimeWork";
import { ActionIdentifier } from "../../Bladeburner/ActionIdentifier";
import { Skills } from "../../PersonObjects/Skills"; import { Skills } from "../../PersonObjects/Skills";
import { calculateSkillProgress } from "../../PersonObjects/formulas/skill"; import { calculateSkillProgress } from "../../PersonObjects/formulas/skill";
import { EventEmitter } from "../../utils/EventEmitter"; import { EventEmitter } from "../../utils/EventEmitter";
@ -240,10 +232,9 @@ export function CharacterOverview({ parentOpen, save, killScripts }: OverviewPro
); );
} }
function ActionText(props: { action: ActionIdentifier }): React.ReactElement { function ActionText({ action }: { action: ActionIdentifier }): React.ReactElement {
const bladeburner = Player.bladeburner; const bladeburner = Player.bladeburner;
if (!bladeburner) return <></>; if (!bladeburner) return <></>;
const action = bladeburner.getTypeAndNameFromActionId(props.action);
return ( return (
<Typography> <Typography>
{action.type}: {action.name} {action.type}: {action.name}
@ -262,9 +253,7 @@ function BladeburnerText(): React.ReactElement {
const action = Player.bladeburner?.action; const action = Player.bladeburner?.action;
return useMemo( return useMemo(
() => () =>
//Action type 1 is Idle, see ActionTypes.ts !action ? (
//TODO 2.3: Revamp typing in bladeburner
!action || action.type === 1 ? (
<></> <></>
) : ( ) : (
<> <>

@ -1,17 +1,16 @@
/* Generic Reviver, toJSON, and fromJSON functions used for saving and loading objects */ /* Generic Reviver, toJSON, and fromJSON functions used for saving and loading objects */
import type { Unknownify } from "../types";
import { ObjectValidator, validateObject } from "./Validator"; import { ObjectValidator, validateObject } from "./Validator";
import { JSONMap, JSONSet } from "../Types/Jsonable"; import { JSONMap, JSONSet } from "../Types/Jsonable";
import { loadActionIdentifier } from "../Bladeburner/utils/loadActionIdentifier";
type JsonableClass = (new () => { toJSON: () => IReviverValue }) & { type JsonableClass = (new () => { toJSON: () => IReviverValue }) & {
fromJSON: (value: IReviverValue) => any; fromJSON: (value: IReviverValue) => any;
validationData?: ObjectValidator<any>; validationData?: ObjectValidator<any>;
}; };
export interface IReviverValue { export interface IReviverValue<T = any> {
ctor: string; ctor: string;
data: any; data: T;
} }
function isReviverValue(value: unknown): value is IReviverValue { function isReviverValue(value: unknown): value is IReviverValue {
return ( return (
@ -37,6 +36,8 @@ export function Reviver(_key: string, value: unknown): any {
case "Faction": // Reviver removed in v2.6.1 case "Faction": // Reviver removed in v2.6.1
console.warn(`Legacy load type ${value.ctor} converted to expected format while loading.`); console.warn(`Legacy load type ${value.ctor} converted to expected format while loading.`);
return value.data; return value.data;
case "ActionIdentifier": // No longer a class as of v2.6.1
return loadActionIdentifier(value.data);
} }
// Missing constructor with no special handling. Throw error. // Missing constructor with no special handling. Throw error.
throw new Error(`Could not locate constructor named ${value.ctor}. If the save data is valid, this is a bug.`); throw new Error(`Could not locate constructor named ${value.ctor}. If the save data is valid, this is a bug.`);
@ -102,7 +103,3 @@ export function Generic_fromJSON<T extends Record<string, any>>(
for (const [key, val] of Object.entries(data) as [keyof T, T[keyof T]][]) obj[key] = val; for (const [key, val] of Object.entries(data) as [keyof T, T[keyof T]][]) obj[key] = val;
return obj; return obj;
} }
// This function is empty because Unknownify<T> is a typesafe assertion on any object with no runtime checks needed.
// eslint-disable-next-line @typescript-eslint/no-empty-function
export function assertLoadingType<T extends object>(val: object): asserts val is Unknownify<T> {}

@ -0,0 +1,4 @@
import type { Unknownify } from "../types";
// This function is empty because Unknownify<T> is a typesafe assertion on any object with no runtime checks needed.
// eslint-disable-next-line @typescript-eslint/no-empty-function
export function assertLoadingType<T extends object>(val: object): asserts val is Unknownify<T> {}

@ -2,14 +2,22 @@ import { CONSTANTS } from "../../Constants";
/** /**
* Clamps the value on a lower and an upper bound * Clamps the value on a lower and an upper bound
* @param {number} value Value to clamp * @param {number} value Value to clamp
* @param {number} min Lower bound, defaults to Number.MIN_VALUE * @param {number} min Lower bound, defaults to negative Number.MAX_VALUE
* @param {number} max Upper bound, defaults to Number.MAX_VALUE * @param {number} max Upper bound, defaults to Number.MAX_VALUE
* @returns {number} Clamped value * @returns {number} Clamped value
*/ */
export function clampNumber(value: number, min = Number.MIN_VALUE, max = Number.MAX_VALUE) { export function clampNumber(value: number, min = -Number.MAX_VALUE, max = Number.MAX_VALUE) {
if (isNaN(value)) { if (isNaN(value)) {
if (CONSTANTS.isDevBranch) throw new Error("NaN passed into clampNumber()"); if (CONSTANTS.isDevBranch) throw new Error("NaN passed into clampNumber()");
return min; return min;
} }
return Math.max(Math.min(value, max), min); return Math.max(Math.min(value, max), min);
} }
export function clampInteger(value: number, min = -Number.MAX_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) {
if (isNaN(value)) {
if (CONSTANTS.isDevBranch) throw new Error("NaN passed into clampInteger()");
return min;
}
return Math.round(Math.max(Math.min(value, max), min));
}

@ -58,34 +58,15 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = `
"bladeburner": { "bladeburner": {
"ctor": "Bladeburner", "ctor": "Bladeburner",
"data": { "data": {
"action": { "action": null,
"ctor": "ActionIdentifier",
"data": {
"name": "",
"type": 1,
},
},
"actionTimeCurrent": 0, "actionTimeCurrent": 0,
"actionTimeOverflow": 0, "actionTimeOverflow": 0,
"actionTimeToComplete": 0, "actionTimeToComplete": 0,
"automateActionHigh": { "automateActionHigh": null,
"ctor": "ActionIdentifier", "automateActionLow": null,
"data": {
"name": "",
"type": 1,
},
},
"automateActionLow": {
"ctor": "ActionIdentifier",
"data": {
"name": "",
"type": 1,
},
},
"automateEnabled": false, "automateEnabled": false,
"automateThreshHigh": 0, "automateThreshHigh": 0,
"automateThreshLow": 0, "automateThreshLow": 0,
"blackops": {},
"cities": { "cities": {
"Aevum": { "Aevum": {
"ctor": "City", "ctor": "City",
@ -159,124 +140,36 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = `
"ctor": "Contract", "ctor": "Contract",
"data": { "data": {
"autoLevel": true, "autoLevel": true,
"baseDifficulty": 266.2162162162162,
"count": 117, "count": 117,
"decays": {
"agi": 0.91,
"cha": 0.8,
"def": 0.91,
"dex": 0.91,
"hack": 0,
"int": 0.9,
"str": 0.91,
},
"difficultyFac": 1.04,
"failures": 0, "failures": 0,
"hpLoss": 1,
"hpLost": 0,
"isKill": true,
"isStealth": false,
"level": 1, "level": 1,
"maxLevel": 1, "maxLevel": 1,
"name": "Bounty Hunter",
"rankGain": 0.9,
"rankLoss": 0,
"rewardFac": 1.085,
"successes": 0, "successes": 0,
"teamCount": 0,
"weights": {
"agi": 0.25,
"cha": 0.1,
"def": 0.15,
"dex": 0.25,
"hack": 0,
"int": 0.1,
"str": 0.15,
},
}, },
}, },
"Retirement": { "Retirement": {
"ctor": "Contract", "ctor": "Contract",
"data": { "data": {
"autoLevel": true, "autoLevel": true,
"baseDifficulty": 216.21621621621622, "count": 125,
"count": 129,
"decays": {
"agi": 0.91,
"cha": 0.8,
"def": 0.91,
"dex": 0.91,
"hack": 0,
"int": 0.9,
"str": 0.91,
},
"difficultyFac": 1.03,
"failures": 0, "failures": 0,
"hpLoss": 1,
"hpLost": 0,
"isKill": true,
"isStealth": false,
"level": 1, "level": 1,
"maxLevel": 1, "maxLevel": 1,
"name": "Retirement",
"rankGain": 0.6,
"rankLoss": 0,
"rewardFac": 1.065,
"successes": 0, "successes": 0,
"teamCount": 0,
"weights": {
"agi": 0.2,
"cha": 0.1,
"def": 0.2,
"dex": 0.2,
"hack": 0,
"int": 0.1,
"str": 0.2,
},
}, },
}, },
"Tracking": { "Tracking": {
"ctor": "Contract", "ctor": "Contract",
"data": { "data": {
"autoLevel": true, "autoLevel": true,
"baseDifficulty": 131.0810810810811, "count": 115,
"count": 111,
"decays": {
"agi": 0.91,
"cha": 0.9,
"def": 0.91,
"dex": 0.91,
"hack": 0,
"int": 1,
"str": 0.91,
},
"difficultyFac": 1.02,
"failures": 0, "failures": 0,
"hpLoss": 0.5,
"hpLost": 0,
"isKill": false,
"isStealth": true,
"level": 1, "level": 1,
"maxLevel": 1, "maxLevel": 1,
"name": "Tracking",
"rankGain": 0.3,
"rankLoss": 0,
"rewardFac": 1.041,
"successes": 0, "successes": 0,
"teamCount": 0,
"weights": {
"agi": 0.35,
"cha": 0.1,
"def": 0.05,
"dex": 0.35,
"hack": 0,
"int": 0.05,
"str": 0.05,
}, },
}, },
}, },
},
"hpLost": 0,
"logging": { "logging": {
"blackops": true, "blackops": true,
"contracts": true, "contracts": true,
@ -287,276 +180,84 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = `
"maxRank": 2000, "maxRank": 2000,
"maxStamina": 1, "maxStamina": 1,
"moneyLost": 0, "moneyLost": 0,
"numBlackOpsComplete": 0,
"numHosp": 0, "numHosp": 0,
"operations": { "operations": {
"Assassination": { "Assassination": {
"ctor": "Operation", "ctor": "Operation",
"data": { "data": {
"autoLevel": true, "autoLevel": true,
"baseDifficulty": 1467.5675675675675, "count": 23,
"count": 51,
"decays": {
"agi": 0.8,
"cha": 0,
"def": 0.8,
"dex": 0.8,
"hack": 0.6,
"int": 0.8,
"str": 0.8,
},
"difficultyFac": 1.06,
"failures": 0, "failures": 0,
"hpLoss": 5,
"hpLost": 0,
"isKill": true,
"isStealth": true,
"level": 1, "level": 1,
"maxLevel": 1, "maxLevel": 1,
"name": "Assassination",
"rankGain": 44,
"rankLoss": 4,
"reqdRank": 50000,
"rewardFac": 1.14,
"successes": 0, "successes": 0,
"teamCount": 0, "teamCount": 0,
"weights": {
"agi": 0.3,
"cha": 0,
"def": 0.1,
"dex": 0.3,
"hack": 0.1,
"int": 0.1,
"str": 0.1,
},
}, },
}, },
"Investigation": { "Investigation": {
"ctor": "Operation", "ctor": "Operation",
"data": { "data": {
"autoLevel": true, "autoLevel": true,
"baseDifficulty": 438.9189189189189, "count": 88,
"count": 94,
"decays": {
"agi": 0.9,
"cha": 0.7,
"def": 0.9,
"dex": 0.9,
"hack": 0.85,
"int": 0.9,
"str": 0.9,
},
"difficultyFac": 1.03,
"failures": 0, "failures": 0,
"hpLoss": 0,
"hpLost": 0,
"isKill": false,
"isStealth": true,
"level": 1, "level": 1,
"maxLevel": 1, "maxLevel": 1,
"name": "Investigation",
"rankGain": 2.2,
"rankLoss": 0.2,
"reqdRank": 25,
"rewardFac": 1.07,
"successes": 0, "successes": 0,
"teamCount": 0, "teamCount": 0,
"weights": {
"agi": 0.1,
"cha": 0.25,
"def": 0.05,
"dex": 0.2,
"hack": 0.25,
"int": 0.1,
"str": 0.05,
},
}, },
}, },
"Raid": { "Raid": {
"ctor": "Operation", "ctor": "Operation",
"data": { "data": {
"autoLevel": true, "autoLevel": true,
"baseDifficulty": 756.7567567567568, "count": 7,
"count": 27,
"decays": {
"agi": 0.8,
"cha": 0,
"def": 0.8,
"dex": 0.8,
"hack": 0.7,
"int": 0.9,
"str": 0.8,
},
"difficultyFac": 1.045,
"failures": 0, "failures": 0,
"hpLoss": 50,
"hpLost": 0,
"isKill": true,
"isStealth": false,
"level": 1, "level": 1,
"maxLevel": 1, "maxLevel": 1,
"name": "Raid",
"rankGain": 55,
"rankLoss": 2.5,
"reqdRank": 3000,
"rewardFac": 1.1,
"successes": 0, "successes": 0,
"teamCount": 0, "teamCount": 0,
"weights": {
"agi": 0.2,
"cha": 0,
"def": 0.2,
"dex": 0.2,
"hack": 0.1,
"int": 0.1,
"str": 0.2,
},
}, },
}, },
"Stealth Retirement Operation": { "Stealth Retirement Operation": {
"ctor": "Operation", "ctor": "Operation",
"data": { "data": {
"autoLevel": true, "autoLevel": true,
"baseDifficulty": 962.1621621621622, "count": 15,
"count": 39,
"decays": {
"agi": 0.8,
"cha": 0,
"def": 0.8,
"dex": 0.8,
"hack": 0.7,
"int": 0.9,
"str": 0.8,
},
"difficultyFac": 1.05,
"failures": 0, "failures": 0,
"hpLoss": 10,
"hpLost": 0,
"isKill": true,
"isStealth": true,
"level": 1, "level": 1,
"maxLevel": 1, "maxLevel": 1,
"name": "Stealth Retirement Operation",
"rankGain": 22,
"rankLoss": 2,
"reqdRank": 20000,
"rewardFac": 1.11,
"successes": 0, "successes": 0,
"teamCount": 0, "teamCount": 0,
"weights": {
"agi": 0.3,
"cha": 0,
"def": 0.1,
"dex": 0.3,
"hack": 0.1,
"int": 0.1,
"str": 0.1,
},
}, },
}, },
"Sting Operation": { "Sting Operation": {
"ctor": "Operation", "ctor": "Operation",
"data": { "data": {
"autoLevel": true, "autoLevel": true,
"baseDifficulty": 604.3243243243244, "count": 148,
"count": 15,
"decays": {
"agi": 0.85,
"cha": 0.7,
"def": 0.85,
"dex": 0.85,
"hack": 0.8,
"int": 0.9,
"str": 0.85,
},
"difficultyFac": 1.04,
"failures": 0, "failures": 0,
"hpLoss": 2.5,
"hpLost": 0,
"isKill": false,
"isStealth": true,
"level": 1, "level": 1,
"maxLevel": 1, "maxLevel": 1,
"name": "Sting Operation",
"rankGain": 5.5,
"rankLoss": 0.5,
"reqdRank": 500,
"rewardFac": 1.095,
"successes": 0, "successes": 0,
"teamCount": 0, "teamCount": 0,
"weights": {
"agi": 0.1,
"cha": 0.2,
"def": 0.05,
"dex": 0.25,
"hack": 0.25,
"int": 0.1,
"str": 0.05,
},
}, },
}, },
"Undercover Operation": { "Undercover Operation": {
"ctor": "Operation", "ctor": "Operation",
"data": { "data": {
"autoLevel": true, "autoLevel": true,
"baseDifficulty": 456.7567567567568, "count": 94,
"count": 2,
"decays": {
"agi": 0.9,
"cha": 0.7,
"def": 0.9,
"dex": 0.9,
"hack": 0.8,
"int": 0.9,
"str": 0.9,
},
"difficultyFac": 1.04,
"failures": 0, "failures": 0,
"hpLoss": 2,
"hpLost": 0,
"isKill": false,
"isStealth": true,
"level": 1, "level": 1,
"maxLevel": 1, "maxLevel": 1,
"name": "Undercover Operation",
"rankGain": 4.4,
"rankLoss": 0.4,
"reqdRank": 100,
"rewardFac": 1.09,
"successes": 0, "successes": 0,
"teamCount": 0, "teamCount": 0,
"weights": {
"agi": 0.2,
"cha": 0.2,
"def": 0.05,
"dex": 0.2,
"hack": 0.2,
"int": 0.1,
"str": 0.05,
},
}, },
}, },
}, },
"randomEventCounter": 303, "randomEventCounter": 303,
"rank": 2000, "rank": 2000,
"skillMultipliers": {
"actionTime": 1,
"effAgi": 1,
"effCha": 1,
"effDef": 1,
"effDex": 1,
"effHack": 1,
"effInt": 1,
"effStr": 1,
"expGain": 1,
"money": 1,
"stamina": 1,
"successChanceAll": 1,
"successChanceContract": 1,
"successChanceEstimate": 1,
"successChanceKill": 1,
"successChanceOperation": 1,
"successChanceStealth": 1,
},
"skillPoints": 666, "skillPoints": 666,
"skills": {}, "skills": {},
"sleeveSize": 0, "sleeveSize": 0,