mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-05-05 07:07:50 +02:00
0ba337f091
bandaids for 3 bugs in bladeburner this really needs proper fixes and a alot of refactoring! the manual action start didnt start tasks the right way, modifying an existing action object instead of creating a new one therefore the current action wasnt shown on the stats overview the api start action didnt check for the BladesSimulacrum Aug and didnt stop current Player tasks so the next time Bladeburner proccessed it stopped the bladeburner tasks again when the player was doing something else like crimes sometimes blops had an action.count of 0 even when they wherent done in that bladeburner instance yet this happends because the BlackOps class instances are only initialized on game load and then later on BlackOps completion manipulated this change doesnt reset on a bitnode change or when bladeburner is deleted through the dev Menu as a quick fix i added a new resetBlackOps function that always runs when Bladeburner processes this isnt the best solution but any proper fix i came up with requires a refactor that i couldnt do at this moment credits to @TheAimMan for finding the clue that the count is the problem not the rank! edit,; added a 4th bandaid to avoid NaN Stamina Penalty when stamina is infinite
2455 lines
82 KiB
TypeScript
2455 lines
82 KiB
TypeScript
import type { PromisePair } from "../Types/Promises";
|
|
import { AugmentationName, CityName, FactionName } from "@enums";
|
|
import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver";
|
|
import { ActionIdentifier } from "./ActionIdentifier";
|
|
import { ActionTypes } from "./data/ActionTypes";
|
|
import { Growths } from "./data/Growths";
|
|
import { BlackOperations } from "./BlackOperations";
|
|
import { BlackOperation } from "./BlackOperation";
|
|
import { Operation } from "./Operation";
|
|
import { Contract } from "./Contract";
|
|
import { GeneralActions } from "./GeneralActions";
|
|
import { formatNumberNoSuffix } from "../ui/formatNumber";
|
|
import { Skills } from "./Skills";
|
|
import { Skill } from "./Skill";
|
|
import { City } from "./City";
|
|
import { Action } from "./Action";
|
|
import { Player } from "@player";
|
|
import { Person } from "../PersonObjects/Person";
|
|
import { Router } from "../ui/GameRoot";
|
|
import { ConsoleHelpText } from "./data/Help";
|
|
import { exceptionAlert } from "../utils/helpers/exceptionAlert";
|
|
import { getRandomInt } from "../utils/helpers/getRandomInt";
|
|
import { BladeburnerConstants } from "./data/Constants";
|
|
import { formatExp, formatMoney, formatPercent, formatBigNumber, formatStamina } from "../ui/formatNumber";
|
|
import { currentNodeMults } from "../BitNode/BitNodeMultipliers";
|
|
import { addOffset } from "../utils/helpers/addOffset";
|
|
import { Factions } from "../Faction/Factions";
|
|
import { calculateHospitalizationCost } from "../Hospital/Hospital";
|
|
import { dialogBoxCreate } from "../ui/React/DialogBox";
|
|
import { Settings } from "../Settings/Settings";
|
|
import { getTimestamp } from "../utils/helpers/getTimestamp";
|
|
import { joinFaction } from "../Faction/FactionHelpers";
|
|
import { WorkerScript } from "../Netscript/WorkerScript";
|
|
import { KEY } from "../utils/helpers/keyCodes";
|
|
import { isSleeveInfiltrateWork } from "../PersonObjects/Sleeve/Work/SleeveInfiltrateWork";
|
|
import { isSleeveSupportWork } from "../PersonObjects/Sleeve/Work/SleeveSupportWork";
|
|
import { WorkStats, newWorkStats } from "../Work/WorkStats";
|
|
import { getEnumHelper } from "../utils/EnumHelper";
|
|
import { createEnumKeyedRecord } from "../Types/Record";
|
|
|
|
export interface BlackOpsAttempt {
|
|
error?: string;
|
|
isAvailable?: boolean;
|
|
action?: BlackOperation;
|
|
}
|
|
export const BladeburnerPromise: PromisePair<number> = { promise: null, resolve: null };
|
|
|
|
export class Bladeburner {
|
|
numHosp = 0;
|
|
moneyLost = 0;
|
|
rank = 0;
|
|
maxRank = 0;
|
|
|
|
skillPoints = 0;
|
|
totalSkillPoints = 0;
|
|
|
|
teamSize = 0;
|
|
sleeveSize = 0;
|
|
teamLost = 0;
|
|
hpLost = 0;
|
|
|
|
storedCycles = 0;
|
|
|
|
randomEventCounter: number = getRandomInt(240, 600);
|
|
|
|
actionTimeToComplete = 0;
|
|
actionTimeCurrent = 0;
|
|
actionTimeOverflow = 0;
|
|
|
|
action: ActionIdentifier = new ActionIdentifier({
|
|
type: ActionTypes.Idle,
|
|
});
|
|
|
|
cities = createEnumKeyedRecord(CityName, (name) => new City(name));
|
|
city = CityName.Sector12;
|
|
// Todo: better types for all these Record<string, etc> types. Will need custom types or enums for the named string categories (e.g. skills).
|
|
skills: Record<string, number> = {};
|
|
skillMultipliers: Record<string, number> = {};
|
|
staminaBonus = 0;
|
|
maxStamina = 0;
|
|
stamina = 0;
|
|
contracts: Record<string, Contract> = {};
|
|
operations: Record<string, Operation> = {};
|
|
blackops: Record<string, boolean> = {};
|
|
logging = {
|
|
general: true,
|
|
contracts: true,
|
|
ops: true,
|
|
blackops: true,
|
|
events: true,
|
|
};
|
|
automateEnabled = false;
|
|
automateActionHigh: ActionIdentifier = new ActionIdentifier({
|
|
type: ActionTypes.Idle,
|
|
});
|
|
automateThreshHigh = 0;
|
|
automateActionLow: ActionIdentifier = new ActionIdentifier({
|
|
type: ActionTypes.Idle,
|
|
});
|
|
automateThreshLow = 0;
|
|
consoleHistory: string[] = [];
|
|
consoleLogs: string[] = ["Bladeburner Console", "Type 'help' to see console commands"];
|
|
|
|
constructor() {
|
|
this.updateSkillMultipliers(); // Calls resetSkillMultipliers()
|
|
|
|
// Max Stamina is based on stats and Bladeburner-specific bonuses
|
|
this.calculateMaxStamina();
|
|
this.stamina = this.maxStamina;
|
|
this.create();
|
|
}
|
|
/*
|
|
just a quick fix for the broken implementation
|
|
BlackOperations are only initialized on game load with a count of 1
|
|
and are not reset on BitNode change or dev menu reset of bladeburner
|
|
*/
|
|
resetBlackOps(): void {
|
|
for (const [blackopName, blackop] of Object.entries(BlackOperations)) {
|
|
blackop.count = Number(!this.blackops[blackopName]);
|
|
}
|
|
}
|
|
|
|
getCurrentCity(): City {
|
|
return this.cities[this.city];
|
|
}
|
|
|
|
calculateStaminaPenalty(): number {
|
|
if (this.stamina === this.maxStamina) return 1;
|
|
return Math.min(1, this.stamina / (0.5 * this.maxStamina));
|
|
}
|
|
|
|
// Todo, deduplicate this functionality
|
|
getNextBlackOp(): { name: string; rank: number } | null {
|
|
let blackops: BlackOperation[] = [];
|
|
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) =>
|
|
!(this.blackops[blackops[i].name] == null && i !== 0 && this.blackops[blackops[i - 1].name] == null),
|
|
);
|
|
|
|
blackops = blackops.reverse();
|
|
const actionID = this.getActionIdFromTypeAndName("Black Op", "Operation Daedalus");
|
|
|
|
return blackops[0].name === "Operation Daedalus" &&
|
|
actionID !== null &&
|
|
!this.canAttemptBlackOp(actionID).isAvailable
|
|
? null
|
|
: { name: blackops[0].name, rank: blackops[0].reqdRank };
|
|
}
|
|
|
|
canAttemptBlackOp(actionId: ActionIdentifier): BlackOpsAttempt {
|
|
// Safety measure - don't repeat BlackOps that are already done
|
|
if (this.blackops[actionId.name] != null) {
|
|
return { error: "Tried to start a Black Operation that had already been completed" };
|
|
}
|
|
|
|
const action = this.getActionObject(actionId);
|
|
if (!(action instanceof BlackOperation)) throw new Error(`Action should be BlackOperation but isn't`);
|
|
if (action == null) throw new Error("Failed to get BlackOperation object for: " + actionId.name);
|
|
|
|
if (action.reqdRank > this.rank) {
|
|
return { error: "Tried to start a Black Operation without the rank requirement" };
|
|
}
|
|
|
|
// Can't start a BlackOp if you haven't done the one before it
|
|
const blackops = [];
|
|
for (const nm of Object.keys(BlackOperations)) {
|
|
if (Object.hasOwn(BlackOperations, nm)) {
|
|
blackops.push(nm);
|
|
}
|
|
}
|
|
blackops.sort(function (a, b) {
|
|
return BlackOperations[a].reqdRank - BlackOperations[b].reqdRank; // Sort black ops in intended order
|
|
});
|
|
|
|
const i = blackops.indexOf(actionId.name);
|
|
if (i === -1) {
|
|
return { error: `Invalid Black Op: '${name}'` };
|
|
}
|
|
|
|
if (i > 0 && this.blackops[blackops[i - 1]] == null) {
|
|
return { error: `Preceding Black Op must be completed before starting '${actionId.name}'.` };
|
|
}
|
|
|
|
return { isAvailable: true, action };
|
|
}
|
|
|
|
/** This function is only for the player. Sleeves use their own functions to perform blade work.
|
|
* Todo: partial unification of player and sleeve methods? */
|
|
startAction(actionId: ActionIdentifier): void {
|
|
if (actionId == null) return;
|
|
this.action = actionId;
|
|
this.actionTimeCurrent = 0;
|
|
switch (actionId.type) {
|
|
case ActionTypes.Idle:
|
|
this.actionTimeToComplete = 0;
|
|
break;
|
|
case ActionTypes.Contract:
|
|
try {
|
|
const action = this.getActionObject(actionId);
|
|
if (action == null) {
|
|
throw new Error("Failed to get Contract Object for: " + actionId.name);
|
|
}
|
|
if (action.count < 1) {
|
|
return this.resetAction();
|
|
}
|
|
this.actionTimeToComplete = action.getActionTime(this, Player);
|
|
} catch (e: unknown) {
|
|
exceptionAlert(e);
|
|
}
|
|
break;
|
|
case ActionTypes.Operation: {
|
|
try {
|
|
const action = this.getActionObject(actionId);
|
|
if (action == null) {
|
|
throw new Error("Failed to get Operation Object for: " + actionId.name);
|
|
}
|
|
if (action.count < 1) {
|
|
return this.resetAction();
|
|
}
|
|
if (actionId.name === "Raid" && this.getCurrentCity().comms === 0) {
|
|
return this.resetAction();
|
|
}
|
|
this.actionTimeToComplete = action.getActionTime(this, Player);
|
|
} catch (e: unknown) {
|
|
exceptionAlert(e);
|
|
}
|
|
break;
|
|
}
|
|
case ActionTypes.BlackOp:
|
|
case ActionTypes.BlackOperation: {
|
|
try {
|
|
const testBlackOp = this.canAttemptBlackOp(actionId);
|
|
if (!testBlackOp.isAvailable) {
|
|
this.resetAction();
|
|
this.log(`Error: ${testBlackOp.error}`);
|
|
break;
|
|
}
|
|
if (testBlackOp.action === undefined) {
|
|
throw new Error("action should not be null");
|
|
}
|
|
this.actionTimeToComplete = testBlackOp.action.getActionTime(this, Player);
|
|
} catch (e: unknown) {
|
|
exceptionAlert(e);
|
|
}
|
|
break;
|
|
}
|
|
case ActionTypes.Recruitment:
|
|
this.actionTimeToComplete = this.getRecruitmentTime(Player);
|
|
break;
|
|
case ActionTypes.Training:
|
|
case ActionTypes.FieldAnalysis:
|
|
case ActionTypes["Field Analysis"]:
|
|
this.actionTimeToComplete = 30;
|
|
break;
|
|
case ActionTypes.Diplomacy:
|
|
case ActionTypes["Hyperbolic Regeneration Chamber"]:
|
|
case ActionTypes["Incite Violence"]:
|
|
this.actionTimeToComplete = 60;
|
|
break;
|
|
default:
|
|
throw new Error("Invalid Action Type in bladeburner.startAction(): " + actionId.type);
|
|
}
|
|
}
|
|
|
|
upgradeSkill(skill: Skill, count = 1): void {
|
|
// This does NOT handle deduction of skill points
|
|
const skillName = skill.name;
|
|
if (this.skills[skillName]) {
|
|
this.skills[skillName] += count;
|
|
} else {
|
|
this.skills[skillName] = count;
|
|
}
|
|
if (isNaN(this.skills[skillName]) || this.skills[skillName] < 0) {
|
|
throw new Error("Level of Skill " + skillName + " is invalid: " + this.skills[skillName]);
|
|
}
|
|
this.updateSkillMultipliers();
|
|
}
|
|
|
|
executeConsoleCommands(commands: string): void {
|
|
try {
|
|
// Console History
|
|
if (this.consoleHistory[this.consoleHistory.length - 1] != commands) {
|
|
this.consoleHistory.push(commands);
|
|
if (this.consoleHistory.length > 50) {
|
|
this.consoleHistory.splice(0, 1);
|
|
}
|
|
}
|
|
|
|
const arrayOfCommands = commands.split(";");
|
|
for (let i = 0; i < arrayOfCommands.length; ++i) {
|
|
this.executeConsoleCommand(arrayOfCommands[i]);
|
|
}
|
|
} catch (e: unknown) {
|
|
exceptionAlert(e);
|
|
}
|
|
}
|
|
|
|
postToConsole(input: string, saveToLogs = true): void {
|
|
const MaxConsoleEntries = 100;
|
|
if (saveToLogs) {
|
|
this.consoleLogs.push(input);
|
|
if (this.consoleLogs.length > MaxConsoleEntries) {
|
|
this.consoleLogs.shift();
|
|
}
|
|
}
|
|
}
|
|
|
|
log(input: string): void {
|
|
// Adds a timestamp and then just calls postToConsole
|
|
this.postToConsole(`[${getTimestamp()}] ${input}`);
|
|
}
|
|
|
|
resetAction(): void {
|
|
this.action = new ActionIdentifier({ type: ActionTypes.Idle });
|
|
this.actionTimeCurrent = 0;
|
|
this.actionTimeToComplete = 0;
|
|
}
|
|
|
|
clearConsole(): void {
|
|
this.consoleLogs.length = 0;
|
|
}
|
|
|
|
prestige(): void {
|
|
this.resetAction();
|
|
const bladeburnerFac = Factions[FactionName.Bladeburners];
|
|
if (this.rank >= BladeburnerConstants.RankNeededForFaction) {
|
|
joinFaction(bladeburnerFac);
|
|
}
|
|
}
|
|
|
|
storeCycles(numCycles = 0): void {
|
|
this.storedCycles += numCycles;
|
|
}
|
|
|
|
getActionIdFromTypeAndName(type = "", name = ""): ActionIdentifier | null {
|
|
if (type === "" || name === "") {
|
|
return null;
|
|
}
|
|
const action = new ActionIdentifier();
|
|
const convertedType = type.toLowerCase().trim();
|
|
const convertedName = name.toLowerCase().trim();
|
|
switch (convertedType) {
|
|
case "contract":
|
|
case "contracts":
|
|
case "contr":
|
|
action.type = ActionTypes.Contract;
|
|
if (Object.hasOwn(this.contracts, name)) {
|
|
action.name = name;
|
|
return action;
|
|
}
|
|
return null;
|
|
case "operation":
|
|
case "operations":
|
|
case "op":
|
|
case "ops":
|
|
action.type = ActionTypes.Operation;
|
|
if (Object.hasOwn(this.operations, name)) {
|
|
action.name = name;
|
|
return action;
|
|
}
|
|
return null;
|
|
case "blackoperation":
|
|
case "black operation":
|
|
case "black operations":
|
|
case "black op":
|
|
case "black ops":
|
|
case "blackop":
|
|
case "blackops":
|
|
action.type = ActionTypes.BlackOp;
|
|
if (Object.hasOwn(BlackOperations, name)) {
|
|
action.name = name;
|
|
return action;
|
|
}
|
|
return null;
|
|
case "general":
|
|
case "general action":
|
|
case "gen":
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
|
|
if (convertedType.startsWith("gen")) {
|
|
switch (convertedName) {
|
|
case "training":
|
|
action.type = ActionTypes.Training;
|
|
action.name = "Training";
|
|
break;
|
|
case "recruitment":
|
|
case "recruit":
|
|
action.type = ActionTypes.Recruitment;
|
|
action.name = "Recruitment";
|
|
break;
|
|
case "field analysis":
|
|
case "fieldanalysis":
|
|
action.type = ActionTypes["Field Analysis"];
|
|
action.name = "Field Analysis";
|
|
break;
|
|
case "diplomacy":
|
|
action.type = ActionTypes.Diplomacy;
|
|
action.name = "Diplomacy";
|
|
break;
|
|
case "hyperbolic regeneration chamber":
|
|
action.type = ActionTypes["Hyperbolic Regeneration Chamber"];
|
|
action.name = "Hyperbolic Regeneration Chamber";
|
|
break;
|
|
case "incite violence":
|
|
action.type = ActionTypes["Incite Violence"];
|
|
action.name = "Incite Violence";
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
return action;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
executeStartConsoleCommand(args: string[]): void {
|
|
if (args.length !== 3) {
|
|
this.postToConsole("Invalid usage of 'start' console command: start [type] [name]");
|
|
this.postToConsole("Use 'help start' for more info");
|
|
return;
|
|
}
|
|
const name = args[2];
|
|
switch (args[1].toLowerCase()) {
|
|
case "general":
|
|
case "gen":
|
|
if (GeneralActions[name] != null) {
|
|
this.action.type = ActionTypes[name];
|
|
this.action.name = name;
|
|
this.startAction(this.action);
|
|
} else {
|
|
this.postToConsole("Invalid action name specified: " + args[2]);
|
|
}
|
|
break;
|
|
case "contract":
|
|
case "contracts":
|
|
if (this.contracts[name] != null) {
|
|
this.action.type = ActionTypes.Contract;
|
|
this.action.name = name;
|
|
this.startAction(this.action);
|
|
} else {
|
|
this.postToConsole("Invalid contract name specified: " + args[2]);
|
|
}
|
|
break;
|
|
case "ops":
|
|
case "op":
|
|
case "operations":
|
|
case "operation":
|
|
if (this.operations[name] != null) {
|
|
this.action.type = ActionTypes.Operation;
|
|
this.action.name = name;
|
|
this.startAction(this.action);
|
|
} else {
|
|
this.postToConsole("Invalid Operation name specified: " + args[2]);
|
|
}
|
|
break;
|
|
case "blackops":
|
|
case "blackop":
|
|
case "black operations":
|
|
case "black operation":
|
|
if (BlackOperations[name] != null) {
|
|
this.action.type = ActionTypes.BlackOperation;
|
|
this.action.name = name;
|
|
this.startAction(this.action);
|
|
} else {
|
|
this.postToConsole("Invalid BlackOp name specified: " + args[2]);
|
|
}
|
|
break;
|
|
default:
|
|
this.postToConsole("Invalid action/event type specified: " + args[1]);
|
|
this.postToConsole("Examples of valid action/event identifiers are: [general, contract, op, blackop]");
|
|
break;
|
|
}
|
|
}
|
|
|
|
executeSkillConsoleCommand(args: string[]): void {
|
|
switch (args.length) {
|
|
case 1: {
|
|
// Display Skill Help Command
|
|
this.postToConsole("Invalid usage of 'skill' console command: skill [action] [name]");
|
|
this.postToConsole("Use 'help skill' for more info");
|
|
break;
|
|
}
|
|
case 2: {
|
|
if (args[1].toLowerCase() === "list") {
|
|
// List all skills and their level
|
|
this.postToConsole("Skills: ");
|
|
const skillNames = Object.keys(Skills);
|
|
for (let i = 0; i < skillNames.length; ++i) {
|
|
const skill = Skills[skillNames[i]];
|
|
let level = 0;
|
|
if (this.skills[skill.name] != null) {
|
|
level = this.skills[skill.name];
|
|
}
|
|
this.postToConsole(skill.name + ": Level " + formatNumberNoSuffix(level, 0));
|
|
}
|
|
this.postToConsole(" ");
|
|
this.postToConsole("Effects: ");
|
|
const multKeys = Object.keys(this.skillMultipliers);
|
|
for (let i = 0; i < multKeys.length; ++i) {
|
|
const mult = this.skillMultipliers[multKeys[i]];
|
|
if (mult && mult !== 1) {
|
|
const mults = formatNumberNoSuffix(mult, 3);
|
|
switch (multKeys[i]) {
|
|
case "successChanceAll":
|
|
this.postToConsole("Total Success Chance: x" + mults);
|
|
break;
|
|
case "successChanceStealth":
|
|
this.postToConsole("Stealth Success Chance: x" + mults);
|
|
break;
|
|
case "successChanceKill":
|
|
this.postToConsole("Retirement Success Chance: x" + mults);
|
|
break;
|
|
case "successChanceContract":
|
|
this.postToConsole("Contract Success Chance: x" + mults);
|
|
break;
|
|
case "successChanceOperation":
|
|
this.postToConsole("Operation Success Chance: x" + mults);
|
|
break;
|
|
case "successChanceEstimate":
|
|
this.postToConsole("Synthoid Data Estimate: x" + mults);
|
|
break;
|
|
case "actionTime":
|
|
this.postToConsole("Action Time: x" + mults);
|
|
break;
|
|
case "effHack":
|
|
this.postToConsole("Hacking Skill: x" + mults);
|
|
break;
|
|
case "effStr":
|
|
this.postToConsole("Strength: x" + mults);
|
|
break;
|
|
case "effDef":
|
|
this.postToConsole("Defense: x" + mults);
|
|
break;
|
|
case "effDex":
|
|
this.postToConsole("Dexterity: x" + mults);
|
|
break;
|
|
case "effAgi":
|
|
this.postToConsole("Agility: x" + mults);
|
|
break;
|
|
case "effCha":
|
|
this.postToConsole("Charisma: x" + mults);
|
|
break;
|
|
case "effInt":
|
|
this.postToConsole("Intelligence: x" + mults);
|
|
break;
|
|
case "stamina":
|
|
this.postToConsole("Stamina: x" + mults);
|
|
break;
|
|
default:
|
|
console.warn(`Unrecognized SkillMult Key: ${multKeys[i]}`);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
this.postToConsole("Invalid usage of 'skill' console command: skill [action] [name]");
|
|
this.postToConsole("Use 'help skill' for more info");
|
|
}
|
|
break;
|
|
}
|
|
case 3: {
|
|
const skillName = args[2];
|
|
const skill = Skills[skillName];
|
|
if (!skill) {
|
|
this.postToConsole("Invalid skill name (Note that it is case-sensitive): " + skillName);
|
|
break;
|
|
}
|
|
if (args[1].toLowerCase() === "list") {
|
|
let level = 0;
|
|
if (this.skills[skill.name] !== undefined) {
|
|
level = this.skills[skill.name];
|
|
}
|
|
this.postToConsole(skill.name + ": Level " + formatNumberNoSuffix(level));
|
|
} else if (args[1].toLowerCase() === "level") {
|
|
let currentLevel = 0;
|
|
if (this.skills[skillName] && !isNaN(this.skills[skillName])) {
|
|
currentLevel = this.skills[skillName];
|
|
}
|
|
const pointCost = skill.calculateCost(currentLevel);
|
|
if (skill.maxLvl !== 0 && currentLevel >= skill.maxLvl) {
|
|
this.postToConsole(`This skill ${skill.name} is already at max level (${currentLevel}/${skill.maxLvl}).`);
|
|
} else if (this.skillPoints >= pointCost) {
|
|
this.skillPoints -= pointCost;
|
|
this.upgradeSkill(skill);
|
|
this.log(skill.name + " upgraded to Level " + this.skills[skillName]);
|
|
} else {
|
|
this.postToConsole(
|
|
"You do not have enough Skill Points to upgrade this. You need " + formatNumberNoSuffix(pointCost, 0),
|
|
);
|
|
}
|
|
} else {
|
|
this.postToConsole("Invalid usage of 'skill' console command: skill [action] [name]");
|
|
this.postToConsole("Use 'help skill' for more info");
|
|
}
|
|
break;
|
|
}
|
|
default: {
|
|
this.postToConsole("Invalid usage of 'skill' console command: skill [action] [name]");
|
|
this.postToConsole("Use 'help skill' for more info");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
executeLogConsoleCommand(args: string[]): void {
|
|
if (args.length < 3) {
|
|
this.postToConsole("Invalid usage of log command: log [enable/disable] [action/event]");
|
|
this.postToConsole("Use 'help log' for more details and examples");
|
|
return;
|
|
}
|
|
|
|
let flag = true;
|
|
if (args[1].toLowerCase().includes("d")) {
|
|
flag = false;
|
|
} // d for disable
|
|
|
|
switch (args[2].toLowerCase()) {
|
|
case "general":
|
|
case "gen":
|
|
this.logging.general = flag;
|
|
this.log("Logging " + (flag ? "enabled" : "disabled") + " for general actions");
|
|
break;
|
|
case "contract":
|
|
case "contracts":
|
|
this.logging.contracts = flag;
|
|
this.log("Logging " + (flag ? "enabled" : "disabled") + " for Contracts");
|
|
break;
|
|
case "ops":
|
|
case "op":
|
|
case "operations":
|
|
case "operation":
|
|
this.logging.ops = flag;
|
|
this.log("Logging " + (flag ? "enabled" : "disabled") + " for Operations");
|
|
break;
|
|
case "blackops":
|
|
case "blackop":
|
|
case "black operations":
|
|
case "black operation":
|
|
this.logging.blackops = flag;
|
|
this.log("Logging " + (flag ? "enabled" : "disabled") + " for BlackOps");
|
|
break;
|
|
case "event":
|
|
case "events":
|
|
this.logging.events = flag;
|
|
this.log("Logging " + (flag ? "enabled" : "disabled") + " for events");
|
|
break;
|
|
case "all":
|
|
this.logging.general = flag;
|
|
this.logging.contracts = flag;
|
|
this.logging.ops = flag;
|
|
this.logging.blackops = flag;
|
|
this.logging.events = flag;
|
|
this.log("Logging " + (flag ? "enabled" : "disabled") + " for everything");
|
|
break;
|
|
default:
|
|
this.postToConsole("Invalid action/event type specified: " + args[2]);
|
|
this.postToConsole(
|
|
"Examples of valid action/event identifiers are: [general, contracts, ops, blackops, events]",
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
|
|
executeHelpConsoleCommand(args: string[]): void {
|
|
if (args.length === 1) {
|
|
for (const line of ConsoleHelpText.helpList) {
|
|
this.postToConsole(line);
|
|
}
|
|
} else {
|
|
for (let i = 1; i < args.length; ++i) {
|
|
if (!(args[i] in ConsoleHelpText)) continue;
|
|
const helpText = ConsoleHelpText[args[i]];
|
|
for (const line of helpText) {
|
|
this.postToConsole(line);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
executeAutomateConsoleCommand(args: string[]): void {
|
|
if (args.length !== 2 && args.length !== 4) {
|
|
this.postToConsole(
|
|
"Invalid use of 'automate' command: automate [var] [val] [hi/low]. Use 'help automate' for more info",
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Enable/Disable
|
|
if (args.length === 2) {
|
|
const flag = args[1];
|
|
if (flag.toLowerCase() === "status") {
|
|
this.postToConsole("Automation: " + (this.automateEnabled ? "enabled" : "disabled"));
|
|
this.postToConsole(
|
|
"When your stamina drops to " +
|
|
formatNumberNoSuffix(this.automateThreshLow, 0) +
|
|
", you will automatically switch to " +
|
|
this.automateActionLow.name +
|
|
". When your stamina recovers to " +
|
|
formatNumberNoSuffix(this.automateThreshHigh, 0) +
|
|
", you will automatically " +
|
|
"switch to " +
|
|
this.automateActionHigh.name +
|
|
".",
|
|
);
|
|
} else if (flag.toLowerCase().includes("en")) {
|
|
if (!this.automateActionLow || !this.automateActionHigh) {
|
|
return this.log("Failed to enable automation. Actions were not set");
|
|
}
|
|
this.automateEnabled = true;
|
|
this.log("Bladeburner automation enabled");
|
|
} else if (flag.toLowerCase().includes("d")) {
|
|
this.automateEnabled = false;
|
|
this.log("Bladeburner automation disabled");
|
|
} else {
|
|
this.log("Invalid argument for 'automate' console command: " + args[1]);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Set variables
|
|
if (args.length === 4) {
|
|
const variable = args[1].toLowerCase(); // allows Action Type to be with or without capitalization.
|
|
const val = args[2];
|
|
|
|
let highLow = false; // True for high, false for low
|
|
if (args[3].toLowerCase().includes("hi")) {
|
|
highLow = true;
|
|
}
|
|
|
|
switch (variable) {
|
|
case "general":
|
|
case "gen":
|
|
if (GeneralActions[val] != null) {
|
|
const action = new ActionIdentifier({
|
|
type: ActionTypes[val],
|
|
name: val,
|
|
});
|
|
if (highLow) {
|
|
this.automateActionHigh = action;
|
|
} else {
|
|
this.automateActionLow = action;
|
|
}
|
|
this.log("Automate (" + (highLow ? "HIGH" : "LOW") + ") action set to " + val);
|
|
} else {
|
|
this.postToConsole("Invalid action name specified: " + val);
|
|
}
|
|
break;
|
|
case "contract":
|
|
case "contracts":
|
|
if (this.contracts[val] != null) {
|
|
const action = new ActionIdentifier({
|
|
type: ActionTypes.Contract,
|
|
name: val,
|
|
});
|
|
if (highLow) {
|
|
this.automateActionHigh = action;
|
|
} else {
|
|
this.automateActionLow = action;
|
|
}
|
|
this.log("Automate (" + (highLow ? "HIGH" : "LOW") + ") action set to " + val);
|
|
} else {
|
|
this.postToConsole("Invalid contract name specified: " + val);
|
|
}
|
|
break;
|
|
case "ops":
|
|
case "op":
|
|
case "operations":
|
|
case "operation":
|
|
if (this.operations[val] != null) {
|
|
const action = new ActionIdentifier({
|
|
type: ActionTypes.Operation,
|
|
name: val,
|
|
});
|
|
if (highLow) {
|
|
this.automateActionHigh = action;
|
|
} else {
|
|
this.automateActionLow = action;
|
|
}
|
|
this.log("Automate (" + (highLow ? "HIGH" : "LOW") + ") action set to " + val);
|
|
} else {
|
|
this.postToConsole("Invalid Operation name specified: " + val);
|
|
}
|
|
break;
|
|
case "stamina":
|
|
if (isNaN(parseFloat(val))) {
|
|
this.postToConsole("Invalid value specified for stamina threshold (must be numeric): " + val);
|
|
} else {
|
|
if (highLow) {
|
|
this.automateThreshHigh = Number(val);
|
|
} else {
|
|
this.automateThreshLow = Number(val);
|
|
}
|
|
this.log("Automate (" + (highLow ? "HIGH" : "LOW") + ") stamina threshold set to " + val);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
parseCommandArguments(command: string): string[] {
|
|
/**
|
|
* Returns an array with command and its arguments in each index.
|
|
* e.g. skill "blade's intuition" foo returns [skill, blade's intuition, foo]
|
|
* The input to the fn will be trimmed and will have all whitespace replaced w/ a single space
|
|
*/
|
|
const args = [];
|
|
let start = 0;
|
|
let i = 0;
|
|
while (i < command.length) {
|
|
const c = command.charAt(i);
|
|
if (c === '"' || c === "'") {
|
|
// Double quotes or Single quotes
|
|
const endQuote = command.indexOf(c, i + 1);
|
|
if (endQuote !== -1 && (endQuote === command.length - 1 || command.charAt(endQuote + 1) === KEY.SPACE)) {
|
|
args.push(command.substr(i + 1, endQuote - i - 1));
|
|
if (endQuote === command.length - 1) {
|
|
start = i = endQuote + 1;
|
|
} else {
|
|
start = i = endQuote + 2; // Skip the space
|
|
}
|
|
continue;
|
|
}
|
|
} else if (c === KEY.SPACE) {
|
|
args.push(command.substr(start, i - start));
|
|
start = i + 1;
|
|
}
|
|
++i;
|
|
}
|
|
if (start !== i) {
|
|
args.push(command.substr(start, i - start));
|
|
}
|
|
return args;
|
|
}
|
|
|
|
executeConsoleCommand(command: string): void {
|
|
command = command.trim();
|
|
command = command.replace(/\s\s+/g, " "); // Replace all whitespace w/ a single space
|
|
|
|
const args = this.parseCommandArguments(command);
|
|
if (args.length <= 0) return; // Log an error?
|
|
|
|
switch (args[0].toLowerCase()) {
|
|
case "automate":
|
|
this.executeAutomateConsoleCommand(args);
|
|
break;
|
|
case "clear":
|
|
case "cls":
|
|
this.clearConsole();
|
|
break;
|
|
case "help":
|
|
this.executeHelpConsoleCommand(args);
|
|
break;
|
|
case "log":
|
|
this.executeLogConsoleCommand(args);
|
|
break;
|
|
case "skill":
|
|
this.executeSkillConsoleCommand(args);
|
|
break;
|
|
case "start":
|
|
this.executeStartConsoleCommand(args);
|
|
break;
|
|
case "stop":
|
|
this.resetAction();
|
|
break;
|
|
default:
|
|
this.postToConsole("Invalid console command");
|
|
break;
|
|
}
|
|
}
|
|
|
|
triggerMigration(sourceCityName: CityName): void {
|
|
const cityHelper = getEnumHelper("CityName");
|
|
let destCityName = cityHelper.random();
|
|
while (destCityName === sourceCityName) destCityName = cityHelper.random();
|
|
|
|
const destCity = this.cities[destCityName];
|
|
const sourceCity = this.cities[sourceCityName];
|
|
|
|
const rand = Math.random();
|
|
let percentage = getRandomInt(3, 15) / 100;
|
|
|
|
if (rand < 0.05 && sourceCity.comms > 0) {
|
|
// 5% chance for community migration
|
|
percentage *= getRandomInt(2, 4); // Migration increases population change
|
|
--sourceCity.comms;
|
|
++destCity.comms;
|
|
}
|
|
const count = Math.round(sourceCity.pop * percentage);
|
|
sourceCity.pop -= count;
|
|
destCity.pop += count;
|
|
if (destCity.pop < BladeburnerConstants.PopGrowthCeiling) {
|
|
destCity.pop += BladeburnerConstants.BasePopGrowth;
|
|
}
|
|
}
|
|
|
|
triggerPotentialMigration(sourceCityName: CityName, chance: number): void {
|
|
if (chance == null || isNaN(chance)) {
|
|
console.error("Invalid 'chance' parameter passed into Bladeburner.triggerPotentialMigration()");
|
|
}
|
|
if (chance > 1) {
|
|
chance /= 100;
|
|
}
|
|
if (Math.random() < chance) {
|
|
this.triggerMigration(sourceCityName);
|
|
}
|
|
}
|
|
|
|
randomEvent(): void {
|
|
const chance = Math.random();
|
|
const cityHelper = getEnumHelper("CityName");
|
|
|
|
// Choose random source/destination city for events
|
|
const sourceCityName = cityHelper.random();
|
|
const sourceCity = this.cities[sourceCityName];
|
|
|
|
let destCityName = cityHelper.random();
|
|
while (destCityName === sourceCityName) destCityName = cityHelper.random();
|
|
const destCity = this.cities[destCityName];
|
|
|
|
if (chance <= 0.05) {
|
|
// New Synthoid Community, 5%
|
|
++sourceCity.comms;
|
|
const percentage = getRandomInt(10, 20) / 100;
|
|
const count = Math.round(sourceCity.pop * percentage);
|
|
sourceCity.pop += count;
|
|
if (sourceCity.pop < BladeburnerConstants.PopGrowthCeiling) {
|
|
sourceCity.pop += BladeburnerConstants.BasePopGrowth;
|
|
}
|
|
if (this.logging.events) {
|
|
this.log("Intelligence indicates that a new Synthoid community was formed in a city");
|
|
}
|
|
} else if (chance <= 0.1) {
|
|
// Synthoid Community Migration, 5%
|
|
if (sourceCity.comms <= 0) {
|
|
// If no comms in source city, then instead trigger a new Synthoid community event
|
|
++sourceCity.comms;
|
|
const percentage = getRandomInt(10, 20) / 100;
|
|
const count = Math.round(sourceCity.pop * percentage);
|
|
sourceCity.pop += count;
|
|
if (sourceCity.pop < BladeburnerConstants.PopGrowthCeiling) {
|
|
sourceCity.pop += BladeburnerConstants.BasePopGrowth;
|
|
}
|
|
if (this.logging.events) {
|
|
this.log("Intelligence indicates that a new Synthoid community was formed in a city");
|
|
}
|
|
} else {
|
|
--sourceCity.comms;
|
|
++destCity.comms;
|
|
|
|
// Change pop
|
|
const percentage = getRandomInt(10, 20) / 100;
|
|
const count = Math.round(sourceCity.pop * percentage);
|
|
sourceCity.pop -= count;
|
|
destCity.pop += count;
|
|
if (destCity.pop < BladeburnerConstants.PopGrowthCeiling) {
|
|
destCity.pop += BladeburnerConstants.BasePopGrowth;
|
|
}
|
|
if (this.logging.events) {
|
|
this.log(
|
|
"Intelligence indicates that a Synthoid community migrated from " + sourceCityName + " to some other city",
|
|
);
|
|
}
|
|
}
|
|
} else if (chance <= 0.3) {
|
|
// New Synthoids (non community), 20%
|
|
const percentage = getRandomInt(8, 24) / 100;
|
|
const count = Math.round(sourceCity.pop * percentage);
|
|
sourceCity.pop += count;
|
|
if (sourceCity.pop < BladeburnerConstants.PopGrowthCeiling) {
|
|
sourceCity.pop += BladeburnerConstants.BasePopGrowth;
|
|
}
|
|
if (this.logging.events) {
|
|
this.log(
|
|
"Intelligence indicates that the Synthoid population of " + sourceCityName + " just changed significantly",
|
|
);
|
|
}
|
|
} else if (chance <= 0.5) {
|
|
// Synthoid migration (non community) 20%
|
|
this.triggerMigration(sourceCityName);
|
|
if (this.logging.events) {
|
|
this.log(
|
|
"Intelligence indicates that a large number of Synthoids migrated from " +
|
|
sourceCityName +
|
|
" to some other city",
|
|
);
|
|
}
|
|
} else if (chance <= 0.7) {
|
|
// Synthoid Riots (+chaos), 20%
|
|
sourceCity.chaos += 1;
|
|
sourceCity.chaos *= 1 + getRandomInt(5, 20) / 100;
|
|
if (this.logging.events) {
|
|
this.log("Tensions between Synthoids and humans lead to riots in " + sourceCityName + "! Chaos increased");
|
|
}
|
|
} else if (chance <= 0.9) {
|
|
// Less Synthoids, 20%
|
|
const percentage = getRandomInt(8, 20) / 100;
|
|
const count = Math.round(sourceCity.pop * percentage);
|
|
sourceCity.pop -= count;
|
|
if (this.logging.events) {
|
|
this.log(
|
|
"Intelligence indicates that the Synthoid population of " + sourceCityName + " just changed significantly",
|
|
);
|
|
}
|
|
}
|
|
// 10% chance of nothing happening
|
|
}
|
|
|
|
/**
|
|
* Return stat to be gained from Contracts, Operations, and Black Operations
|
|
* @param action(Action obj) - Derived action class
|
|
* @param success(bool) - Whether action was successful
|
|
*/
|
|
getActionStats(action: Action, person: Person, success: boolean): WorkStats {
|
|
const difficulty = action.getDifficulty();
|
|
|
|
/**
|
|
* Gain multiplier based on difficulty. If it changes then the
|
|
* same variable calculated in completeAction() needs to change too
|
|
*/
|
|
const difficultyMult =
|
|
Math.pow(difficulty, BladeburnerConstants.DiffMultExponentialFactor) +
|
|
difficulty / BladeburnerConstants.DiffMultLinearFactor;
|
|
|
|
const time = action.getActionTime(this, person);
|
|
const successMult = success ? 1 : 0.5;
|
|
|
|
const unweightedGain = time * BladeburnerConstants.BaseStatGain * successMult * difficultyMult;
|
|
const unweightedIntGain = time * BladeburnerConstants.BaseIntGain * successMult * difficultyMult;
|
|
const skillMult = this.skillMultipliers.expGain;
|
|
|
|
return {
|
|
hackExp: unweightedGain * action.weights.hack * skillMult,
|
|
strExp: unweightedGain * action.weights.str * skillMult,
|
|
defExp: unweightedGain * action.weights.def * skillMult,
|
|
dexExp: unweightedGain * action.weights.dex * skillMult,
|
|
agiExp: unweightedGain * action.weights.agi * skillMult,
|
|
chaExp: unweightedGain * action.weights.cha * skillMult,
|
|
intExp: unweightedIntGain * action.weights.int * skillMult,
|
|
money: 0,
|
|
reputation: 0,
|
|
};
|
|
}
|
|
|
|
getDiplomacyEffectiveness(person: Person): number {
|
|
// Returns a decimal by which the city's chaos level should be multiplied (e.g. 0.98)
|
|
const CharismaLinearFactor = 1e3;
|
|
const CharismaExponentialFactor = 0.045;
|
|
|
|
const charismaEff =
|
|
Math.pow(person.skills.charisma, CharismaExponentialFactor) + person.skills.charisma / CharismaLinearFactor;
|
|
return (100 - charismaEff) / 100;
|
|
}
|
|
|
|
getRecruitmentSuccessChance(person: Person): number {
|
|
return Math.pow(person.skills.charisma, 0.45) / (this.teamSize - this.sleeveSize + 1);
|
|
}
|
|
|
|
getRecruitmentTime(person: Person): number {
|
|
const effCharisma = person.skills.charisma * this.skillMultipliers.effCha;
|
|
const charismaFactor = Math.pow(effCharisma, 0.81) + effCharisma / 90;
|
|
return Math.max(10, Math.round(BladeburnerConstants.BaseRecruitmentTimeNeeded - charismaFactor));
|
|
}
|
|
|
|
sleeveSupport(joining: boolean): void {
|
|
if (joining) {
|
|
this.sleeveSize += 1;
|
|
this.teamSize += 1;
|
|
} else {
|
|
this.sleeveSize -= 1;
|
|
this.teamSize -= 1;
|
|
}
|
|
}
|
|
|
|
resetSkillMultipliers(): void {
|
|
this.skillMultipliers = {
|
|
successChanceAll: 1,
|
|
successChanceStealth: 1,
|
|
successChanceKill: 1,
|
|
successChanceContract: 1,
|
|
successChanceOperation: 1,
|
|
successChanceEstimate: 1,
|
|
actionTime: 1,
|
|
effHack: 1,
|
|
effStr: 1,
|
|
effDef: 1,
|
|
effDex: 1,
|
|
effAgi: 1,
|
|
effCha: 1,
|
|
effInt: 1,
|
|
stamina: 1,
|
|
money: 1,
|
|
expGain: 1,
|
|
};
|
|
}
|
|
|
|
updateSkillMultipliers(): void {
|
|
this.resetSkillMultipliers();
|
|
for (const skillName of Object.keys(this.skills)) {
|
|
if (Object.hasOwn(this.skills, skillName)) {
|
|
const skill = Skills[skillName];
|
|
if (skill == null) {
|
|
throw new Error("Could not find Skill Object for: " + skillName);
|
|
}
|
|
const level = this.skills[skillName];
|
|
if (level == null || level <= 0) {
|
|
continue;
|
|
} //Not upgraded
|
|
|
|
const multiplierNames = Object.keys(this.skillMultipliers);
|
|
for (let i = 0; i < multiplierNames.length; ++i) {
|
|
const multiplierName = multiplierNames[i];
|
|
if (skill.getMultiplier(multiplierName) != null && !isNaN(skill.getMultiplier(multiplierName))) {
|
|
const value = skill.getMultiplier(multiplierName) * level;
|
|
let multiplierValue = 1 + value / 100;
|
|
if (multiplierName === "actionTime") {
|
|
multiplierValue = 1 - value / 100;
|
|
}
|
|
this.skillMultipliers[multiplierName] *= multiplierValue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
completeOperation(success: boolean): void {
|
|
if (this.action.type !== ActionTypes.Operation) {
|
|
throw new Error("completeOperation() called even though current action is not an Operation");
|
|
}
|
|
const action = this.getActionObject(this.action);
|
|
if (action == null) {
|
|
throw new Error("Failed to get Contract/Operation Object for: " + this.action.name);
|
|
}
|
|
|
|
// Calculate team losses
|
|
const teamCount = action.teamCount;
|
|
if (teamCount >= 1) {
|
|
let max;
|
|
if (success) {
|
|
max = Math.ceil(teamCount / 2);
|
|
} else {
|
|
max = Math.floor(teamCount);
|
|
}
|
|
const losses = getRandomInt(0, max);
|
|
this.teamSize -= losses;
|
|
if (this.teamSize < this.sleeveSize) {
|
|
const sup = Player.sleeves.filter((x) => isSleeveSupportWork(x.currentWork));
|
|
for (let i = 0; i > this.teamSize - this.sleeveSize; i--) {
|
|
const r = Math.floor(Math.random() * sup.length);
|
|
sup[r].takeDamage(sup[r].hp.max);
|
|
sup.splice(r, 1);
|
|
}
|
|
this.teamSize += this.sleeveSize;
|
|
}
|
|
this.teamLost += losses;
|
|
if (this.logging.ops && losses > 0) {
|
|
this.log("Lost " + formatNumberNoSuffix(losses, 0) + " team members during this " + action.name);
|
|
}
|
|
}
|
|
|
|
const city = this.getCurrentCity();
|
|
switch (action.name) {
|
|
case "Investigation":
|
|
if (success) {
|
|
city.improvePopulationEstimateByPercentage(0.4 * this.skillMultipliers.successChanceEstimate);
|
|
} else {
|
|
this.triggerPotentialMigration(this.city, 0.1);
|
|
}
|
|
break;
|
|
case "Undercover Operation":
|
|
if (success) {
|
|
city.improvePopulationEstimateByPercentage(0.8 * this.skillMultipliers.successChanceEstimate);
|
|
} else {
|
|
this.triggerPotentialMigration(this.city, 0.15);
|
|
}
|
|
break;
|
|
case "Sting Operation":
|
|
if (success) {
|
|
city.changePopulationByPercentage(-0.1, {
|
|
changeEstEqually: true,
|
|
nonZero: true,
|
|
});
|
|
}
|
|
city.changeChaosByCount(0.1);
|
|
break;
|
|
case "Raid":
|
|
if (success) {
|
|
city.changePopulationByPercentage(-1, {
|
|
changeEstEqually: true,
|
|
nonZero: true,
|
|
});
|
|
--city.comms;
|
|
} else {
|
|
const change = getRandomInt(-10, -5) / 10;
|
|
city.changePopulationByPercentage(change, {
|
|
nonZero: true,
|
|
changeEstEqually: false,
|
|
});
|
|
}
|
|
city.changeChaosByPercentage(getRandomInt(1, 5));
|
|
break;
|
|
case "Stealth Retirement Operation":
|
|
if (success) {
|
|
city.changePopulationByPercentage(-0.5, {
|
|
changeEstEqually: true,
|
|
nonZero: true,
|
|
});
|
|
}
|
|
city.changeChaosByPercentage(getRandomInt(-3, -1));
|
|
break;
|
|
case "Assassination":
|
|
if (success) {
|
|
city.changePopulationByCount(-1, { estChange: -1, estOffset: 0 });
|
|
}
|
|
city.changeChaosByPercentage(getRandomInt(-5, 5));
|
|
break;
|
|
default:
|
|
throw new Error("Invalid Action name in completeOperation: " + this.action.name);
|
|
}
|
|
}
|
|
|
|
getActionObject(actionId: ActionIdentifier): Action | null {
|
|
/**
|
|
* Given an ActionIdentifier object, returns the corresponding
|
|
* GeneralAction, Contract, Operation, or BlackOperation object
|
|
*/
|
|
switch (actionId.type) {
|
|
case ActionTypes.Contract:
|
|
return this.contracts[actionId.name];
|
|
case ActionTypes.Operation:
|
|
return this.operations[actionId.name];
|
|
case ActionTypes.BlackOp:
|
|
case ActionTypes.BlackOperation:
|
|
return BlackOperations[actionId.name];
|
|
case ActionTypes.Training:
|
|
return GeneralActions.Training;
|
|
case ActionTypes["Field Analysis"]:
|
|
return GeneralActions["Field Analysis"];
|
|
case ActionTypes.Recruitment:
|
|
return GeneralActions.Recruitment;
|
|
case ActionTypes.Diplomacy:
|
|
return GeneralActions.Diplomacy;
|
|
case ActionTypes["Hyperbolic Regeneration Chamber"]:
|
|
return GeneralActions["Hyperbolic Regeneration Chamber"];
|
|
case ActionTypes["Incite Violence"]:
|
|
return GeneralActions["Incite Violence"];
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
completeContract(success: boolean, actionIdent: ActionIdentifier): void {
|
|
if (actionIdent.type !== ActionTypes.Contract) {
|
|
throw new Error("completeContract() called even though current action is not a Contract");
|
|
}
|
|
const city = this.getCurrentCity();
|
|
if (success) {
|
|
switch (actionIdent.name) {
|
|
case "Tracking":
|
|
// Increase estimate accuracy by a relatively small amount
|
|
city.improvePopulationEstimateByCount(getRandomInt(100, 1e3) * this.skillMultipliers.successChanceEstimate);
|
|
break;
|
|
case "Bounty Hunter":
|
|
city.changePopulationByCount(-1, { estChange: -1, estOffset: 0 });
|
|
city.changeChaosByCount(0.02);
|
|
break;
|
|
case "Retirement":
|
|
city.changePopulationByCount(-1, { estChange: -1, estOffset: 0 });
|
|
city.changeChaosByCount(0.04);
|
|
break;
|
|
default:
|
|
throw new Error("Invalid Action name in completeContract: " + actionIdent.name);
|
|
}
|
|
}
|
|
}
|
|
|
|
completeAction(person: Person, actionIdent: ActionIdentifier, isPlayer = true): WorkStats {
|
|
let retValue = newWorkStats();
|
|
switch (actionIdent.type) {
|
|
case ActionTypes.Contract:
|
|
case ActionTypes.Operation: {
|
|
try {
|
|
const isOperation = actionIdent.type === ActionTypes.Operation;
|
|
const action = this.getActionObject(actionIdent);
|
|
if (action == null) {
|
|
throw new Error("Failed to get Contract/Operation Object for: " + actionIdent.name);
|
|
}
|
|
const difficulty = action.getDifficulty();
|
|
const difficultyMultiplier =
|
|
Math.pow(difficulty, BladeburnerConstants.DiffMultExponentialFactor) +
|
|
difficulty / BladeburnerConstants.DiffMultLinearFactor;
|
|
const rewardMultiplier = Math.pow(action.rewardFac, action.level - 1);
|
|
|
|
if (isPlayer) {
|
|
// Stamina loss is based on difficulty
|
|
this.stamina -= BladeburnerConstants.BaseStaminaLoss * difficultyMultiplier;
|
|
if (this.stamina < 0) {
|
|
this.stamina = 0;
|
|
}
|
|
}
|
|
|
|
// Process Contract/Operation success/failure
|
|
if (action.attempt(this, person)) {
|
|
retValue = this.getActionStats(action, person, true);
|
|
++action.successes;
|
|
--action.count;
|
|
|
|
// Earn money for contracts
|
|
let moneyGain = 0;
|
|
if (!isOperation) {
|
|
moneyGain = BladeburnerConstants.ContractBaseMoneyGain * rewardMultiplier * this.skillMultipliers.money;
|
|
retValue.money = moneyGain;
|
|
}
|
|
|
|
if (isOperation) {
|
|
action.setMaxLevel(BladeburnerConstants.OperationSuccessesPerLevel);
|
|
} else {
|
|
action.setMaxLevel(BladeburnerConstants.ContractSuccessesPerLevel);
|
|
}
|
|
if (action.rankGain) {
|
|
const gain = addOffset(action.rankGain * rewardMultiplier * currentNodeMults.BladeburnerRank, 10);
|
|
this.changeRank(person, gain);
|
|
if (isOperation && this.logging.ops) {
|
|
this.log(
|
|
`${person.whoAmI()}: ${action.name} successfully completed! Gained ${formatBigNumber(gain)} rank`,
|
|
);
|
|
} else if (!isOperation && this.logging.contracts) {
|
|
this.log(
|
|
`${person.whoAmI()}: ${action.name} contract successfully completed! Gained ` +
|
|
`${formatBigNumber(gain)} rank and ${formatMoney(moneyGain)}`,
|
|
);
|
|
}
|
|
}
|
|
isOperation ? this.completeOperation(true) : this.completeContract(true, actionIdent);
|
|
} else {
|
|
retValue = this.getActionStats(action, person, false);
|
|
++action.failures;
|
|
--action.count;
|
|
let loss = 0,
|
|
damage = 0;
|
|
if (action.rankLoss) {
|
|
loss = addOffset(action.rankLoss * rewardMultiplier, 10);
|
|
this.changeRank(person, -1 * loss);
|
|
}
|
|
if (action.hpLoss) {
|
|
damage = action.hpLoss * difficultyMultiplier;
|
|
damage = Math.ceil(addOffset(damage, 10));
|
|
this.hpLost += damage;
|
|
const cost = calculateHospitalizationCost(damage);
|
|
if (person.takeDamage(damage)) {
|
|
++this.numHosp;
|
|
this.moneyLost += cost;
|
|
}
|
|
}
|
|
let logLossText = "";
|
|
if (loss > 0) {
|
|
logLossText += "Lost " + formatNumberNoSuffix(loss, 3) + " rank. ";
|
|
}
|
|
if (damage > 0) {
|
|
logLossText += "Took " + formatNumberNoSuffix(damage, 0) + " damage.";
|
|
}
|
|
if (isOperation && this.logging.ops) {
|
|
this.log(`${person.whoAmI()}: ` + action.name + " failed! " + logLossText);
|
|
} else if (!isOperation && this.logging.contracts) {
|
|
this.log(`${person.whoAmI()}: ` + action.name + " contract failed! " + logLossText);
|
|
}
|
|
isOperation ? this.completeOperation(false) : this.completeContract(false, actionIdent);
|
|
}
|
|
if (action.autoLevel) {
|
|
action.level = action.maxLevel;
|
|
} // Autolevel
|
|
} catch (e: unknown) {
|
|
exceptionAlert(e);
|
|
}
|
|
break;
|
|
}
|
|
case ActionTypes.BlackOp:
|
|
case ActionTypes.BlackOperation: {
|
|
try {
|
|
const action = this.getActionObject(actionIdent);
|
|
if (action == null || !(action instanceof BlackOperation)) {
|
|
throw new Error("Failed to get BlackOperation Object for: " + actionIdent.name);
|
|
}
|
|
const difficulty = action.getDifficulty();
|
|
const difficultyMultiplier =
|
|
Math.pow(difficulty, BladeburnerConstants.DiffMultExponentialFactor) +
|
|
difficulty / BladeburnerConstants.DiffMultLinearFactor;
|
|
|
|
// Stamina loss is based on difficulty
|
|
this.stamina -= BladeburnerConstants.BaseStaminaLoss * difficultyMultiplier;
|
|
if (this.stamina < 0) {
|
|
this.stamina = 0;
|
|
}
|
|
|
|
// Team loss variables
|
|
const teamCount = action.teamCount;
|
|
let teamLossMax;
|
|
|
|
if (action.attempt(this, person)) {
|
|
retValue = this.getActionStats(action, person, true);
|
|
action.count = 0;
|
|
this.blackops[action.name] = true;
|
|
let rankGain = 0;
|
|
if (action.rankGain) {
|
|
rankGain = addOffset(action.rankGain * currentNodeMults.BladeburnerRank, 10);
|
|
this.changeRank(person, rankGain);
|
|
}
|
|
teamLossMax = Math.ceil(teamCount / 2);
|
|
|
|
if (this.logging.blackops) {
|
|
this.log(
|
|
`${person.whoAmI()}: ` +
|
|
action.name +
|
|
" successful! Gained " +
|
|
formatNumberNoSuffix(rankGain, 1) +
|
|
" rank",
|
|
);
|
|
}
|
|
} else {
|
|
retValue = this.getActionStats(action, person, false);
|
|
let rankLoss = 0;
|
|
let damage = 0;
|
|
if (action.rankLoss) {
|
|
rankLoss = addOffset(action.rankLoss, 10);
|
|
this.changeRank(person, -1 * rankLoss);
|
|
}
|
|
if (action.hpLoss) {
|
|
damage = action.hpLoss * difficultyMultiplier;
|
|
damage = Math.ceil(addOffset(damage, 10));
|
|
const cost = calculateHospitalizationCost(damage);
|
|
if (person.takeDamage(damage)) {
|
|
++this.numHosp;
|
|
this.moneyLost += cost;
|
|
}
|
|
}
|
|
teamLossMax = Math.floor(teamCount);
|
|
|
|
if (this.logging.blackops) {
|
|
this.log(
|
|
`${person.whoAmI()}: ` +
|
|
action.name +
|
|
" failed! Lost " +
|
|
formatNumberNoSuffix(rankLoss, 1) +
|
|
" rank and took " +
|
|
formatNumberNoSuffix(damage, 0) +
|
|
" damage",
|
|
);
|
|
}
|
|
}
|
|
|
|
this.resetAction(); // Stop regardless of success or fail
|
|
|
|
// Calculate team losses
|
|
if (teamCount >= 1) {
|
|
const losses = getRandomInt(1, teamLossMax);
|
|
this.teamSize -= losses;
|
|
if (this.teamSize < this.sleeveSize) {
|
|
const sup = Player.sleeves.filter((x) => isSleeveSupportWork(x.currentWork));
|
|
for (let i = 0; i > this.teamSize - this.sleeveSize; i--) {
|
|
const r = Math.floor(Math.random() * sup.length);
|
|
sup[r].takeDamage(sup[r].hp.max);
|
|
sup.splice(r, 1);
|
|
}
|
|
this.teamSize += this.sleeveSize;
|
|
}
|
|
this.teamLost += losses;
|
|
if (this.logging.blackops) {
|
|
this.log(
|
|
`${person.whoAmI()}: You lost ${formatNumberNoSuffix(losses, 0)} team members during ${action.name}`,
|
|
);
|
|
}
|
|
}
|
|
} catch (e: unknown) {
|
|
exceptionAlert(String(e));
|
|
}
|
|
break;
|
|
}
|
|
case ActionTypes.Training: {
|
|
this.stamina -= 0.5 * BladeburnerConstants.BaseStaminaLoss;
|
|
const strExpGain = 30 * person.mults.strength_exp,
|
|
defExpGain = 30 * person.mults.defense_exp,
|
|
dexExpGain = 30 * person.mults.dexterity_exp,
|
|
agiExpGain = 30 * person.mults.agility_exp,
|
|
staminaGain = 0.04 * this.skillMultipliers.stamina;
|
|
retValue.strExp = strExpGain;
|
|
retValue.defExp = defExpGain;
|
|
retValue.dexExp = dexExpGain;
|
|
retValue.agiExp = agiExpGain;
|
|
this.staminaBonus += staminaGain;
|
|
if (this.logging.general) {
|
|
this.log(
|
|
`${person.whoAmI()}: ` +
|
|
"Training completed. Gained: " +
|
|
formatExp(strExpGain) +
|
|
" str exp, " +
|
|
formatExp(defExpGain) +
|
|
" def exp, " +
|
|
formatExp(dexExpGain) +
|
|
" dex exp, " +
|
|
formatExp(agiExpGain) +
|
|
" agi exp, " +
|
|
formatBigNumber(staminaGain) +
|
|
" max stamina",
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
case ActionTypes.FieldAnalysis:
|
|
case ActionTypes["Field Analysis"]: {
|
|
// Does not use stamina. Effectiveness depends on hacking, int, and cha
|
|
let eff =
|
|
0.04 * Math.pow(person.skills.hacking, 0.3) +
|
|
0.04 * Math.pow(person.skills.intelligence, 0.9) +
|
|
0.02 * Math.pow(person.skills.charisma, 0.3);
|
|
eff *= person.mults.bladeburner_analysis;
|
|
if (isNaN(eff) || eff < 0) {
|
|
throw new Error("Field Analysis Effectiveness calculated to be NaN or negative");
|
|
}
|
|
const hackingExpGain = 20 * person.mults.hacking_exp;
|
|
const charismaExpGain = 20 * person.mults.charisma_exp;
|
|
const rankGain = 0.1 * currentNodeMults.BladeburnerRank;
|
|
retValue.hackExp = hackingExpGain;
|
|
retValue.chaExp = charismaExpGain;
|
|
retValue.intExp = BladeburnerConstants.BaseIntGain;
|
|
this.changeRank(person, rankGain);
|
|
this.getCurrentCity().improvePopulationEstimateByPercentage(eff * this.skillMultipliers.successChanceEstimate);
|
|
if (this.logging.general) {
|
|
this.log(
|
|
`${person.whoAmI()}: ` +
|
|
`Field analysis completed. Gained ${formatBigNumber(rankGain)} rank, ` +
|
|
`${formatExp(hackingExpGain)} hacking exp, and ` +
|
|
`${formatExp(charismaExpGain)} charisma exp`,
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
case ActionTypes.Recruitment: {
|
|
const successChance = this.getRecruitmentSuccessChance(person);
|
|
const recruitTime = this.getRecruitmentTime(person) * 1000;
|
|
if (Math.random() < successChance) {
|
|
const expGain = 2 * BladeburnerConstants.BaseStatGain * recruitTime;
|
|
retValue.chaExp = expGain;
|
|
++this.teamSize;
|
|
if (this.logging.general) {
|
|
this.log(
|
|
`${person.whoAmI()}: ` +
|
|
"Successfully recruited a team member! Gained " +
|
|
formatExp(expGain) +
|
|
" charisma exp",
|
|
);
|
|
}
|
|
} else {
|
|
const expGain = BladeburnerConstants.BaseStatGain * recruitTime;
|
|
retValue.chaExp = expGain;
|
|
if (this.logging.general) {
|
|
this.log(
|
|
`${person.whoAmI()}: ` +
|
|
"Failed to recruit a team member. Gained " +
|
|
formatExp(expGain) +
|
|
" charisma exp",
|
|
);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case ActionTypes.Diplomacy: {
|
|
const eff = this.getDiplomacyEffectiveness(person);
|
|
this.getCurrentCity().chaos *= eff;
|
|
if (this.getCurrentCity().chaos < 0) {
|
|
this.getCurrentCity().chaos = 0;
|
|
}
|
|
if (this.logging.general) {
|
|
this.log(
|
|
`${person.whoAmI()}: Diplomacy completed. Chaos levels in the current city fell by ${formatPercent(
|
|
1 - eff,
|
|
)}`,
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
case ActionTypes["Hyperbolic Regeneration Chamber"]: {
|
|
person.regenerateHp(BladeburnerConstants.HrcHpGain);
|
|
|
|
const staminaGain = this.maxStamina * (BladeburnerConstants.HrcStaminaGain / 100);
|
|
this.stamina = Math.min(this.maxStamina, this.stamina + staminaGain);
|
|
if (this.logging.general) {
|
|
this.log(
|
|
`${person.whoAmI()}: Rested in Hyperbolic Regeneration Chamber. Restored ${
|
|
BladeburnerConstants.HrcHpGain
|
|
} HP and gained ${formatStamina(staminaGain)} stamina`,
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
case ActionTypes["Incite Violence"]: {
|
|
for (const contract of Object.keys(this.contracts)) {
|
|
const growthF = Growths[contract];
|
|
if (!growthF) throw new Error("trying to generate count for action that doesn't exist? " + contract);
|
|
this.contracts[contract].count += (60 * 3 * growthF()) / BladeburnerConstants.ActionCountGrowthPeriod;
|
|
}
|
|
for (const operation of Object.keys(this.operations)) {
|
|
const growthF = Growths[operation];
|
|
if (!growthF) throw new Error("trying to generate count for action that doesn't exist? " + operation);
|
|
this.operations[operation].count += (60 * 3 * growthF()) / BladeburnerConstants.ActionCountGrowthPeriod;
|
|
}
|
|
if (this.logging.general) {
|
|
this.log(`${person.whoAmI()}: Incited violence in the synthoid communities.`);
|
|
}
|
|
for (const cityName of Object.values(CityName)) {
|
|
const city = this.cities[cityName];
|
|
city.chaos += 10;
|
|
city.chaos += city.chaos / (Math.log(city.chaos) / Math.log(10));
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
console.error(`Bladeburner.completeAction() called for invalid action: ${actionIdent.type}`);
|
|
break;
|
|
}
|
|
return retValue;
|
|
}
|
|
|
|
infiltrateSynthoidCommunities(): void {
|
|
const infilSleeves = Player.sleeves.filter((s) => isSleeveInfiltrateWork(s.currentWork)).length;
|
|
const amt = Math.pow(infilSleeves, -0.5) / 2;
|
|
for (const contract of Object.keys(this.contracts)) {
|
|
this.contracts[contract].count += amt;
|
|
}
|
|
for (const operation of Object.keys(this.operations)) {
|
|
this.operations[operation].count += amt;
|
|
}
|
|
if (this.logging.general) {
|
|
this.log(`Sleeve: Infiltrate the synthoid communities.`);
|
|
}
|
|
}
|
|
|
|
changeRank(person: Person, change: number): void {
|
|
if (isNaN(change)) {
|
|
throw new Error("NaN passed into Bladeburner.changeRank()");
|
|
}
|
|
this.rank += change;
|
|
if (this.rank < 0) {
|
|
this.rank = 0;
|
|
}
|
|
this.maxRank = Math.max(this.rank, this.maxRank);
|
|
|
|
const bladeburnersFactionName = FactionName.Bladeburners;
|
|
const bladeburnerFac = Factions[bladeburnersFactionName];
|
|
if (bladeburnerFac.isMember) {
|
|
const favorBonus = 1 + bladeburnerFac.favor / 100;
|
|
bladeburnerFac.playerReputation +=
|
|
BladeburnerConstants.RankToFactionRepFactor * change * person.mults.faction_rep * favorBonus;
|
|
}
|
|
|
|
// Gain skill points
|
|
const rankNeededForSp = (this.totalSkillPoints + 1) * BladeburnerConstants.RanksPerSkillPoint;
|
|
if (this.maxRank >= rankNeededForSp) {
|
|
// Calculate how many skill points to gain
|
|
const gainedSkillPoints = Math.floor(
|
|
(this.maxRank - rankNeededForSp) / BladeburnerConstants.RanksPerSkillPoint + 1,
|
|
);
|
|
this.skillPoints += gainedSkillPoints;
|
|
this.totalSkillPoints += gainedSkillPoints;
|
|
}
|
|
}
|
|
|
|
processAction(seconds: number): void {
|
|
if (this.action.type === ActionTypes.Idle) return;
|
|
if (this.actionTimeToComplete <= 0) {
|
|
throw new Error(`Invalid actionTimeToComplete value: ${this.actionTimeToComplete}, type; ${this.action.type}`);
|
|
}
|
|
if (!this.action) {
|
|
throw new Error("Bladeburner.action is not an ActionIdentifier Object");
|
|
}
|
|
//Check to see if action is a contract, and then to verify a sleeve didn't finish it first
|
|
if (this.action.type === 2) {
|
|
const remainingActions = this.contracts[this.action.name].count;
|
|
if (remainingActions < 1) {
|
|
return this.resetAction();
|
|
}
|
|
}
|
|
// If the previous action went past its completion time, add to the next action
|
|
// This is not added immediately in case the automation changes the action
|
|
this.actionTimeCurrent += seconds + this.actionTimeOverflow;
|
|
this.actionTimeOverflow = 0;
|
|
if (this.actionTimeCurrent >= this.actionTimeToComplete) {
|
|
this.actionTimeOverflow = this.actionTimeCurrent - this.actionTimeToComplete;
|
|
const action = this.getActionObject(this.action);
|
|
const retValue = this.completeAction(Player, this.action);
|
|
Player.gainMoney(retValue.money, "bladeburner");
|
|
Player.gainStats(retValue);
|
|
// Operation Daedalus
|
|
if (action == null) {
|
|
throw new Error("Failed to get BlackOperation Object for: " + this.action.name);
|
|
} else if (this.action.type != ActionTypes.BlackOperation && this.action.type != ActionTypes.BlackOp) {
|
|
this.startAction(this.action); // Repeat action
|
|
}
|
|
}
|
|
}
|
|
|
|
calculateStaminaGainPerSecond(): number {
|
|
const effAgility = Player.skills.agility * this.skillMultipliers.effAgi;
|
|
const maxStaminaBonus = this.maxStamina / BladeburnerConstants.MaxStaminaToGainFactor;
|
|
const gain = (BladeburnerConstants.StaminaGainPerSecond + maxStaminaBonus) * Math.pow(effAgility, 0.17);
|
|
return gain * (this.skillMultipliers.stamina * Player.mults.bladeburner_stamina_gain);
|
|
}
|
|
|
|
calculateMaxStamina(): void {
|
|
const effAgility = Player.skills.agility * this.skillMultipliers.effAgi;
|
|
const maxStamina =
|
|
(Math.pow(effAgility, 0.8) + this.staminaBonus) *
|
|
this.skillMultipliers.stamina *
|
|
Player.mults.bladeburner_max_stamina;
|
|
if (this.maxStamina !== maxStamina) {
|
|
const oldMax = this.maxStamina;
|
|
this.maxStamina = maxStamina;
|
|
this.stamina = (this.maxStamina * this.stamina) / oldMax;
|
|
}
|
|
if (isNaN(maxStamina)) {
|
|
throw new Error("Max Stamina calculated to be NaN in Bladeburner.calculateMaxStamina()");
|
|
}
|
|
}
|
|
|
|
create(): void {
|
|
this.contracts.Tracking = new Contract({
|
|
name: "Tracking",
|
|
baseDifficulty: 125,
|
|
difficultyFac: 1.02,
|
|
rewardFac: 1.041,
|
|
rankGain: 0.3,
|
|
hpLoss: 0.5,
|
|
count: getRandomInt(25, 150),
|
|
weights: {
|
|
hack: 0,
|
|
str: 0.05,
|
|
def: 0.05,
|
|
dex: 0.35,
|
|
agi: 0.35,
|
|
cha: 0.1,
|
|
int: 0.05,
|
|
},
|
|
decays: {
|
|
hack: 0,
|
|
str: 0.91,
|
|
def: 0.91,
|
|
dex: 0.91,
|
|
agi: 0.91,
|
|
cha: 0.9,
|
|
int: 1,
|
|
},
|
|
isStealth: true,
|
|
});
|
|
this.contracts["Bounty Hunter"] = new Contract({
|
|
name: "Bounty Hunter",
|
|
baseDifficulty: 250,
|
|
difficultyFac: 1.04,
|
|
rewardFac: 1.085,
|
|
rankGain: 0.9,
|
|
hpLoss: 1,
|
|
count: getRandomInt(5, 150),
|
|
weights: {
|
|
hack: 0,
|
|
str: 0.15,
|
|
def: 0.15,
|
|
dex: 0.25,
|
|
agi: 0.25,
|
|
cha: 0.1,
|
|
int: 0.1,
|
|
},
|
|
decays: {
|
|
hack: 0,
|
|
str: 0.91,
|
|
def: 0.91,
|
|
dex: 0.91,
|
|
agi: 0.91,
|
|
cha: 0.8,
|
|
int: 0.9,
|
|
},
|
|
isKill: true,
|
|
});
|
|
this.contracts.Retirement = new Contract({
|
|
name: "Retirement",
|
|
baseDifficulty: 200,
|
|
difficultyFac: 1.03,
|
|
rewardFac: 1.065,
|
|
rankGain: 0.6,
|
|
hpLoss: 1,
|
|
count: getRandomInt(5, 150),
|
|
weights: {
|
|
hack: 0,
|
|
str: 0.2,
|
|
def: 0.2,
|
|
dex: 0.2,
|
|
agi: 0.2,
|
|
cha: 0.1,
|
|
int: 0.1,
|
|
},
|
|
decays: {
|
|
hack: 0,
|
|
str: 0.91,
|
|
def: 0.91,
|
|
dex: 0.91,
|
|
agi: 0.91,
|
|
cha: 0.8,
|
|
int: 0.9,
|
|
},
|
|
isKill: true,
|
|
});
|
|
|
|
this.operations.Investigation = new Operation({
|
|
name: "Investigation",
|
|
baseDifficulty: 400,
|
|
difficultyFac: 1.03,
|
|
rewardFac: 1.07,
|
|
reqdRank: 25,
|
|
rankGain: 2.2,
|
|
rankLoss: 0.2,
|
|
count: getRandomInt(1, 100),
|
|
weights: {
|
|
hack: 0.25,
|
|
str: 0.05,
|
|
def: 0.05,
|
|
dex: 0.2,
|
|
agi: 0.1,
|
|
cha: 0.25,
|
|
int: 0.1,
|
|
},
|
|
decays: {
|
|
hack: 0.85,
|
|
str: 0.9,
|
|
def: 0.9,
|
|
dex: 0.9,
|
|
agi: 0.9,
|
|
cha: 0.7,
|
|
int: 0.9,
|
|
},
|
|
isStealth: true,
|
|
});
|
|
this.operations["Undercover Operation"] = new Operation({
|
|
name: "Undercover Operation",
|
|
baseDifficulty: 500,
|
|
difficultyFac: 1.04,
|
|
rewardFac: 1.09,
|
|
reqdRank: 100,
|
|
rankGain: 4.4,
|
|
rankLoss: 0.4,
|
|
hpLoss: 2,
|
|
count: getRandomInt(1, 100),
|
|
weights: {
|
|
hack: 0.2,
|
|
str: 0.05,
|
|
def: 0.05,
|
|
dex: 0.2,
|
|
agi: 0.2,
|
|
cha: 0.2,
|
|
int: 0.1,
|
|
},
|
|
decays: {
|
|
hack: 0.8,
|
|
str: 0.9,
|
|
def: 0.9,
|
|
dex: 0.9,
|
|
agi: 0.9,
|
|
cha: 0.7,
|
|
int: 0.9,
|
|
},
|
|
isStealth: true,
|
|
});
|
|
this.operations["Sting Operation"] = new Operation({
|
|
name: "Sting Operation",
|
|
baseDifficulty: 650,
|
|
difficultyFac: 1.04,
|
|
rewardFac: 1.095,
|
|
reqdRank: 500,
|
|
rankGain: 5.5,
|
|
rankLoss: 0.5,
|
|
hpLoss: 2.5,
|
|
count: getRandomInt(1, 150),
|
|
weights: {
|
|
hack: 0.25,
|
|
str: 0.05,
|
|
def: 0.05,
|
|
dex: 0.25,
|
|
agi: 0.1,
|
|
cha: 0.2,
|
|
int: 0.1,
|
|
},
|
|
decays: {
|
|
hack: 0.8,
|
|
str: 0.85,
|
|
def: 0.85,
|
|
dex: 0.85,
|
|
agi: 0.85,
|
|
cha: 0.7,
|
|
int: 0.9,
|
|
},
|
|
isStealth: true,
|
|
});
|
|
this.operations.Raid = new Operation({
|
|
name: "Raid",
|
|
baseDifficulty: 800,
|
|
difficultyFac: 1.045,
|
|
rewardFac: 1.1,
|
|
reqdRank: 3000,
|
|
rankGain: 55,
|
|
rankLoss: 2.5,
|
|
hpLoss: 50,
|
|
count: getRandomInt(1, 150),
|
|
weights: {
|
|
hack: 0.1,
|
|
str: 0.2,
|
|
def: 0.2,
|
|
dex: 0.2,
|
|
agi: 0.2,
|
|
cha: 0,
|
|
int: 0.1,
|
|
},
|
|
decays: {
|
|
hack: 0.7,
|
|
str: 0.8,
|
|
def: 0.8,
|
|
dex: 0.8,
|
|
agi: 0.8,
|
|
cha: 0,
|
|
int: 0.9,
|
|
},
|
|
isKill: true,
|
|
});
|
|
this.operations["Stealth Retirement Operation"] = new Operation({
|
|
name: "Stealth Retirement Operation",
|
|
baseDifficulty: 1000,
|
|
difficultyFac: 1.05,
|
|
rewardFac: 1.11,
|
|
reqdRank: 20e3,
|
|
rankGain: 22,
|
|
rankLoss: 2,
|
|
hpLoss: 10,
|
|
count: getRandomInt(1, 150),
|
|
weights: {
|
|
hack: 0.1,
|
|
str: 0.1,
|
|
def: 0.1,
|
|
dex: 0.3,
|
|
agi: 0.3,
|
|
cha: 0,
|
|
int: 0.1,
|
|
},
|
|
decays: {
|
|
hack: 0.7,
|
|
str: 0.8,
|
|
def: 0.8,
|
|
dex: 0.8,
|
|
agi: 0.8,
|
|
cha: 0,
|
|
int: 0.9,
|
|
},
|
|
isStealth: true,
|
|
isKill: true,
|
|
});
|
|
this.operations.Assassination = new Operation({
|
|
name: "Assassination",
|
|
baseDifficulty: 1500,
|
|
difficultyFac: 1.06,
|
|
rewardFac: 1.14,
|
|
reqdRank: 50e3,
|
|
rankGain: 44,
|
|
rankLoss: 4,
|
|
hpLoss: 5,
|
|
count: getRandomInt(1, 150),
|
|
weights: {
|
|
hack: 0.1,
|
|
str: 0.1,
|
|
def: 0.1,
|
|
dex: 0.3,
|
|
agi: 0.3,
|
|
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.8,
|
|
},
|
|
isStealth: true,
|
|
isKill: true,
|
|
});
|
|
}
|
|
|
|
process(): void {
|
|
// Edge race condition when the engine checks the processing counters and attempts to route before the router is initialized.
|
|
if (!Router.isInitialized) return;
|
|
//safety measure this needs to be removed in a bigger refactor
|
|
this.resetBlackOps();
|
|
|
|
// If the Player starts doing some other actions, set action to idle and alert
|
|
if (!Player.hasAugmentation(AugmentationName.BladesSimulacrum, true) && Player.currentWork) {
|
|
if (this.action.type !== ActionTypes.Idle) {
|
|
let msg = "Your Bladeburner action was cancelled because you started doing something else.";
|
|
if (this.automateEnabled) {
|
|
msg += `\n\nYour automation was disabled as well. You will have to re-enable it through the Bladeburner console`;
|
|
this.automateEnabled = false;
|
|
}
|
|
if (!Settings.SuppressBladeburnerPopup) {
|
|
dialogBoxCreate(msg);
|
|
}
|
|
}
|
|
this.resetAction();
|
|
}
|
|
|
|
// If the Player has no Stamina, set action to idle
|
|
if (this.stamina <= 0) {
|
|
this.log("Your Bladeburner action was cancelled because your stamina hit 0");
|
|
this.resetAction();
|
|
}
|
|
|
|
// A 'tick' for this mechanic is one second (= 5 game cycles)
|
|
if (this.storedCycles >= BladeburnerConstants.CyclesPerSecond) {
|
|
let seconds = Math.floor(this.storedCycles / BladeburnerConstants.CyclesPerSecond);
|
|
seconds = Math.min(seconds, 5); // Max of 5 'ticks'
|
|
this.storedCycles -= seconds * BladeburnerConstants.CyclesPerSecond;
|
|
|
|
// Stamina
|
|
this.calculateMaxStamina();
|
|
this.stamina += this.calculateStaminaGainPerSecond() * seconds;
|
|
this.stamina = Math.min(this.maxStamina, this.stamina);
|
|
|
|
// Count increase for contracts/operations
|
|
for (const contract of Object.values(this.contracts)) {
|
|
const growthF = Growths[contract.name];
|
|
if (growthF === undefined) throw new Error(`growth formula for action '${contract.name}' is undefined`);
|
|
contract.count += (seconds * growthF()) / BladeburnerConstants.ActionCountGrowthPeriod;
|
|
}
|
|
for (const op of Object.values(this.operations)) {
|
|
const growthF = Growths[op.name];
|
|
if (growthF === undefined) throw new Error(`growth formula for action '${op.name}' is undefined`);
|
|
if (growthF !== undefined) {
|
|
op.count += (seconds * growthF()) / BladeburnerConstants.ActionCountGrowthPeriod;
|
|
}
|
|
}
|
|
|
|
// Chaos goes down very slowly
|
|
for (const cityName of Object.values(CityName)) {
|
|
const city = this.cities[cityName];
|
|
if (!city) throw new Error("Invalid city when processing passive chaos reduction in Bladeburner.process");
|
|
city.chaos -= 0.0001 * seconds;
|
|
city.chaos = Math.max(0, city.chaos);
|
|
}
|
|
|
|
// Random Events
|
|
this.randomEventCounter -= seconds;
|
|
if (this.randomEventCounter <= 0) {
|
|
this.randomEvent();
|
|
// Add instead of setting because we might have gone over the required time for the event
|
|
this.randomEventCounter += getRandomInt(240, 600);
|
|
}
|
|
|
|
this.processAction(seconds);
|
|
|
|
// Automation
|
|
if (this.automateEnabled) {
|
|
// Note: Do NOT set this.action = this.automateActionHigh/Low since it creates a reference
|
|
if (this.stamina <= this.automateThreshLow) {
|
|
if (this.action.name !== this.automateActionLow.name || this.action.type !== this.automateActionLow.type) {
|
|
this.action = new ActionIdentifier({
|
|
type: this.automateActionLow.type,
|
|
name: this.automateActionLow.name,
|
|
});
|
|
this.startAction(this.action);
|
|
}
|
|
} else if (this.stamina >= this.automateThreshHigh) {
|
|
if (this.action.name !== this.automateActionHigh.name || this.action.type !== this.automateActionHigh.type) {
|
|
this.action = new ActionIdentifier({
|
|
type: this.automateActionHigh.type,
|
|
name: this.automateActionHigh.name,
|
|
});
|
|
this.startAction(this.action);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle "nextUpdate" resolver after this update
|
|
if (BladeburnerPromise.resolve) {
|
|
BladeburnerPromise.resolve(seconds * 1000);
|
|
BladeburnerPromise.resolve = null;
|
|
BladeburnerPromise.promise = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
getTypeAndNameFromActionId(actionId: ActionIdentifier): {
|
|
type: string;
|
|
name: string;
|
|
} {
|
|
const res = { type: "", name: "" };
|
|
const types = Object.keys(ActionTypes);
|
|
for (let i = 0; i < types.length; ++i) {
|
|
if (actionId.type === ActionTypes[types[i]]) {
|
|
res.type = types[i];
|
|
break;
|
|
}
|
|
}
|
|
const gen = [
|
|
"Training",
|
|
"Recruitment",
|
|
"FieldAnalysis",
|
|
"Field Analysis",
|
|
"Diplomacy",
|
|
"Hyperbolic Regeneration Chamber",
|
|
"Incite Violence",
|
|
];
|
|
if (gen.includes(res.type)) {
|
|
res.type = "General";
|
|
}
|
|
|
|
if (res.type == null) {
|
|
res.type = "Idle";
|
|
}
|
|
|
|
res.name = actionId.name != null ? actionId.name : "Idle";
|
|
return res;
|
|
}
|
|
|
|
getContractNamesNetscriptFn(): string[] {
|
|
return Object.keys(this.contracts);
|
|
}
|
|
|
|
getOperationNamesNetscriptFn(): string[] {
|
|
return Object.keys(this.operations);
|
|
}
|
|
|
|
getBlackOpNamesNetscriptFn(): string[] {
|
|
return Object.keys(BlackOperations);
|
|
}
|
|
|
|
getGeneralActionNamesNetscriptFn(): string[] {
|
|
return Object.keys(GeneralActions);
|
|
}
|
|
|
|
getSkillNamesNetscriptFn(): string[] {
|
|
return Object.keys(Skills);
|
|
}
|
|
|
|
startActionNetscriptFn(type: string, name: string, workerScript: WorkerScript): boolean {
|
|
const errorLogText = `Invalid action: type='${type}' name='${name}'`;
|
|
const actionId = this.getActionIdFromTypeAndName(type, name);
|
|
if (actionId == null) {
|
|
workerScript.log("bladeburner.startAction", () => errorLogText);
|
|
return false;
|
|
}
|
|
|
|
// Special logic for Black Ops
|
|
if (actionId.type === ActionTypes.BlackOp) {
|
|
const canRunOp = this.canAttemptBlackOp(actionId);
|
|
if (!canRunOp.isAvailable) {
|
|
workerScript.log("bladeburner.startAction", () => canRunOp.error + "");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
try {
|
|
this.startAction(actionId);
|
|
if (!Player.hasAugmentation(AugmentationName.BladesSimulacrum, true)) Player.finishWork(true);
|
|
workerScript.log(
|
|
"bladeburner.startAction",
|
|
() => `Starting bladeburner action with type '${type}' and name '${name}'`,
|
|
);
|
|
return true;
|
|
} catch (e: unknown) {
|
|
console.error(e);
|
|
this.resetAction();
|
|
workerScript.log("bladeburner.startAction", () => errorLogText);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
getActionTimeNetscriptFn(person: Person, type: string, name: string): number | string {
|
|
const actionId = this.getActionIdFromTypeAndName(type, name);
|
|
if (actionId == null) {
|
|
return "bladeburner.getActionTime";
|
|
}
|
|
|
|
const actionObj = this.getActionObject(actionId);
|
|
if (actionObj == null) {
|
|
return "bladeburner.getActionTime";
|
|
}
|
|
switch (actionId.type) {
|
|
case ActionTypes.Contract:
|
|
case ActionTypes.Operation:
|
|
case ActionTypes.BlackOp:
|
|
case ActionTypes.BlackOperation:
|
|
return actionObj.getActionTime(this, person) * 1000;
|
|
case ActionTypes.Training:
|
|
case ActionTypes["Field Analysis"]:
|
|
case ActionTypes.FieldAnalysis:
|
|
return 30000;
|
|
case ActionTypes.Recruitment:
|
|
return this.getRecruitmentTime(person) * 1000;
|
|
case ActionTypes.Diplomacy:
|
|
case ActionTypes["Hyperbolic Regeneration Chamber"]:
|
|
case ActionTypes["Incite Violence"]:
|
|
return 60000;
|
|
default:
|
|
return "bladeburner.getActionTime";
|
|
}
|
|
}
|
|
|
|
getActionEstimatedSuccessChanceNetscriptFn(person: Person, type: string, name: string): [number, number] | string {
|
|
const actionId = this.getActionIdFromTypeAndName(type, name);
|
|
if (actionId == null) {
|
|
return "bladeburner.getActionEstimatedSuccessChance";
|
|
}
|
|
|
|
const actionObj = this.getActionObject(actionId);
|
|
if (actionObj == null) {
|
|
return "bladeburner.getActionEstimatedSuccessChance";
|
|
}
|
|
switch (actionId.type) {
|
|
case ActionTypes.Contract:
|
|
case ActionTypes.Operation:
|
|
case ActionTypes.BlackOp:
|
|
case ActionTypes.BlackOperation:
|
|
return actionObj.getEstSuccessChance(this, person);
|
|
case ActionTypes.Training:
|
|
case ActionTypes["Field Analysis"]:
|
|
case ActionTypes.FieldAnalysis:
|
|
case ActionTypes.Diplomacy:
|
|
case ActionTypes["Hyperbolic Regeneration Chamber"]:
|
|
case ActionTypes["Incite Violence"]:
|
|
return [1, 1];
|
|
case ActionTypes.Recruitment: {
|
|
const recChance = this.getRecruitmentSuccessChance(person);
|
|
return [recChance, recChance];
|
|
}
|
|
default:
|
|
return "bladeburner.getActionEstimatedSuccessChance";
|
|
}
|
|
}
|
|
|
|
getActionCountRemainingNetscriptFn(type: string, name: string, workerScript: WorkerScript): number {
|
|
const errorLogText = `Invalid action: type='${type}' name='${name}'`;
|
|
const actionId = this.getActionIdFromTypeAndName(type, name);
|
|
if (actionId == null) {
|
|
workerScript.log("bladeburner.getActionCountRemaining", () => errorLogText);
|
|
return -1;
|
|
}
|
|
|
|
const actionObj = this.getActionObject(actionId);
|
|
if (actionObj == null) {
|
|
workerScript.log("bladeburner.getActionCountRemaining", () => errorLogText);
|
|
return -1;
|
|
}
|
|
|
|
switch (actionId.type) {
|
|
case ActionTypes.Contract:
|
|
case ActionTypes.Operation:
|
|
return Math.floor(actionObj.count);
|
|
case ActionTypes.BlackOp:
|
|
case ActionTypes.BlackOperation:
|
|
if (this.blackops[name] != null) {
|
|
return 0;
|
|
} else {
|
|
return 1;
|
|
}
|
|
case ActionTypes.Training:
|
|
case ActionTypes.Recruitment:
|
|
case ActionTypes["Field Analysis"]:
|
|
case ActionTypes.FieldAnalysis:
|
|
case ActionTypes.Diplomacy:
|
|
case ActionTypes["Hyperbolic Regeneration Chamber"]:
|
|
case ActionTypes["Incite Violence"]:
|
|
return Infinity;
|
|
default:
|
|
workerScript.log("bladeburner.getActionCountRemaining", () => errorLogText);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
getSkillLevelNetscriptFn(skillName: string, workerScript: WorkerScript): number {
|
|
if (skillName === "" || !Object.hasOwn(Skills, skillName)) {
|
|
workerScript.log("bladeburner.getSkillLevel", () => `Invalid skill: '${skillName}'`);
|
|
return -1;
|
|
}
|
|
|
|
if (this.skills[skillName] == null) {
|
|
return 0;
|
|
} else {
|
|
return this.skills[skillName];
|
|
}
|
|
}
|
|
|
|
getSkillUpgradeCostNetscriptFn(skillName: string, count: number, workerScript: WorkerScript): number {
|
|
if (skillName === "" || !Object.hasOwn(Skills, skillName)) {
|
|
workerScript.log("bladeburner.getSkillUpgradeCost", () => `Invalid skill: '${skillName}'`);
|
|
return -1;
|
|
}
|
|
|
|
const skill = Skills[skillName];
|
|
const currentLevel = this.skills[skillName] ?? 0;
|
|
|
|
if (skill.maxLvl !== 0 && currentLevel + count > skill.maxLvl) {
|
|
return Infinity;
|
|
}
|
|
|
|
return skill.calculateCost(currentLevel, count);
|
|
}
|
|
|
|
upgradeSkillNetscriptFn(skillName: string, count: number, workerScript: WorkerScript): boolean {
|
|
const errorLogText = `Invalid skill: '${skillName}'`;
|
|
if (!Object.hasOwn(Skills, skillName)) {
|
|
workerScript.log("bladeburner.upgradeSkill", () => errorLogText);
|
|
return false;
|
|
}
|
|
|
|
const skill = Skills[skillName];
|
|
let currentLevel = 0;
|
|
if (this.skills[skillName] && !isNaN(this.skills[skillName])) {
|
|
currentLevel = this.skills[skillName];
|
|
}
|
|
const cost = skill.calculateCost(currentLevel, count);
|
|
|
|
if (skill.maxLvl && currentLevel + count > skill.maxLvl) {
|
|
workerScript.log("bladeburner.upgradeSkill", () => `Skill '${skillName}' cannot be upgraded ${count} time(s).`);
|
|
return false;
|
|
}
|
|
|
|
if (this.skillPoints < cost) {
|
|
workerScript.log(
|
|
"bladeburner.upgradeSkill",
|
|
() =>
|
|
`You do not have enough skill points to upgrade ${skillName} ${count} time(s). (You have ${this.skillPoints}, you need ${cost})`,
|
|
);
|
|
return false;
|
|
}
|
|
|
|
this.skillPoints -= cost;
|
|
this.upgradeSkill(skill, count);
|
|
workerScript.log("bladeburner.upgradeSkill", () => `'${skillName}' upgraded to level ${this.skills[skillName]}`);
|
|
return true;
|
|
}
|
|
|
|
getTeamSizeNetscriptFn(type: string, name: string, workerScript: WorkerScript): number {
|
|
if (type === "" && name === "") {
|
|
return this.teamSize;
|
|
}
|
|
|
|
const errorLogText = `Invalid action: type='${type}' name='${name}'`;
|
|
const actionId = this.getActionIdFromTypeAndName(type, name);
|
|
if (actionId == null) {
|
|
workerScript.log("bladeburner.getTeamSize", () => errorLogText);
|
|
return -1;
|
|
}
|
|
|
|
const actionObj = this.getActionObject(actionId);
|
|
if (actionObj == null) {
|
|
workerScript.log("bladeburner.getTeamSize", () => errorLogText);
|
|
return -1;
|
|
}
|
|
|
|
if (
|
|
actionId.type === ActionTypes.Operation ||
|
|
actionId.type === ActionTypes.BlackOp ||
|
|
actionId.type === ActionTypes.BlackOperation
|
|
) {
|
|
return actionObj.teamCount;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
setTeamSizeNetscriptFn(type: string, name: string, size: number, workerScript: WorkerScript): number {
|
|
const errorLogText = `Invalid action: type='${type}' name='${name}'`;
|
|
const actionId = this.getActionIdFromTypeAndName(type, name);
|
|
if (actionId == null) {
|
|
workerScript.log("bladeburner.setTeamSize", () => errorLogText);
|
|
return -1;
|
|
}
|
|
|
|
if (
|
|
actionId.type !== ActionTypes.Operation &&
|
|
actionId.type !== ActionTypes.BlackOp &&
|
|
actionId.type !== ActionTypes.BlackOperation
|
|
) {
|
|
workerScript.log("bladeburner.setTeamSize", () => "Only valid for 'Operations' and 'BlackOps'");
|
|
return -1;
|
|
}
|
|
|
|
const actionObj = this.getActionObject(actionId);
|
|
if (actionObj == null) {
|
|
workerScript.log("bladeburner.setTeamSize", () => errorLogText);
|
|
return -1;
|
|
}
|
|
|
|
let sanitizedSize = Math.round(size);
|
|
if (isNaN(sanitizedSize) || sanitizedSize < 0) {
|
|
workerScript.log("bladeburner.setTeamSize", () => `Invalid size: ${size}`);
|
|
return -1;
|
|
}
|
|
if (this.teamSize < sanitizedSize) {
|
|
sanitizedSize = this.teamSize;
|
|
}
|
|
actionObj.teamCount = sanitizedSize;
|
|
workerScript.log("bladeburner.setTeamSize", () => `Team size for '${name}' set to ${sanitizedSize}.`);
|
|
return sanitizedSize;
|
|
}
|
|
|
|
joinBladeburnerFactionNetscriptFn(workerScript: WorkerScript): boolean {
|
|
const bladeburnerFac = Factions[FactionName.Bladeburners];
|
|
if (bladeburnerFac.isMember) {
|
|
return true;
|
|
} else if (this.rank >= BladeburnerConstants.RankNeededForFaction) {
|
|
joinFaction(bladeburnerFac);
|
|
workerScript.log("bladeburner.joinBladeburnerFaction", () => `Joined ${FactionName.Bladeburners} faction.`);
|
|
return true;
|
|
} else {
|
|
workerScript.log(
|
|
"bladeburner.joinBladeburnerFaction",
|
|
() => `You do not have the required rank (${this.rank}/${BladeburnerConstants.RankNeededForFaction}).`,
|
|
);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** Serialize the current object to a JSON save state. */
|
|
toJSON(): IReviverValue {
|
|
return Generic_toJSON("Bladeburner", this);
|
|
}
|
|
|
|
/** Initializes a Bladeburner object from a JSON save state. */
|
|
static fromJSON(value: IReviverValue): Bladeburner {
|
|
return Generic_fromJSON(Bladeburner, value.data);
|
|
}
|
|
}
|
|
|
|
constructorsForReviver.Bladeburner = Bladeburner;
|