bitburner-src/src/Bladeburner/Bladeburner.tsx

2426 lines
81 KiB
TypeScript
Raw Normal View History

2021-09-25 20:42:57 +02:00
import { Reviver, Generic_toJSON, Generic_fromJSON } from "../utils/JSONReviver";
import { IBladeburner } from "./IBladeburner";
import { IActionIdentifier } from "./IActionIdentifier";
import { ActionIdentifier } from "./ActionIdentifier";
import { ActionTypes } from "./data/ActionTypes";
import { Growths } from "./data/Growths";
import { BlackOperations } from "./BlackOperations";
2021-08-16 07:35:05 +02:00
import { BlackOperation } from "./BlackOperation";
2021-08-16 23:45:26 +02:00
import { Operation } from "./Operation";
import { Contract } from "./Contract";
import { GeneralActions } from "./GeneralActions";
2021-09-25 20:42:57 +02:00
import { formatNumber } from "../utils/StringHelperFunctions";
import { Skills } from "./Skills";
import { Skill } from "./Skill";
import { City } from "./City";
import { IAction } from "./IAction";
import { IPlayer } from "../PersonObjects/IPlayer";
import { createTaskTracker, ITaskTracker } from "../PersonObjects/ITaskTracker";
import { IPerson } from "../PersonObjects/IPerson";
2022-07-11 21:58:23 +02:00
import { IRouter } from "../ui/Router";
import { ConsoleHelpText } from "./data/Help";
2021-09-25 20:42:57 +02:00
import { exceptionAlert } from "../utils/helpers/exceptionAlert";
import { getRandomInt } from "../utils/helpers/getRandomInt";
import { BladeburnerConstants } from "./data/Constants";
2021-08-16 07:35:05 +02:00
import { numeralWrapper } from "../ui/numeralFormat";
import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers";
2021-09-25 20:42:57 +02:00
import { addOffset } from "../utils/helpers/addOffset";
2021-08-16 07:35:05 +02:00
import { Faction } from "../Faction/Faction";
import { Factions, factionExists } from "../Faction/Factions";
import { calculateHospitalizationCost } from "../Hospital/Hospital";
2021-09-25 20:42:57 +02:00
import { dialogBoxCreate } from "../ui/React/DialogBox";
2021-08-16 23:45:26 +02:00
import { Settings } from "../Settings/Settings";
import { AugmentationNames } from "../Augmentation/data/AugmentationNames";
2021-09-25 20:42:57 +02:00
import { getTimestamp } from "../utils/helpers/getTimestamp";
2021-08-16 23:45:26 +02:00
import { joinFaction } from "../Faction/FactionHelpers";
import { WorkerScript } from "../Netscript/WorkerScript";
import { FactionNames } from "../Faction/data/FactionNames";
2022-03-24 16:09:24 +01:00
import { KEY } from "../utils/helpers/keyCodes";
interface BlackOpsAttempt {
error?: string;
isAvailable?: boolean;
action?: BlackOperation;
}
export class Bladeburner implements IBladeburner {
2021-09-05 01:09:30 +02:00
numHosp = 0;
moneyLost = 0;
rank = 0;
maxRank = 0;
skillPoints = 0;
totalSkillPoints = 0;
teamSize = 0;
sleeveSize = 0;
2021-09-05 01:09:30 +02:00
teamLost = 0;
hpLost = 0;
storedCycles = 0;
randomEventCounter: number = getRandomInt(240, 600);
actionTimeToComplete = 0;
actionTimeCurrent = 0;
actionTimeOverflow = 0;
action: IActionIdentifier = new ActionIdentifier({
type: ActionTypes["Idle"],
});
2022-05-25 21:08:48 +02:00
cities: Record<string, City> = {};
2021-09-05 01:09:30 +02:00
city: string = BladeburnerConstants.CityNames[2];
2022-05-25 21:08:48 +02:00
skills: Record<string, number> = {};
skillMultipliers: Record<string, number> = {};
2021-09-05 01:09:30 +02:00
staminaBonus = 0;
maxStamina = 0;
stamina = 0;
2022-05-25 21:08:48 +02:00
contracts: Record<string, Contract> = {};
operations: Record<string, Operation> = {};
blackops: Record<string, boolean> = {};
logging = {
2021-09-05 01:09:30 +02:00
general: true,
contracts: true,
ops: true,
blackops: true,
events: true,
};
automateEnabled = false;
automateActionHigh: IActionIdentifier = new ActionIdentifier({
type: ActionTypes["Idle"],
});
automateThreshHigh = 0;
automateActionLow: IActionIdentifier = new ActionIdentifier({
type: ActionTypes["Idle"],
});
automateThreshLow = 0;
consoleHistory: string[] = [];
2021-09-09 05:47:34 +02:00
consoleLogs: string[] = ["Bladeburner Console", "Type 'help' to see console commands"];
2021-09-05 01:09:30 +02:00
constructor(player?: IPlayer) {
for (let i = 0; i < BladeburnerConstants.CityNames.length; ++i) {
2021-09-09 05:47:34 +02:00
this.cities[BladeburnerConstants.CityNames[i]] = new City(BladeburnerConstants.CityNames[i]);
2021-09-05 01:09:30 +02:00
}
2021-09-05 01:09:30 +02:00
this.updateSkillMultipliers(); // Calls resetSkillMultipliers()
2021-09-05 01:09:30 +02:00
// Max Stamina is based on stats and Bladeburner-specific bonuses
if (player) this.calculateMaxStamina(player);
this.stamina = this.maxStamina;
this.create();
}
2021-09-05 01:09:30 +02:00
getCurrentCity(): City {
const city = this.cities[this.city];
if (!(city instanceof City)) {
2021-09-09 05:47:34 +02:00
throw new Error("Bladeburner.getCurrentCity() did not properly return a City object");
}
2021-09-05 01:09:30 +02:00
return city;
}
calculateStaminaPenalty(): number {
return Math.min(1, this.stamina / (0.5 * this.maxStamina));
}
canAttemptBlackOp(actionId: IActionIdentifier): BlackOpsAttempt {
// Safety measure - don't repeat BlackOps that are already done
if (this.blackops[actionId.name] != null) {
2021-12-21 18:00:12 +01:00
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 = [];
2022-01-16 01:45:03 +01:00
for (const nm of Object.keys(BlackOperations)) {
if (BlackOperations.hasOwnProperty(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) {
2021-12-21 18:00:12 +01:00
return { error: `Preceding Black Op must be completed before starting '${actionId.name}'.` };
}
2021-12-21 18:00:12 +01:00
return { isAvailable: true, action };
}
startAction(person: IPerson, actionId: IActionIdentifier): void {
2021-09-05 01:09:30 +02:00
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) {
2021-09-09 05:47:34 +02:00
throw new Error("Failed to get Contract Object for: " + actionId.name);
2021-09-05 01:09:30 +02:00
}
if (action.count < 1) {
return this.resetAction();
}
this.actionTimeToComplete = action.getActionTime(this, person);
2021-10-13 08:27:55 +02:00
} catch (e: any) {
2021-09-05 01:09:30 +02:00
exceptionAlert(e);
}
2021-09-05 01:09:30 +02:00
break;
case ActionTypes["Operation"]: {
try {
2021-09-05 01:09:30 +02:00
const action = this.getActionObject(actionId);
if (action == null) {
2021-09-09 05:47:34 +02:00
throw new Error("Failed to get Operation Object for: " + actionId.name);
2021-09-05 01:09:30 +02:00
}
if (action.count < 1) {
return this.resetAction();
}
2021-10-14 23:35:22 +02:00
if (actionId.name === "Raid" && this.getCurrentCity().comms === 0) {
2021-09-05 01:09:30 +02:00
return this.resetAction();
}
this.actionTimeToComplete = action.getActionTime(this, person);
2021-10-13 08:27:55 +02:00
} catch (e: any) {
2021-09-05 01:09:30 +02:00
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;
}
2021-12-21 18:00:12 +01:00
if (testBlackOp.action === undefined) {
throw new Error("action should not be null");
}
this.actionTimeToComplete = testBlackOp.action.getActionTime(this, person);
2021-10-13 08:27:55 +02:00
} catch (e: any) {
2021-09-05 01:09:30 +02:00
exceptionAlert(e);
}
break;
}
case ActionTypes["Recruitment"]:
this.actionTimeToComplete = this.getRecruitmentTime(person);
2021-09-05 01:09:30 +02:00
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;
2021-09-05 01:09:30 +02:00
break;
default:
2021-09-09 05:47:34 +02:00
throw new Error("Invalid Action Type in startAction(Bladeburner,player, ): " + actionId.type);
}
2021-09-05 01:09:30 +02:00
}
upgradeSkill(skill: Skill): void {
// This does NOT handle deduction of skill points
const skillName = skill.name;
if (this.skills[skillName]) {
++this.skills[skillName];
} else {
this.skills[skillName] = 1;
}
2021-09-05 01:09:30 +02:00
if (isNaN(this.skills[skillName]) || this.skills[skillName] < 0) {
2021-09-09 05:47:34 +02:00
throw new Error("Level of Skill " + skillName + " is invalid: " + this.skills[skillName]);
}
2021-09-05 01:09:30 +02:00
this.updateSkillMultipliers();
}
executeConsoleCommands(player: IPlayer, 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(player, arrayOfCommands[i]);
}
2021-10-13 08:27:55 +02:00
} catch (e: any) {
2021-09-05 01:09:30 +02:00
exceptionAlert(e);
}
2021-09-05 01:09:30 +02:00
}
postToConsole(input: string, saveToLogs = true): void {
const MaxConsoleEntries = 100;
if (saveToLogs) {
this.consoleLogs.push(input);
if (this.consoleLogs.length > MaxConsoleEntries) {
this.consoleLogs.shift();
}
}
2021-09-05 01:09:30 +02:00
}
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;
2021-09-05 01:09:30 +02:00
}
clearConsole(): void {
this.consoleLogs.length = 0;
}
prestige(): void {
this.resetAction();
const bladeburnerFac = Factions[FactionNames.Bladeburners];
2021-09-05 01:09:30 +02:00
if (this.rank >= BladeburnerConstants.RankNeededForFaction) {
joinFaction(bladeburnerFac);
}
2021-09-05 01:09:30 +02:00
}
2021-09-05 01:09:30 +02:00
storeCycles(numCycles = 0): void {
this.storedCycles += numCycles;
}
2021-09-05 01:09:30 +02:00
// working on
getActionIdFromTypeAndName(type = "", name = ""): IActionIdentifier | null {
if (type === "" || name === "") {
return null;
}
2021-09-05 01:09:30 +02:00
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 (this.contracts.hasOwnProperty(name)) {
action.name = name;
return action;
}
2022-03-11 16:46:43 +01:00
return null;
2021-09-05 01:09:30 +02:00
case "operation":
case "operations":
case "op":
case "ops":
action.type = ActionTypes["Operation"];
if (this.operations.hasOwnProperty(name)) {
action.name = name;
return action;
}
2022-03-11 16:46:43 +01:00
return null;
2021-09-05 01:09:30 +02:00
case "blackoperation":
case "black operation":
case "black operations":
case "black op":
case "black ops":
case "blackop":
case "blackops":
action.type = ActionTypes["BlackOp"];
if (BlackOperations.hasOwnProperty(name)) {
action.name = name;
return action;
}
2022-03-11 16:46:43 +01:00
return null;
2021-09-05 01:09:30 +02:00
case "general":
case "general action":
case "gen":
break;
default:
return null;
}
2021-09-05 01:09:30 +02:00
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;
2021-09-05 01:09:30 +02:00
default:
return null;
}
return action;
}
2021-09-05 01:09:30 +02:00
return null;
}
2021-09-05 01:09:30 +02:00
executeStartConsoleCommand(player: IPlayer, args: string[]): void {
if (args.length !== 3) {
2021-09-09 05:47:34 +02:00
this.postToConsole("Invalid usage of 'start' console command: start [type] [name]");
2021-09-05 01:09:30 +02:00
this.postToConsole("Use 'help start' for more info");
return;
}
2021-09-05 01:09:30 +02:00
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(player, this.action);
} else {
2021-09-05 01:09:30 +02:00
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(player, 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(player, 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(player, this.action);
} else {
this.postToConsole("Invalid BlackOp name specified: " + args[2]);
}
break;
default:
this.postToConsole("Invalid action/event type specified: " + args[1]);
2021-09-09 05:47:34 +02:00
this.postToConsole("Examples of valid action/event identifiers are: [general, contract, op, blackop]");
2021-09-05 01:09:30 +02:00
break;
}
2021-09-05 01:09:30 +02:00
}
executeSkillConsoleCommand(args: string[]): void {
switch (args.length) {
case 1: {
// Display Skill Help Command
2021-09-09 05:47:34 +02:00
this.postToConsole("Invalid usage of 'skill' console command: skill [action] [name]");
2021-09-05 01:09:30 +02:00
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];
}
2021-09-09 05:47:34 +02:00
this.postToConsole(skill.name + ": Level " + formatNumber(level, 0));
2021-09-05 01:09:30 +02:00
}
this.postToConsole(" ");
this.postToConsole("Effects: ");
const multKeys = Object.keys(this.skillMultipliers);
for (let i = 0; i < multKeys.length; ++i) {
2022-05-25 21:08:48 +02:00
const mult = this.skillMultipliers[multKeys[i]];
2021-09-05 01:09:30 +02:00
if (mult && mult !== 1) {
2022-05-25 21:08:48 +02:00
const mults = formatNumber(mult, 3);
2021-09-05 01:09:30 +02:00
switch (multKeys[i]) {
case "successChanceAll":
2022-05-25 21:08:48 +02:00
this.postToConsole("Total Success Chance: x" + mults);
2021-09-05 01:09:30 +02:00
break;
case "successChanceStealth":
2022-05-25 21:08:48 +02:00
this.postToConsole("Stealth Success Chance: x" + mults);
2021-09-05 01:09:30 +02:00
break;
case "successChanceKill":
2022-05-25 21:08:48 +02:00
this.postToConsole("Retirement Success Chance: x" + mults);
2021-09-05 01:09:30 +02:00
break;
case "successChanceContract":
2022-05-25 21:08:48 +02:00
this.postToConsole("Contract Success Chance: x" + mults);
2021-09-05 01:09:30 +02:00
break;
case "successChanceOperation":
2022-05-25 21:08:48 +02:00
this.postToConsole("Operation Success Chance: x" + mults);
2021-09-05 01:09:30 +02:00
break;
case "successChanceEstimate":
2022-05-25 21:08:48 +02:00
this.postToConsole("Synthoid Data Estimate: x" + mults);
2021-09-05 01:09:30 +02:00
break;
case "actionTime":
2022-05-25 21:08:48 +02:00
this.postToConsole("Action Time: x" + mults);
2021-09-05 01:09:30 +02:00
break;
case "effHack":
2022-05-25 21:08:48 +02:00
this.postToConsole("Hacking Skill: x" + mults);
2021-09-05 01:09:30 +02:00
break;
case "effStr":
2022-05-25 21:08:48 +02:00
this.postToConsole("Strength: x" + mults);
2021-09-05 01:09:30 +02:00
break;
case "effDef":
2022-05-25 21:08:48 +02:00
this.postToConsole("Defense: x" + mults);
2021-09-05 01:09:30 +02:00
break;
case "effDex":
2022-05-25 21:08:48 +02:00
this.postToConsole("Dexterity: x" + mults);
2021-09-05 01:09:30 +02:00
break;
case "effAgi":
2022-05-25 21:08:48 +02:00
this.postToConsole("Agility: x" + mults);
2021-09-05 01:09:30 +02:00
break;
case "effCha":
2022-05-25 21:08:48 +02:00
this.postToConsole("Charisma: x" + mults);
2021-09-05 01:09:30 +02:00
break;
case "effInt":
2022-05-25 21:08:48 +02:00
this.postToConsole("Intelligence: x" + mults);
2021-09-05 01:09:30 +02:00
break;
case "stamina":
2022-05-25 21:08:48 +02:00
this.postToConsole("Stamina: x" + mults);
2021-09-05 01:09:30 +02:00
break;
default:
2021-09-05 01:09:30 +02:00
console.warn(`Unrecognized SkillMult Key: ${multKeys[i]}`);
break;
}
}
2021-09-05 01:09:30 +02:00
}
} else {
2021-09-09 05:47:34 +02:00
this.postToConsole("Invalid usage of 'skill' console command: skill [action] [name]");
2021-09-05 01:09:30 +02:00
this.postToConsole("Use 'help skill' for more info");
}
break;
}
case 3: {
const skillName = args[2];
const skill = Skills[skillName];
if (skill == null || !(skill instanceof Skill)) {
2021-09-09 05:47:34 +02:00
this.postToConsole("Invalid skill name (Note that it is case-sensitive): " + skillName);
break;
2021-09-05 01:09:30 +02:00
}
if (args[1].toLowerCase() === "list") {
let level = 0;
if (this.skills[skill.name] !== undefined) {
level = this.skills[skill.name];
}
this.postToConsole(skill.name + ": Level " + formatNumber(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) {
2021-12-21 18:00:12 +01:00
this.postToConsole(`This skill ${skill.name} is already at max level (${currentLevel}/${skill.maxLvl}).`);
} else if (this.skillPoints >= pointCost) {
2021-09-05 01:09:30 +02:00
this.skillPoints -= pointCost;
this.upgradeSkill(skill);
2021-09-09 05:47:34 +02:00
this.log(skill.name + " upgraded to Level " + this.skills[skillName]);
2021-09-05 01:09:30 +02:00
} else {
this.postToConsole(
2021-09-09 05:47:34 +02:00
"You do not have enough Skill Points to upgrade this. You need " + formatNumber(pointCost, 0),
2021-09-05 01:09:30 +02:00
);
}
} else {
2021-09-09 05:47:34 +02:00
this.postToConsole("Invalid usage of 'skill' console command: skill [action] [name]");
2021-09-05 01:09:30 +02:00
this.postToConsole("Use 'help skill' for more info");
}
break;
}
default: {
2021-09-09 05:47:34 +02:00
this.postToConsole("Invalid usage of 'skill' console command: skill [action] [name]");
2021-09-05 01:09:30 +02:00
this.postToConsole("Use 'help skill' for more info");
break;
}
}
2021-09-05 01:09:30 +02:00
}
executeLogConsoleCommand(args: string[]): void {
if (args.length < 3) {
2021-09-09 05:47:34 +02:00
this.postToConsole("Invalid usage of log command: log [enable/disable] [action/event]");
2021-09-05 01:09:30 +02:00
this.postToConsole("Use 'help log' for more details and examples");
return;
}
2021-09-05 01:09:30 +02:00
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;
2021-09-09 05:47:34 +02:00
this.log("Logging " + (flag ? "enabled" : "disabled") + " for general actions");
2021-09-05 01:09:30 +02:00
break;
case "contract":
case "contracts":
this.logging.contracts = flag;
2021-09-09 05:47:34 +02:00
this.log("Logging " + (flag ? "enabled" : "disabled") + " for Contracts");
2021-09-05 01:09:30 +02:00
break;
case "ops":
case "op":
case "operations":
case "operation":
this.logging.ops = flag;
2021-09-09 05:47:34 +02:00
this.log("Logging " + (flag ? "enabled" : "disabled") + " for Operations");
2021-09-05 01:09:30 +02:00
break;
case "blackops":
case "blackop":
case "black operations":
case "black operation":
this.logging.blackops = flag;
2021-09-09 05:47:34 +02:00
this.log("Logging " + (flag ? "enabled" : "disabled") + " for BlackOps");
2021-09-05 01:09:30 +02:00
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;
2021-09-09 05:47:34 +02:00
this.log("Logging " + (flag ? "enabled" : "disabled") + " for everything");
2021-09-05 01:09:30 +02:00
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;
}
2021-09-05 01:09:30 +02:00
}
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;
}
2021-09-05 01:09:30 +02:00
// Enable/Disable
if (args.length === 2) {
const flag = args[1];
if (flag.toLowerCase() === "status") {
2021-09-09 05:47:34 +02:00
this.postToConsole("Automation: " + (this.automateEnabled ? "enabled" : "disabled"));
2021-09-05 01:09:30 +02:00
this.postToConsole(
"When your stamina drops to " +
2022-03-21 05:30:11 +01:00
formatNumber(this.automateThreshLow, 0) +
", you will automatically switch to " +
this.automateActionLow.name +
". When your stamina recovers to " +
formatNumber(this.automateThreshHigh, 0) +
", you will automatically " +
"switch to " +
this.automateActionHigh.name +
".",
2021-09-05 01:09:30 +02:00
);
} else if (flag.toLowerCase().includes("en")) {
if (
!(this.automateActionLow instanceof ActionIdentifier) ||
!(this.automateActionHigh instanceof ActionIdentifier)
) {
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;
}
2021-09-05 01:09:30 +02:00
// Set variables
if (args.length === 4) {
const variable = args[1].toLowerCase(); // allows Action Type to be with or without capitalisation.
2021-09-05 01:09:30 +02:00
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 {
2021-09-05 01:09:30 +02:00
this.automateActionLow = action;
}
2021-09-09 05:47:34 +02:00
this.log("Automate (" + (highLow ? "HIGH" : "LOW") + ") action set to " + val);
2021-09-05 01:09:30 +02:00
} 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;
}
2021-09-09 05:47:34 +02:00
this.log("Automate (" + (highLow ? "HIGH" : "LOW") + ") action set to " + val);
2021-09-05 01:09:30 +02:00
} 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;
}
2021-09-09 05:47:34 +02:00
this.log("Automate (" + (highLow ? "HIGH" : "LOW") + ") action set to " + val);
2021-09-05 01:09:30 +02:00
} else {
this.postToConsole("Invalid Operation name specified: " + val);
}
break;
case "stamina":
if (isNaN(parseFloat(val))) {
2021-09-09 05:47:34 +02:00
this.postToConsole("Invalid value specified for stamina threshold (must be numeric): " + val);
2021-09-05 01:09:30 +02:00
} else {
if (highLow) {
this.automateThreshHigh = Number(val);
} else {
this.automateThreshLow = Number(val);
}
2021-09-09 05:47:34 +02:00
this.log("Automate (" + (highLow ? "HIGH" : "LOW") + ") stamina threshold set to " + val);
2021-09-05 01:09:30 +02:00
}
break;
default:
break;
}
return;
}
2021-09-05 01:09:30 +02:00
}
2021-09-05 01:09:30 +02:00
parseCommandArguments(command: string): string[] {
/**
2021-09-05 01:09:30 +02:00
* 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
*/
2021-09-05 01:09:30 +02:00
const args = [];
let start = 0;
let i = 0;
while (i < command.length) {
const c = command.charAt(i);
if (c === '"') {
// Double quotes
const endQuote = command.indexOf('"', i + 1);
2022-03-24 16:09:24 +01:00
if (endQuote !== -1 && (endQuote === command.length - 1 || command.charAt(endQuote + 1) === KEY.SPACE)) {
2021-09-05 01:09:30 +02:00
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 === "'") {
// Single quotes, same thing as above
const endQuote = command.indexOf("'", i + 1);
2022-03-24 16:09:24 +01:00
if (endQuote !== -1 && (endQuote === command.length - 1 || command.charAt(endQuote + 1) === KEY.SPACE)) {
2021-09-05 01:09:30 +02:00
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;
2021-08-16 07:35:05 +02:00
}
2022-03-24 16:09:24 +01:00
} else if (c === KEY.SPACE) {
2021-09-05 01:09:30 +02:00
args.push(command.substr(start, i - start));
start = i + 1;
}
++i;
2021-08-16 07:35:05 +02:00
}
2021-09-05 01:09:30 +02:00
if (start !== i) {
args.push(command.substr(start, i - start));
}
return args;
}
executeConsoleCommand(player: IPlayer, 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(player, args);
break;
case "stop":
this.resetAction();
break;
default:
this.postToConsole("Invalid console command");
break;
}
}
2021-08-16 07:35:05 +02:00
2021-09-05 01:09:30 +02:00
triggerMigration(sourceCityName: string): void {
let destCityName = BladeburnerConstants.CityNames[getRandomInt(0, 5)];
while (destCityName === sourceCityName) {
destCityName = BladeburnerConstants.CityNames[getRandomInt(0, 5)];
}
const destCity = this.cities[destCityName];
const sourceCity = this.cities[sourceCityName];
if (destCity == null || sourceCity == null) {
throw new Error("Failed to find City with name: " + destCityName);
}
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;
}
triggerPotentialMigration(sourceCityName: string, chance: number): void {
if (chance == null || isNaN(chance)) {
2021-09-09 05:47:34 +02:00
console.error("Invalid 'chance' parameter passed into Bladeburner.triggerPotentialMigration()");
2021-09-05 01:09:30 +02:00
}
if (chance > 1) {
chance /= 100;
}
if (Math.random() < chance) {
this.triggerMigration(sourceCityName);
}
}
randomEvent(): void {
const chance = Math.random();
// Choose random source/destination city for events
const sourceCityName = BladeburnerConstants.CityNames[getRandomInt(0, 5)];
const sourceCity = this.cities[sourceCityName];
if (!(sourceCity instanceof City)) {
2021-09-09 05:47:34 +02:00
throw new Error("sourceCity was not a City object in Bladeburner.randomEvent()");
2021-09-05 01:09:30 +02:00
}
2021-08-16 07:35:05 +02:00
2021-09-05 01:09:30 +02:00
let destCityName = BladeburnerConstants.CityNames[getRandomInt(0, 5)];
while (destCityName === sourceCityName) {
destCityName = BladeburnerConstants.CityNames[getRandomInt(0, 5)];
}
const destCity = this.cities[destCityName];
2021-08-16 07:35:05 +02:00
2021-09-05 01:09:30 +02:00
if (!(sourceCity instanceof City) || !(destCity instanceof City)) {
2021-09-09 05:47:34 +02:00
throw new Error("sourceCity/destCity was not a City object in Bladeburner.randomEvent()");
2021-08-16 07:35:05 +02:00
}
2021-09-05 01:09:30 +02:00
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 (this.logging.events) {
2021-09-09 05:47:34 +02:00
this.log("Intelligence indicates that a new Synthoid community was formed in a city");
2021-09-05 01:09:30 +02:00
}
} 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 (this.logging.events) {
2021-09-09 05:47:34 +02:00
this.log("Intelligence indicates that a new Synthoid community was formed in a city");
2021-09-05 01:09:30 +02:00
}
} 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 (this.logging.events) {
this.log(
2021-09-09 05:47:34 +02:00
"Intelligence indicates that a Synthoid community migrated from " + sourceCityName + " to some other city",
2021-09-05 01:09:30 +02:00
);
}
}
} 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 (this.logging.events) {
this.log(
2021-09-09 05:47:34 +02:00
"Intelligence indicates that the Synthoid population of " + sourceCityName + " just changed significantly",
2021-09-05 01:09:30 +02:00
);
}
} 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 " +
2022-03-21 05:30:11 +01:00
sourceCityName +
" to some other city",
2021-09-05 01:09:30 +02:00
);
}
} else if (chance <= 0.7) {
// Synthoid Riots (+chaos), 20%
sourceCity.chaos += 1;
sourceCity.chaos *= 1 + getRandomInt(5, 20) / 100;
if (this.logging.events) {
2021-09-09 05:47:34 +02:00
this.log("Tensions between Synthoids and humans lead to riots in " + sourceCityName + "! Chaos increased");
2021-09-05 01:09:30 +02:00
}
} 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(
2021-09-09 05:47:34 +02:00
"Intelligence indicates that the Synthoid population of " + sourceCityName + " just changed significantly",
2021-09-05 01:09:30 +02:00
);
}
}
2021-09-05 01:09:30 +02:00
// 10% chance of nothing happening
}
2021-09-05 01:09:30 +02:00
/**
* Return stat to be gained from Contracts, Operations, and Black Operations
2021-09-05 01:09:30 +02:00
* @param action(Action obj) - Derived action class
* @param success(bool) - Whether action was successful
*/
getActionStats(action: IAction, success: boolean): ITaskTracker {
2021-09-05 01:09:30 +02:00
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 = this.actionTimeToComplete;
const successMult = success ? 1 : 0.5;
2021-09-09 05:47:34 +02:00
const unweightedGain = time * BladeburnerConstants.BaseStatGain * successMult * difficultyMult;
const unweightedIntGain = time * BladeburnerConstants.BaseIntGain * successMult * difficultyMult;
2021-09-05 01:09:30 +02:00
const skillMult = this.skillMultipliers.expGain;
2022-04-14 17:57:01 +02:00
return {
hack: unweightedGain * action.weights.hack * skillMult,
str: unweightedGain * action.weights.str * skillMult,
def: unweightedGain * action.weights.def * skillMult,
dex: unweightedGain * action.weights.dex * skillMult,
agi: unweightedGain * action.weights.agi * skillMult,
cha: unweightedGain * action.weights.cha * skillMult,
int: unweightedIntGain * action.weights.int * skillMult,
money: 0,
2022-04-14 17:59:49 +02:00
};
2021-09-05 01:09:30 +02:00
}
getDiplomacyEffectiveness(person: IPerson): number {
2021-09-05 01:09:30 +02:00
// 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.charisma, CharismaExponentialFactor) + person.charisma / CharismaLinearFactor;
2021-09-05 01:09:30 +02:00
return (100 - charismaEff) / 100;
}
getRecruitmentSuccessChance(person: IPerson): number {
return Math.pow(person.charisma, 0.45) / (this.teamSize - this.sleeveSize + 1);
2021-09-05 01:09:30 +02:00
}
getRecruitmentTime(person: IPerson): number {
const effCharisma = person.charisma * this.skillMultipliers.effCha;
2021-09-05 01:09:30 +02:00
const charismaFactor = Math.pow(effCharisma, 0.81) + effCharisma / 90;
2021-11-03 18:43:03 +01:00
return Math.max(10, Math.round(BladeburnerConstants.BaseRecruitmentTimeNeeded - charismaFactor));
2021-09-05 01:09:30 +02:00
}
sleeveSupport(joining: boolean): void {
2022-04-14 17:59:49 +02:00
if (joining) {
this.sleeveSize += 1;
this.teamSize += 1;
} else {
this.sleeveSize -= 1;
this.teamSize -= 1;
}
}
2021-09-05 01:09:30 +02:00
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,
};
}
2021-09-05 01:09:30 +02:00
updateSkillMultipliers(): void {
this.resetSkillMultipliers();
2022-01-16 01:45:03 +01:00
for (const skillName of Object.keys(this.skills)) {
2021-09-05 01:09:30 +02:00
if (this.skills.hasOwnProperty(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];
2021-09-09 05:47:34 +02:00
if (skill.getMultiplier(multiplierName) != null && !isNaN(skill.getMultiplier(multiplierName))) {
2021-09-05 01:09:30 +02:00
const value = skill.getMultiplier(multiplierName) * level;
let multiplierValue = 1 + value / 100;
if (multiplierName === "actionTime") {
multiplierValue = 1 - value / 100;
2021-08-16 07:35:05 +02:00
}
2021-09-05 01:09:30 +02:00
this.skillMultipliers[multiplierName] *= multiplierValue;
}
}
2021-09-05 01:09:30 +02:00
}
2021-08-16 23:45:26 +02:00
}
2021-09-05 01:09:30 +02:00
}
2021-08-16 23:45:26 +02:00
completeOperation(success: boolean, player: IPlayer): void {
2021-09-05 01:09:30 +02:00
if (this.action.type !== ActionTypes.Operation) {
2021-09-09 05:47:34 +02:00
throw new Error("completeOperation() called even though current action is not an Operation");
2021-09-05 01:09:30 +02:00
}
const action = this.getActionObject(this.action);
if (action == null) {
2021-09-09 05:47:34 +02:00
throw new Error("Failed to get Contract/Operation Object for: " + this.action.name);
2021-09-05 01:09:30 +02:00
}
2021-08-16 23:45:26 +02:00
2021-09-05 01:09:30 +02:00
// 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;
2022-04-14 17:59:49 +02:00
if (this.teamSize < this.sleeveSize) {
const sup = player.sleeves.filter((x) => x.bbAction == "Support main sleeve");
for (let i = 0; i > this.teamSize - this.sleeveSize; i--) {
const r = Math.floor(Math.random() * sup.length);
sup[r].takeDamage(sup[r].max_hp);
sup.splice(r, 1);
}
this.teamSize += this.sleeveSize;
}
2021-09-05 01:09:30 +02:00
this.teamLost += losses;
if (this.logging.ops && losses > 0) {
2021-09-09 05:47:34 +02:00
this.log("Lost " + formatNumber(losses, 0) + " team members during this " + action.name);
2021-09-05 01:09:30 +02:00
}
2021-08-16 23:45:26 +02:00
}
2021-09-05 01:09:30 +02:00
const city = this.getCurrentCity();
switch (action.name) {
case "Investigation":
if (success) {
2021-09-09 05:47:34 +02:00
city.improvePopulationEstimateByPercentage(0.4 * this.skillMultipliers.successChanceEstimate);
2021-09-05 01:09:30 +02:00
} else {
this.triggerPotentialMigration(this.city, 0.1);
2021-08-16 23:45:26 +02:00
}
2021-09-05 01:09:30 +02:00
break;
case "Undercover Operation":
if (success) {
2021-09-09 05:47:34 +02:00
city.improvePopulationEstimateByPercentage(0.8 * this.skillMultipliers.successChanceEstimate);
2021-09-05 01:09:30 +02:00
} else {
this.triggerPotentialMigration(this.city, 0.15);
2021-08-16 23:45:26 +02:00
}
2021-09-05 01:09:30 +02:00
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:
2021-09-09 05:47:34 +02:00
throw new Error("Invalid Action name in completeOperation: " + this.action.name);
2021-09-05 01:09:30 +02:00
}
}
2021-08-16 23:45:26 +02:00
2021-09-05 01:09:30 +02:00
getActionObject(actionId: IActionIdentifier): IAction | 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"];
2021-10-15 05:39:30 +02:00
case ActionTypes["Incite Violence"]:
return GeneralActions["Incite Violence"];
2021-09-05 01:09:30 +02:00
default:
return null;
}
2021-09-05 01:09:30 +02:00
}
completeContract(success: boolean, actionIdent: IActionIdentifier): void {
if (actionIdent.type !== ActionTypes.Contract) {
2021-09-09 05:47:34 +02:00
throw new Error("completeContract() called even though current action is not a Contract");
2021-09-05 01:09:30 +02:00
}
const city = this.getCurrentCity();
if (success) {
switch (actionIdent.name) {
2021-09-05 01:09:30 +02:00
case "Tracking":
// Increase estimate accuracy by a relatively small amount
city.improvePopulationEstimateByCount(getRandomInt(100, 1e3));
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);
2021-09-05 01:09:30 +02:00
}
}
2021-09-05 01:09:30 +02:00
}
2021-08-16 23:45:26 +02:00
completeAction(player: IPlayer, person: IPerson, actionIdent: IActionIdentifier, isPlayer = true): ITaskTracker {
let retValue = createTaskTracker();
switch (actionIdent.type) {
2021-09-05 01:09:30 +02:00
case ActionTypes["Contract"]:
case ActionTypes["Operation"]: {
try {
const isOperation = actionIdent.type === ActionTypes["Operation"];
const action = this.getActionObject(actionIdent);
2021-09-05 01:09:30 +02:00
if (action == null) {
throw new Error("Failed to get Contract/Operation Object for: " + actionIdent.name);
2021-09-05 01:09:30 +02:00
}
const difficulty = action.getDifficulty();
const difficultyMultiplier =
2021-09-09 05:47:34 +02:00
Math.pow(difficulty, BladeburnerConstants.DiffMultExponentialFactor) +
2021-09-05 01:09:30 +02:00
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;
}
2021-09-05 01:09:30 +02:00
}
2021-08-16 23:45:26 +02:00
2021-09-05 01:09:30 +02:00
// Process Contract/Operation success/failure
if (action.attempt(this, person)) {
retValue = this.getActionStats(action, true);
2021-09-05 01:09:30 +02:00
++action.successes;
--action.count;
// Earn money for contracts
let moneyGain = 0;
if (!isOperation) {
2021-09-09 05:47:34 +02:00
moneyGain = BladeburnerConstants.ContractBaseMoneyGain * rewardMultiplier * this.skillMultipliers.money;
retValue.money = moneyGain;
2021-08-16 23:45:26 +02:00
}
2021-09-05 01:09:30 +02:00
if (isOperation) {
2021-09-09 05:47:34 +02:00
action.setMaxLevel(BladeburnerConstants.OperationSuccessesPerLevel);
2021-09-05 01:09:30 +02:00
} else {
2021-09-09 05:47:34 +02:00
action.setMaxLevel(BladeburnerConstants.ContractSuccessesPerLevel);
2021-09-05 01:09:30 +02:00
}
if (action.rankGain) {
2021-09-09 05:47:34 +02:00
const gain = addOffset(action.rankGain * rewardMultiplier * BitNodeMultipliers.BladeburnerRank, 10);
this.changeRank(person, gain);
2021-09-05 01:09:30 +02:00
if (isOperation && this.logging.ops) {
2022-04-14 17:59:49 +02:00
this.log(
`${person.whoAmI()}: ` +
action.name +
" successfully completed! Gained " +
formatNumber(gain, 3) +
" rank",
);
2021-09-05 01:09:30 +02:00
} else if (!isOperation && this.logging.contracts) {
this.log(
2022-04-14 17:57:01 +02:00
`${person.whoAmI()}: ` +
2022-04-14 17:59:49 +02:00
action.name +
2022-03-21 05:30:11 +01:00
" contract successfully completed! Gained " +
formatNumber(gain, 3) +
" rank and " +
numeralWrapper.formatMoney(moneyGain),
2021-09-05 01:09:30 +02:00
);
}
}
isOperation ? this.completeOperation(true, player) : this.completeContract(true, actionIdent);
2021-09-05 01:09:30 +02:00
} else {
retValue = this.getActionStats(action, false);
2021-09-05 01:09:30 +02:00
++action.failures;
let loss = 0,
damage = 0;
if (action.rankLoss) {
loss = addOffset(action.rankLoss * rewardMultiplier, 10);
this.changeRank(person, -1 * loss);
2021-09-05 01:09:30 +02:00
}
if (action.hpLoss) {
damage = action.hpLoss * difficultyMultiplier;
damage = Math.ceil(addOffset(damage, 10));
this.hpLost += damage;
2022-05-21 00:18:42 +02:00
const cost = calculateHospitalizationCost(player, damage);
if (person.takeDamage(damage)) {
2021-09-05 01:09:30 +02:00
++this.numHosp;
this.moneyLost += cost;
}
}
let logLossText = "";
if (loss > 0) {
logLossText += "Lost " + formatNumber(loss, 3) + " rank. ";
}
2021-09-05 01:09:30 +02:00
if (damage > 0) {
logLossText += "Took " + formatNumber(damage, 0) + " damage.";
}
2021-09-05 01:09:30 +02:00
if (isOperation && this.logging.ops) {
this.log(`${person.whoAmI()}: ` + action.name + " failed! " + logLossText);
2021-09-05 01:09:30 +02:00
} else if (!isOperation && this.logging.contracts) {
this.log(`${person.whoAmI()}: ` + action.name + " contract failed! " + logLossText);
2021-09-05 01:09:30 +02:00
}
isOperation ? this.completeOperation(false, player) : this.completeContract(false, actionIdent);
2021-09-05 01:09:30 +02:00
}
if (action.autoLevel) {
action.level = action.maxLevel;
} // Autolevel
2021-10-13 08:27:55 +02:00
} catch (e: any) {
2021-09-05 01:09:30 +02:00
exceptionAlert(e);
}
break;
}
case ActionTypes["BlackOp"]:
case ActionTypes["BlackOperation"]: {
try {
const action = this.getActionObject(actionIdent);
2021-09-05 01:09:30 +02:00
if (action == null || !(action instanceof BlackOperation)) {
throw new Error("Failed to get BlackOperation Object for: " + actionIdent.name);
2021-09-05 01:09:30 +02:00
}
const difficulty = action.getDifficulty();
const difficultyMultiplier =
2021-09-09 05:47:34 +02:00
Math.pow(difficulty, BladeburnerConstants.DiffMultExponentialFactor) +
2021-09-05 01:09:30 +02:00
difficulty / BladeburnerConstants.DiffMultLinearFactor;
// Stamina loss is based on difficulty
2021-09-09 05:47:34 +02:00
this.stamina -= BladeburnerConstants.BaseStaminaLoss * difficultyMultiplier;
2021-09-05 01:09:30 +02:00
if (this.stamina < 0) {
this.stamina = 0;
}
2021-08-16 23:45:26 +02:00
2021-09-05 01:09:30 +02:00
// Team loss variables
const teamCount = action.teamCount;
let teamLossMax;
if (action.attempt(this, person)) {
retValue = this.getActionStats(action, true);
2021-09-05 01:09:30 +02:00
action.count = 0;
this.blackops[action.name] = true;
let rankGain = 0;
if (action.rankGain) {
2021-09-09 05:47:34 +02:00
rankGain = addOffset(action.rankGain * BitNodeMultipliers.BladeburnerRank, 10);
this.changeRank(person, rankGain);
}
2021-09-05 01:09:30 +02:00
teamLossMax = Math.ceil(teamCount / 2);
2021-08-16 23:45:26 +02:00
2021-09-05 01:09:30 +02:00
if (this.logging.blackops) {
2022-04-14 17:59:49 +02:00
this.log(
`${person.whoAmI()}: ` + action.name + " successful! Gained " + formatNumber(rankGain, 1) + " rank",
);
}
2021-09-05 01:09:30 +02:00
} else {
retValue = this.getActionStats(action, false);
2021-09-05 01:09:30 +02:00
let rankLoss = 0;
let damage = 0;
if (action.rankLoss) {
rankLoss = addOffset(action.rankLoss, 10);
this.changeRank(person, -1 * rankLoss);
2021-09-05 01:09:30 +02:00
}
if (action.hpLoss) {
damage = action.hpLoss * difficultyMultiplier;
damage = Math.ceil(addOffset(damage, 10));
2022-05-21 00:18:42 +02:00
const cost = calculateHospitalizationCost(player, damage);
if (person.takeDamage(damage)) {
2021-09-05 01:09:30 +02:00
++this.numHosp;
this.moneyLost += cost;
}
}
teamLossMax = Math.floor(teamCount);
if (this.logging.blackops) {
this.log(
2022-04-14 17:57:01 +02:00
`${person.whoAmI()}: ` +
2022-04-14 17:59:49 +02:00
action.name +
2022-03-21 05:30:11 +01:00
" failed! Lost " +
formatNumber(rankLoss, 1) +
" rank and took " +
formatNumber(damage, 0) +
" damage",
2021-09-05 01:09:30 +02:00
);
}
}
this.resetAction(); // Stop regardless of success or fail
2021-08-16 23:45:26 +02:00
2021-09-05 01:09:30 +02:00
// Calculate team lossses
if (teamCount >= 1) {
const losses = getRandomInt(1, teamLossMax);
this.teamSize -= losses;
2022-04-14 17:59:49 +02:00
if (this.teamSize < this.sleeveSize) {
const sup = player.sleeves.filter((x) => x.bbAction == "Support main sleeve");
for (let i = 0; i > this.teamSize - this.sleeveSize; i--) {
const r = Math.floor(Math.random() * sup.length);
sup[r].takeDamage(sup[r].max_hp);
sup.splice(r, 1);
}
this.teamSize += this.sleeveSize;
}
2021-09-05 01:09:30 +02:00
this.teamLost += losses;
if (this.logging.blackops) {
2022-04-14 21:28:13 +02:00
this.log(`${person.whoAmI()}: You lost ${formatNumber(losses, 0)} team members during ${action.name}`);
}
2021-09-05 01:09:30 +02:00
}
2021-10-13 08:27:55 +02:00
} catch (e: any) {
2021-09-05 01:09:30 +02:00
exceptionAlert(e);
}
break;
}
case ActionTypes["Training"]: {
this.stamina -= 0.5 * BladeburnerConstants.BaseStaminaLoss;
const strExpGain = 30 * person.strength_exp_mult,
defExpGain = 30 * person.defense_exp_mult,
dexExpGain = 30 * person.dexterity_exp_mult,
agiExpGain = 30 * person.agility_exp_mult,
2021-09-05 01:09:30 +02:00
staminaGain = 0.04 * this.skillMultipliers.stamina;
retValue.str = strExpGain;
retValue.def = defExpGain;
retValue.dex = dexExpGain;
retValue.agi = agiExpGain;
2021-09-05 01:09:30 +02:00
this.staminaBonus += staminaGain;
if (this.logging.general) {
this.log(
2022-04-14 17:57:01 +02:00
`${person.whoAmI()}: ` +
2022-04-14 17:59:49 +02:00
"Training completed. Gained: " +
2022-03-21 05:30:11 +01:00
formatNumber(strExpGain, 1) +
" str exp, " +
formatNumber(defExpGain, 1) +
" def exp, " +
formatNumber(dexExpGain, 1) +
" dex exp, " +
formatNumber(agiExpGain, 1) +
" agi exp, " +
formatNumber(staminaGain, 3) +
" max stamina",
2021-09-05 01:09:30 +02:00
);
}
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.hacking, 0.3) +
0.04 * Math.pow(person.intelligence, 0.9) +
0.02 * Math.pow(person.charisma, 0.3);
eff *= person.bladeburner_analysis_mult;
2021-09-05 01:09:30 +02:00
if (isNaN(eff) || eff < 0) {
2021-09-09 05:47:34 +02:00
throw new Error("Field Analysis Effectiveness calculated to be NaN or negative");
2021-09-05 01:09:30 +02:00
}
const hackingExpGain = 20 * person.hacking_exp_mult;
const charismaExpGain = 20 * person.charisma_exp_mult;
const rankGain = 0.1 * BitNodeMultipliers.BladeburnerRank;
retValue.hack = hackingExpGain;
retValue.cha = charismaExpGain;
retValue.int = BladeburnerConstants.BaseIntGain;
this.changeRank(person, rankGain);
2021-09-09 05:47:34 +02:00
this.getCurrentCity().improvePopulationEstimateByPercentage(eff * this.skillMultipliers.successChanceEstimate);
2021-09-05 01:09:30 +02:00
if (this.logging.general) {
this.log(
2022-04-14 17:57:01 +02:00
`${person.whoAmI()}: ` +
2022-04-14 17:59:49 +02:00
`Field analysis completed. Gained ${formatNumber(rankGain, 2)} rank, ` +
`${formatNumber(hackingExpGain, 1)} hacking exp, and ` +
`${formatNumber(charismaExpGain, 1)} charisma exp`,
2021-09-05 01:09:30 +02:00
);
}
break;
}
case ActionTypes["Recruitment"]: {
const successChance = this.getRecruitmentSuccessChance(person);
const recruitTime = this.getRecruitmentTime(person) * 1000;
2021-09-05 01:09:30 +02:00
if (Math.random() < successChance) {
const expGain = 2 * BladeburnerConstants.BaseStatGain * recruitTime;
retValue.cha = expGain;
2021-09-05 01:09:30 +02:00
++this.teamSize;
if (this.logging.general) {
2022-04-14 17:59:49 +02:00
this.log(
`${person.whoAmI()}: ` +
"Successfully recruited a team member! Gained " +
formatNumber(expGain, 1) +
" charisma exp",
);
2021-09-05 01:09:30 +02:00
}
} else {
const expGain = BladeburnerConstants.BaseStatGain * recruitTime;
retValue.cha = expGain;
2021-09-05 01:09:30 +02:00
if (this.logging.general) {
2022-04-14 17:59:49 +02:00
this.log(
`${person.whoAmI()}: ` +
"Failed to recruit a team member. Gained " +
formatNumber(expGain, 1) +
" charisma exp",
);
2021-09-05 01:09:30 +02:00
}
}
2021-09-05 01:09:30 +02:00
break;
}
case ActionTypes["Diplomacy"]: {
const eff = this.getDiplomacyEffectiveness(person);
2021-09-05 01:09:30 +02:00
this.getCurrentCity().chaos *= eff;
if (this.getCurrentCity().chaos < 0) {
this.getCurrentCity().chaos = 0;
}
if (this.logging.general) {
this.log(
2022-04-14 17:59:49 +02:00
`${person.whoAmI()}: Diplomacy completed. Chaos levels in the current city fell by ${numeralWrapper.formatPercentage(
1 - eff,
)}`,
2021-09-05 01:09:30 +02:00
);
}
break;
}
case ActionTypes["Hyperbolic Regeneration Chamber"]: {
person.regenerateHp(BladeburnerConstants.HrcHpGain);
2021-09-05 01:09:30 +02:00
2021-09-09 05:47:34 +02:00
const staminaGain = this.maxStamina * (BladeburnerConstants.HrcStaminaGain / 100);
2021-09-05 01:09:30 +02:00
this.stamina = Math.min(this.maxStamina, this.stamina + staminaGain);
if (this.logging.general) {
this.log(
`${person.whoAmI()}: Rested in Hyperbolic Regeneration Chamber. Restored ${
2022-03-21 05:30:11 +01:00
BladeburnerConstants.HrcHpGain
2021-09-09 05:47:34 +02:00
} HP and gained ${numeralWrapper.formatStamina(staminaGain)} stamina`,
2021-09-05 01:09:30 +02:00
);
}
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);
2021-10-27 01:55:34 +02:00
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);
2021-10-27 01:55:34 +02:00
this.operations[operation].count += (60 * 3 * growthF()) / BladeburnerConstants.ActionCountGrowthPeriod;
}
2021-10-15 05:39:30 +02:00
if (this.logging.general) {
2022-04-14 21:28:13 +02:00
this.log(`${person.whoAmI()}: Incited violence in the synthoid communities.`);
2021-10-15 05:39:30 +02:00
}
for (const cityName of Object.keys(this.cities)) {
const city = this.cities[cityName];
city.chaos += 10;
city.chaos += city.chaos / (Math.log(city.chaos) / Math.log(10));
}
break;
}
2021-09-05 01:09:30 +02:00
default:
console.error(`Bladeburner.completeAction() called for invalid action: ${actionIdent.type}`);
2021-09-05 01:09:30 +02:00
break;
2021-08-16 23:45:26 +02:00
}
return retValue;
2021-09-05 01:09:30 +02:00
}
2021-08-19 07:45:26 +02:00
2022-05-21 00:18:42 +02:00
infiltrateSynthoidCommunities(p: IPlayer): void {
const infilSleeves = p.sleeves.filter((s) => s.bbAction === "Infiltrate synthoids").length;
const amt = Math.pow(infilSleeves, -0.5) / 2;
2022-04-14 17:59:49 +02:00
for (const contract of Object.keys(this.contracts)) {
2022-05-21 00:18:42 +02:00
this.contracts[contract].count += amt;
2022-04-14 17:59:49 +02:00
}
for (const operation of Object.keys(this.operations)) {
2022-05-21 00:18:42 +02:00
this.operations[operation].count += amt;
2022-04-14 17:59:49 +02:00
}
if (this.logging.general) {
this.log(`Sleeve: Infiltrate the synthoid communities.`);
}
}
changeRank(person: IPerson, change: number): void {
2021-09-05 01:09:30 +02:00
if (isNaN(change)) {
throw new Error("NaN passed into Bladeburner.changeRank()");
2021-08-16 23:45:26 +02:00
}
2021-09-05 01:09:30 +02:00
this.rank += change;
if (this.rank < 0) {
this.rank = 0;
2021-08-16 23:45:26 +02:00
}
2021-09-05 01:09:30 +02:00
this.maxRank = Math.max(this.rank, this.maxRank);
const bladeburnersFactionName = FactionNames.Bladeburners;
2021-09-05 01:09:30 +02:00
if (factionExists(bladeburnersFactionName)) {
const bladeburnerFac = Factions[bladeburnersFactionName];
if (!(bladeburnerFac instanceof Faction)) {
2022-03-21 05:30:11 +01:00
throw new Error(
`Could not properly get ${FactionNames.Bladeburners} Faction object in ${FactionNames.Bladeburners} UI Overview Faction button`,
);
2021-09-05 01:09:30 +02:00
}
if (bladeburnerFac.isMember) {
const favorBonus = 1 + bladeburnerFac.favor / 100;
bladeburnerFac.playerReputation +=
BladeburnerConstants.RankToFactionRepFactor * change * person.faction_rep_mult * favorBonus;
2021-09-05 01:09:30 +02:00
}
2021-08-16 23:45:26 +02:00
}
2021-08-19 07:45:26 +02:00
2021-09-05 01:09:30 +02:00
// Gain skill points
2021-09-09 05:47:34 +02:00
const rankNeededForSp = (this.totalSkillPoints + 1) * BladeburnerConstants.RanksPerSkillPoint;
2021-09-05 01:09:30 +02:00
if (this.maxRank >= rankNeededForSp) {
// Calculate how many skill points to gain
const gainedSkillPoints = Math.floor(
2021-09-09 05:47:34 +02:00
(this.maxRank - rankNeededForSp) / BladeburnerConstants.RanksPerSkillPoint + 1,
2021-09-05 01:09:30 +02:00
);
this.skillPoints += gainedSkillPoints;
this.totalSkillPoints += gainedSkillPoints;
2021-08-16 23:45:26 +02:00
}
2021-09-05 01:09:30 +02:00
}
2021-09-18 01:43:08 +02:00
processAction(router: IRouter, player: IPlayer, seconds: number): void {
2021-09-05 01:09:30 +02:00
if (this.action.type === ActionTypes["Idle"]) return;
if (this.actionTimeToComplete <= 0) {
2021-09-09 05:47:34 +02:00
throw new Error(`Invalid actionTimeToComplete value: ${this.actionTimeToComplete}, type; ${this.action.type}`);
2021-09-05 01:09:30 +02:00
}
if (!(this.action instanceof ActionIdentifier)) {
throw new Error("Bladeburner.action is not an ActionIdentifier Object");
2021-08-16 23:45:26 +02:00
}
2021-08-19 07:45:26 +02:00
2021-09-05 01:09:30 +02:00
// If the previous action went past its completion time, add to the next action
// This is not added inmediatly in case the automation changes the action
this.actionTimeCurrent += seconds + this.actionTimeOverflow;
this.actionTimeOverflow = 0;
if (this.actionTimeCurrent >= this.actionTimeToComplete) {
2021-09-09 05:47:34 +02:00
this.actionTimeOverflow = this.actionTimeCurrent - this.actionTimeToComplete;
const action = this.getActionObject(this.action);
2022-04-14 17:57:01 +02:00
const retValue = this.completeAction(player, 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);
2022-04-14 17:59:49 +02:00
} else if (this.action.type != ActionTypes["BlackOperation"] && this.action.type != ActionTypes["BlackOp"]) {
this.startAction(player, this.action); // Repeat action
}
2021-09-05 01:09:30 +02:00
}
}
calculateStaminaGainPerSecond(player: IPlayer): number {
const effAgility = player.agility * this.skillMultipliers.effAgi;
2021-09-09 05:47:34 +02:00
const maxStaminaBonus = this.maxStamina / BladeburnerConstants.MaxStaminaToGainFactor;
const gain = (BladeburnerConstants.StaminaGainPerSecond + maxStaminaBonus) * Math.pow(effAgility, 0.17);
return gain * (this.skillMultipliers.stamina * player.bladeburner_stamina_gain_mult);
2021-09-05 01:09:30 +02:00
}
calculateMaxStamina(player: IPlayer): void {
const effAgility = player.agility * this.skillMultipliers.effAgi;
const maxStamina =
(Math.pow(effAgility, 0.8) + this.staminaBonus) *
this.skillMultipliers.stamina *
player.bladeburner_max_stamina_mult;
if (this.maxStamina !== maxStamina) {
const oldMax = this.maxStamina;
this.maxStamina = maxStamina;
this.stamina = (this.maxStamina * this.stamina) / oldMax;
}
if (isNaN(maxStamina)) {
2021-09-09 05:47:34 +02:00
throw new Error("Max Stamina calculated to be NaN in Bladeburner.calculateMaxStamina()");
2021-09-05 01:09:30 +02:00
}
}
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,
});
}
2021-09-18 01:43:08 +02:00
process(router: IRouter, player: IPlayer): void {
// Edge race condition when the engine checks the processing counters and attempts to route before the router is initialized.
if (!router.isInitialized) return;
2021-09-05 01:09:30 +02:00
// If the Player starts doing some other actions, set action to idle and alert
if (!player.hasAugmentation(AugmentationNames.BladesSimulacrum, true) && player.currentWork) {
2021-09-05 01:09:30 +02:00
if (this.action.type !== ActionTypes["Idle"]) {
2021-09-09 05:47:34 +02:00
let msg = "Your Bladeburner action was cancelled because you started doing something else.";
2021-09-05 01:09:30 +02:00
if (this.automateEnabled) {
2021-09-27 23:09:48 +02:00
msg += `<br /><br />Your automation was disabled as well. You will have to re-enable it through the Bladeburner console`;
2021-09-05 01:09:30 +02:00
this.automateEnabled = false;
}
if (!Settings.SuppressBladeburnerPopup) {
dialogBoxCreate(msg);
}
}
this.resetAction();
}
2021-08-16 23:45:26 +02:00
2021-09-05 01:09:30 +02:00
// If the Player has no Stamina, set action to idle
if (this.stamina <= 0) {
2021-09-09 05:47:34 +02:00
this.log("Your Bladeburner action was cancelled because your stamina hit 0");
2021-09-05 01:09:30 +02:00
this.resetAction();
}
2021-08-16 23:45:26 +02:00
2021-09-05 01:09:30 +02:00
// A 'tick' for this mechanic is one second (= 5 game cycles)
if (this.storedCycles >= BladeburnerConstants.CyclesPerSecond) {
2021-09-09 05:47:34 +02:00
let seconds = Math.floor(this.storedCycles / BladeburnerConstants.CyclesPerSecond);
2021-09-05 01:09:30 +02:00
seconds = Math.min(seconds, 5); // Max of 5 'ticks'
this.storedCycles -= seconds * BladeburnerConstants.CyclesPerSecond;
// Stamina
this.calculateMaxStamina(player);
this.stamina += this.calculateStaminaGainPerSecond(player) * seconds;
this.stamina = Math.min(this.maxStamina, this.stamina);
// Count increase for contracts/operations
2022-05-25 21:08:48 +02:00
for (const contract of Object.values(this.contracts)) {
2021-09-09 09:17:01 +02:00
const growthF = Growths[contract.name];
2021-09-09 05:47:34 +02:00
if (growthF === undefined) throw new Error(`growth formula for action '${contract.name}' is undefined`);
contract.count += (seconds * growthF()) / BladeburnerConstants.ActionCountGrowthPeriod;
2021-09-05 01:09:30 +02:00
}
2022-05-25 21:08:48 +02:00
for (const op of Object.values(this.operations)) {
const growthF = Growths[op.name];
2021-09-09 05:47:34 +02:00
if (growthF === undefined) throw new Error(`growth formula for action '${op.name}' is undefined`);
if (growthF !== undefined) {
2021-09-09 05:47:34 +02:00
op.count += (seconds * growthF()) / BladeburnerConstants.ActionCountGrowthPeriod;
}
2021-09-05 01:09:30 +02:00
}
// Chaos goes down very slowly
for (const cityName of BladeburnerConstants.CityNames) {
const city = this.cities[cityName];
if (!(city instanceof City)) {
2021-09-09 05:47:34 +02:00
throw new Error("Invalid City object when processing passive chaos reduction in Bladeburner.process");
2021-09-05 01:09:30 +02:00
}
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);
}
2021-09-18 01:43:08 +02:00
this.processAction(router, player, seconds);
2021-09-05 01:09:30 +02:00
// Automation
if (this.automateEnabled) {
// Note: Do NOT set this.action = this.automateActionHigh/Low since it creates a reference
if (this.stamina <= this.automateThreshLow) {
2021-09-09 05:47:34 +02:00
if (this.action.name !== this.automateActionLow.name || this.action.type !== this.automateActionLow.type) {
2021-09-05 01:09:30 +02:00
this.action = new ActionIdentifier({
type: this.automateActionLow.type,
name: this.automateActionLow.name,
});
2021-09-05 01:09:30 +02:00
this.startAction(player, this.action);
}
} else if (this.stamina >= this.automateThreshHigh) {
2021-09-09 05:47:34 +02:00
if (this.action.name !== this.automateActionHigh.name || this.action.type !== this.automateActionHigh.type) {
2021-09-05 01:09:30 +02:00
this.action = new ActionIdentifier({
type: this.automateActionHigh.type,
name: this.automateActionHigh.name,
});
this.startAction(player, this.action);
}
2021-08-16 23:45:26 +02:00
}
2021-09-05 01:09:30 +02:00
}
2021-08-16 23:45:26 +02:00
}
2021-09-05 01:09:30 +02:00
}
getTypeAndNameFromActionId(actionId: IActionIdentifier): {
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";
}
2021-09-05 01:09:30 +02:00
if (res.type == null) {
res.type = "Idle";
2021-08-16 23:45:26 +02:00
}
2021-08-19 07:45:26 +02:00
2021-09-05 01:09:30 +02:00
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);
}
2021-09-09 05:47:34 +02:00
startActionNetscriptFn(player: IPlayer, type: string, name: string, workerScript: WorkerScript): boolean {
2021-09-05 01:09:30 +02:00
const errorLogText = `Invalid action: type='${type}' name='${name}'`;
const actionId = this.getActionIdFromTypeAndName(type, name);
if (actionId == null) {
workerScript.log("bladeburner.startAction", () => errorLogText);
2021-09-05 01:09:30 +02:00
return false;
}
2021-08-16 23:45:26 +02:00
2021-09-05 01:09:30 +02:00
// Special logic for Black Ops
if (actionId.type === ActionTypes["BlackOp"]) {
const canRunOp = this.canAttemptBlackOp(actionId);
if (!canRunOp.isAvailable) {
2021-12-21 18:00:12 +01:00
workerScript.log("bladeburner.startAction", () => canRunOp.error + "");
2021-09-05 01:09:30 +02:00
return false;
}
2021-08-16 23:45:26 +02:00
}
2021-08-19 07:45:26 +02:00
2021-09-05 01:09:30 +02:00
try {
this.startAction(player, actionId);
workerScript.log(
"bladeburner.startAction",
() => `Starting bladeburner action with type '${type}' and name '${name}'`,
);
2021-09-05 01:09:30 +02:00
return true;
2021-10-13 08:27:55 +02:00
} catch (e: any) {
2021-09-05 01:09:30 +02:00
this.resetAction();
workerScript.log("bladeburner.startAction", () => errorLogText);
2021-09-05 01:09:30 +02:00
return false;
}
}
2022-04-14 17:59:49 +02:00
getActionTimeNetscriptFn(person: IPerson, type: string, name: string): number | string {
2021-09-05 01:09:30 +02:00
const actionId = this.getActionIdFromTypeAndName(type, name);
if (actionId == null) {
return "bladeburner.getActionTime";
2021-09-05 01:09:30 +02:00
}
2021-08-16 23:45:26 +02:00
2021-09-05 01:09:30 +02:00
const actionObj = this.getActionObject(actionId);
if (actionObj == null) {
return "bladeburner.getActionTime";
2021-09-05 01:09:30 +02:00
}
switch (actionId.type) {
case ActionTypes["Contract"]:
case ActionTypes["Operation"]:
case ActionTypes["BlackOp"]:
case ActionTypes["BlackOperation"]:
return actionObj.getActionTime(this, person) * 1000;
2021-09-05 01:09:30 +02:00
case ActionTypes["Training"]:
case ActionTypes["Field Analysis"]:
case ActionTypes["FieldAnalysis"]:
2021-11-03 03:11:22 +01:00
return 30000;
2021-09-05 01:09:30 +02:00
case ActionTypes["Recruitment"]:
return this.getRecruitmentTime(person) * 1000;
2021-09-05 01:09:30 +02:00
case ActionTypes["Diplomacy"]:
case ActionTypes["Hyperbolic Regeneration Chamber"]:
case ActionTypes["Incite Violence"]:
2021-11-03 03:11:22 +01:00
return 60000;
2021-09-05 01:09:30 +02:00
default:
return "bladeburner.getActionTime";
2021-09-05 01:09:30 +02:00
}
}
2022-04-14 17:59:49 +02:00
getActionEstimatedSuccessChanceNetscriptFn(person: IPerson, type: string, name: string): [number, number] | string {
2021-09-05 01:09:30 +02:00
const actionId = this.getActionIdFromTypeAndName(type, name);
if (actionId == null) {
return "bladeburner.getActionEstimatedSuccessChance";
2021-08-16 23:45:26 +02:00
}
2021-08-19 07:45:26 +02:00
2021-09-05 01:09:30 +02:00
const actionObj = this.getActionObject(actionId);
if (actionObj == null) {
return "bladeburner.getActionEstimatedSuccessChance";
2021-09-05 01:09:30 +02:00
}
switch (actionId.type) {
case ActionTypes["Contract"]:
case ActionTypes["Operation"]:
case ActionTypes["BlackOp"]:
case ActionTypes["BlackOperation"]:
return actionObj.getEstSuccessChance(this, person);
2021-09-05 01:09:30 +02:00
case ActionTypes["Training"]:
case ActionTypes["Field Analysis"]:
case ActionTypes["FieldAnalysis"]:
case ActionTypes["Diplomacy"]:
case ActionTypes["Hyperbolic Regeneration Chamber"]:
case ActionTypes["Incite Violence"]:
2021-09-05 01:09:30 +02:00
return [1, 1];
2021-09-06 21:06:08 +02:00
case ActionTypes["Recruitment"]: {
2022-04-14 17:59:49 +02:00
const recChance = this.getRecruitmentSuccessChance(person);
return [recChance, recChance];
}
2021-09-05 01:09:30 +02:00
default:
return "bladeburner.getActionEstimatedSuccessChance";
2021-09-05 01:09:30 +02:00
}
}
2021-09-09 05:47:34 +02:00
getActionCountRemainingNetscriptFn(type: string, name: string, workerScript: WorkerScript): number {
2021-09-05 01:09:30 +02:00
const errorLogText = `Invalid action: type='${type}' name='${name}'`;
const actionId = this.getActionIdFromTypeAndName(type, name);
if (actionId == null) {
workerScript.log("bladeburner.getActionCountRemaining", () => errorLogText);
2021-09-05 01:09:30 +02:00
return -1;
2021-08-16 23:45:26 +02:00
}
2021-08-19 07:45:26 +02:00
2021-09-05 01:09:30 +02:00
const actionObj = this.getActionObject(actionId);
if (actionObj == null) {
workerScript.log("bladeburner.getActionCountRemaining", () => errorLogText);
2021-09-05 01:09:30 +02:00
return -1;
}
2021-08-16 23:45:26 +02:00
2021-09-05 01:09:30 +02:00
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 {
2021-09-05 01:09:30 +02:00
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"]:
2021-09-05 01:09:30 +02:00
return Infinity;
default:
workerScript.log("bladeburner.getActionCountRemaining", () => errorLogText);
2021-09-05 01:09:30 +02:00
return -1;
2021-08-16 23:45:26 +02:00
}
2021-09-05 01:09:30 +02:00
}
2021-09-09 05:47:34 +02:00
getSkillLevelNetscriptFn(skillName: string, workerScript: WorkerScript): number {
2021-09-05 01:09:30 +02:00
if (skillName === "" || !Skills.hasOwnProperty(skillName)) {
workerScript.log("bladeburner.getSkillLevel", () => `Invalid skill: '${skillName}'`);
2021-09-05 01:09:30 +02:00
return -1;
2021-08-16 23:45:26 +02:00
}
2021-08-19 07:45:26 +02:00
2021-09-05 01:09:30 +02:00
if (this.skills[skillName] == null) {
return 0;
} else {
return this.skills[skillName];
}
}
2021-09-09 05:47:34 +02:00
getSkillUpgradeCostNetscriptFn(skillName: string, workerScript: WorkerScript): number {
2021-09-05 01:09:30 +02:00
if (skillName === "" || !Skills.hasOwnProperty(skillName)) {
workerScript.log("bladeburner.getSkillUpgradeCost", () => `Invalid skill: '${skillName}'`);
2021-09-05 01:09:30 +02:00
return -1;
}
2021-09-05 01:09:30 +02:00
const skill = Skills[skillName];
if (this.skills[skillName] == null) {
return skill.calculateCost(0);
} else {
return skill.calculateCost(this.skills[skillName]);
}
}
2021-09-09 05:47:34 +02:00
upgradeSkillNetscriptFn(skillName: string, workerScript: WorkerScript): boolean {
2021-09-05 01:09:30 +02:00
const errorLogText = `Invalid skill: '${skillName}'`;
if (!Skills.hasOwnProperty(skillName)) {
workerScript.log("bladeburner.upgradeSkill", () => errorLogText);
2021-09-05 01:09:30 +02:00
return false;
}
2021-09-05 01:09:30 +02:00
const skill = Skills[skillName];
let currentLevel = 0;
if (this.skills[skillName] && !isNaN(this.skills[skillName])) {
currentLevel = this.skills[skillName];
}
const cost = skill.calculateCost(currentLevel);
if (skill.maxLvl && currentLevel >= skill.maxLvl) {
workerScript.log("bladeburner.upgradeSkill", () => `Skill '${skillName}' is already maxed.`);
2021-09-05 01:09:30 +02:00
return false;
}
2021-08-16 23:45:26 +02:00
2021-09-05 01:09:30 +02:00
if (this.skillPoints < cost) {
workerScript.log(
"bladeburner.upgradeSkill",
() =>
`You do not have enough skill points to upgrade ${skillName} (You have ${this.skillPoints}, you need ${cost})`,
2021-09-05 01:09:30 +02:00
);
return false;
2021-08-16 23:45:26 +02:00
}
2021-08-19 07:45:26 +02:00
2021-09-05 01:09:30 +02:00
this.skillPoints -= cost;
this.upgradeSkill(skill);
workerScript.log("bladeburner.upgradeSkill", () => `'${skillName}' upgraded to level ${this.skills[skillName]}`);
2021-09-05 01:09:30 +02:00
return true;
}
2021-09-09 05:47:34 +02:00
getTeamSizeNetscriptFn(type: string, name: string, workerScript: WorkerScript): number {
2021-09-05 01:09:30 +02:00
if (type === "" && name === "") {
return this.teamSize;
}
2021-09-05 01:09:30 +02:00
const errorLogText = `Invalid action: type='${type}' name='${name}'`;
const actionId = this.getActionIdFromTypeAndName(type, name);
if (actionId == null) {
workerScript.log("bladeburner.getTeamSize", () => errorLogText);
2021-09-05 01:09:30 +02:00
return -1;
}
2021-08-16 23:45:26 +02:00
2021-09-05 01:09:30 +02:00
const actionObj = this.getActionObject(actionId);
if (actionObj == null) {
workerScript.log("bladeburner.getTeamSize", () => errorLogText);
2021-09-05 01:09:30 +02:00
return -1;
}
2021-09-05 01:09:30 +02:00
if (
actionId.type === ActionTypes["Operation"] ||
actionId.type === ActionTypes["BlackOp"] ||
actionId.type === ActionTypes["BlackOperation"]
) {
return actionObj.teamCount;
} else {
return 0;
}
}
2021-09-09 05:47:34 +02:00
setTeamSizeNetscriptFn(type: string, name: string, size: number, workerScript: WorkerScript): number {
2021-09-05 01:09:30 +02:00
const errorLogText = `Invalid action: type='${type}' name='${name}'`;
const actionId = this.getActionIdFromTypeAndName(type, name);
if (actionId == null) {
workerScript.log("bladeburner.setTeamSize", () => errorLogText);
2021-09-05 01:09:30 +02:00
return -1;
2021-08-16 23:45:26 +02:00
}
2021-09-05 01:09:30 +02:00
if (
actionId.type !== ActionTypes["Operation"] &&
actionId.type !== ActionTypes["BlackOp"] &&
actionId.type !== ActionTypes["BlackOperation"]
) {
workerScript.log("bladeburner.setTeamSize", () => "Only valid for 'Operations' and 'BlackOps'");
2021-09-05 01:09:30 +02:00
return -1;
}
2021-08-16 23:45:26 +02:00
2021-09-05 01:09:30 +02:00
const actionObj = this.getActionObject(actionId);
if (actionObj == null) {
workerScript.log("bladeburner.setTeamSize", () => errorLogText);
2021-09-05 01:09:30 +02:00
return -1;
2021-08-16 23:45:26 +02:00
}
2021-09-05 01:09:30 +02:00
let sanitizedSize = Math.round(size);
if (isNaN(sanitizedSize) || sanitizedSize < 0) {
workerScript.log("bladeburner.setTeamSize", () => `Invalid size: ${size}`);
2021-09-05 01:09:30 +02:00
return -1;
}
if (this.teamSize < sanitizedSize) {
sanitizedSize = this.teamSize;
}
actionObj.teamCount = sanitizedSize;
workerScript.log("bladeburner.setTeamSize", () => `Team size for '${name}' set to ${sanitizedSize}.`);
2021-09-05 01:09:30 +02:00
return sanitizedSize;
}
joinBladeburnerFactionNetscriptFn(workerScript: WorkerScript): boolean {
const bladeburnerFac = Factions[FactionNames.Bladeburners];
2021-09-05 01:09:30 +02:00
if (bladeburnerFac.isMember) {
return true;
} else if (this.rank >= BladeburnerConstants.RankNeededForFaction) {
joinFaction(bladeburnerFac);
workerScript.log("bladeburner.joinBladeburnerFaction", () => `Joined ${FactionNames.Bladeburners} faction.`);
2021-09-05 01:09:30 +02:00
return true;
} else {
workerScript.log(
"bladeburner.joinBladeburnerFaction",
() => `You do not have the required rank (${this.rank}/${BladeburnerConstants.RankNeededForFaction}).`,
2021-09-05 01:09:30 +02:00
);
return false;
2021-08-16 23:45:26 +02:00
}
2021-09-05 01:09:30 +02:00
}
/**
* Serialize the current object to a JSON save state.
*/
toJSON(): any {
return Generic_toJSON("Bladeburner", this);
}
/**
* Initiatizes a Bladeburner object from a JSON save state.
*/
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
static fromJSON(value: any): Bladeburner {
return Generic_fromJSON(Bladeburner, value.data);
}
2021-08-16 23:45:26 +02:00
}
Reviver.constructors.Bladeburner = Bladeburner;